homescreens/folio: Add widgets support

This commit is contained in:
Devin Lin 2023-11-04 22:14:39 -07:00
parent 930f26a23e
commit 4f48f127a4
47 changed files with 2560 additions and 272 deletions

View file

@ -11,10 +11,13 @@ set(homescreen_SRCS
folioapplicationfolder.cpp
foliodelegate.cpp
foliosettings.cpp
foliowidget.cpp
pagemodel.cpp
pagelistmodel.cpp
delegatetoucharea.cpp
dragstate.cpp
widgetcontainer.cpp
widgetsmanager.cpp
)
add_library(org.kde.plasma.mobile.homescreen.folio MODULE ${homescreen_SRCS})

View file

@ -16,11 +16,18 @@ As such, all of the positioning and placement of delegates on the screen are top
#### TODO
- Add folio/halcyon switcher in initial-start
- If an app gets uninstalled, the homescreen UI needs to ensure that delegates are updated
- BUG: the position of where things think the dragged icon is during drag-and-drop is slightly off because of the label
- BUG: landscape favourites bar duplication when dragging icon from it sometimes
- BUG: can't insert delegates in-between very well in landscape favourites bar
- can make the touch area only the icon?
- FEATURE: add import/export
- FEATURE: add widget import/export
- FEATURE: keyboard navigation
- FEATURE: touchpad navigation
- BUG: it's possible to get stuck in an unswipeable state after swiping down from the app drawer
- FEATURE: option to darken wallpaper
- FEATURE: option to turn off row/column swap
- BUG: drag and drop animation when rejected on a different page
- FEATURE: animate homescreen config opening
- RESTORE app drawer overshoot
- PERFORMANCE: ensure that the widget config overlays are in loaders

View file

