diff --git a/containments/homescreens/folio/CMakeLists.txt b/containments/homescreens/folio/CMakeLists.txt index 8bc66bc2..13a0af80 100644 --- a/containments/homescreens/folio/CMakeLists.txt +++ b/containments/homescreens/folio/CMakeLists.txt @@ -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}) diff --git a/containments/homescreens/folio/README.md b/containments/homescreens/folio/README.md index fc9684fd..ae34420c 100644 --- a/containments/homescreens/folio/README.md +++ b/containments/homescreens/folio/README.md @@ -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 diff --git a/containments/homescreens/folio/delegatetoucharea.cpp b/containments/homescreens/folio/delegatetoucharea.cpp index 02399830..ef194b0a 100644 --- a/containments/homescreens/folio/delegatetoucharea.cpp +++ b/containments/homescreens/folio/delegatetoucharea.cpp @@ -5,6 +5,8 @@ #include "delegatetoucharea.h" #include +#include +#include // 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(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(event) && !dynamic_cast(event) && !dynamic_cast(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); } } diff --git a/containments/homescreens/folio/delegatetoucharea.h b/containments/homescreens/folio/delegatetoucharea.h index c609b7a8..a4456361 100644 --- a/containments/homescreens/folio/delegatetoucharea.h +++ b/containments/homescreens/folio/delegatetoucharea.h @@ -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}; }; diff --git a/containments/homescreens/folio/dragstate.cpp b/containments/homescreens/folio/dragstate.cpp index 8e8cfaac..f5dc8f8f 100644 --- a/containments/homescreens/folio/dragstate.cpp +++ b/containments/homescreens/folio/dragstate.cpp @@ -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(); +} diff --git a/containments/homescreens/folio/dragstate.h b/containments/homescreens/folio/dragstate.h index b1c9381a..f63e3646 100644 --- a/containments/homescreens/folio/dragstate.h +++ b/containments/homescreens/folio/dragstate.h @@ -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}; }; diff --git a/containments/homescreens/folio/favouritesmodel.cpp b/containments/homescreens/folio/favouritesmodel.cpp index 259efe33..fb61d9df 100644 --- a/containments/homescreens/folio/favouritesmodel.cpp +++ b/containments/homescreens/folio/favouritesmodel.cpp @@ -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 diff --git a/containments/homescreens/folio/favouritesmodel.h b/containments/homescreens/folio/favouritesmodel.h index 11837cc1..e5a87f3d 100644 --- a/containments/homescreens/folio/favouritesmodel.h +++ b/containments/homescreens/folio/favouritesmodel.h @@ -11,7 +11,7 @@ #include #include -#include +#include #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 m_delegates; - Plasma::Applet *m_applet{nullptr}; + Plasma::Containment *m_containment{nullptr}; }; diff --git a/containments/homescreens/folio/folioapplication.cpp b/containments/homescreens/folio/folioapplication.cpp index e4741e08..66f7636c 100644 --- a/containments/homescreens/folio/folioapplication.cpp +++ b/containments/homescreens/folio/folioapplication.cpp @@ -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"; diff --git a/containments/homescreens/folio/folioapplication.h b/containments/homescreens/folio/folioapplication.h index e816c562..9892c600 100644 --- a/containments/homescreens/folio/folioapplication.h +++ b/containments/homescreens/folio/folioapplication.h @@ -31,7 +31,7 @@ public: FolioApplication(QObject *parent = nullptr, KService::Ptr service = QExplicitlySharedDataPointer{nullptr}); static FolioApplication *fromJson(QJsonObject &obj, QObject *parent); // may return nullptr - QJsonObject toJson(); + QJsonObject toJson() const; bool running() const; QString name() const; diff --git a/containments/homescreens/folio/folioapplicationfolder.cpp b/containments/homescreens/folio/folioapplicationfolder.cpp index f2ecfc2c..6156f451 100644 --- a/containments/homescreens/folio/folioapplicationfolder.cpp +++ b/containments/homescreens/folio/folioapplicationfolder.cpp @@ -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}); diff --git a/containments/homescreens/folio/folioapplicationfolder.h b/containments/homescreens/folio/folioapplicationfolder.h index bcbf16e0..7e3973d3 100644 --- a/containments/homescreens/folio/folioapplicationfolder.h +++ b/containments/homescreens/folio/folioapplicationfolder.h @@ -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); diff --git a/containments/homescreens/folio/foliodelegate.cpp b/containments/homescreens/folio/foliodelegate.cpp index e2eda4ed..354e5394 100644 --- a/containments/homescreens/folio/foliodelegate.cpp +++ b/containments/homescreens/folio/foliodelegate.cpp @@ -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; +} diff --git a/containments/homescreens/folio/foliodelegate.h b/containments/homescreens/folio/foliodelegate.h index 58a1b4cd..2bdf06b8 100644 --- a/containments/homescreens/folio/foliodelegate.h +++ b/containments/homescreens/folio/foliodelegate.h @@ -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}; }; diff --git a/containments/homescreens/folio/foliowidget.cpp b/containments/homescreens/folio/foliowidget.cpp new file mode 100644 index 00000000..dabf5d52 --- /dev/null +++ b/containments/homescreens/folio/foliowidget.cpp @@ -0,0 +1,272 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// 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(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(applet->id()) == m_id) { + setApplet(applet); + } + }); + connect(WidgetsManager::self(), &WidgetsManager::widgetRemoved, this, [this](Plasma::Applet *applet) { + if (applet && static_cast(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(); + } +} diff --git a/containments/homescreens/folio/foliowidget.h b/containments/homescreens/folio/foliowidget.h new file mode 100644 index 00000000..17ff4a77 --- /dev/null +++ b/containments/homescreens/folio/foliowidget.h @@ -0,0 +1,89 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include +#include + +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; +}; diff --git a/containments/homescreens/folio/homescreen.cpp b/containments/homescreens/folio/homescreen.cpp index 70515984..ee52d404 100644 --- a/containments/homescreens/folio/homescreen.cpp +++ b/containments/homescreens/folio/homescreen.cpp @@ -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 @@ -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(uri, 1, 0, "ApplicationListModel", [](QQmlEngine *, QJSEngine *) -> QObject * { return ApplicationListModel::self(); @@ -60,13 +64,18 @@ HomeScreen::HomeScreen(QObject *parent, const KPluginMetaData &data, const QVari qmlRegisterType(uri, 1, 0, "FolioApplication"); qmlRegisterType(uri, 1, 0, "FolioApplicationFolder"); + qmlRegisterType(uri, 1, 0, "FolioWidget"); qmlRegisterType(uri, 1, 0, "FolioDelegate"); qmlRegisterType(uri, 1, 0, "PageModel"); qmlRegisterType(uri, 1, 0, "FolioPageDelegate"); qmlRegisterType(uri, 1, 0, "DelegateTouchArea"); qmlRegisterType(uri, 1, 0, "DelegateDragPosition"); + qmlRegisterType(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" diff --git a/containments/homescreens/folio/homescreen.h b/containments/homescreens/folio/homescreen.h index d20a387c..a2d8b6ae 100644 --- a/containments/homescreens/folio/homescreen.h +++ b/containments/homescreens/folio/homescreen.h @@ -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); }; diff --git a/containments/homescreens/folio/homescreenstate.cpp b/containments/homescreens/folio/homescreenstate.cpp index 67539e3c..6be3ed24 100644 --- a/containments/homescreens/folio/homescreenstate.cpp +++ b/containments/homescreens/folio/homescreenstate.cpp @@ -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(); diff --git a/containments/homescreens/folio/homescreenstate.h b/containments/homescreens/folio/homescreenstate.h index 15d6007e..32165e19 100644 --- a/containments/homescreens/folio/homescreenstate.h +++ b/containments/homescreens/folio/homescreenstate.h @@ -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}; }; diff --git a/containments/homescreens/folio/package/contents/ui/AppDrawer.qml b/containments/homescreens/folio/package/contents/ui/AppDrawer.qml index 382b5587..7910d428 100644 --- a/containments/homescreens/folio/package/contents/ui/AppDrawer.qml +++ b/containments/homescreens/folio/package/contents/ui/AppDrawer.qml @@ -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 } } } diff --git a/containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml b/containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml index aa06f928..2c8d0caa 100644 --- a/containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml +++ b/containments/homescreens/folio/package/contents/ui/AppDrawerGrid.qml @@ -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 ); } diff --git a/containments/homescreens/folio/package/contents/ui/DelegateDragItem.qml b/containments/homescreens/folio/package/contents/ui/DelegateDragItem.qml index 6379c107..871b1337 100644 --- a/containments/homescreens/folio/package/contents/ui/DelegateDragItem.qml +++ b/containments/homescreens/folio/package/contents/ui/DelegateDragItem.qml @@ -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 diff --git a/containments/homescreens/folio/package/contents/ui/FavouritesBar.qml b/containments/homescreens/folio/package/contents/ui/FavouritesBar.qml index c175b36d..66dc9a9e 100644 --- a/containments/homescreens/folio/package/contents/ui/FavouritesBar.qml +++ b/containments/homescreens/folio/package/contents/ui/FavouritesBar.qml @@ -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 ); diff --git a/containments/homescreens/folio/package/contents/ui/FolderView.qml b/containments/homescreens/folio/package/contents/ui/FolderView.qml index d21e1583..4fcb1009 100644 --- a/containments/homescreens/folio/package/contents/ui/FolderView.qml +++ b/containments/homescreens/folio/package/contents/ui/FolderView.qml @@ -229,6 +229,8 @@ Folio.DelegateTouchArea { Folio.HomeScreenState.startDelegateFolderDrag( mappedCoords.x, mappedCoords.y, + appDelegate.pressPosition.x, + appDelegate.pressPosition.y, root.folder, delegate.index ); diff --git a/containments/homescreens/folio/package/contents/ui/HomeScreen.qml b/containments/homescreens/folio/package/contents/ui/HomeScreen.qml index ee110ffc..5914f24c 100644 --- a/containments/homescreens/folio/package/contents/ui/HomeScreen.qml +++ b/containments/homescreens/folio/package/contents/ui/HomeScreen.qml @@ -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 diff --git a/containments/homescreens/folio/package/contents/ui/HomeScreenPage.qml b/containments/homescreens/folio/package/contents/ui/HomeScreenPage.qml index 0c5c9de0..e90d4cee 100644 --- a/containments/homescreens/folio/package/contents/ui/HomeScreenPage.qml +++ b/containments/homescreens/folio/package/contents/ui/HomeScreenPage.qml @@ -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 + } + } + } } } } diff --git a/containments/homescreens/folio/package/contents/ui/WidgetDragItem.qml b/containments/homescreens/folio/package/contents/ui/WidgetDragItem.qml new file mode 100644 index 00000000..b12edd7d --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/WidgetDragItem.qml @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// 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 {} + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml b/containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml index 7accf78d..bf810887 100644 --- a/containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml +++ b/containments/homescreens/folio/package/contents/ui/delegate/AbstractDelegate.qml @@ -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 diff --git a/containments/homescreens/folio/package/contents/ui/delegate/WidgetDelegate.qml b/containments/homescreens/folio/package/contents/ui/delegate/WidgetDelegate.qml new file mode 100644 index 00000000..4bd97cee --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/delegate/WidgetDelegate.qml @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// 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(); + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/delegate/WidgetDelegateConfig.qml b/containments/homescreens/folio/package/contents/ui/delegate/WidgetDelegateConfig.qml new file mode 100644 index 00000000..2a6deeed --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/delegate/WidgetDelegateConfig.qml @@ -0,0 +1,251 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// 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(); + } +} diff --git a/containments/homescreens/folio/package/contents/ui/private/DarkenEffect.qml b/containments/homescreens/folio/package/contents/ui/private/DarkenEffect.qml new file mode 100644 index 00000000..0c700fd9 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/private/DarkenEffect.qml @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick +import QtQuick.Effects + +MultiEffect { + colorization: 0.3 + colorizationColor: 'black' +} \ No newline at end of file diff --git a/containments/homescreens/folio/package/contents/ui/private/FlickableOpacityGradient.qml b/containments/homescreens/folio/package/contents/ui/private/FlickableOpacityGradient.qml new file mode 100644 index 00000000..6755e0d4 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/private/FlickableOpacityGradient.qml @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// 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' } + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/private/Orientation.qml b/containments/homescreens/folio/package/contents/ui/private/Orientation.qml new file mode 100644 index 00000000..29070d55 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/private/Orientation.qml @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick + +QtObject { + enum Orientation { + Above, + Below, + Left, + Right + } +} diff --git a/containments/homescreens/folio/package/contents/ui/private/WidgetHandlePosition.qml b/containments/homescreens/folio/package/contents/ui/private/WidgetHandlePosition.qml new file mode 100644 index 00000000..bd72929b --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/private/WidgetHandlePosition.qml @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +import QtQuick + +QtObject { + enum Position { + TopCenter, + LeftCenter, + RightCenter, + BottomCenter + } +} diff --git a/containments/homescreens/folio/package/contents/ui/private/WidgetResizeHandle.qml b/containments/homescreens/folio/package/contents/ui/private/WidgetResizeHandle.qml new file mode 100644 index 00000000..1823eae0 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/private/WidgetResizeHandle.qml @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// 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 + } + } +} diff --git a/containments/homescreens/folio/package/contents/ui/private/WidgetResizeHandleFrame.qml b/containments/homescreens/folio/package/contents/ui/private/WidgetResizeHandleFrame.qml new file mode 100644 index 00000000..20e1a37d --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/private/WidgetResizeHandleFrame.qml @@ -0,0 +1,238 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// 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) + } +} diff --git a/containments/homescreens/folio/package/contents/ui/settings/AppletListViewer.qml b/containments/homescreens/folio/package/contents/ui/settings/AppletListViewer.qml new file mode 100644 index 00000000..85cdbc31 --- /dev/null +++ b/containments/homescreens/folio/package/contents/ui/settings/AppletListViewer.qml @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// 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 + } +} diff --git a/containments/homescreens/folio/package/contents/ui/settings/SettingsComponent.qml b/containments/homescreens/folio/package/contents/ui/settings/SettingsComponent.qml index 4084c10e..0908c316 100644 --- a/containments/homescreens/folio/package/contents/ui/settings/SettingsComponent.qml +++ b/containments/homescreens/folio/package/contents/ui/settings/SettingsComponent.qml @@ -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 diff --git a/containments/homescreens/folio/pagelistmodel.cpp b/containments/homescreens/folio/pagelistmodel.cpp index 1dd9575d..f99d65cb 100644 --- a/containments/homescreens/folio/pagelistmodel.cpp +++ b/containments/homescreens/folio/pagelistmodel.cpp @@ -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; } diff --git a/containments/homescreens/folio/pagelistmodel.h b/containments/homescreens/folio/pagelistmodel.h index bc78ed64..ae5c34b7 100644 --- a/containments/homescreens/folio/pagelistmodel.h +++ b/containments/homescreens/folio/pagelistmodel.h @@ -8,7 +8,7 @@ #include #include -#include +#include 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 m_pages; - Plasma::Applet *m_applet{nullptr}; + Plasma::Containment *m_containment{nullptr}; }; diff --git a/containments/homescreens/folio/pagemodel.cpp b/containments/homescreens/folio/pagemodel.cpp index 3269805f..b29f1a51 100644 --- a/containments/homescreens/folio/pagemodel.cpp +++ b/containments/homescreens/folio/pagemodel.cpp @@ -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 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 delegates; - QList 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 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(); diff --git a/containments/homescreens/folio/pagemodel.h b/containments/homescreens/folio/pagemodel.h index b9eda204..4987057c 100644 --- a/containments/homescreens/folio/pagemodel.h +++ b/containments/homescreens/folio/pagemodel.h @@ -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 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 m_delegates; }; diff --git a/containments/homescreens/folio/widgetcontainer.cpp b/containments/homescreens/folio/widgetcontainer.cpp new file mode 100644 index 00000000..523ccae6 --- /dev/null +++ b/containments/homescreens/folio/widgetcontainer.cpp @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2019 Marco Martin +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "widgetcontainer.h" + +#include +#include +#include + +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(event); + if (me->buttons() & Qt::LeftButton) { + mousePressEvent(me); + } + break; + } + case QEvent::MouseMove: { + QMouseEvent *me = static_cast(event); + mouseMoveEvent(me); + break; + } + case QEvent::MouseButtonRelease: { + QMouseEvent *me = static_cast(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); + } +} diff --git a/containments/homescreens/folio/widgetcontainer.h b/containments/homescreens/folio/widgetcontainer.h new file mode 100644 index 00000000..cee3ce85 --- /dev/null +++ b/containments/homescreens/folio/widgetcontainer.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include +#include +#include + +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) diff --git a/containments/homescreens/folio/widgetsmanager.cpp b/containments/homescreens/folio/widgetsmanager.cpp new file mode 100644 index 00000000..40cc421e --- /dev/null +++ b/containments/homescreens/folio/widgetsmanager.cpp @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// 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(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); + } +} diff --git a/containments/homescreens/folio/widgetsmanager.h b/containments/homescreens/folio/widgetsmanager.h new file mode 100644 index 00000000..91a93d00 --- /dev/null +++ b/containments/homescreens/folio/widgetsmanager.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +#include + +// 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 m_widgets; +};