diff --git a/main.cpp b/main.cpp index e119905e..760f3360 100644 --- a/main.cpp +++ b/main.cpp @@ -21,8 +21,10 @@ #include #include #include +#include #include #include +#include DCORE_USE_NAMESPACE DGUI_USE_NAMESPACE @@ -78,6 +80,7 @@ int main(int argc, char* argv[]) } qmlRegisterType("org.deepin.vendored", 1, 0, "KSortFilterProxyModel"); + qmlRegisterType("org.deepin.launchpad", 1, 0, "MultipageSortFilterProxyModel"); qmlRegisterUncreatableType("org.deepin.launchpad", 1, 0, "AppItem", "AppItem should only be created from C++ side"); qmlRegisterSingletonInstance("org.deepin.launchpad", 1, 0, "AppsModel", &AppsModel::instance()); qmlRegisterSingletonInstance("org.deepin.launchpad", 1, 0, "FavoritedProxyModel", &FavoritedProxyModel::instance()); @@ -95,6 +98,7 @@ int main(int argc, char* argv[]) QQuickStyle::setStyle("Chameleon"); engine.addImageProvider(QLatin1String("app-icon"), new LauncherAppIconProvider); + engine.addImageProvider(QLatin1String("folder-icon"), new LauncherFolderIconProvider); engine.addImageProvider(QLatin1String("blurhash"), new BlurhashImageProvider); QQmlContext * ctx = engine.rootContext(); diff --git a/qml/AppListView.qml b/qml/AppListView.qml index 1ab13e62..8df13a63 100644 --- a/qml/AppListView.qml +++ b/qml/AppListView.qml @@ -56,30 +56,7 @@ Item { if (CategorizedSortProxyModel.categoryType === CategorizedSortProxyModel.Alphabetary) { return section.toUpperCase(); } else { - switch (Number(section)) { - case AppItem.Internet: - return qsTr("Internet"); - case AppItem.Chat: - return qsTr("Chat"); - case AppItem.Music: - return qsTr("Music"); - case AppItem.Video: - return qsTr("Video"); - case AppItem.Graphics: - return qsTr("Graphics"); - case AppItem.Game: - return qsTr("Game"); - case AppItem.Office: - return qsTr("Office"); - case AppItem.Reading: - return qsTr("Reading"); - case AppItem.Development: - return qsTr("Development"); - case AppItem.System: - return qsTr("System"); - default: - return qsTr("Others"); - } + return getCategoryName(section) } } diff --git a/qml/FullscreenFrame.qml b/qml/FullscreenFrame.qml index a65892f0..c966575e 100644 --- a/qml/FullscreenFrame.qml +++ b/qml/FullscreenFrame.qml @@ -128,14 +128,13 @@ Control { sourceComponent: Rectangle { color: "transparent" - KSortFilterProxyModel { + property var grids: gridViewContainer + + MultipageSortFilterProxyModel { id: proxyModel sourceModel: MultipageProxyModel - filterRowCallback: (source_row, source_parent) => { - var index = sourceModel.index(source_row, 0, source_parent); - return sourceModel.data(index, MultipageProxyModel.PageRole) === modelData && - sourceModel.data(index, MultipageProxyModel.FolderIdNumberRole) === 0; - } + pageId: modelData + folderId: 0 } GridViewContainer { @@ -149,6 +148,12 @@ Control { focus: true activeGridViewFocusOnTab: gridViewLoader.SwipeView.isCurrentItem delegate: IconItemDelegate { + dndEnabled: false + Drag.mimeData: { + "application/x-dde-launcher-dnd-fullscreen": ("0," + modelData + "," + index), // "folder,page,index" + "application/x-dde-launcher-dnd-desktopId": model.desktopId + } + visible: model.desktopId !== dropArea.currentDraggedDesktopId iconSource: "image://app-icon/" + iconName width: gridViewContainer.cellSize height: gridViewContainer.cellSize @@ -162,7 +167,7 @@ Control { let idNum = Number(idStr.replace("internal/folders/", "")) folderLoader.currentFolderId = idNum folderGridViewPopup.open() - folderLoader.folderName = model.display + folderLoader.folderName = model.display.startsWith("internal/category/") ? getCategoryName(model.display.substring(18)) : model.display console.log("open folder id:" + idNum) } onMenuTriggered: { @@ -236,6 +241,47 @@ Control { } } + DropArea { + id: dropArea + anchors.fill: parent + + property string currentDraggedDesktopId: "" + + onPositionChanged: { + currentDraggedDesktopId = drag.getDataAsString("application/x-dde-launcher-dnd-desktopId") + dndDebug.text = drag.x + "," + drag.y + "." + drag.getDataAsString("application/x-dde-launcher-dnd-desktopId") + } + + onDropped: { + let curGridView = pages.currentItem.item.grids + let curPoint = curGridView.mapFromItem(dropArea, drag.x, drag.y) + let curItem = curGridView.itemAt(curPoint.x, curPoint.y) + if (curItem) { + // drop on the left, center or right? + let itemX = curGridView.mapFromItem(curItem.parent, curItem.x, curItem.y).x + let itemWidth = curItem.width + let sideOpPadding = itemWidth / 4 + let op = 0 + if (curPoint.x < (itemX + sideOpPadding)) { + op = -1 + } else if (curPoint.x > (itemX + curItem.width - sideOpPadding)) { + op = 1 + } + + let targetItemInfo = curItem.Drag.mimeData["application/x-dde-launcher-dnd-desktopId"] + dndDebug.text = "drag " + currentDraggedDesktopId + " onto " + targetItemInfo + " with " + op + MultipageProxyModel.commitDndOperation(currentDraggedDesktopId, targetItemInfo, op) + } + currentDraggedDesktopId = "" + } + + Label { + id: dndDebug + visible: DebugHelper.qtDebugEnabled + text: "DnD DEBUG" + } + } + Popup { id: folderGridViewPopup @@ -301,14 +347,11 @@ Control { anchors.fill: parent color: "transparent" - KSortFilterProxyModel { + MultipageSortFilterProxyModel { id: folderProxyModel sourceModel: MultipageProxyModel - filterRowCallback: (source_row, source_parent) => { - var index = sourceModel.index(source_row, 0, source_parent); - return sourceModel.data(index, MultipageProxyModel.PageRole) === modelData && - sourceModel.data(index, MultipageProxyModel.FolderIdNumberRole) === folderLoader.currentFolderId; - } + pageId: modelData + folderId: folderLoader.currentFolderId } GridViewContainer { diff --git a/qml/GridViewContainer.qml b/qml/GridViewContainer.qml index 484f910a..13bf255c 100644 --- a/qml/GridViewContainer.qml +++ b/qml/GridViewContainer.qml @@ -73,6 +73,12 @@ FocusScope { visible: gridView.activeFocus } } + + // working (on drag into folder): + displaced: Transition { NumberAnimation { properties: "x,y"; duration: 250 } } + // not wroking + move: Transition { NumberAnimation { properties: "x,y"; duration: 250 } } + moveDisplaced: Transition { NumberAnimation { properties: "x,y"; duration: 250 } } } } diff --git a/qml/IconItemDelegate.qml b/qml/IconItemDelegate.qml index 4c120f80..2c2b801f 100644 --- a/qml/IconItemDelegate.qml +++ b/qml/IconItemDelegate.qml @@ -19,6 +19,7 @@ Control { property int preferredIconSize: 48 property string iconSource + property bool dndEnabled: false Accessible.name: iconItemLabel.text @@ -26,6 +27,21 @@ Control { signal itemClicked() signal menuTriggered() + Drag.dragType: Drag.Automatic + Drag.active: dragHandler.active + + states: State { + name: "dragged"; + when: dragHandler.active + // FIXME: When dragging finished, the position of the item is changed for unknown reason, + // so we use the state to reset the x and y here. + PropertyChanges { + target: root + x: x + y: y + } + } + contentItem: ToolButton { focusPolicy: Qt.NoFocus contentItem: Column { @@ -48,14 +64,13 @@ Control { radius: width / 2 } - Rectangle { + Item { width: root.width / 2 height: root.height / 2 anchors.horizontalCenter: parent.horizontalCenter - radius: 8 - color: root.icons ? Qt.rgba(0, 0, 0, 0.5) : "transparent" Loader { + id: iconLoader anchors.fill: parent sourceComponent: root.icons !== undefined ? folderComponent : imageComponent } @@ -63,29 +78,11 @@ Control { Component { id: folderComponent - Grid { - id: folderGrid + Image { + id: iconImage anchors.fill: parent - rows: 2 - columns: 2 - spacing: 5 - padding: 5 - - Repeater { - model: icons - delegate: Rectangle { - visible: true - color: "transparent" - width: (folderGrid.width - (folderGrid.columns - 1) * folderGrid.spacing - folderGrid.padding * 2) / folderGrid.columns - height: (folderGrid.height - (folderGrid.rows - 1) * folderGrid.spacing - folderGrid.padding * 2) / folderGrid.rows - - Image { - anchors.fill: parent - source: "image://app-icon/" + modelData - sourceSize: Qt.size(parent.width, parent.height) - } - } - } + source: "image://folder-icon/" + icons.join(':') + sourceSize: Qt.size(parent.width, parent.height) } } @@ -104,7 +101,7 @@ Control { Label { id: iconItemLabel - text: display + text: display.startsWith("internal/category/") ? getCategoryName(display.substring(18)) : display textFormat: Text.PlainText width: parent.width horizontalAlignment: Text.AlignHCenter @@ -141,6 +138,21 @@ Control { id: stylus } + DragHandler { + id: dragHandler + enabled: root.dndEnabled + + onActiveChanged: { + if (active) { + // TODO: 1. this way we couldn't give it an image size hint, + // 2. also not able to set offset to drag image, so the cursor is always + // at the top-left of the image + // 3. we should also handle folder icon + parent.Drag.imageSource = icons ? ("image://folder-icon/" + icons.join(':')) : parent.iconSource + } + } + } + Keys.onSpacePressed: { root.itemClicked() } diff --git a/qml/Main.qml b/qml/Main.qml index 842e3a99..efa4c4d7 100644 --- a/qml/Main.qml +++ b/qml/Main.qml @@ -45,6 +45,33 @@ ApplicationWindow { } } + function getCategoryName(section) { + switch (Number(section)) { + case AppItem.Internet: + return qsTr("Internet"); + case AppItem.Chat: + return qsTr("Chat"); + case AppItem.Music: + return qsTr("Music"); + case AppItem.Video: + return qsTr("Video"); + case AppItem.Graphics: + return qsTr("Graphics"); + case AppItem.Game: + return qsTr("Game"); + case AppItem.Office: + return qsTr("Office"); + case AppItem.Reading: + return qsTr("Reading"); + case AppItem.Development: + return qsTr("Development"); + case AppItem.System: + return qsTr("System"); + default: + return qsTr("Others"); + } + } + function descaledRect(rect) { let ratio = Screen.devicePixelRatio return Qt.rect(rect.left / ratio, rect.top / ratio, rect.width / ratio, rect.height / ratio) diff --git a/src/models/CMakeLists.txt b/src/models/CMakeLists.txt index f1432662..a0bcc813 100644 --- a/src/models/CMakeLists.txt +++ b/src/models/CMakeLists.txt @@ -12,6 +12,7 @@ FILES categorizedsortproxymodel.h favoritedproxymodel.h multipageproxymodel.h + multipagesortfilterproxymodel.h ) target_sources(launcher-models @@ -23,6 +24,7 @@ PRIVATE favoritedproxymodel.cpp itemspage.cpp itemspage.h multipageproxymodel.cpp + multipagesortfilterproxymodel.cpp ) target_link_libraries(launcher-models PRIVATE diff --git a/src/models/multipageproxymodel.cpp b/src/models/multipageproxymodel.cpp index 934787ef..f2b05ee1 100644 --- a/src/models/multipageproxymodel.cpp +++ b/src/models/multipageproxymodel.cpp @@ -5,6 +5,7 @@ #include "multipageproxymodel.h" #include "appsmodel.h" +#include "categoryutils.h" #include #include @@ -27,6 +28,48 @@ int MultipageProxyModel::pageCount(int folderId) const return m_folders.value(fullId)->pageCount(); } +void MultipageProxyModel::commitDndOperation(const QString &dragId, const QString &dropId, const DndOperation op) +{ + if (dragId == dropId) return; + + std::tuple dragOrigPos = findItem(dragId); + std::tuple dropOrigPos = findItem(dropId); + + if (op != DndOperation::DndJoin) { + // move to dropId's front or back + // DnD can only happen in the same folder + Q_ASSERT(std::get<0>(dragOrigPos) == std::get<0>(dropOrigPos)); + ItemsPage * folder = folderById(std::get<0>(dropOrigPos)); + const int dragOrigPage = std::get<1>(dragOrigPos); + const int dropOrigPage = std::get<1>(dropOrigPos); + // FIXME: drop position not correct + folder->moveItem(dragOrigPage, std::get<2>(dragOrigPos), dropOrigPage, std::get<2>(dropOrigPos)); + } else { + if (dragId.startsWith("internal/folders/")) return; // cannot drag folder onto something + if (dropId.startsWith("internal/folders/")) { + // drop into existing folder + m_topLevel->removeItem(dragId); + m_folders.value(dropId)->appendItem(dragId); + } else { + // make a new folder, move two items into the folder + int folderCount = m_folders.count(); + QString folderNumStr(QString::number(folderCount + 1)); + ItemsPage * folder = createFolder(folderNumStr); + folder->appendPage({dragId, dropId}); + AppItem * dropItem = AppsModel::instance().itemFromDesktopId(dropId); + AppItem::DDECategories dropCategories = AppItem::DDECategories(CategoryUtils::parseBestMatchedCategory(dropItem->categories())); + folder->setName("internal/category/" + QString::number(dropCategories)); + m_topLevel->removeItem(dragId); + m_topLevel->removeItem(dropId); + m_topLevel->insertItem("internal/folders/" + folderNumStr, std::get<1>(dropOrigPos), std::get<2>(dropOrigPos)); + } + } + + saveItemArrangementToUserData(); + // Lazy solution, just notify the view that all rows and its roles are changed so they need to be updated. + emit dataChanged(index(0, 0, QModelIndex()), index(rowCount(QModelIndex()), 0, QModelIndex())); +} + QModelIndex MultipageProxyModel::index(int row, int column, const QModelIndex &parent) const { if (row >= sourceModel()->rowCount()) { @@ -224,7 +267,7 @@ void MultipageProxyModel::onSourceModelChanged() // add all existing ones if they are not already in int appsCount = sourceModel()->rowCount(); - for (int i = 1; i <= appsCount; i++) { + for (int i = 0; i < appsCount; i++) { QString desktopId(sourceModel()->data(sourceModel()->index(i, 0), AppItem::DesktopIdRole).toString()); int folder, page, idx; std::tie(folder, std::ignore, std::ignore) = findItem(desktopId); @@ -248,9 +291,18 @@ ItemsPage *MultipageProxyModel::createFolder(const QString &idNumber) // int insertTo = rowCount(QModelIndex()); // beginInsertRows(QModelIndex(), insertTo, insertTo); - + beginInsertRows(QModelIndex(), rowCount(QModelIndex()), rowCount(QModelIndex())); ItemsPage * page = new ItemsPage(4 * 3, this); m_folders.insert(fullId, page); + endInsertRows(); return page; } + +// get folder by id. 0 is top level, >=1 is folder +ItemsPage *MultipageProxyModel::folderById(int id) +{ + if (id == 0) return m_topLevel; + const QString folderId("internal/folders/" + QString::number(id)); + return m_folders.value(folderId); +} diff --git a/src/models/multipageproxymodel.h b/src/models/multipageproxymodel.h index 08f4872a..23639df7 100644 --- a/src/models/multipageproxymodel.h +++ b/src/models/multipageproxymodel.h @@ -21,6 +21,13 @@ class MultipageProxyModel : public QIdentityProxyModel }; Q_ENUM(Roles) + enum DndOperation { + DndPrepend = -1, + DndJoin = 0, + DndAppend = 1 + }; + Q_ENUM(DndOperation) + static MultipageProxyModel &instance() { static MultipageProxyModel _instance; @@ -30,6 +37,7 @@ class MultipageProxyModel : public QIdentityProxyModel ~MultipageProxyModel(); Q_INVOKABLE int pageCount(int folderId = 0) const; + Q_INVOKABLE void commitDndOperation(const QString & dragId, const QString & dropId, const DndOperation op); // QAbstractItemModel interface public: @@ -52,6 +60,7 @@ class MultipageProxyModel : public QIdentityProxyModel void onSourceModelChanged(); ItemsPage * createFolder(const QString & idNumber); + ItemsPage * folderById(int id); // folder-id: internal/folder/ ItemsPage * m_topLevel; diff --git a/src/models/multipagesortfilterproxymodel.cpp b/src/models/multipagesortfilterproxymodel.cpp new file mode 100644 index 00000000..38e6492c --- /dev/null +++ b/src/models/multipagesortfilterproxymodel.cpp @@ -0,0 +1,53 @@ +#include "multipagesortfilterproxymodel.h" + +#include + +#include "multipageproxymodel.h" + +MultipageSortFilterProxyModel::MultipageSortFilterProxyModel(QObject *parent) + : QSortFilterProxyModel(parent) +{ + setSortRole(MultipageProxyModel::FolderIdNumberRole); + setDynamicSortFilter(true); + + connect(this, &MultipageSortFilterProxyModel::onFolderIdChanged, this, [this](){ + invalidateFilter(); + }); + + connect(this, &MultipageSortFilterProxyModel::onPageIdChanged, this, [this](){ + invalidateFilter(); + }); +} + +MultipageSortFilterProxyModel::~MultipageSortFilterProxyModel() +{ + +} + +void MultipageSortFilterProxyModel::setModel(QAbstractItemModel *model) +{ + if (model == sourceModel()) { + return; + } + QSortFilterProxyModel::setSourceModel(model); + sort(0); +} + +bool MultipageSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + return sourceModel()->data(sourceModel()->index(source_row, 0, source_parent), MultipageProxyModel::FolderIdNumberRole).toInt() == m_folderId && + sourceModel()->data(sourceModel()->index(source_row, 0, source_parent), MultipageProxyModel::PageRole).toInt() == m_pageId; +} + +bool MultipageSortFilterProxyModel::lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const +{ + if (source_left.data(MultipageProxyModel::FolderIdNumberRole).toInt() < source_right.data(MultipageProxyModel::FolderIdNumberRole).toInt()) { + return true; + } else if (source_left.data(MultipageProxyModel::PageRole).toInt() < source_right.data(MultipageProxyModel::PageRole).toInt()) { + return true; + } else if (source_left.data(MultipageProxyModel::IndexInPageRole).toInt() < source_right.data(MultipageProxyModel::IndexInPageRole).toInt()) { + return true; + } else { + return false; + } +} diff --git a/src/models/multipagesortfilterproxymodel.h b/src/models/multipagesortfilterproxymodel.h new file mode 100644 index 00000000..dc386584 --- /dev/null +++ b/src/models/multipagesortfilterproxymodel.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +class MultipageSortFilterProxyModel : public QSortFilterProxyModel +{ + Q_OBJECT + + Q_PROPERTY(QAbstractItemModel *sourceModel READ sourceModel WRITE setModel NOTIFY sourceModelChanged) + Q_PROPERTY(int folderId MEMBER m_folderId NOTIFY onFolderIdChanged) + Q_PROPERTY(int pageId MEMBER m_pageId NOTIFY onPageIdChanged) + +public: + explicit MultipageSortFilterProxyModel(QObject *parent = nullptr); + ~MultipageSortFilterProxyModel(); + + void setModel(QAbstractItemModel *model); + +protected: + bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override; + bool lessThan(const QModelIndex &source_left, const QModelIndex &source_right) const override; + +signals: + void onFolderIdChanged(int); + void onPageIdChanged(int); + void sourceModelChanged(QObject *); + +private: + int m_folderId; + int m_pageId; +}; diff --git a/src/quick/CMakeLists.txt b/src/quick/CMakeLists.txt index c6d564a3..f7846694 100644 --- a/src/quick/CMakeLists.txt +++ b/src/quick/CMakeLists.txt @@ -9,6 +9,7 @@ add_library(launcher-qml-utils OBJECT) target_sources(launcher-qml-utils PRIVATE launcherappiconprovider.cpp + launcherfoldericonprovider.cpp blurhashimageprovider.cpp ksortfilterproxymodel.cpp ) @@ -17,6 +18,7 @@ target_sources(launcher-qml-utils PUBLIC FILE_SET HEADERS FILES launcherappiconprovider.h + launcherfoldericonprovider.h blurhashimageprovider.h ksortfilterproxymodel.h ) diff --git a/src/quick/launcherappiconprovider.cpp b/src/quick/launcherappiconprovider.cpp index 753a0aae..5a963dd4 100644 --- a/src/quick/launcherappiconprovider.cpp +++ b/src/quick/launcherappiconprovider.cpp @@ -21,13 +21,17 @@ QPixmap LauncherAppIconProvider::requestPixmap(const QString &id, QSize *size, c { Q_UNUSED(size) - QPixmap result(requestedSize); + QSize preferredSize = requestedSize.isValid() + ? requestedSize + : ((size && size->isValid()) ? *size : QSize(64, 64)); + + QPixmap result(preferredSize); result.fill(Qt::transparent); // uri: image://provider/icon-name // id: icon-name - IconUtils::getThemeIcon(result, id, requestedSize.width()); + IconUtils::getThemeIcon(result, id, preferredSize.width()); return result; } diff --git a/src/quick/launcherfoldericonprovider.cpp b/src/quick/launcherfoldericonprovider.cpp new file mode 100644 index 00000000..8306e9e4 --- /dev/null +++ b/src/quick/launcherfoldericonprovider.cpp @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "launcherfoldericonprovider.h" + +#include "iconutils.h" + +#include + +LauncherFolderIconProvider::LauncherFolderIconProvider(): + QQuickImageProvider(QQuickImageProvider::Pixmap) +{ + +} + +LauncherFolderIconProvider::~LauncherFolderIconProvider() +{ + +} + +QPixmap LauncherFolderIconProvider::requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) +{ + Q_UNUSED(size) + + constexpr int iconPerRow = 2; + constexpr int iconSpacing = 5; + constexpr int padding = 5; + + QSize preferredSize = requestedSize.isValid() + ? requestedSize + : ((size && size->isValid()) ? *size : QSize(64, 64)); + const int iconSize = (preferredSize.width() - padding * 2 - (iconPerRow - 1) * iconSpacing) / iconPerRow; + + QPixmap result(preferredSize); + result.fill(Qt::transparent); + + QPainter painter; + painter.begin(&result); + + // folder background + painter.setBrush(QBrush(QColor(0, 0, 0, 128))); + painter.setPen(Qt::NoPen); + painter.drawRoundedRect(result.rect(), 8.0, 8.0); + + // icons + // uri: image://provider/icon-name:icon-name:icon-name + // ids: icon-name:icon-name:icon-name + const QStringList ids(id.split(':')); + int curIdx = 0; + for (const QString & icon : ids) { + int curRow = curIdx / iconPerRow; + int curCol = curIdx % iconPerRow; + QPixmap iconPixmap(QSize(iconSize, iconSize)); + IconUtils::getThemeIcon(iconPixmap, icon, iconSize); + QRect iconRect; + iconRect.setTop(padding + curRow * (iconSize + iconSpacing)); + iconRect.setLeft(padding + curCol * (iconSize + iconSpacing)); + iconRect.setSize(QSize(iconSize, iconSize)); + painter.drawPixmap(iconRect, iconPixmap); + curIdx++; + } + + painter.end(); + + return result; +} diff --git a/src/quick/launcherfoldericonprovider.h b/src/quick/launcherfoldericonprovider.h new file mode 100644 index 00000000..0d6e8a9e --- /dev/null +++ b/src/quick/launcherfoldericonprovider.h @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2023 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +class LauncherFolderIconProvider : public QQuickImageProvider { +public: + LauncherFolderIconProvider(); + ~LauncherFolderIconProvider(); + + QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize) override; +};