@ -5,6 +5,8 @@
#include "delegatetoucharea.h"
#include <QCursor>
#include <QGuiApplication>
#include <QStyleHints>
// Some code taken from MouseArea
@ -12,7 +14,6 @@ DelegateTouchArea::DelegateTouchArea(QQuickItem *parent)
: QQuickItem{parent}
, m_pressAndHoldTimer{new QTimer{this}}
{
// TODO: currently hardcoded 2s press and hold interval
m_pressAndHoldTimer->setInterval(600);
m_pressAndHoldTimer->setSingleShot(true);
connect(m_pressAndHoldTimer, &QTimer::timeout, this, &DelegateTouchArea::startPressAndHold);
@ -24,7 +25,7 @@ DelegateTouchArea::DelegateTouchArea(QQuickItem *parent)
setAcceptHoverEvents(true);
setAcceptTouchEvents(true);
// setFiltersChildMouseEvents(true);
setFlags(QQuickItem::ItemIsFocusScope);
setAcceptedMouseButtons(Qt::LeftButton | Qt::RightButton);
}
@ -54,19 +55,6 @@ void DelegateTouchArea::setHovered(bool hovered)
}
}
bool DelegateTouchArea::dragging()
{
return m_dragging;
}
void DelegateTouchArea::setDragging(bool dragging)
{
if (dragging != m_dragging) {
m_dragging = dragging;
Q_EMIT draggingChanged(dragging);
}
}
Qt::CursorShape DelegateTouchArea::cursorShape()
{
return cursor().shape();
@ -87,6 +75,11 @@ void DelegateTouchArea::unsetCursor()
setCursorShape(Qt::ArrowCursor);
}
QPointF DelegateTouchArea::pressPosition()
{
return m_mouseDownPosition;
}
void DelegateTouchArea::mousePressEvent(QMouseEvent *event)
{
if (event->button() & Qt::RightButton) {
@ -178,86 +171,6 @@ void DelegateTouchArea::hoverLeaveEvent(QHoverEvent *event)
event->ignore();
}
// bool DelegateTouchArea::childMouseEventFilter(QQuickItem *item, QEvent *event)
// {
// if (!isVisible() || !isEnabled()) {
// handleReleaseEvent(nullptr, false);
// return QQuickItem::childMouseEventFilter(item, event);
// }
//
// if (event->isPointerEvent() && event->type() != QEvent::UngrabMouse) {
// return filterPointerEvent(item, static_cast<QPointerEvent *>(event));
// }
//
// return QQuickItem::childMouseEventFilter(item, event);
// }
//
// // take exclusive grab from children
// bool DelegateTouchArea::filterPointerEvent(QQuickItem *receiver, QPointerEvent *event)
// {
// // only filter mouse, touch or tablet events
// if (!dynamic_cast<QMouseEvent *>(event) && !dynamic_cast<QTabletEvent *>(event) && !dynamic_cast<QTouchEvent *>(event)) {
// return false;
// }
//
// const auto &firstPoint = event->points().first();
//
// if (event->pointCount() == 1 && event->exclusiveGrabber(firstPoint) == this) {
// // We have an exclusive grab (since we're e.g dragging), but at the same time, we have
// // a child with a passive grab (which is why this filter is being called). And because
// // of that, we end up getting the same pointer events twice; First in our own event
// // handlers (because of the grab), then once more in here, since we filter the child.
// // To avoid processing the event twice (e.g avoid calling handleReleaseEvent once more
// // from below), we mark the event as filtered, and simply return.
// event->setAccepted(true);
// return true;
// }
//
// QPointF localPos = mapFromScene(firstPoint.scenePosition());
// bool receiverDisabled = receiver && !receiver->isEnabled();
// bool receiverKeepsGrab = receiver && (receiver->keepMouseGrab() || receiver->keepTouchGrab());
//
// if ((m_pressAndHeld || contains(localPos)) && (!receiver || !receiverKeepsGrab || receiverDisabled)) {
// // clone the event, and set the first point's local position
// // HACK: we can't change QPointerEvent's points since it's const, so we have to pass localPos into the handlers
// QPointerEvent *localizedEvent = event->clone();
// localizedEvent->setAccepted(false);
//
// switch (firstPoint.state()) {
// case QEventPoint::State::Updated:
// handleMoveEvent(localizedEvent, localPos);
// break;
// case QEventPoint::State::Pressed:
// handlePressEvent(localizedEvent, localPos);
// break;
// case QEventPoint::State::Released:
// handleReleaseEvent(localizedEvent, true);
// break;
// case QEventPoint::State::Stationary:
// case QEventPoint::State::Unknown:
// break;
// }
//
// if ((receiver && m_pressAndHeld && !receiverKeepsGrab && receiver != this) || receiverDisabled) {
// event->setExclusiveGrabber(firstPoint, this);
// }
//
// bool filtered = m_pressAndHeld || receiverDisabled;
// if (filtered) {
// event->setAccepted(true);
// }
//
// return filtered;
// }
//
// if (firstPoint.state() == QEventPoint::State::Released || (receiverKeepsGrab && !receiverDisabled)) {
// // mouse released, or another item has claimed the grab
// handleReleaseEvent(nullptr, false);
// }
//
// return false;
// }
void DelegateTouchArea::handlePressEvent(QPointerEvent *event, QPointF point)
{
// ignore multiple press events
@ -266,6 +179,11 @@ void DelegateTouchArea::handlePressEvent(QPointerEvent *event, QPointF point)
}
setPressed(true);
forceActiveFocus(Qt::MouseFocusReason);
m_mouseDownPosition = point;
Q_EMIT pressPositionChanged();
m_pressAndHoldTimer->start();
}
@ -273,10 +191,10 @@ void DelegateTouchArea::handleReleaseEvent(QPointerEvent *event, bool click)
{
// NOTE: event can be nullptr!
bool wasPressed = m_pressed;
setPressed(false);
setDragging(false);
if (!m_pressAndHeld && click) {
if (!m_pressAndHeld && click && wasPressed) {
Q_EMIT clicked();
}
@ -290,8 +208,9 @@ void DelegateTouchArea::handleReleaseEvent(QPointerEvent *event, bool click)
void DelegateTouchArea::handleMoveEvent(QPointerEvent *event, QPointF point)
{
if (m_pressAndHeld) {
// TODO
if (QPointF(point - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) {
m_pressAndHoldTimer->stop();
setPressed(false);
}
}

View file

@ -19,8 +19,8 @@ class DelegateTouchArea : public QQuickItem
Q_PROPERTY(bool pressed READ pressed NOTIFY pressedChanged FINAL)
Q_PROPERTY(bool hovered READ hovered NOTIFY hoveredChanged FINAL)
Q_PROPERTY(bool dragging READ dragging NOTIFY draggingChanged FINAL)
Q_PROPERTY(Qt::CursorShape cursorShape READ cursorShape WRITE setCursorShape RESET unsetCursor NOTIFY cursorShapeChanged FINAL)
Q_PROPERTY(QPointF pressPosition READ pressPosition NOTIFY pressPositionChanged FINAL)
QML_NAMED_ELEMENT(DelegateTouchArea)
@ -29,21 +29,20 @@ public:
bool pressed();
bool hovered();
bool dragging();
Qt::CursorShape cursorShape();
void setCursorShape(Qt::CursorShape cursorShape);
void unsetCursor();
QPointF pressPosition();
Q_SIGNALS:
void clicked();
void rightMousePress();
void pressAndHold();
void pressAndHoldReleased();
void drag(qreal x, qreal y);
void pressedChanged(bool pressed);
void hoveredChanged(bool hovered);
void draggingChanged(bool dragging);
void cursorShapeChanged();
void pressPositionChanged();
protected:
void mousePressEvent(QMouseEvent *event) override;
@ -54,13 +53,11 @@ protected:
void touchUngrabEvent() override;
void hoverEnterEvent(QHoverEvent *event) override;
void hoverLeaveEvent(QHoverEvent *event) override;
// bool childMouseEventFilter(QQuickItem *i, QEvent *e) override;
private Q_SLOTS:
void startPressAndHold();
private:
// bool filterPointerEvent(QQuickItem *receiver, QPointerEvent *event);
void setPressed(bool pressed);
void setHovered(bool hovered);
void setDragging(bool dragging);
@ -71,9 +68,9 @@ private:
bool m_pressed{false};
bool m_hovered{false};
bool m_dragging{false};
bool m_pressAndHeld{false};
Qt::CursorShape m_cursorShape{Qt::ArrowCursor};
QPointF m_mouseDownPosition{};
QTimer *m_pressAndHoldTimer{nullptr};
};

View file

@ -168,6 +168,7 @@ DragState::DragState(HomeScreenState *state, QObject *parent)
connect(m_state, &HomeScreenState::delegateDragFromAppDrawerStarted, this, &DragState::onDelegateDragFromAppDrawerStarted);
connect(m_state, &HomeScreenState::delegateDragFromFavouritesStarted, this, &DragState::onDelegateDragFromFavouritesStarted);
connect(m_state, &HomeScreenState::delegateDragFromFolderStarted, this, &DragState::onDelegateDragFromFolderStarted);
connect(m_state, &HomeScreenState::delegateDragFromWidgetListStarted, this, &DragState::onDelegateDragFromWidgetListStarted);
connect(m_state, &HomeScreenState::swipeStateChanged, this, [this]() {
if (HomeScreenState::self()->swipeState() == HomeScreenState::DraggingDelegate) {
onDelegateDraggingStarted();
@ -214,8 +215,8 @@ void DragState::onDelegateDragPositionChanged()
// we want to update the candidate drop position variable in this function!
qreal x = getDraggedDelegateX();
qreal y = getDraggedDelegateY();
qreal x = getPointerX();
qreal y = getPointerY();
bool inFolder = m_state->viewState() == HomeScreenState::FolderView;
bool inFavouritesArea = !inFolder;
@ -258,8 +259,8 @@ void DragState::onDelegateDragPositionChanged()
void DragState::onDelegateDragPositionOverFolderViewChanged()
{
// if the drag position changes while in the folder view
qreal x = getDraggedDelegateX();
qreal y = getDraggedDelegateY();
qreal x = getPointerX();
qreal y = getPointerY();
auto *folder = m_state->currentFolder();
if (!folder) {
@ -311,8 +312,8 @@ void DragState::onDelegateDragPositionOverFavouritesChanged()
{
// the drag position changed while over the favourites strip
qreal x = getDraggedDelegateX();
qreal y = getDraggedDelegateY();
qreal x = getPointerX();
qreal y = getPointerY();
int dropIndex = FavouritesModel::self()->dropInsertPosition(x, y);
// if the drop position changed, cancel the open folder timer
@ -327,6 +328,11 @@ void DragState::onDelegateDragPositionOverFavouritesChanged()
m_favouritesInsertBetweenTimer->stop();
}
// ignore widget drop delegates (since they can't be placed in the favourites)
if (m_dropDelegate && m_dropDelegate->type() == FolioDelegate::Widget) {
return;
}
if (FavouritesModel::self()->dropPositionIsEdge(x, y)) {
// if we need to make space for the delegate
@ -369,16 +375,28 @@ void DragState::onDelegateDragPositionOverPageViewChanged()
{
// the drag position changed while over the homescreen pages strip
qreal x = getDraggedDelegateX();
qreal y = getDraggedDelegateY();
qreal delegateX = getDraggedDelegateX();
qreal delegateY = getDraggedDelegateY();
qreal x = getPointerX();
qreal y = getPointerY();
int page = m_state->currentPage();
// calculate the row and column the delegate is over
qreal pageHorizontalMargin = (m_state->pageWidth() - m_state->pageContentWidth()) / 2;
qreal pageVerticalMargin = (m_state->pageHeight() - m_state->pageContentHeight()) / 2;
int row = (y - pageVerticalMargin) / m_state->pageCellHeight();
int column = (x - pageHorizontalMargin) / m_state->pageCellWidth();
int row = 0;
int column = 0;
if (m_dropDelegate && m_dropDelegate->type() == FolioDelegate::Widget) {
// for widgets, we use their top left position to determine where they are placed (since they are larger than one cell)
row = (delegateY - pageVerticalMargin) / m_state->pageCellHeight();
column = (delegateX - pageHorizontalMargin) / m_state->pageCellWidth();
} else {
// otherwise, we base it on the pointer position
row = (y - pageVerticalMargin) / m_state->pageCellHeight();
column = (x - pageHorizontalMargin) / m_state->pageCellWidth();
}
// ensure it's in bounds
row = std::max(0, std::min(m_state->pageRows() - 1, row));
@ -488,6 +506,17 @@ void DragState::onDelegateDragFromFolderStarted(FolioApplicationFolder *folder,
m_startPosition->setLocation(DelegateDragPosition::Folder);
}
void DragState::onDelegateDragFromWidgetListStarted(QString appletPluginId)
{
// default widget has dimensions of 1x1, and id of -1
m_createdAppletPluginId = appletPluginId;
FolioWidget *widget = new FolioWidget{this, -1, 1, 1};
setDropDelegate(new FolioDelegate{widget, this});
// set start location
m_startPosition->setLocation(DelegateDragPosition::WidgetList);
}
void DragState::onDelegateDropped()
{
if (!m_dropDelegate) {
@ -495,7 +524,7 @@ void DragState::onDelegateDropped()
}
// add dropped delegate
createDropPositionDelegate();
bool success = createDropPositionDelegate();
// delete empty pages at the end if they exist
// (it can be created if user drags app to new page, but doesn't place it there)
@ -513,8 +542,14 @@ void DragState::onDelegateDropped()
m_changePageTimer->stop();
m_favouritesInsertBetweenTimer->stop();
// emit signal
Q_EMIT delegateDroppedAndPlaced();
// emit corresponding signal
// -> if we couldn't drop a new delegate at a spot, emit newDelegateDropAbandoned()
// -> otherwise, emit delegateDroppedAndPlaced()
if (!success && (m_startPosition->location() == DelegateDragPosition::WidgetList || m_startPosition->location() == DelegateDragPosition::AppDrawer)) {
Q_EMIT newDelegateDropAbandoned();
} else {
Q_EMIT delegateDroppedAndPlaced();
}
}
void DragState::onLeaveCurrentFolder()
@ -543,7 +578,7 @@ void DragState::onChangePageTimerFinished()
const int leftPagePosition = 0;
const int rightPagePosition = m_state->pageWidth();
qreal x = getDraggedDelegateX();
qreal x = getPointerX();
if (qAbs(leftPagePosition - x) <= PAGE_CHANGE_THRESHOLD) {
// if we are at the left edge, go left
int page = m_state->currentPage() - 1;
@ -623,7 +658,7 @@ void DragState::onLeaveFolderTimerFinished()
}
// check if the drag position is outside of the folder
if (m_state->currentFolder()->isDropPositionOutside(getDraggedDelegateX(), getDraggedDelegateY())) {
if (m_state->currentFolder()->isDropPositionOutside(getPointerX(), getPointerY())) {
m_state->closeFolder();
}
}
@ -635,16 +670,17 @@ void DragState::onChangeFolderPageTimerFinished()
}
auto *folder = m_state->currentFolder();
qreal x = getPointerX();
qreal y = getPointerY();
// check if the drag position is outside of the folder
if (folder->isDropPositionOutside(getDraggedDelegateX(), getDraggedDelegateY())) {
if (folder->isDropPositionOutside(x, y)) {
return;
}
const qreal leftPagePosition = folder->applications()->leftMarginFromScreenEdge();
const qreal rightPagePosition = m_state->viewWidth() - leftPagePosition;
qreal x = getDraggedDelegateX();
if (x <= leftPagePosition + PAGE_CHANGE_THRESHOLD) {
// if we are at the left edge, go left
int page = m_state->currentFolderPage() - 1;
@ -656,12 +692,6 @@ void DragState::onChangeFolderPageTimerFinished()
// if we are at the right edge, go right
int page = m_state->currentFolderPage() + 1;
// TODO!!!!
// if we are at the right-most page, try to create a new one if the current page isn't empty
// if (page == folder->applications()->rowCount() && !PageListModel::self()->isLastPageEmpty()) {
// PageListModel::self()->addPageAtEnd();
// }
// go to page if it exists
if (page < folder->applications()->numTotalPages()) {
m_state->goToFolderPage(page);
@ -683,7 +713,7 @@ void DragState::onFolderInsertBetweenTimerFinished()
m_candidateDropPosition->setLocation(DelegateDragPosition::Folder);
// insert it at this position, shifting existing apps to the side
// TODO the ghost entry may shift the m_folderInsertBetweenIndex, we should update??
// TODO the ghost entry may shift the m_folderInsertBetweenIndex, perhaps we should update??
folder->applications()->setGhostEntry(m_folderInsertBetweenIndex);
}
@ -715,17 +745,21 @@ void DragState::deleteStartPositionDelegate()
m_startPosition->folder()->removeDelegate(m_startPosition->folderPosition());
break;
case DelegateDragPosition::AppDrawer:
case DelegateDragPosition::WidgetList:
default:
break;
}
}
void DragState::createDropPositionDelegate()
bool DragState::createDropPositionDelegate()
{
if (!m_dropDelegate) {
return;
return false;
}
// whether the drop goes successfully
bool added = false;
// creates the delegate at the drop position
switch (m_candidateDropPosition->location()) {
case DelegateDragPosition::Pages: {
@ -753,6 +787,7 @@ void DragState::createDropPositionDelegate()
auto existingFolder = existingDelegate->folder();
existingFolder->addDelegate(delegate, existingFolder->applications()->rowCount());
added = true;
break;
} else if (existingDelegate->type() == FolioDelegate::Application && !isStartPositionEqualDropPosition()) {
// create a folder from the two apps
@ -765,19 +800,20 @@ void DragState::createDropPositionDelegate()
page->removeDelegate(row, column);
page->addDelegate(folderDelegate);
added = true;
break;
}
}
}
// default behavior for folders or dropping an app at an empty spot
// default behavior for widgets, folders or dropping an app at an empty spot
bool added = page->addDelegate(delegate);
added = page->addDelegate(delegate);
// if we couldn't add the delegate, try again but at the start position (return to start)
if (!added && !isStartPositionEqualDropPosition()) {
m_candidateDropPosition->copyFrom(m_startPosition);
createDropPositionDelegate();
added = createDropPositionDelegate();
}
break;
}
@ -794,6 +830,7 @@ void DragState::createDropPositionDelegate()
auto existingFolder = existingDelegate->folder();
existingFolder->addDelegate(m_dropDelegate, existingFolder->applications()->rowCount());
added = true;
break;
} else if (existingDelegate->type() == FolioDelegate::Application && !isStartPositionEqualDropPosition()) {
// create a folder from the two apps
@ -806,6 +843,7 @@ void DragState::createDropPositionDelegate()
FavouritesModel::self()->removeEntry(m_candidateDropPosition->favouritesPosition());
FavouritesModel::self()->addEntry(m_candidateDropPosition->favouritesPosition(), folderDelegate);
added = true;
break;
}
}
@ -813,12 +851,12 @@ void DragState::createDropPositionDelegate()
// otherwise, just add the delegate at this position
bool added = FavouritesModel::self()->addEntry(m_candidateDropPosition->favouritesPosition(), m_dropDelegate);
added = FavouritesModel::self()->addEntry(m_candidateDropPosition->favouritesPosition(), m_dropDelegate);
// if we couldn't add the delegate, try again but at the start position
if (!added && !isStartPositionEqualDropPosition()) {
m_candidateDropPosition->copyFrom(m_startPosition);
createDropPositionDelegate();
added = createDropPositionDelegate();
}
// correct position when we delete from an entry earlier in the favourites
@ -833,33 +871,42 @@ void DragState::createDropPositionDelegate()
case DelegateDragPosition::Folder: {
auto *folder = m_candidateDropPosition->folder();
if (!folder) {
return;
break;
}
// only support dropping apps into folders
if (m_dropDelegate->type() != FolioDelegate::Application) {
return;
break;
}
bool added = folder->addDelegate(m_dropDelegate, m_candidateDropPosition->folderPosition());
added = folder->addDelegate(m_dropDelegate, m_candidateDropPosition->folderPosition());
// if we couldn't add the delegate, try again but at the start position
if (!added && !isStartPositionEqualDropPosition()) {
m_candidateDropPosition->copyFrom(m_startPosition);
createDropPositionDelegate();
added = createDropPositionDelegate();
}
if (added) {
folder->applications()->deleteGhostEntry();
// TODO correct m_startPosition?
}
break;
}
case DelegateDragPosition::AppDrawer:
case DelegateDragPosition::WidgetList:
default:
break;
}
// if we are dropping a new widget, we need to now create the applet in the containment
if (added && m_startPosition->location() == DelegateDragPosition::WidgetList && m_dropDelegate->type() == FolioDelegate::Widget && m_state->containment()) {
Plasma::Applet *applet = m_state->containment()->createApplet(m_createdAppletPluginId);
// associate the new delegate with the Plasma::Applet
m_dropDelegate->widget()->setApplet(applet);
}
return added;
}
bool DragState::isStartPositionEqualDropPosition()
@ -881,3 +928,13 @@ qreal DragState::getDraggedDelegateY()
// adjust to get the position of the center of the delegate
return m_state->delegateDragY() + m_state->pageCellHeight() / 2;
}
qreal DragState::getPointerX()
{
return m_state->delegateDragX() + m_state->delegateDragPointerOffsetX();
}
qreal DragState::getPointerY()
{
return m_state->delegateDragY() + m_state->delegateDragPointerOffsetY();
}

View file

@ -24,7 +24,7 @@ class DelegateDragPosition : public QObject
Q_PROPERTY(FolioApplicationFolder *folder READ folder NOTIFY folderChanged)
public:
enum Location { Pages, Favourites, AppDrawer, Folder };
enum Location { Pages, Favourites, AppDrawer, Folder, WidgetList };
Q_ENUM(Location)
DelegateDragPosition(QObject *parent = nullptr);
@ -94,6 +94,9 @@ Q_SIGNALS:
void dropDelegateChanged();
void delegateDroppedAndPlaced();
// if you drop a new delegate on an invalid spot
void newDelegateDropAbandoned();
private Q_SLOTS:
void onDelegateDragPositionChanged();
void onDelegateDragPositionOverFolderViewChanged();
@ -105,6 +108,7 @@ private Q_SLOTS:
void onDelegateDragFromFavouritesStarted(int position);
void onDelegateDragFromAppDrawerStarted(QString storageId);
void onDelegateDragFromFolderStarted(FolioApplicationFolder *folder, int position);
void onDelegateDragFromWidgetListStarted(QString appletPluginId);
void onDelegateDropped();
void onLeaveCurrentFolder();
@ -120,8 +124,8 @@ private:
// deletes the delegate at m_startPosition
void deleteStartPositionDelegate();
// deletes the delegate at m_candidateDropPosition
void createDropPositionDelegate();
// places the delegate at m_candidateDropPosition, returning whether it was successful
bool createDropPositionDelegate();
// whether m_startPosition = m_candidateDropPosition
bool isStartPositionEqualDropPosition();
@ -130,6 +134,10 @@ private:
qreal getDraggedDelegateX();
qreal getDraggedDelegateY();
// position of the dragging pointer
qreal getPointerX();
qreal getPointerY();
QTimer *m_changePageTimer{nullptr};
QTimer *m_openFolderTimer{nullptr};
QTimer *m_leaveFolderTimer{nullptr};
@ -152,5 +160,8 @@ private:
// this is the original start position of the drag
DelegateDragPosition *const m_startPosition{nullptr};
// when dropping a new widget, this is the applet name
QString m_createdAppletPluginId{};
HomeScreenState *m_state{nullptr};
};

View file

@ -119,7 +119,7 @@ void FavouritesModel::moveEntry(int fromRow, int toRow)
save();
}
bool FavouritesModel::addEntry(int row, FolioDelegate *delegate)
bool FavouritesModel::canAddEntry(int row, FolioDelegate *delegate)
{
if (!delegate) {
return false;
@ -129,6 +129,15 @@ bool FavouritesModel::addEntry(int row, FolioDelegate *delegate)
return false;
}
return true;
}
bool FavouritesModel::addEntry(int row, FolioDelegate *delegate)
{
if (!canAddEntry(row, delegate)) {
return false;
}
if (row == m_delegates.size()) {
beginInsertRows(QModelIndex(), row, row);
m_delegates.append({delegate, 0});
@ -143,6 +152,9 @@ bool FavouritesModel::addEntry(int row, FolioDelegate *delegate)
endInsertRows();
}
// ensure saves are connected when requested by the delegate
connectSaveRequests(delegate);
evaluateDelegatePositions();
save();
@ -230,24 +242,24 @@ QJsonArray FavouritesModel::exportToJson()
void FavouritesModel::save()
{
if (!m_applet) {
if (!m_containment) {
return;
}
QJsonArray arr = exportToJson();
QByteArray data = QJsonDocument(arr).toJson(QJsonDocument::Compact);
m_applet->config().writeEntry("Favourites", QString::fromStdString(data.toStdString()));
Q_EMIT m_applet->configNeedsSaving();
m_containment->config().writeEntry("Favourites", QString::fromStdString(data.toStdString()));
Q_EMIT m_containment->configNeedsSaving();
}
void FavouritesModel::load()
{
if (!m_applet) {
if (!m_containment) {
return;
}
QJsonDocument doc = QJsonDocument::fromJson(m_applet->config().readEntry("Favourites", "{}").toUtf8());
QJsonDocument doc = QJsonDocument::fromJson(m_containment->config().readEntry("Favourites", "{}").toUtf8());
loadFromJson(doc.array());
}
@ -262,10 +274,7 @@ void FavouritesModel::loadFromJson(QJsonArray arr)
FolioDelegate *delegate = FolioDelegate::fromJson(obj, this);
if (delegate) {
if (delegate->type() == FolioDelegate::Folder) {
connect(delegate->folder(), &FolioApplicationFolder::saveRequested, this, &FavouritesModel::save);
}
connectSaveRequests(delegate);
m_delegates.append({delegate, 0});
}
}
@ -274,9 +283,16 @@ void FavouritesModel::loadFromJson(QJsonArray arr)
endResetModel();
}
void FavouritesModel::setApplet(Plasma::Applet *applet)
void FavouritesModel::connectSaveRequests(FolioDelegate *delegate)
{
m_applet = applet;
if (delegate->type() == FolioDelegate::Folder && delegate->folder()) {
connect(delegate->folder(), &FolioApplicationFolder::saveRequested, this, &FavouritesModel::save);
}
}
void FavouritesModel::setContainment(Plasma::Containment *containment)
{
m_containment = containment;
}
bool FavouritesModel::dropPositionIsEdge(qreal x, qreal y) const

View file

@ -11,7 +11,7 @@
#include <QQuickItem>
#include <QSet>
#include <Plasma/Applet>
#include <Plasma/Containment>
#include "foliodelegate.h"
@ -39,6 +39,7 @@ public:
Q_INVOKABLE void removeEntry(int row);
void moveEntry(int fromRow, int toRow);
bool canAddEntry(int row, FolioDelegate *delegate);
bool addEntry(int row, FolioDelegate *delegate);
FolioDelegate *getEntryAt(int row);
@ -64,9 +65,10 @@ public:
Q_INVOKABLE void load();
void loadFromJson(QJsonArray arr);
void setApplet(Plasma::Applet *applet);
void setContainment(Plasma::Containment *containment);
private:
void connectSaveRequests(FolioDelegate *delegate);
void evaluateDelegatePositions(bool emitSignal = true);
// get the x (or y) position where delegates start being placed
@ -78,5 +80,5 @@ private:
QList<FavouritesDelegate> m_delegates;
Plasma::Applet *m_applet{nullptr};
Plasma::Containment *m_containment{nullptr};
};

View file

@ -44,7 +44,7 @@ FolioApplication *FolioApplication::fromJson(QJsonObject &obj, QObject *parent)
return nullptr;
}
QJsonObject FolioApplication::toJson()
QJsonObject FolioApplication::toJson() const
{
QJsonObject obj;
obj[QStringLiteral("type")] = "application";

View file

@ -31,7 +31,7 @@ public:
FolioApplication(QObject *parent = nullptr, KService::Ptr service = QExplicitlySharedDataPointer<KService>{nullptr});
static FolioApplication *fromJson(QJsonObject &obj, QObject *parent); // may return nullptr
QJsonObject toJson();
QJsonObject toJson() const;
bool running() const;
QString name() const;

View file

@ -29,7 +29,7 @@ FolioApplicationFolder *FolioApplicationFolder::fromJson(QJsonObject &obj, QObje
return folder;
}
QJsonObject FolioApplicationFolder::toJson()
QJsonObject FolioApplicationFolder::toJson() const
{
QJsonObject obj;
obj[QStringLiteral("type")] = "folder";
@ -213,7 +213,7 @@ void ApplicationFolderModel::moveEntry(int fromRow, int toRow)
Q_EMIT m_folder->saveRequested();
}
bool ApplicationFolderModel::addDelegate(FolioDelegate *delegate, int index)
bool ApplicationFolderModel::canAddDelegate(FolioDelegate *delegate, int index)
{
if (index < 0 || index > m_folder->m_delegates.size()) {
return false;
@ -223,6 +223,15 @@ bool ApplicationFolderModel::addDelegate(FolioDelegate *delegate, int index)
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});

View file

@ -36,7 +36,7 @@ public:
FolioApplicationFolder(QObject *parent = nullptr, QString name = QString{});
static FolioApplicationFolder *fromJson(QJsonObject &obj, QObject *parent);
QJsonObject toJson();
QJsonObject toJson() const;
QString name() const;
void setName(QString &name);
@ -92,6 +92,7 @@ public:
FolioDelegate *getDelegate(int index);
void moveEntry(int fromRow, int toRow);
bool canAddDelegate(FolioDelegate *delegate, int index);
bool addDelegate(FolioDelegate *delegate, int index);
void removeDelegate(int index);
QPointF getDelegatePosition(int index);

View file

@ -9,6 +9,7 @@ FolioDelegate::FolioDelegate(QObject *parent)
, m_type{FolioDelegate::None}
, m_application{nullptr}
, m_folder{nullptr}
, m_widget{nullptr}
{
}
@ -17,6 +18,7 @@ FolioDelegate::FolioDelegate(FolioApplication *application, QObject *parent)
, m_type{FolioDelegate::Application}
, m_application{application}
, m_folder{nullptr}
, m_widget{nullptr}
{
}
@ -25,6 +27,16 @@ FolioDelegate::FolioDelegate(FolioApplicationFolder *folder, QObject *parent)
, m_type{FolioDelegate::Folder}
, m_application{nullptr}
, m_folder{folder}
, m_widget{nullptr}
{
}
FolioDelegate::FolioDelegate(FolioWidget *widget, QObject *parent)
: QObject{parent}
, m_type{FolioDelegate::Widget}
, m_application{nullptr}
, m_folder{nullptr}
, m_widget{widget}
{
}
@ -47,6 +59,13 @@ FolioDelegate *FolioDelegate::fromJson(QJsonObject &obj, QObject *parent)
return new FolioDelegate{folder, parent};
}
} else if (type == "widget") {
// read widget
FolioWidget *widget = FolioWidget::fromJson(obj, parent);
if (widget) {
return new FolioDelegate{widget, parent};
}
} else if (type == "none") {
return new FolioDelegate{parent};
}
@ -61,6 +80,8 @@ QJsonObject FolioDelegate::toJson() const
return m_application->toJson();
case FolioDelegate::Folder:
return m_folder->toJson();
case FolioDelegate::Widget:
return m_widget->toJson();
case FolioDelegate::None: {
QJsonObject obj;
obj[QStringLiteral("type")] = "none";
@ -86,3 +107,8 @@ FolioApplicationFolder *FolioDelegate::folder()
{
return m_folder;
}
FolioWidget *FolioDelegate::widget()
{
return m_widget;
}

View file

@ -7,6 +7,7 @@
#include "folioapplication.h"
#include "folioapplicationfolder.h"
#include "foliowidget.h"
class FolioApplication;
class FolioApplicationFolder;
@ -16,18 +17,21 @@ class FolioDelegate : public QObject
Q_PROPERTY(FolioDelegate::Type type READ type CONSTANT)
Q_PROPERTY(FolioApplication *application READ application CONSTANT)
Q_PROPERTY(FolioApplicationFolder *folder READ folder CONSTANT)
Q_PROPERTY(FolioWidget *widget READ widget CONSTANT)
public:
enum Type {
None,
Application,
Folder,
Widget,
};
Q_ENUM(Type)
FolioDelegate(QObject *parent = nullptr);
FolioDelegate(FolioApplication *application, QObject *parent);
FolioDelegate(FolioApplicationFolder *folder, QObject *parent);
FolioDelegate(FolioWidget *widget, QObject *parent);
static FolioDelegate *fromJson(QJsonObject &obj, QObject *parent);
@ -36,9 +40,11 @@ public:
FolioDelegate::Type type();
FolioApplication *application();
FolioApplicationFolder *folder();
FolioWidget *widget();
protected:
FolioDelegate::Type m_type;
FolioApplication *m_application{nullptr};
FolioApplicationFolder *m_folder{nullptr};
FolioWidget *m_widget{nullptr};
};

View file

@ -0,0 +1,272 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "foliowidget.h"
#include "homescreenstate.h"
#include "widgetsmanager.h"
FolioWidget::FolioWidget(QObject *parent, int id, int realGridWidth, int realGridHeight)
: QObject{parent}
, m_id{id}
, m_realGridWidth{realGridWidth}
, m_realGridHeight{realGridHeight}
, m_applet{nullptr}
, m_quickApplet{nullptr}
{
auto *applet = WidgetsManager::self()->getWidget(id);
if (applet) {
setApplet(applet);
}
init();
}
FolioWidget::FolioWidget(QObject *parent, Plasma::Applet *applet, int realGridWidth, int realGridHeight)
: QObject{parent}
, m_id{applet ? static_cast<int>(applet->id()) : -1}
, m_realGridWidth{realGridWidth}
, m_realGridHeight{realGridHeight}
{
setApplet(applet);
init();
}
void FolioWidget::init()
{
connect(HomeScreenState::self(), &HomeScreenState::pageOrientationChanged, this, [this]() {
Q_EMIT gridWidthChanged();
Q_EMIT gridHeightChanged();
});
connect(WidgetsManager::self(), &WidgetsManager::widgetAdded, this, [this](Plasma::Applet *applet) {
if (applet && static_cast<int>(applet->id()) == m_id) {
setApplet(applet);
}
});
connect(WidgetsManager::self(), &WidgetsManager::widgetRemoved, this, [this](Plasma::Applet *applet) {
if (applet && static_cast<int>(applet->id()) == m_id) {
setApplet(nullptr);
}
});
}
FolioWidget *FolioWidget::fromJson(QJsonObject &obj, QObject *parent)
{
int id = obj[QStringLiteral("id")].toInt();
int gridWidth = obj[QStringLiteral("gridWidth")].toInt();
int gridHeight = obj[QStringLiteral("gridHeight")].toInt();
return new FolioWidget(parent, id, gridWidth, gridHeight);
}
QJsonObject FolioWidget::toJson() const
{
QJsonObject obj;
obj[QStringLiteral("type")] = "widget";
obj[QStringLiteral("id")] = m_id;
obj[QStringLiteral("gridWidth")] = m_realGridWidth;
obj[QStringLiteral("gridHeight")] = m_realGridHeight;
return obj;
}
int FolioWidget::id() const
{
return m_id;
}
int FolioWidget::gridWidth() const
{
switch (HomeScreenState::self()->pageOrientation()) {
case HomeScreenState::RegularPosition:
return m_realGridWidth;
case HomeScreenState::RotateClockwise:
return m_realGridHeight;
case HomeScreenState::RotateCounterClockwise:
return m_realGridHeight;
case HomeScreenState::RotateUpsideDown:
return m_realGridWidth;
}
return m_realGridWidth;
}
void FolioWidget::setGridWidth(int gridWidth)
{
switch (HomeScreenState::self()->pageOrientation()) {
case HomeScreenState::RegularPosition:
setRealGridWidth(gridWidth);
break;
case HomeScreenState::RotateClockwise: {
int oldGridHeight = m_realGridHeight;
setRealGridHeight(gridWidth);
Q_EMIT realTopLeftPositionChanged(oldGridHeight - gridWidth, 0);
break;
}
case HomeScreenState::RotateCounterClockwise:
setRealGridHeight(gridWidth);
break;
case HomeScreenState::RotateUpsideDown: {
int oldGridWidth = m_realGridWidth;
setRealGridWidth(gridWidth);
Q_EMIT realTopLeftPositionChanged(0, oldGridWidth - gridWidth);
break;
}
}
}
int FolioWidget::gridHeight() const
{
switch (HomeScreenState::self()->pageOrientation()) {
case HomeScreenState::RegularPosition:
return m_realGridHeight;
case HomeScreenState::RotateClockwise:
return m_realGridWidth;
case HomeScreenState::RotateCounterClockwise:
return m_realGridWidth;
case HomeScreenState::RotateUpsideDown:
return m_realGridHeight;
}
return m_realGridHeight;
}
void FolioWidget::setGridHeight(int gridHeight)
{
switch (HomeScreenState::self()->pageOrientation()) {
case HomeScreenState::RegularPosition:
setRealGridHeight(gridHeight);
break;
case HomeScreenState::RotateClockwise:
setRealGridWidth(gridHeight);
break;
case HomeScreenState::RotateCounterClockwise: {
int oldGridWidth = m_realGridWidth;
setRealGridWidth(gridHeight);
Q_EMIT realTopLeftPositionChanged(0, oldGridWidth - gridHeight);
break;
}
case HomeScreenState::RotateUpsideDown: {
int oldGridHeight = m_realGridHeight;
setRealGridHeight(gridHeight);
Q_EMIT realTopLeftPositionChanged(oldGridHeight - gridHeight, 0);
break;
}
}
}
int FolioWidget::realGridWidth() const
{
return m_realGridWidth;
}
void FolioWidget::setRealGridWidth(int gridWidth)
{
if (m_realGridWidth != gridWidth) {
m_realGridWidth = gridWidth;
// emit both because realGridWidth could be either gridWidth or gridHeight
Q_EMIT gridWidthChanged();
Q_EMIT gridHeightChanged();
Q_EMIT saveRequested();
}
}
int FolioWidget::realGridHeight() const
{
return m_realGridHeight;
}
void FolioWidget::setRealGridHeight(int gridHeight)
{
if (m_realGridHeight != gridHeight) {
m_realGridHeight = gridHeight;
// emit both because realGridHeight could be either gridWidth or gridHeight
Q_EMIT gridWidthChanged();
Q_EMIT gridHeightChanged();
Q_EMIT saveRequested();
}
}
GridPosition FolioWidget::topLeftCorner(int row, int column)
{
switch (HomeScreenState::self()->pageOrientation()) {
case HomeScreenState::RegularPosition:
return {row, column};
case HomeScreenState::RotateClockwise:
return {row, column - gridWidth() + 1};
case HomeScreenState::RotateCounterClockwise:
return {row - gridHeight() + 1, column};
case HomeScreenState::RotateUpsideDown:
return {row - gridHeight() + 1, column - gridWidth() + 1};
}
return {row, column};
}
bool FolioWidget::isInBounds(int widgetRow, int widgetColumn, int row, int column)
{
return (row >= widgetRow) && (row <= widgetRow + gridHeight() - 1) && (column >= widgetColumn) && (column <= widgetColumn + gridWidth() - 1);
}
bool FolioWidget::overlapsWidget(int widgetRow, int widgetColumn, FolioWidget *otherWidget, int otherWidgetRow, int otherWidgetColumn)
{
if (!otherWidget) {
return false;
}
// property: if they overlap, then at least one corner of one widget is in the other widget
int widgetMaxRow = widgetRow + gridHeight() - 1;
int widgetMaxColumn = widgetColumn + gridWidth() - 1;
int otherWidgetMaxRow = otherWidgetRow + otherWidget->gridHeight() - 1;
int otherWidgetMaxColumn = otherWidgetColumn + otherWidget->gridWidth() - 1;
return isInBounds(widgetRow, widgetColumn, otherWidgetRow, otherWidgetColumn) || isInBounds(widgetRow, widgetColumn, otherWidgetMaxRow, otherWidgetColumn)
|| isInBounds(widgetRow, widgetColumn, otherWidgetRow, otherWidgetMaxColumn)
|| isInBounds(widgetRow, widgetColumn, otherWidgetMaxRow, otherWidgetMaxColumn)
|| otherWidget->isInBounds(otherWidgetRow, otherWidgetColumn, widgetRow, widgetColumn)
|| otherWidget->isInBounds(otherWidgetRow, otherWidgetColumn, widgetMaxRow, widgetColumn)
|| otherWidget->isInBounds(otherWidgetRow, otherWidgetColumn, widgetRow, widgetMaxColumn)
|| otherWidget->isInBounds(otherWidgetRow, otherWidgetColumn, widgetMaxRow, widgetMaxColumn);
}
Plasma::Applet *FolioWidget::applet() const
{
return m_applet;
}
void FolioWidget::setApplet(Plasma::Applet *applet)
{
m_applet = applet;
Q_EMIT appletChanged();
int id = applet ? applet->id() : -1;
if (m_id != id) {
m_id = id;
Q_EMIT idChanged();
// ensure the id is saved
Q_EMIT saveRequested();
}
if (m_applet) {
setVisualApplet(PlasmaQuick::AppletQuickItem::itemForApplet(m_applet));
} else {
setVisualApplet(nullptr);
}
}
PlasmaQuick::AppletQuickItem *FolioWidget::visualApplet() const
{
return m_quickApplet;
}
void FolioWidget::setVisualApplet(PlasmaQuick::AppletQuickItem *quickItem)
{
m_quickApplet = quickItem;
Q_EMIT visualAppletChanged();
}
void FolioWidget::destroyApplet()
{
if (m_applet) {
m_applet->destroy();
}
}

View file

@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <Plasma/Applet>
#include <PlasmaQuick/AppletQuickItem>
struct GridPosition {
Q_GADGET
public:
int row;
int column;
};
/**
* @short Object that represents a widget on the homescreen.
*/
class FolioWidget : public QObject
{
Q_OBJECT
Q_PROPERTY(int id READ id NOTIFY idChanged)
Q_PROPERTY(int gridWidth READ gridWidth NOTIFY gridWidthChanged)
Q_PROPERTY(int gridHeight READ gridHeight NOTIFY gridHeightChanged)
Q_PROPERTY(Plasma::Applet *applet READ applet NOTIFY appletChanged)
Q_PROPERTY(PlasmaQuick::AppletQuickItem *visualApplet READ visualApplet NOTIFY visualAppletChanged)
public:
FolioWidget(QObject *parent = nullptr, int id = -1, int gridWidth = 0, int gridHeight = 0);
FolioWidget(QObject *parent, Plasma::Applet *applet, int gridWidth, int gridHeight);
static FolioWidget *fromJson(QJsonObject &obj, QObject *parent);
QJsonObject toJson() const;
int id() const;
int gridWidth() const;
void setGridWidth(int gridWidth);
int gridHeight() const;
void setGridHeight(int gridHeight);
int realGridWidth() const;
void setRealGridWidth(int gridWidth);
int realGridHeight() const;
void setRealGridHeight(int gridHeight);
// takes in the stored position of the widget (top left when in portrait orientation)
// returns the position of the widget corners on a page grid, factoring in the current page orientation
GridPosition topLeftCorner(int row, int column);
// query whether (row, column) is inside this widget, if it was at position (widgetRow, widgetColumn)
bool isInBounds(int widgetRow, int widgetColumn, int row, int column);
bool overlapsWidget(int widgetRow, int widgetColumn, FolioWidget *otherWidget, int otherWidgetRow, int otherWidgetColumn);
Plasma::Applet *applet() const;
void setApplet(Plasma::Applet *applet);
PlasmaQuick::AppletQuickItem *visualApplet() const;
Q_INVOKABLE void destroyApplet();
Q_SIGNALS:
void idChanged();
void appletChanged();
void visualAppletChanged();
void gridWidthChanged();
void gridHeightChanged();
void saveRequested();
// when we resize while the screen is rotated, the stored top left position
// changes, so we need to notify the model
void realTopLeftPositionChanged(int offsetRows, int offsetColumns);
private:
void init();
void setVisualApplet(PlasmaQuick::AppletQuickItem *quickApplet);
int m_id = -1;
int m_realGridWidth = 1;
int m_realGridHeight = 1;
Plasma::Applet *m_applet = nullptr;
PlasmaQuick::AppletQuickItem *m_quickApplet = nullptr;
};

View file

@ -11,9 +11,12 @@
#include "folioapplicationfolder.h"
#include "foliodelegate.h"
#include "foliosettings.h"
#include "foliowidget.h"
#include "homescreenstate.h"
#include "pagelistmodel.h"
#include "pagemodel.h"
#include "widgetcontainer.h"
#include "widgetsmanager.h"
#include <KWindowSystem>
@ -31,12 +34,13 @@ HomeScreen::HomeScreen(QObject *parent, const KPluginMetaData &data, const QVari
// pre-initialize
FolioSettings::self()->setApplet(this);
HomeScreenState::self();
HomeScreenState::self()->setContainment(this);
WidgetsManager::self();
// models are loaded in main.qml
ApplicationListModel::self();
FavouritesModel::self()->setApplet(this);
PageListModel::self()->setApplet(this);
FavouritesModel::self()->setContainment(this);
PageListModel::self()->setContainment(this);
qmlRegisterSingletonType<ApplicationListModel>(uri, 1, 0, "ApplicationListModel", [](QQmlEngine *, QJSEngine *) -> QObject * {
return ApplicationListModel::self();
@ -60,13 +64,18 @@ HomeScreen::HomeScreen(QObject *parent, const KPluginMetaData &data, const QVari
qmlRegisterType<FolioApplication>(uri, 1, 0, "FolioApplication");
qmlRegisterType<FolioApplicationFolder>(uri, 1, 0, "FolioApplicationFolder");
qmlRegisterType<FolioWidget>(uri, 1, 0, "FolioWidget");
qmlRegisterType<FolioDelegate>(uri, 1, 0, "FolioDelegate");
qmlRegisterType<PageModel>(uri, 1, 0, "PageModel");
qmlRegisterType<FolioPageDelegate>(uri, 1, 0, "FolioPageDelegate");
qmlRegisterType<DelegateTouchArea>(uri, 1, 0, "DelegateTouchArea");
qmlRegisterType<DelegateDragPosition>(uri, 1, 0, "DelegateDragPosition");
qmlRegisterType<WidgetContainer>(uri, 1, 0, "WidgetContainer");
connect(KWindowSystem::self(), &KWindowSystem::showingDesktopChanged, this, &HomeScreen::showingDesktopChanged);
connect(this, &Plasma::Containment::appletAdded, this, &HomeScreen::onAppletAdded);
connect(this, &Plasma::Containment::appletAboutToBeRemoved, this, &HomeScreen::onAppletAboutToBeRemoved);
}
HomeScreen::~HomeScreen() = default;
@ -76,6 +85,16 @@ void HomeScreen::configChanged()
Plasma::Containment::configChanged();
}
void HomeScreen::onAppletAdded(Plasma::Applet *applet, const QRectF &geometryHint)
{
WidgetsManager::self()->addWidget(applet);
}
void HomeScreen::onAppletAboutToBeRemoved(Plasma::Applet *applet)
{
WidgetsManager::self()->removeWidget(applet);
}
K_PLUGIN_CLASS(HomeScreen)
#include "homescreen.moc"

View file

@ -19,4 +19,8 @@ public:
Q_SIGNALS:
void showingDesktopChanged(bool showingDesktop);
private Q_SLOTS:
void onAppletAdded(Plasma::Applet *applet, const QRectF &geometryHint);
void onAppletAboutToBeRemoved(Plasma::Applet *applet);
};

View file

@ -582,6 +582,26 @@ void HomeScreenState::setDelegateDragY(qreal delegateDragY)
Q_EMIT delegateDragYChanged();
}
qreal HomeScreenState::delegateDragPointerOffsetX()
{
return m_delegateDragPointerOffsetX;
}
void HomeScreenState::setDelegateDragPointerOffsetX(qreal delegateDragPointerOffsetX)
{
m_delegateDragPointerOffsetX = delegateDragPointerOffsetX;
}
qreal HomeScreenState::delegateDragPointerOffsetY()
{
return m_delegateDragPointerOffsetY;
}
void HomeScreenState::setDelegateDragPointerOffsetY(qreal delegateDragPointerOffsetY)
{
m_delegateDragPointerOffsetY = delegateDragPointerOffsetY;
}
int HomeScreenState::currentPage()
{
return m_pageNum;
@ -659,6 +679,16 @@ QPointF HomeScreenState::getFolderDelegateScreenPosition(int position)
return {x, y};
}
Plasma::Containment *HomeScreenState::containment()
{
return m_containment;
}
void HomeScreenState::setContainment(Plasma::Containment *containment)
{
m_containment = containment;
}
void HomeScreenState::openAppDrawer()
{
cancelAppDrawerAnimations();
@ -786,11 +816,13 @@ void HomeScreenState::closeSettingsView()
m_closeSettingsAnim->start();
}
void HomeScreenState::startDelegateDrag(qreal startX, qreal startY)
void HomeScreenState::startDelegateDrag(qreal startX, qreal startY, qreal pointerOffsetX, qreal pointerOffsetY)
{
// start drag and drop positions
setDelegateDragX(startX);
setDelegateDragY(startY);
setDelegateDragPointerOffsetX(pointerOffsetX);
setDelegateDragPointerOffsetY(pointerOffsetY);
// end current swipe
swipeEnded();
@ -799,21 +831,21 @@ void HomeScreenState::startDelegateDrag(qreal startX, qreal startY)
setSwipeState(SwipeState::AwaitingDraggingDelegate);
}
void HomeScreenState::startDelegatePageDrag(qreal startX, qreal startY, int page, int row, int column)
void HomeScreenState::startDelegatePageDrag(qreal startX, qreal startY, qreal pointerOffsetX, qreal pointerOffsetY, int page, int row, int column)
{
startDelegateDrag(startX, startY);
startDelegateDrag(startX, startY, pointerOffsetX, pointerOffsetY);
Q_EMIT delegateDragFromPageStarted(page, row, column);
}
void HomeScreenState::startDelegateFavouritesDrag(qreal startX, qreal startY, int position)
void HomeScreenState::startDelegateFavouritesDrag(qreal startX, qreal startY, qreal pointerOffsetX, qreal pointerOffsetY, int position)
{
startDelegateDrag(startX, startY);
startDelegateDrag(startX, startY, pointerOffsetX, pointerOffsetY);
Q_EMIT delegateDragFromFavouritesStarted(position);
}
void HomeScreenState::startDelegateAppDrawerDrag(qreal startX, qreal startY, QString storageId)
void HomeScreenState::startDelegateAppDrawerDrag(qreal startX, qreal startY, qreal pointerOffsetX, qreal pointerOffsetY, QString storageId)
{
startDelegateDrag(startX, startY);
startDelegateDrag(startX, startY, pointerOffsetX, pointerOffsetY);
Q_EMIT delegateDragFromAppDrawerStarted(storageId);
// we start dragging the delegate immediately from the app drawer, because we don't have a context menu to deal with!
@ -821,12 +853,27 @@ void HomeScreenState::startDelegateAppDrawerDrag(qreal startX, qreal startY, QSt
setSwipeState(SwipeState::DraggingDelegate);
}
void HomeScreenState::startDelegateFolderDrag(qreal startX, qreal startY, FolioApplicationFolder *folder, int position)
void HomeScreenState::startDelegateFolderDrag(qreal startX,
qreal startY,
qreal pointerOffsetX,
qreal pointerOffsetY,
FolioApplicationFolder *folder,
int position)
{
startDelegateDrag(startX, startY);
startDelegateDrag(startX, startY, pointerOffsetX, pointerOffsetY);
Q_EMIT delegateDragFromFolderStarted(folder, position);
}
void HomeScreenState::startDelegateWidgetListDrag(qreal startX, qreal startY, qreal pointerOffsetX, qreal pointerOffsetY, QString appletPluginId)
{
startDelegateDrag(startX, startY, pointerOffsetX, pointerOffsetY);
Q_EMIT delegateDragFromWidgetListStarted(appletPluginId);
// we start dragging the delegate immediately from the app drawer, because we don't have a context menu to deal with!
// NOTE: this has to happen after delegateDragFromAppDrawerStarted, because slots for that expect SwipeState::AwaitingDraggingDelegate
setSwipeState(SwipeState::DraggingDelegate);
}
void HomeScreenState::cancelDelegateDrag()
{
swipeEnded();

View file

@ -228,12 +228,22 @@ public:
qreal searchWidgetY();
void setSearchWidgetY(qreal searchWidgetY);
// the top left x-position of the delegate being dragged
qreal delegateDragX();
void setDelegateDragX(qreal delegateDragX);
// the top left y-position of the delegate being dragged
qreal delegateDragY();
void setDelegateDragY(qreal delegateDragY);
// the offset from delegateDragX where the mouse/finger is
qreal delegateDragPointerOffsetX();
void setDelegateDragPointerOffsetX(qreal delegateDragPointerOffsetX);
// the offset from delegateDragY where the mouse/finger is
qreal delegateDragPointerOffsetY();
void setDelegateDragPointerOffsetY(qreal delegateDragPointerOffsetY);
int currentPage();
void setCurrentPage(int currentPage);
@ -247,6 +257,9 @@ public:
Q_INVOKABLE QPointF getFavouritesDelegateScreenPosition(int position);
Q_INVOKABLE QPointF getFolderDelegateScreenPosition(int position);
Plasma::Containment *containment();
void setContainment(Plasma::Containment *containment);
Q_SIGNALS:
void swipeStateChanged();
void viewStateChanged();
@ -290,6 +303,7 @@ Q_SIGNALS:
void delegateDragFromFavouritesStarted(int position);
void delegateDragFromAppDrawerStarted(QString storageId);
void delegateDragFromFolderStarted(FolioApplicationFolder *folder, int position);
void delegateDragFromWidgetListStarted(QString appletPluginId);
void pageNumChanged();
void folderPageNumChanged();
@ -314,10 +328,11 @@ public Q_SLOTS:
void openSettingsView();
void closeSettingsView();
void startDelegatePageDrag(qreal startX, qreal startY, int page, int row, int column);
void startDelegateFavouritesDrag(qreal startX, qreal startY, int position);
void startDelegateAppDrawerDrag(qreal startX, qreal startY, QString storageId);
void startDelegateFolderDrag(qreal startX, qreal startY, FolioApplicationFolder *folder, int position);
void startDelegatePageDrag(qreal startX, qreal startY, qreal pointerOffsetX, qreal pointerOffsetY, int page, int row, int column);
void startDelegateFavouritesDrag(qreal startX, qreal startY, qreal pointerOffsetX, qreal pointerOffsetY, int position);
void startDelegateAppDrawerDrag(qreal startX, qreal startY, qreal pointerOffsetX, qreal pointerOffsetY, QString storageId);
void startDelegateFolderDrag(qreal startX, qreal startY, qreal pointerOffsetX, qreal pointerOffsetY, FolioApplicationFolder *folder, int position);
void startDelegateWidgetListDrag(qreal startX, qreal startY, qreal pointerOffsetX, qreal pointerOffsetY, QString appletPluginId);
void cancelDelegateDrag();
// from SwipeArea
@ -329,7 +344,7 @@ private:
void setViewState(ViewState viewState);
void setSwipeState(SwipeState swipeState);
void startDelegateDrag(qreal startX, qreal startY);
void startDelegateDrag(qreal startX, qreal startY, qreal pointerOffsetX, qreal pointerOffsetY);
void cancelAppDrawerAnimations();
void cancelSearchWidgetAnimations();
@ -380,6 +395,8 @@ private:
qreal m_searchWidgetY{0};
qreal m_delegateDragX{0};
qreal m_delegateDragY{0};
qreal m_delegateDragPointerOffsetX{0};
qreal m_delegateDragPointerOffsetY{0};
int m_pageNum{0};
int m_folderPageNum{0};
@ -397,4 +414,6 @@ private:
QPropertyAnimation *m_folderPageAnim{nullptr};
QPropertyAnimation *m_openSettingsAnim{nullptr};
QPropertyAnimation *m_closeSettingsAnim{nullptr};
Plasma::Containment *m_containment{nullptr};
};

View file

@ -6,12 +6,13 @@ import QtQuick.Layouts
import QtQuick.Controls as Controls
import Qt5Compat.GraphicalEffects
import org.kde.plasma.plasmoid 2.0
import org.kde.plasma.components 3.0 as PC3
import org.kde.kirigami 2.10 as Kirigami
import org.kde.kirigami as Kirigami
import org.kde.plasma.private.mobileshell as MobileShell
import 'private'
Item {
id: root
@ -58,26 +59,13 @@ Item {
anchors.right: parent.right
anchors.bottom: parent.bottom
opacity: 0
headerHeight: root.headerHeight
}
// opacity gradient at grid edges
OpacityMask {
FlickableOpacityGradient {
anchors.fill: appDrawerGrid
source: appDrawerGrid
maskSource: Rectangle {
id: mask
width: appDrawerGrid.width
height: appDrawerGrid.height
property real gradientPct: (Kirigami.Units.gridUnit * 2) / appDrawerGrid.height
gradient: Gradient {
GradientStop { position: 0.0; color: appDrawerGrid.atYBeginning ? 'white' : 'transparent' }
GradientStop { position: mask.gradientPct; color: 'white' }
GradientStop { position: 1.0 - mask.gradientPct; color: 'white' }
GradientStop { position: 1.0; color: appDrawerGrid.atYEnd ? 'white' : 'transparent' }
}
}
flickable: appDrawerGrid
}
}
}

View file

@ -22,6 +22,7 @@ MobileShell.GridView {
layer.enabled: true
property var homeScreen
property real headerHeight
readonly property int reservedSpaceForLabel: Folio.HomeScreenState.pageDelegateLabelHeight
readonly property real effectiveContentWidth: width - leftMargin - rightMargin
@ -30,7 +31,7 @@ MobileShell.GridView {
leftMargin: horizontalMargin
rightMargin: horizontalMargin
cellWidth: effectiveContentWidth / Math.min(Math.floor(effectiveContentWidth / (Folio.FolioSettings.delegateIconSize + Kirigami.Units.largeSpacing * 3)), 8)
cellWidth: effectiveContentWidth / Math.min(Math.floor(effectiveContentWidth / (Folio.FolioSettings.delegateIconSize + Kirigami.Units.largeSpacing * 3.5)), 8)
cellHeight: cellWidth + reservedSpaceForLabel
boundsBehavior: Flickable.StopAtBounds
@ -79,11 +80,18 @@ MobileShell.GridView {
height: root.cellHeight
onPressAndHold: {
const mappedCoords = root.homeScreen.prepareStartDelegateDrag(model.delegate, delegate.delegateItem);
Folio.HomeScreenState.closeAppDrawer();
let mappedCoords = root.homeScreen.prepareStartDelegateDrag(model.delegate, delegate.delegateItem);
// we need to adjust because app drawer delegates have a different size than regular homescreen delegates
const centerX = mappedCoords.x + root.cellWidth / 2;
const centerY = mappedCoords.y + root.cellHeight / 2;
Folio.HomeScreenState.startDelegateAppDrawerDrag(
mappedCoords.x,
mappedCoords.y,
centerX - Folio.HomeScreenState.pageCellWidth / 2,
centerY - Folio.HomeScreenState.pageCellHeight / 2,
delegate.pressPosition.x,
delegate.pressPosition.y,
model.delegate.application.storageId
);
}

View file

@ -18,15 +18,18 @@ Item {
readonly property real dropAnimationRunning: dragXAnim.running || dragYAnim.running
// ignore widget dragging, that is not handled by this component
readonly property bool isWidgetDrag: Folio.HomeScreenState.dragState.dropDelegate && Folio.HomeScreenState.dragState.dropDelegate.type === Folio.FolioDelegate.Widget
visible: false
x: Math.round(Folio.HomeScreenState.delegateDragX)
y: Math.round(Folio.HomeScreenState.delegateDragY)
x: Folio.HomeScreenState.delegateDragX
y: Folio.HomeScreenState.delegateDragY
function setXBinding() {
x = Qt.binding(() => Math.round(Folio.HomeScreenState.delegateDragX));
x = Qt.binding(() => Folio.HomeScreenState.delegateDragX);
}
function setYBinding() {
y = Qt.binding(() => Math.round(Folio.HomeScreenState.delegateDragY));
y = Qt.binding(() => Folio.HomeScreenState.delegateDragY);
}
// animate drop x
@ -70,7 +73,7 @@ Item {
// reset and show drag item
function onSwipeStateChanged() {
if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate && !isWidgetDrag) {
root.scale = 1.0;
root.visible = true;
}
@ -78,6 +81,10 @@ Item {
// save the existing delegate at the spot (this is called before the delegate is dropped)
function onDelegateDragEnded() {
if (root.isWidgetDrag) {
return;
}
let dragState = Folio.HomeScreenState.dragState;
let dropPosition = dragState.candidateDropPosition;
@ -89,7 +96,7 @@ Item {
stateWatcher.delegateDroppedOn = Folio.HomeScreenState.getFavouritesDelegateAt(dropPosition.favouritesPosition);
break;
case Folio.DelegateDragPosition.Folder:
stateWatcher.delegateDroppedOn = null
stateWatcher.delegateDroppedOn = null;
break;
}
}
@ -100,6 +107,10 @@ Item {
// animate from when the delegate is dropped to its drop position
function onDelegateDroppedAndPlaced() {
if (root.isWidgetDrag) {
return;
}
let dragState = Folio.HomeScreenState.dragState;
let dropPosition = dragState.candidateDropPosition;
@ -130,6 +141,11 @@ Item {
scaleAnim.restart();
}
}
// if the drop has been abandoned, just hide
function onNewDelegateDropAbandoned() {
root.visible = false;
}
}
// simulate an icon delegate

View file

@ -107,6 +107,8 @@ MouseArea {
Folio.HomeScreenState.startDelegateFavouritesDrag(
mappedCoords.x,
mappedCoords.y,
appDelegate.pressPosition.x,
appDelegate.pressPosition.y,
delegate.index
);
@ -178,6 +180,8 @@ MouseArea {
Folio.HomeScreenState.startDelegateFavouritesDrag(
mappedCoords.x,
mappedCoords.y,
appFolderDelegate.pressPosition.x,
appFolderDelegate.pressPosition.y,
delegate.index
);

View file

@ -229,6 +229,8 @@ Folio.DelegateTouchArea {
Folio.HomeScreenState.startDelegateFolderDrag(
mappedCoords.x,
mappedCoords.y,
appDelegate.pressPosition.x,
appDelegate.pressPosition.y,
root.folder,
delegate.index
);

View file

@ -29,8 +29,13 @@ Item {
property Folio.HomeScreenState homeScreenState: Folio.HomeScreenState
readonly property bool dropAnimationRunning: delegateDragItem.dropAnimationRunning
// non-widget drop animation
readonly property bool dropAnimationRunning: delegateDragItem.dropAnimationRunning || widgetDragItem.dropAnimationRunning
// widget that is currently being dragged (or dropped)
readonly property Folio.FolioWidget currentlyDraggedWidget: widgetDragItem.widget
// how much to scale out in the settings mode
readonly property real settingsModeHomeScreenScale: 0.8
onTopMarginChanged: Folio.HomeScreenState.viewTopPadding = root.topMargin
@ -43,7 +48,9 @@ Item {
function prepareStartDelegateDrag(delegate, item) {
swipeArea.setSkipSwipeThreshold(true);
delegateDragItem.delegate = delegate;
if (delegate) {
delegateDragItem.delegate = delegate;
}
return root.mapFromItem(item, 0, 0);
}
@ -82,12 +89,18 @@ Item {
onHeightChanged: Folio.HomeScreenState.viewHeight = height;
}
// a way of stopping focus
FocusScope {
id: noFocus
}
// area that can be swiped
MobileShell.SwipeArea {
id: swipeArea
anchors.fill: parent
interactive: root.interactive &&
settings.homeScreenInteractive &&
!appDrawer.flickable.moving &&
(appDrawer.flickable.atYBeginning || // disable the swipe area when we are swiping in the app drawer, and not in drag-and-drop
Folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate ||
@ -104,13 +117,25 @@ Item {
homeScreenState.swipeMoved(totalDeltaX, totalDeltaY, deltaX, deltaY);
}
onPressedChanged: {
if (pressed) {
// ensures that components like the widget settings overlay close when swiping
noFocus.forceActiveFocus();
}
}
SettingsComponent {
id: settings
anchors.fill: parent
width: parent.width
height: parent.height
opacity: Folio.HomeScreenState.settingsOpenProgress
visible: opacity > 0
z: 1
// move the settings out of the way if it is not visible
// NOTE: we do this instead of setting visible to false, because
// it doesn't mess with widget drag and drop
y: (opacity > 0) ? 0 : parent.height
settingsModeHomeScreenScale: root.settingsModeHomeScreenScale
homeScreen: root
@ -333,6 +358,11 @@ Item {
id: delegateDragItem
}
// drag and drop for widgets
WidgetDragItem {
id: widgetDragItem
}
// bottom app drawer
AppDrawer {
id: appDrawer

View file

@ -4,13 +4,16 @@
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Effects
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.private.mobileshell.state as MobileShellState
import org.kde.plasma.private.mobileshell as MobileShell
import org.kde.private.mobile.homescreen.folio 1.0 as Folio
import org.kde.kirigami 2.10 as Kirigami
import org.kde.kirigami as Kirigami
import "./delegate"
import "./private"
Item {
id: root
@ -36,17 +39,48 @@ Item {
height: Folio.HomeScreenState.pageCellHeight
property var dropPosition: Folio.HomeScreenState.dragState.candidateDropPosition
property var dropDelegate: Folio.HomeScreenState.dragState.dropDelegate
property bool dropDelegateIsWidget: dropDelegate && dropDelegate.type === Folio.FolioDelegate.Widget
// only show if it is an empty spot on this page
visible: Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate &&
dropPosition.location === Folio.DelegateDragPosition.Pages &&
dropPosition.page === root.pageNum &&
!dropDelegateIsWidget &&
Folio.HomeScreenState.getPageDelegateAt(root.pageNum, dropPosition.pageRow, dropPosition.pageColumn) === null
x: dropPosition.pageColumn * Folio.HomeScreenState.pageCellWidth
y: dropPosition.pageRow * Folio.HomeScreenState.pageCellHeight
}
// square that shows when a widget hovers over a spot to drop a delegate on
Rectangle {
id: widgetDragDropFeedback
width: (dropDelegateIsWidget ? dropDelegate.widget.gridWidth : 0) * Folio.HomeScreenState.pageCellWidth
height: (dropDelegateIsWidget ? dropDelegate.widget.gridHeight : 0) * Folio.HomeScreenState.pageCellHeight
property var dropPosition: Folio.HomeScreenState.dragState.candidateDropPosition
property var dropDelegate: Folio.HomeScreenState.dragState.dropDelegate
property bool dropDelegateIsWidget: dropDelegate && dropDelegate.type === Folio.FolioDelegate.Widget
// only show if the widget can be placed here
visible: Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate &&
dropPosition.location === Folio.DelegateDragPosition.Pages &&
dropPosition.page === root.pageNum &&
dropDelegateIsWidget &&
pageModel.canAddDelegate(dropPosition.pageRow, dropPosition.pageColumn, dropDelegate)
radius: Kirigami.Units.smallSpacing
color: Qt.rgba(255, 255, 255, 0.3)
x: dropPosition.pageColumn * Folio.HomeScreenState.pageCellWidth
y: dropPosition.pageRow * Folio.HomeScreenState.pageCellHeight
layer.enabled: true
layer.effect: DelegateShadow {}
}
// repeater of all delegates in the page
Repeater {
model: root.pageModel
@ -69,10 +103,10 @@ Item {
dragState.dropDelegate.type === Folio.FolioDelegate.Application &&
isDropPositionThis
implicitWidth: Folio.HomeScreenState.pageCellWidth
implicitHeight: Folio.HomeScreenState.pageCellHeight
width: Folio.HomeScreenState.pageCellWidth
height: Folio.HomeScreenState.pageCellHeight
implicitWidth: loader.item ? loader.item.implicitWidth : 0
implicitHeight: loader.item ? loader.item.implicitHeight : 0
width: loader.item ? loader.item.width : 0
height: loader.item ? loader.item.height : 0
x: column * Folio.HomeScreenState.pageCellWidth
y: row * Folio.HomeScreenState.pageCellHeight
@ -81,13 +115,17 @@ Item {
column >= 0 && column < Folio.HomeScreenState.pageColumns
Loader {
anchors.fill: parent
id: loader
anchors.top: parent.top
anchors.left: parent.left
sourceComponent: {
if (delegate.pageDelegate.type === Folio.FolioDelegate.Application) {
return appComponent;
} else if (delegate.pageDelegate.type === Folio.FolioDelegate.Folder) {
return folderComponent;
} else if (delegate.pageDelegate.type === Folio.FolioDelegate.Widget) {
return widgetComponent;
} else {
return noneComponent;
}
@ -110,6 +148,11 @@ Item {
turnToFolder: delegate.isAppHoveredOver
turnToFolderAnimEnabled: Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate
implicitWidth: Folio.HomeScreenState.pageCellWidth
implicitHeight: Folio.HomeScreenState.pageCellHeight
width: Folio.HomeScreenState.pageCellWidth
height: Folio.HomeScreenState.pageCellHeight
// do not show if the drop animation is running to this delegate
visible: !(root.homeScreen.dropAnimationRunning && delegate.isDropPositionThis)
@ -121,6 +164,8 @@ Item {
Folio.HomeScreenState.startDelegatePageDrag(
mappedCoords.x,
mappedCoords.y,
appDelegate.pressPosition.x,
appDelegate.pressPosition.y,
root.pageNum,
delegate.pageDelegate.row,
delegate.pageDelegate.column
@ -173,6 +218,11 @@ Item {
name: Folio.FolioSettings.showPagesAppLabels ? delegate.pageDelegate.folder.name : ""
folder: delegate.pageDelegate.folder
implicitWidth: Folio.HomeScreenState.pageCellWidth
implicitHeight: Folio.HomeScreenState.pageCellHeight
width: Folio.HomeScreenState.pageCellWidth
height: Folio.HomeScreenState.pageCellHeight
// do not show if the drop animation is running to this delegate, and the drop delegate is a folder
visible: !(root.homeScreen.dropAnimationRunning &&
delegate.isDropPositionThis &&
@ -193,6 +243,8 @@ Item {
Folio.HomeScreenState.startDelegatePageDrag(
mappedCoords.x,
mappedCoords.y,
appFolderDelegate.pressPosition.x,
appFolderDelegate.pressPosition.y,
root.pageNum,
delegate.pageDelegate.row,
delegate.pageDelegate.column
@ -237,6 +289,88 @@ Item {
}
}
}
Component {
id: widgetComponent
WidgetDelegate {
id: widgetDelegate
// don't reparent applet if the drop animation is running to this delegate
// background: there is only one "visual" instance of the widget, once this delegate loads
// it will reparent it to here (but we don't want it to happen while the drop animation is running)
property bool suppressAppletReparent: (root.homeScreen.currentlyDraggedWidget === delegate.pageDelegate.widget)
&& delegate.isDropPositionThis
visible: !suppressAppletReparent
widget: suppressAppletReparent ? null : delegate.pageDelegate.widget
onStartEditMode: (pressPoint) => {
let mappedCoords = root.homeScreen.prepareStartDelegateDrag(delegate.pageDelegate, widgetDelegate);
Folio.HomeScreenState.startDelegatePageDrag(
mappedCoords.x,
mappedCoords.y,
pressPoint.x - mappedCoords.x,
pressPoint.y - mappedCoords.y,
root.pageNum,
delegate.pageDelegate.row,
delegate.pageDelegate.column
);
widgetConfig.startOpen();
}
onPressReleased: {
// cancel the event if the delegate is not dragged
if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.AwaitingDraggingDelegate) {
Folio.HomeScreenState.cancelDelegateDrag();
widgetConfig.fullyOpen();
}
}
layer.enabled: widgetDelegate.editMode
layer.effect: DarkenEffect {}
PC3.ToolTip {
visible: widgetDelegate.editMode && pressed
text: i18n('Release to configure, drag to move')
}
WidgetDelegateConfig {
id: widgetConfig
homeScreen: root.homeScreen
pageModel: root.pageModel
pageDelegate: delegate.pageDelegate
widget: delegate.pageDelegate.widget
pageNum: root.pageNum
row: delegate.row
column: delegate.column
widgetWidth: widgetDelegate.widgetWidth
widgetHeight: widgetDelegate.widgetHeight
widgetX: delegate.x + root.anchors.leftMargin + root.homeScreen.leftMargin
widgetY: delegate.y + root.anchors.topMargin + root.homeScreen.topMargin
topWidgetBackgroundPadding: widgetDelegate.topWidgetBackgroundPadding
bottomWidgetBackgroundPadding: widgetDelegate.bottomWidgetBackgroundPadding
leftWidgetBackgroundPadding: widgetDelegate.leftWidgetBackgroundPadding
rightWidgetBackgroundPadding: widgetDelegate.rightWidgetBackgroundPadding
anchors.fill: parent
onRemoveRequested: {
if (widget.applet) {
widget.destroyApplet();
}
root.pageModel.removeDelegate(delegate.row, delegate.column);
}
onClosed: widgetDelegate.editMode = false
}
}
}
}
}
}

View file

@ -0,0 +1,123 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
import org.kde.ksvg 1.0 as KSvg
import org.kde.plasma.components 3.0 as PC3
import org.kde.private.mobile.homescreen.folio 1.0 as Folio
import './delegate'
import './private'
Item {
id: root
width: widgetLoader.item ? widgetLoader.item.width : 0
height: widgetLoader.item ? widgetLoader.item.height : 0
property Folio.FolioWidget widget
readonly property bool isWidgetDelegate: Folio.HomeScreenState.dragState.dropDelegate && Folio.HomeScreenState.dragState.dropDelegate.type === Folio.FolioDelegate.Widget
readonly property bool dropAnimationRunning: dragXAnim.running || dragYAnim.running
visible: false
x: Math.round(Folio.HomeScreenState.delegateDragX)
y: Math.round(Folio.HomeScreenState.delegateDragY)
function startDrag(widget) {
root.widget = widget;
visible = true;
}
function setXBinding() {
x = Qt.binding(() => Math.round(Folio.HomeScreenState.delegateDragX));
}
function setYBinding() {
y = Qt.binding(() => Math.round(Folio.HomeScreenState.delegateDragY));
}
// animate drop x
XAnimator on x {
id: dragXAnim
running: false
duration: Kirigami.Units.longDuration
easing.type: Easing.OutCubic
onFinished: {
root.visible = false;
root.widget = null;
root.setXBinding();
}
}
// animate drop y
YAnimator on y {
id: dragYAnim
running: false
duration: Kirigami.Units.longDuration
easing.type: Easing.OutCubic
onFinished: {
root.visible = false;
root.widget = null;
root.setYBinding();
}
}
Connections {
id: stateWatcher
target: Folio.HomeScreenState
function onSwipeStateChanged() {
if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate &&
Folio.HomeScreenState.dragState.dropDelegate &&
Folio.HomeScreenState.dragState.dropDelegate.type === Folio.FolioDelegate.Widget) {
root.startDrag(Folio.HomeScreenState.dragState.dropDelegate.widget);
}
}
}
Connections {
target: Folio.HomeScreenState.dragState
// animate from when the delegate is dropped to its drop position
function onDelegateDroppedAndPlaced() {
if (!root.isWidgetDelegate) {
return;
}
let dragState = Folio.HomeScreenState.dragState;
let dropPosition = dragState.candidateDropPosition;
let pos = Folio.HomeScreenState.getPageDelegateScreenPosition(dropPosition.page, dropPosition.pageRow, dropPosition.pageColumn);
dragXAnim.to = pos.x;
dragYAnim.to = pos.y;
dragXAnim.restart();
dragYAnim.restart();
}
// if the drop has been abandoned, just hide
function onNewDelegateDropAbandoned() {
root.visible = false;
}
}
Loader {
id: widgetLoader
active: root.widget
sourceComponent: WidgetDelegate {
widget: root.widget
layer.enabled: true
layer.effect: DarkenEffect {}
}
}
}

View file

@ -9,7 +9,6 @@ import QtQuick.Effects
import org.kde.kirigami 2.20 as Kirigami
import org.kde.plasma.components 3.0 as PlasmaComponents
import org.kde.kquickcontrolsaddons 2.0
import org.kde.private.mobile.homescreen.folio 1.0 as Folio

View file

@ -0,0 +1,142 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
import org.kde.ksvg 1.0 as KSvg
import org.kde.plasma.components 3.0 as PC3
import org.kde.private.mobile.homescreen.folio 1.0 as Folio
import '../private'
Folio.WidgetContainer {
id: root
property Folio.FolioWidget widget
readonly property real widgetWidth: widgetHolder.width
readonly property real widgetHeight: widgetHolder.height
readonly property real topWidgetBackgroundPadding: widgetBackground.margins.top
readonly property real bottomWidgetBackgroundPadding: widgetBackground.margins.bottom
readonly property real leftWidgetBackgroundPadding: widgetBackground.margins.left
readonly property real rightWidgetBackgroundPadding: widgetBackground.margins.right
implicitWidth: (widget ? widget.gridWidth : 0) * Folio.HomeScreenState.pageCellWidth
implicitHeight: (widget ? widget.gridHeight : 0) * Folio.HomeScreenState.pageCellHeight
width: implicitWidth
height: implicitHeight
// prevent widget contents from going outside of the container
clip: true
function updateVisualApplet() {
if (!widget || !widget.visualApplet) {
return;
}
widget.visualApplet.expanded = true;
widget.visualApplet.parent = widgetHolder;
widget.visualApplet.anchors.fill = widgetHolder;
if (widget.visualApplet.fullRepresentationItem) {
widget.visualApplet.fullRepresentationItem.parent = widgetHolder;
widget.visualApplet.fullRepresentationItem.anchors.fill = widgetHolder;
}
}
onWidgetChanged: updateVisualApplet()
Component.onCompleted: {
updateVisualApplet();
}
Connections {
target: widget
function onVisualAppletChanged() {
if (!widget.visualApplet) {
return;
}
root.updateVisualApplet();
}
}
Item {
id: widgetComponent
anchors.fill: parent
KSvg.FrameSvgItem {
id: widgetBackground
anchors.fill: parent
enabledBorders: KSvg.FrameSvgItem.AllBorders
imagePath: {
if (!root.widget || !root.widget.applet || root.widget.applet.effectiveBackgroundHints === PlasmaCore.Types.NoBackground) {
return '';
} else if (root.widget.applet.effectiveBackgroundHints & PlasmaCore.Types.StandardBackground) {
return 'widgets/background';
} else if (root.widget.applet.effectiveBackgroundHints & PlasmaCore.Types.TranslucentBackground) {
return 'widgets/translucentbackground';
}
return '';
}
}
Rectangle {
id: temporaryBackground
anchors.fill: parent
visible: root.widget && !root.widget.applet
color: Qt.rgba(255, 255, 255, 0.3)
radius: Kirigami.Units.smallSpacing
}
Item {
id: widgetHolder
anchors.fill: parent
anchors.leftMargin: (root.widget && root.widget.applet && root.widget.applet.constraintHints === PlasmaCore.Types.CanFillArea) ? 0 : widgetBackground.margins.left
anchors.rightMargin: (root.widget && root.widget.applet && root.widget.applet.constraintHints === PlasmaCore.Types.CanFillArea) ? 0 : widgetBackground.margins.right
anchors.topMargin: (root.widget && root.widget.applet && root.widget.applet.constraintHints === PlasmaCore.Types.CanFillArea) ? 0 : widgetBackground.margins.top
anchors.bottomMargin: (root.widget && root.widget.applet && root.widget.applet.constraintHints === PlasmaCore.Types.CanFillArea) ? 0 : widgetBackground.margins.bottom
}
// TODO implement blur behind, see plasma-workspace BasicAppletContainer for how to do this
layer.enabled: root.widget && root.widget.applet && root.widget.applet.effectiveBackgroundHints === PlasmaCore.Types.ShadowBackground
layer.effect: DelegateShadow {}
PC3.Label {
id: noWidget
visible: root.widget && !root.widget.visualApplet
color: 'white'
wrapMode: Text.Wrap
text: i18n('This widget was not found.')
horizontalAlignment: Text.AlignHCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
PC3.BusyIndicator {
id: loadingIndicator
anchors.centerIn: parent
visible: root.widget && root.widget.applet && root.widget.applet.busy
running: visible
}
PC3.Button {
id: configurationRequiredButton
anchors.centerIn: parent
text: i18n('Configure…')
icon.name: 'configure'
visible: root.widget && root.widget.applet && root.widget.applet.configurationRequired
onClicked: root.widget.applet.internalAction('configure').trigger();
}
}
}

View file

@ -0,0 +1,251 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami
import org.kde.plasma.core as PlasmaCore
import org.kde.ksvg 1.0 as KSvg
import org.kde.plasma.components 3.0 as PC3
import org.kde.private.mobile.homescreen.folio 1.0 as Folio
import '../private'
Item {
id: root
property var homeScreen
property int pageNum
property int row
property int column
property real widgetWidth
property real widgetHeight
property real widgetX
property real widgetY
property real topWidgetBackgroundPadding
property real bottomWidgetBackgroundPadding
property real leftWidgetBackgroundPadding
property real rightWidgetBackgroundPadding
property Folio.FolioPageDelegate pageDelegate
property Folio.FolioWidget widget
property var pageModel
signal removeRequested()
signal closed()
function startOpen() {
configOverlay.open();
}
function fullyOpen() {
configPopup.open();
configOverlay.close();
}
// HACK: this shows the config when we are in the "press to hold" state, prior to mouse release
// we can't just open the popup, because the potential drag-and-drop swipe would get lost
MouseArea {
id: configOverlay
parent: root.homeScreen
anchors.fill: parent
width: configPopup.width
height: configPopup.height
opacity: 0
visible: opacity > 0
// in case this gets stuck open over the homescreen, just close on tap
onClicked: close()
NumberAnimation on opacity { id: configOverlayOpacityAnim; duration: 200 }
function open() {
configOverlayOpacityAnim.to = 1;
configOverlayOpacityAnim.restart();
}
function animClose() {
if (opacity !== 0) {
configOverlayOpacityAnim.to = 0;
configOverlayOpacityAnim.restart();
}
}
function close() {
opacity = 0;
}
Connections {
target: Folio.HomeScreenState
// if we are starting drag-and-drop, close the menu immediately
function onSwipeStateChanged() {
if (Folio.HomeScreenState.swipeState === Folio.HomeScreenState.DraggingDelegate) {
configOverlay.animClose();
root.closed();
}
}
}
// the config overlay
FastBlur {
anchors.fill: parent
source: configPopup.contentItem
radius: 0
}
}
// this is the actual interactive popup for widget settings, only
// opened when the user releases their press (and doesn't drag)
QQC2.Popup {
id: configPopup
width: root.homeScreen.width
height: root.homeScreen.height
parent: root.homeScreen
onClosed: {
configOverlay.close(); // ensure overlay is closed
root.closed();
}
topPadding: 0
bottomPadding: 0
leftPadding: 0
rightPadding: 0
closePolicy: QQC2.Popup.CloseOnEscape | QQC2.Popup.CloseOnPressOutsideParent
readonly property real barWidth: Kirigami.Units.gridUnit * 3.5
readonly property real barSpacing: Kirigami.Units.largeSpacing
readonly property real minimumBarLength: Kirigami.Units.gridUnit * 8
background: Item {}
QQC2.Overlay.modal: Item {}
exit: Transition {
NumberAnimation { property: "opacity"; duration: 200; from: 1.0; to: 0.0 }
}
Connections {
target: Folio.HomeScreenState
// don't show config overlay if we have navigated to another page
function onCurrentPageChanged() {
if (configPopup.visible) {
configPopup.close();
}
}
}
contentItem: MouseArea {
id: configItem
onClicked: configPopup.close()
WidgetResizeHandleFrame {
id: resizeFrame
anchors.fill: parent
widgetWidth: root.widgetWidth
widgetHeight: root.widgetHeight
widgetX: root.widgetX + root.leftWidgetBackgroundPadding
widgetY: root.widgetY + root.topWidgetBackgroundPadding
widgetTopMargin: root.topWidgetBackgroundPadding
widgetBottomMargin: root.bottomWidgetBackgroundPadding
widgetLeftMargin: root.leftWidgetBackgroundPadding
widgetRightMargin: root.rightWidgetBackgroundPadding
widgetRow: root.row
widgetColumn: root.column
widgetGridWidth: root.widget.gridWidth
widgetGridHeight: root.widget.gridHeight
onWidgetChangeAfterDrag: (widgetRow, widgetColumn, widgetGridWidth, widgetGridHeight) => {
if (resizeFrame.lockDrag !== null) triggerWidgetChanges(widgetRow, widgetColumn, widgetGridWidth, widgetGridHeight);
}
function triggerWidgetChanges(widgetRow, widgetColumn, widgetGridWidth, widgetGridHeight) {
root.pageModel.moveAndResizeWidgetDelegate(
root.pageDelegate,
widgetRow,
widgetColumn,
widgetGridWidth,
widgetGridHeight
);
}
}
PC3.Button {
id: button
icon.name: 'settings-configure'
text: i18n('Options')
readonly property var handleContainer: resizeFrame.handleContainer
x: Math.round(handleContainer.x + (handleContainer.width / 2) - (width / 2))
y: Math.round(handleContainer.y + (handleContainer.height / 2) - (height / 2))
onClicked: contextMenuDialog.open()
}
Kirigami.Dialog {
id: contextMenuDialog
preferredWidth: Kirigami.Units.gridUnit * 20
padding: 0
title: i18n('Widget Options')
// close parent dialog too
onClosed: configPopup.close()
ColumnLayout {
id: column
spacing: 0
Repeater {
model: root.widget.applet ? [...root.widget.applet.contextualActions, configureAppletAction, removeDelegateAction] : [removeDelegateAction]
delegate: QQC2.ItemDelegate {
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 2
action: modelData
text: modelData.text
icon.name: modelData.icon.name
icon.width: Kirigami.Units.gridUnit
icon.height: Kirigami.Units.gridUnit
leftPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
rightPadding: Kirigami.Units.largeSpacing + Kirigami.Units.smallSpacing
onClicked: contextMenuDialog.close()
}
}
}
}
}
}
Kirigami.Action {
id: removeDelegateAction
icon.name: 'edit-delete-remove'
text: i18n('Remove widget')
onTriggered: root.removeRequested()
}
Kirigami.Action {
id: configureAppletAction
icon.name: 'settings-configure'
text: i18n('Configure widget')
onTriggered: root.widget.applet.internalAction('configure').trigger();
}
}

View file

@ -0,0 +1,10 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Effects
MultiEffect {
colorization: 0.3
colorizationColor: 'black'
}

View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as Controls
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami
OpacityMask {
id: root
property var flickable
source: flickable
maskSource: Rectangle {
id: mask
width: flickable.width
height: flickable.height
property real gradientPct: (Kirigami.Units.gridUnit * 2) / flickable.height
gradient: Gradient {
GradientStop { position: 0.0; color: flickable.atYBeginning ? 'white' : 'transparent' }
GradientStop { position: mask.gradientPct; color: 'white' }
GradientStop { position: 1.0 - mask.gradientPct; color: 'white' }
GradientStop { position: 1.0; color: flickable.atYEnd ? 'white' : 'transparent' }
}
}
}

View file

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
QtObject {
enum Orientation {
Above,
Below,
Left,
Right
}
}

View file

@ -0,0 +1,13 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
QtObject {
enum Position {
TopCenter,
LeftCenter,
RightCenter,
BottomCenter
}
}

View file

@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import '../delegate'
MouseArea {
id: root
height: 10
width: 10
cursorShape: Qt.PointingHandCursor
property int orientation
signal dragEvent(real leftEdgeDelta, real rightEdgeDelta, real topEdgeDelta, real bottomEdgeDelta)
drag {
target: root
axis: {
switch (orientation) {
case WidgetHandlePosition.TopCenter:
return Drag.YAxis;
case WidgetHandlePosition.LeftCenter:
return Drag.XAxis;
case WidgetHandlePosition.RightCenter:
return Drag.XAxis;
case WidgetHandlePosition.BottomCenter:
return Drag.YAxis;
}
return Drag.XAndYAxis;
}
}
property real pressX
property real pressY
onPressed: {
pressX = mouseX;
pressY = mouseY;
}
onPositionChanged: {
// HACK: need to call it twice to work
updateDrag();
updateDrag();
}
drag { target: root; axis: Drag.XAndYAxis }
function updateDrag() {
if (!drag.active) return;
const dx = mouseX;
const dy = mouseY;
switch (orientation) {
case WidgetHandlePosition.TopCenter:
root.dragEvent(0, 0, -dy, 0);
break;
case WidgetHandlePosition.LeftCenter:
root.dragEvent(-dx, 0, 0, 0);
break;
case WidgetHandlePosition.RightCenter:
root.dragEvent(0, dx, 0, 0);
break;
case WidgetHandlePosition.BottomCenter:
root.dragEvent(0, 0, 0, dy);
break;
}
}
Rectangle {
anchors.fill: parent
color: 'white'
radius: width / 2
transform: Scale {
property real scaleFactor: root.pressed ? 1.2 : 1.0
Behavior on scaleFactor {
NumberAnimation { duration: 400; easing.type: Easing.OutExpo }
}
xScale: scaleFactor
yScale: scaleFactor
origin.x: root.width / 2
origin.y: root.height / 2
}
}
}

View file

@ -0,0 +1,238 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import Qt5Compat.GraphicalEffects
import org.kde.kirigami as Kirigami
import org.kde.private.mobile.homescreen.folio 1.0 as Folio
import '../delegate'
Item {
id: root
// given by parent:
property real widgetWidth
property real widgetHeight
property real widgetX
property real widgetY
property real widgetTopMargin
property real widgetBottomMargin
property real widgetLeftMargin
property real widgetRightMargin
property int widgetRow
property int widgetColumn
property int widgetGridWidth
property int widgetGridHeight
// filled here, given to parent:
// what the drag intends for the dimensions and position of the widget
property int widgetRowAfterDrag: 0
property int widgetColumnAfterDrag: 0
property int widgetGridWidthAfterDrag: 0
property int widgetGridHeightAfterDrag: 0
property var lockDrag: null
property alias handleContainer: handleContainer
signal widgetChangeAfterDrag(int widgetRow, int widgetColumn, int widgetGridWidth, int widgetGridHeight)
// solely used here:
property real startDragWidth: 0
property real startDragHeight: 0
property real startX: 0
property real startY: 0
property int startWidgetRow: 0
property int startWidgetColumn: 0
onWidgetWidthChanged: {
if (lockDrag === null) updateDimensions();
}
onWidgetHeightChanged: {
if (lockDrag === null) updateDimensions();
}
onWidgetXChanged: {
if (lockDrag === null) updateDimensions();
}
onWidgetYChanged: {
if (lockDrag === null) updateDimensions();
}
function updateDimensions() {
handleContainer.width = widgetWidth;
handleContainer.height = widgetHeight;
handleContainer.x = widgetX;
handleContainer.y = widgetY;
}
function startDrag() {
startDragWidth = handleContainer.width;
startDragHeight = handleContainer.height;
startX = handleContainer.x;
startY = handleContainer.y;
startWidgetRow = root.widgetRow;
startWidgetColumn = root.widgetColumn;
root.widgetChangeAfterDrag(startWidgetRow, startWidgetColumn, root.widgetGridWidth, root.widgetGridHeight);
}
function snapEdges() {
lockDrag = null;
// snaps the bounds to what we ended up at
widthAnim.to = widgetWidth;
widthAnim.restart();
heightAnim.to = widgetHeight;
heightAnim.restart();
xAnim.to = widgetX;
xAnim.restart();
yAnim.to = widgetY;
yAnim.restart();
}
function pressedHandler(orientation) {
if (root.lockDrag !== orientation) {
root.startDrag();
root.lockDrag = orientation;
}
}
function dragHandler(orientation, leftEdgeDelta, rightEdgeDelta, topEdgeDelta, bottomEdgeDelta) {
if (root.lockDrag === orientation) {
// update the handle container dimensions and position
handleContainer.x = root.startX - leftEdgeDelta;
handleContainer.y = root.startY - topEdgeDelta;
handleContainer.width = root.startDragWidth + rightEdgeDelta + leftEdgeDelta;
handleContainer.height = root.startDragHeight + bottomEdgeDelta + topEdgeDelta;
// update the widget dimensions and position
const columnsMovedRight = Math.round((handleContainer.x - root.startX) / Folio.HomeScreenState.pageCellWidth);
const rowsMovedDown = Math.round((handleContainer.y - root.startY) / Folio.HomeScreenState.pageCellHeight);
const realWidgetWidth = handleContainer.width + widgetLeftMargin + widgetRightMargin;
const realWidgetHeight = handleContainer.height + widgetTopMargin + widgetBottomMargin;
const widgetRowAfterDrag = startWidgetRow + rowsMovedDown;
const widgetColumnAfterDrag = startWidgetColumn + columnsMovedRight;
const widgetGridWidthAfterDrag = Math.round(realWidgetWidth / Folio.HomeScreenState.pageCellWidth);
const widgetGridHeightAfterDrag = Math.round(realWidgetHeight / Folio.HomeScreenState.pageCellHeight);
root.widgetChangeAfterDrag(widgetRowAfterDrag, widgetColumnAfterDrag, widgetGridWidthAfterDrag, widgetGridHeightAfterDrag);
}
}
function releaseHandler(orientation) {
if (root.lockDrag === orientation) {
root.snapEdges();
}
}
Item {
id: handleContainer
NumberAnimation on width {
id: widthAnim
duration: 200
easing.type: Easing.InOutQuad
}
NumberAnimation on height {
id: heightAnim
duration: 200
easing.type: Easing.InOutQuad
}
NumberAnimation on x {
id: xAnim
duration: 200
easing.type: Easing.InOutQuad
}
NumberAnimation on y {
id: yAnim
duration: 200
easing.type: Easing.InOutQuad
}
}
Rectangle {
id: resizeOutline
color: 'transparent'
border.color: 'white'
radius: Kirigami.Units.smallSpacing
border.width: 1
anchors.fill: handleContainer
anchors.leftMargin: -root.widgetLeftMargin
anchors.rightMargin: -root.widgetRightMargin
anchors.topMargin: -root.widgetTopMargin
anchors.bottomMargin: -root.widgetBottomMargin
}
WidgetResizeHandle {
id: topHandle
orientation: WidgetHandlePosition.TopCenter
x: resizeOutline.x + Math.round(resizeOutline.width / 2) - Math.round(width / 2)
y: resizeOutline.y - Math.round(height / 2)
width: Math.round(Math.max(height, resizeOutline.width * 0.3))
onPressed: pressedHandler(orientation)
onDragEvent: (leftEdgeDelta, rightEdgeDelta, topEdgeDelta, bottomEdgeDelta) => dragHandler(orientation, leftEdgeDelta, rightEdgeDelta, topEdgeDelta, bottomEdgeDelta)
onReleased: releaseHandler(orientation)
}
WidgetResizeHandle {
id: leftHandle
orientation: WidgetHandlePosition.LeftCenter
x: resizeOutline.x - (width / 2)
y: resizeOutline.y + (resizeOutline.height / 2) - (height / 2)
height: Math.round(Math.max(width, resizeOutline.height * 0.3))
onPressed: pressedHandler(orientation)
onDragEvent: (leftEdgeDelta, rightEdgeDelta, topEdgeDelta, bottomEdgeDelta) => dragHandler(orientation, leftEdgeDelta, rightEdgeDelta, topEdgeDelta, bottomEdgeDelta)
onReleased: releaseHandler(orientation)
}
WidgetResizeHandle {
id: rightHandle
orientation: WidgetHandlePosition.RightCenter
x: resizeOutline.x + resizeOutline.width - (width / 2)
y: resizeOutline.y + (resizeOutline.height / 2) - (height / 2)
height: Math.round(Math.max(width, resizeOutline.height * 0.3))
onPressed: pressedHandler(orientation)
onDragEvent: (leftEdgeDelta, rightEdgeDelta, topEdgeDelta, bottomEdgeDelta) => dragHandler(orientation, leftEdgeDelta, rightEdgeDelta, topEdgeDelta, bottomEdgeDelta)
onReleased: releaseHandler(orientation)
}
WidgetResizeHandle {
id: bottomHandle
orientation: WidgetHandlePosition.BottomCenter
x: resizeOutline.x + (resizeOutline.width / 2) - (width / 2)
y: resizeOutline.y + resizeOutline.height - (height / 2)
width: Math.round(Math.max(height, resizeOutline.width * 0.3))
onPressed: pressedHandler(orientation)
onDragEvent: (leftEdgeDelta, rightEdgeDelta, topEdgeDelta, bottomEdgeDelta) => dragHandler(orientation, leftEdgeDelta, rightEdgeDelta, topEdgeDelta, bottomEdgeDelta)
onReleased: releaseHandler(orientation)
}
}

View file

@ -0,0 +1,185 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
import QtQuick
import QtQuick.Window
import QtQuick.Layouts
import QtQuick.Dialogs
import QtQuick.Controls as QQC2
import org.kde.kirigami as Kirigami
import org.kde.plasma.plasmoid
import org.kde.plasma.private.shell 2.0
import org.kde.private.mobile.homescreen.folio 1.0 as Folio
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.plasma.components 3.0 as PC3
import '../delegate'
import '../private'
MouseArea {
id: root
property var homeScreen
signal requestClose()
onClicked: root.requestClose()
Kirigami.Theme.inherit: false
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Rectangle {
anchors.fill: parent
color: Qt.rgba(0, 0, 0, 0.7)
}
PC3.Label {
id: heading
color: 'white'
text: i18n("Widgets")
font.weight: Font.Bold
font.pointSize: Kirigami.Theme.defaultFont.pointSize * 1.5
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: Kirigami.Units.gridUnit * 3 + root.homeScreen.topMargin
}
GridView {
id: gridView
clip: true
reuseItems: true
opacity: 0 // we display with the opacity gradient below
anchors.top: heading.bottom
anchors.topMargin: Kirigami.Units.gridUnit
anchors.left: parent.left
anchors.leftMargin: root.homeScreen.leftMargin
anchors.right: parent.right
anchors.rightMargin: root.homeScreen.rightMargin
anchors.bottom: parent.bottom
anchors.bottomMargin: root.homeScreen.bottomMargin
model: widgetExplorer.widgetsModel
readonly property real maxCellWidth: Kirigami.Units.gridUnit * 20
readonly property real intendedCellWidth: Kirigami.Units.gridUnit * 8
readonly property int columns: Math.min(5, (width - leftMargin - rightMargin) / intendedCellWidth)
cellWidth: (width - leftMargin - rightMargin) / columns
cellHeight: cellWidth + Kirigami.Units.gridUnit * 3
readonly property real horizontalMargin: Math.round(width * 0.05)
leftMargin: horizontalMargin
rightMargin: horizontalMargin
MouseArea {
z: -1
anchors.fill: parent
onClicked: root.requestClose()
}
delegate: MouseArea {
id: delegate
width: gridView.cellWidth
height: gridView.cellHeight
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
readonly property string pluginName: model.pluginName
onPressAndHold: {
root.requestClose();
Folio.HomeScreenState.closeSettingsView();
let mappedCoords = root.homeScreen.prepareStartDelegateDrag(null, delegate);
const widthOffset = Folio.HomeScreenState.pageCellWidth / 2;
const heightOffset = Folio.HomeScreenState.pageCellHeight / 2;
Folio.HomeScreenState.startDelegateWidgetListDrag(
mappedCoords.x + mouseX - widthOffset,
mappedCoords.y + mouseY - heightOffset,
widthOffset,
heightOffset,
pluginName
);
}
Rectangle {
id: background
color: Qt.rgba(255, 255, 255, 0.3)
visible: delegate.containsMouse
radius: Kirigami.Units.smallSpacing
anchors.fill: parent
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Kirigami.Units.largeSpacing
Item {
id: iconWidget
Layout.fillWidth: true
Layout.maximumWidth: delegate.width
Layout.preferredHeight: Kirigami.Units.iconSizes.large
Layout.preferredWidth: Kirigami.Units.iconSizes.large
Layout.alignment: Qt.AlignBottom
Kirigami.Icon {
anchors.centerIn: parent
source: model.decoration
visible: model.screenshot == ""
implicitWidth: Kirigami.Units.iconSizes.large
implicitHeight: Kirigami.Units.iconSizes.large
}
Image {
anchors.centerIn: parent
fillMode: Image.PreserveAspectFit
source: model.screenshot
width: Kirigami.Units.iconSizes.large
height: Kirigami.Units.iconSizes.large
}
}
PC3.Label {
id: heading
Layout.fillWidth: true
Layout.maximumWidth: delegate.width
Layout.alignment: Qt.AlignCenter
text: model.name
elide: Text.ElideRight
wrapMode: Text.Wrap
maximumLineCount: 2
horizontalAlignment: Text.AlignHCenter
font.weight: Font.Bold
}
PC3.Label {
Layout.fillWidth: true
Layout.maximumWidth: delegate.width
Layout.alignment: Qt.AlignTop
// otherwise causes binding loop due to the way the Plasma sets the height
height: implicitHeight
text: model.description
font.pointSize: Kirigami.Theme.smallFont.pointSize
wrapMode: Text.Wrap
elide: Text.ElideRight
maximumLineCount: heading.lineCount === 1 ? 3 : 2
horizontalAlignment: Text.AlignHCenter
}
}
}
}
// opacity gradient at grid edges
FlickableOpacityGradient {
anchors.fill: gridView
flickable: gridView
}
WidgetExplorer {
id: widgetExplorer
containment: Plasmoid
}
}

View file

@ -20,6 +20,8 @@ Item {
property var homeScreen
property real settingsModeHomeScreenScale
readonly property bool homeScreenInteractive: !appletListViewer.open
signal requestLeaveSettingsMode()
MouseArea {
@ -80,17 +82,42 @@ Item {
PC3.ToolButton {
text: 'Widgets'
enabled: false
display: PC3.ToolButton.TextUnderIcon
icon.name: 'widget-alternatives'
implicitHeight: Kirigami.Units.gridUnit * 4
implicitWidth: Kirigami.Units.gridUnit * 5
onClicked: {
appletListViewer.open = true;
}
}
}
}
AppletListViewer {
id: appletListViewer
width: parent.width
height: parent.height
property bool open: false
onRequestClose: open = false
opacity: open ? 1 : 0
// move the settings out of the way if it is not visible
// NOTE: we do this instead of setting visible to false, because
// it doesn't mess with widget drag and drop
y: (opacity === 0) ? appletListViewer.height : 0
homeScreen: root.homeScreen
Behavior on opacity {
NumberAnimation { duration: Kirigami.Units.shortDuration }
}
}
SettingsWindow {
id: settingsWindow
visible: false

View file

@ -105,24 +105,24 @@ QJsonArray PageListModel::exportToJson()
void PageListModel::save()
{
if (!m_applet) {
if (!m_containment) {
return;
}
QJsonArray arr = exportToJson();
QByteArray data = QJsonDocument(arr).toJson(QJsonDocument::Compact);
m_applet->config().writeEntry("Pages", QString::fromStdString(data.toStdString()));
Q_EMIT m_applet->configNeedsSaving();
m_containment->config().writeEntry("Pages", QString::fromStdString(data.toStdString()));
Q_EMIT m_containment->configNeedsSaving();
}
void PageListModel::load()
{
if (!m_applet) {
if (!m_containment) {
return;
}
QJsonDocument doc = QJsonDocument::fromJson(m_applet->config().readEntry("Pages", "{}").toUtf8());
QJsonDocument doc = QJsonDocument::fromJson(m_containment->config().readEntry("Pages", "{}").toUtf8());
loadFromJson(doc.array());
}
@ -152,7 +152,7 @@ void PageListModel::loadFromJson(QJsonArray arr)
}
}
void PageListModel::setApplet(Plasma::Applet *applet)
void PageListModel::setContainment(Plasma::Containment *containment)
{
m_applet = applet;
m_containment = containment;
}

View file

@ -8,7 +8,7 @@
#include <QAbstractListModel>
#include <QList>
#include <Plasma/Applet>
#include <Plasma/Containment>
class PageListModel : public QAbstractListModel
{
@ -38,7 +38,7 @@ public:
Q_INVOKABLE void load();
void loadFromJson(QJsonArray arr);
void setApplet(Plasma::Applet *applet);
void setContainment(Plasma::Containment *containment);
Q_SIGNALS:
void lengthChanged();
@ -46,5 +46,5 @@ Q_SIGNALS:
private:
QList<PageModel *> m_pages;
Plasma::Applet *m_applet{nullptr};
Plasma::Containment *m_containment{nullptr};
};

View file

@ -4,6 +4,7 @@
#include "pagemodel.h"
#include "foliosettings.h"
#include "homescreenstate.h"
#include "widgetsmanager.h"
FolioPageDelegate::FolioPageDelegate(int row, int column, QObject *parent)
: FolioDelegate{parent}
@ -29,6 +30,14 @@ FolioPageDelegate::FolioPageDelegate(int row, int column, FolioApplicationFolder
init();
}
FolioPageDelegate::FolioPageDelegate(int row, int column, FolioWidget *widget, QObject *parent)
: FolioDelegate{widget, parent}
, m_row{row}
, m_column{column}
{
init();
}
FolioPageDelegate::FolioPageDelegate(int row, int column, FolioDelegate *delegate, QObject *parent)
: FolioDelegate{parent}
, m_row{row}
@ -37,6 +46,7 @@ FolioPageDelegate::FolioPageDelegate(int row, int column, FolioDelegate *delegat
m_type = delegate->type();
m_application = delegate->application();
m_folder = delegate->folder();
m_widget = delegate->widget();
init();
}
@ -52,20 +62,46 @@ void FolioPageDelegate::init()
case HomeScreenState::RotateClockwise:
m_realRow = HomeScreenState::self()->pageColumns() - m_column - 1;
m_realColumn = m_row;
if (m_widget) {
// since top-left in cw is bottom-left in portrait
m_realRow -= m_widget->realGridHeight() - 1;
}
break;
case HomeScreenState::RotateCounterClockwise: // (0, 4) -> (4, 3)
case HomeScreenState::RotateCounterClockwise:
m_realRow = m_column;
m_realColumn = HomeScreenState::self()->pageRows() - m_row - 1;
if (m_widget) {
// since top-left in ccw is top-right in portrait
m_realColumn -= m_widget->realGridWidth() - 1;
}
break;
case HomeScreenState::RotateUpsideDown:
m_realRow = HomeScreenState::self()->pageRows() - m_row - 1;
m_realColumn = HomeScreenState::self()->pageColumns() - m_column - 1;
if (m_widget) {
// since top-left in upside-down is bottom-right in portrait
m_realRow -= m_widget->realGridHeight() - 1;
m_realColumn -= m_widget->realGridWidth() - 1;
}
break;
}
if (m_widget) {
connect(m_widget, &FolioWidget::realTopLeftPositionChanged, this, [this](int rowOffset, int columnOffset) {
m_realRow += rowOffset;
m_realColumn += columnOffset;
});
}
connect(HomeScreenState::self(), &HomeScreenState::pageOrientationChanged, this, [this]() {
setRow(getTranslatedRow(m_realRow, m_realColumn));
setColumn(getTranslatedColumn(m_realRow, m_realColumn));
setRowOnly(getTranslatedTopLeftRow(m_realRow, m_realColumn, this));
setColumnOnly(getTranslatedTopLeftColumn(m_realRow, m_realColumn, this));
});
}
@ -80,8 +116,8 @@ FolioPageDelegate *FolioPageDelegate::fromJson(QJsonObject &obj, QObject *parent
int realRow = obj[QStringLiteral("row")].toInt();
int realColumn = obj[QStringLiteral("column")].toInt();
int row = getTranslatedRow(realRow, realColumn);
int column = getTranslatedColumn(realRow, realColumn);
int row = getTranslatedTopLeftRow(realRow, realColumn, fd);
int column = getTranslatedTopLeftColumn(realRow, realColumn, fd);
FolioPageDelegate *delegate = new FolioPageDelegate{row, column, fd, parent};
fd->deleteLater();
@ -89,6 +125,32 @@ FolioPageDelegate *FolioPageDelegate::fromJson(QJsonObject &obj, QObject *parent
return delegate;
}
int FolioPageDelegate::getTranslatedTopLeftRow(int realRow, int realColumn, FolioDelegate *fd)
{
int row = getTranslatedRow(realRow, realColumn);
int column = getTranslatedColumn(realRow, realColumn);
// special logic to return "top left" for widgets, since they take more than one tile
if (fd->type() == FolioDelegate::Widget) {
return fd->widget()->topLeftCorner(row, column).row;
} else {
return row;
}
}
int FolioPageDelegate::getTranslatedTopLeftColumn(int realRow, int realColumn, FolioDelegate *fd)
{
int row = getTranslatedRow(realRow, realColumn);
int column = getTranslatedColumn(realRow, realColumn);
// special logic to return "top left" for widgets, since they take more than one tile
if (fd->type() == FolioDelegate::Widget) {
return fd->widget()->topLeftCorner(row, column).column;
} else {
return column;
}
}
int FolioPageDelegate::getTranslatedRow(int realRow, int realColumn)
{
// we have to use the "real" rows and columns, so fetch them from FolioSettings instead of HomeScreenState
@ -136,8 +198,33 @@ int FolioPageDelegate::row()
void FolioPageDelegate::setRow(int row)
{
m_row = row;
Q_EMIT rowChanged();
if (m_row != row) {
// adjust stored data too
switch (HomeScreenState::self()->pageOrientation()) {
case HomeScreenState::RegularPosition:
m_realRow = row;
break;
case HomeScreenState::RotateClockwise:
m_realColumn += row - m_row;
break;
case HomeScreenState::RotateCounterClockwise:
m_realColumn += m_row - row;
break;
case HomeScreenState::RotateUpsideDown:
m_realRow += m_row - row;
break;
}
setRowOnly(row);
}
}
void FolioPageDelegate::setRowOnly(int row)
{
if (m_row != row) {
m_row = row;
Q_EMIT rowChanged();
}
}
int FolioPageDelegate::column()
@ -147,14 +234,51 @@ int FolioPageDelegate::column()
void FolioPageDelegate::setColumn(int column)
{
m_column = column;
Q_EMIT columnChanged();
if (m_column != column) {
// adjust stored data too
switch (HomeScreenState::self()->pageOrientation()) {
case HomeScreenState::RegularPosition:
m_realColumn = column;
break;
case HomeScreenState::RotateClockwise:
m_realRow += m_column - column;
break;
case HomeScreenState::RotateCounterClockwise:
m_realRow += column - m_column;
break;
case HomeScreenState::RotateUpsideDown:
m_realColumn += m_column - column;
break;
}
setColumnOnly(column);
}
}
void FolioPageDelegate::setColumnOnly(int column)
{
if (m_column != column) {
m_column = column;
Q_EMIT columnChanged();
}
}
PageModel::PageModel(QList<FolioPageDelegate *> delegates, QObject *parent)
: QAbstractListModel{parent}
, m_delegates{delegates}
{
connect(WidgetsManager::self(), &WidgetsManager::widgetRemoved, this, [this](Plasma::Applet *applet) {
if (applet) {
// delete any instance of this widget
for (int i = 0; i < m_delegates.size(); i++) {
auto *delegate = m_delegates[i];
if (delegate->type() == FolioDelegate::Widget && delegate->widget()->applet() == applet) {
removeDelegate(i);
break;
}
}
}
});
}
PageModel::~PageModel() = default;
@ -162,7 +286,6 @@ PageModel::~PageModel() = default;
PageModel *PageModel::fromJson(QJsonArray &arr, QObject *parent)
{
QList<FolioPageDelegate *> delegates;
QList<FolioPageDelegate *> folderDelegates;
for (QJsonValueRef r : arr) {
QJsonObject obj = r.toObject();
@ -170,18 +293,14 @@ PageModel *PageModel::fromJson(QJsonArray &arr, QObject *parent)
FolioPageDelegate *delegate = FolioPageDelegate::fromJson(obj, parent);
if (delegate) {
delegates.append(delegate);
if (delegate->type() == FolioDelegate::Folder) {
folderDelegates.append(delegate);
}
}
}
PageModel *model = new PageModel{delegates, parent};
// ensure folders request saves
for (auto *delegate : folderDelegates) {
connect(delegate->folder(), &FolioApplicationFolder::saveRequested, model, &PageModel::save);
// ensure delegates can request saves
for (auto *delegate : delegates) {
model->connectSaveRequests(delegate);
}
return model;
@ -229,42 +348,87 @@ QHash<int, QByteArray> PageModel::roleNames() const
void PageModel::removeDelegate(int row, int col)
{
bool removed = false;
for (int i = 0; i < m_delegates.size(); ++i) {
if (m_delegates[i]->row() == row && m_delegates[i]->column() == col) {
beginRemoveRows(QModelIndex(), i, i);
// HACK: do not deleteLater(), because the delegate might still be used somewhere else
m_delegates.removeAt(i);
endRemoveRows();
removeDelegate(i);
break;
}
}
}
removed = true;
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)
{
if (row < 0 || row >= HomeScreenState::self()->pageRows() || column < 0 || column >= HomeScreenState::self()->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::self()->pageRows()) || (maxRow < 0 || maxRow >= HomeScreenState::self()->pageRows())
|| (column < 0 || column >= HomeScreenState::self()->pageColumns()) || (maxColumn < 0 || maxColumn >= HomeScreenState::self()->pageColumns())) {
return false;
}
// check if any delegate exists at any of the spots where the widget is being added
for (FolioPageDelegate *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 *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;
}
}
}
if (removed) {
save();
}
return true;
}
bool PageModel::addDelegate(FolioPageDelegate *delegate)
{
if (delegate->row() < 0 || delegate->row() >= HomeScreenState::self()->pageRows() || delegate->column() < 0
|| delegate->column() >= HomeScreenState::self()->pageColumns()) {
if (!canAddDelegate(delegate->row(), delegate->column(), delegate)) {
return false;
}
// check if there already exists a delegate in this space
for (FolioPageDelegate *d : m_delegates) {
if (d->row() == delegate->row() && d->column() == delegate->column()) {
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;
@ -276,15 +440,69 @@ FolioPageDelegate *PageModel::getDelegate(int row, int col)
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 *testWidget = new FolioWidget(this, 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 *testDelegate = new FolioDelegate(testWidget, this);
// NOT THREAD SAFE!
// which is fine, because the GUI isn't multithreaded
int index = m_delegates.indexOf(delegate);
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);
m_delegates.insert(index, delegate); // add it back
// cleanup test delegate
testDelegate->deleteLater();
testWidget->deleteLater();
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 *delegate)
{
if (delegate->type() == FolioDelegate::Folder && delegate->folder()) {
connect(delegate->folder(), &FolioApplicationFolder::saveRequested, this, &PageModel::save);
} else if (delegate->type() == FolioDelegate::Widget && delegate->widget()) {
connect(delegate->widget(), &FolioWidget::saveRequested, this, &PageModel::save);
}
}
void PageModel::save()
{
Q_EMIT saveRequested();

View file

@ -23,9 +23,12 @@ public:
FolioPageDelegate(int row = 0, int column = 0, QObject *parent = nullptr);
FolioPageDelegate(int row, int column, FolioApplication *application, QObject *parent);
FolioPageDelegate(int row, int column, FolioApplicationFolder *folder, QObject *parent);
FolioPageDelegate(int row, int column, FolioWidget *widget, QObject *parent);
FolioPageDelegate(int row, int column, FolioDelegate *delegate, QObject *parent);
static FolioPageDelegate *fromJson(QJsonObject &obj, QObject *parent);
static int getTranslatedTopLeftRow(int realRow, int realColumn, FolioDelegate *fd);
static int getTranslatedTopLeftColumn(int realRow, int realColumn, FolioDelegate *fd);
static int getTranslatedRow(int realRow, int realColumn);
static int getTranslatedColumn(int realRow, int realColumn);
@ -42,6 +45,8 @@ Q_SIGNALS:
void columnChanged();
private:
void setRowOnly(int row);
void setColumnOnly(int column);
void init();
int m_realRow;
@ -73,9 +78,13 @@ public:
QHash<int, QByteArray> roleNames() const override;
Q_INVOKABLE void removeDelegate(int row, int col);
Q_INVOKABLE void removeDelegate(int index);
Q_INVOKABLE bool canAddDelegate(int row, int column, FolioDelegate *delegate);
bool addDelegate(FolioPageDelegate *delegate);
FolioPageDelegate *getDelegate(int row, int col);
Q_INVOKABLE void moveAndResizeWidgetDelegate(FolioPageDelegate *delegate, int newRow, int newColumn, int newGridWidth, int newGridHeight);
bool isPageEmpty();
public Q_SLOTS:
@ -85,5 +94,6 @@ Q_SIGNALS:
void saveRequested();
private:
void connectSaveRequests(FolioDelegate *delegate);
QList<FolioPageDelegate *> m_delegates;
};

View file

@ -0,0 +1,131 @@
// SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#include "widgetcontainer.h"
#include <QCursor>
#include <QGuiApplication>
#include <QStyleHints>
WidgetContainer::WidgetContainer(QQuickItem *parent)
: QQuickItem(parent)
, m_pressAndHoldTimer{new QTimer{this}}
{
m_pressAndHoldTimer->setInterval(QGuiApplication::styleHints()->mousePressAndHoldInterval());
m_pressAndHoldTimer->setSingleShot(true);
connect(m_pressAndHoldTimer, &QTimer::timeout, this, &WidgetContainer::startPressAndHold);
setFiltersChildMouseEvents(true);
setFlags(QQuickItem::ItemIsFocusScope);
setActiveFocusOnTab(true);
setAcceptedMouseButtons(Qt::LeftButton);
connect(this, &WidgetContainer::activeFocusChanged, this, &WidgetContainer::onActiveFocusChanged);
}
bool WidgetContainer::editMode() const
{
return m_editMode;
}
void WidgetContainer::setEditMode(bool editMode)
{
if (m_editMode != editMode) {
m_editMode = editMode;
if (m_editMode) {
setZ(1);
if (m_pressed) {
// sendUngrabRecursive(m_contentItem);
QMouseEvent ev(QEvent::MouseButtonPress, mapFromScene(m_mouseDownPosition), m_mouseDownPosition, QPointF(), Qt::LeftButton, {}, {});
ev.setExclusiveGrabber(ev.point(0), this);
QCoreApplication::sendEvent(this, &ev);
}
} else {
setZ(0);
}
Q_EMIT editModeChanged();
}
}
bool WidgetContainer::childMouseEventFilter(QQuickItem *item, QEvent *event)
{
switch (event->type()) {
case QEvent::MouseButtonPress: {
QMouseEvent *me = static_cast<QMouseEvent *>(event);
if (me->buttons() & Qt::LeftButton) {
mousePressEvent(me);
}
break;
}
case QEvent::MouseMove: {
QMouseEvent *me = static_cast<QMouseEvent *>(event);
mouseMoveEvent(me);
break;
}
case QEvent::MouseButtonRelease: {
QMouseEvent *me = static_cast<QMouseEvent *>(event);
mouseReleaseEvent(me);
break;
}
case QEvent::UngrabMouse:
mouseUngrabEvent();
break;
default:
break;
}
return QQuickItem::childMouseEventFilter(item, event);
}
void WidgetContainer::mousePressEvent(QMouseEvent *event)
{
forceActiveFocus(Qt::MouseFocusReason);
m_pressed = true;
m_pressAndHoldTimer->start();
m_mouseDownPosition = event->scenePosition();
event->accept();
}
void WidgetContainer::mouseMoveEvent(QMouseEvent *event)
{
if (!m_editMode && QPointF(event->scenePosition() - m_mouseDownPosition).manhattanLength() >= QGuiApplication::styleHints()->startDragDistance()) {
m_pressAndHoldTimer->stop();
}
QQuickItem::mouseMoveEvent(event);
}
void WidgetContainer::mouseReleaseEvent(QMouseEvent *event)
{
Q_EMIT pressReleased();
m_pressAndHoldTimer->stop();
m_pressed = false;
event->accept();
}
void WidgetContainer::mouseUngrabEvent()
{
m_pressAndHoldTimer->stop();
m_pressed = false;
}
void WidgetContainer::startPressAndHold()
{
setEditMode(true);
Q_EMIT startEditMode(m_mouseDownPosition);
}
void WidgetContainer::onActiveFocusChanged(bool activeFocus)
{
if (!activeFocus) {
setEditMode(false);
}
}

View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QMouseEvent>
#include <QQuickItem>
#include <QTimer>
class WidgetContainer : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(bool editMode READ editMode WRITE setEditMode NOTIFY editModeChanged)
QML_NAMED_ELEMENT(WidgetContainer)
public:
WidgetContainer(QQuickItem *parent = nullptr);
bool editMode() const;
void setEditMode(bool editMode);
Q_SIGNALS:
void editModeChanged();
void pressReleased();
void startEditMode(QPointF pressPoint);
protected:
bool childMouseEventFilter(QQuickItem *item, QEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
void mouseUngrabEvent() override;
private Q_SLOTS:
void startPressAndHold();
void onActiveFocusChanged(bool activeFocus);
private:
bool m_pressed{false};
bool m_editMode{false};
QTimer *m_pressAndHoldTimer{nullptr};
QPointF m_mouseDownPosition{};
};
QML_DECLARE_TYPE(WidgetContainer)

View file

@ -0,0 +1,42 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "widgetsmanager.h"
WidgetsManager::WidgetsManager(QObject *parent)
: QObject{parent}
{
}
WidgetsManager *WidgetsManager::self()
{
static WidgetsManager *manager = new WidgetsManager{nullptr};
return manager;
}
Plasma::Applet *WidgetsManager::getWidget(int id)
{
for (auto *widget : m_widgets) {
if (static_cast<int>(widget->id()) == id) {
return widget;
}
}
return nullptr;
}
void WidgetsManager::addWidget(Plasma::Applet *applet)
{
if (!m_widgets.contains(applet)) {
m_widgets.push_back(applet);
Q_EMIT widgetAdded(applet);
}
}
void WidgetsManager::removeWidget(Plasma::Applet *applet)
{
if (m_widgets.contains(applet)) {
m_widgets.remove(m_widgets.indexOf(applet));
Q_EMIT widgetRemoved(applet);
}
}

View file

@ -0,0 +1,31 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QObject>
#include <Plasma/Applet>
// keeps a list of all of instances of Plasma::Applet that are loaded into the containment
// allows for FolioWidgets to find their corresponding Plasma::Applet
class WidgetsManager : public QObject
{
Q_OBJECT
public:
WidgetsManager(QObject *parent = nullptr);
static WidgetsManager *self();
Plasma::Applet *getWidget(int id);
void addWidget(Plasma::Applet *applet);
void removeWidget(Plasma::Applet *applet);
Q_SIGNALS:
void widgetAdded(Plasma::Applet *applet);
void widgetRemoved(Plasma::Applet *applet);
private:
QList<Plasma::Applet *> m_widgets;
};