diff --git a/containments/desktop/package/contents/config/main.xml b/containments/desktop/package/contents/config/main.xml --- a/containments/desktop/package/contents/config/main.xml +++ b/containments/desktop/package/contents/config/main.xml @@ -37,6 +37,10 @@ + + desktop:/ diff --git a/containments/desktop/package/contents/ui/FolderView.qml b/containments/desktop/package/contents/ui/FolderView.qml --- a/containments/desktop/package/contents/ui/FolderView.qml +++ b/containments/desktop/package/contents/ui/FolderView.qml @@ -1072,6 +1072,8 @@ parseDesktopFiles: (plasmoid.configuration.url == "desktop:/") previews: plasmoid.configuration.previews previewPlugins: plasmoid.configuration.previewPlugins + screenMapper: Folder.ScreenMapper + appletInterface: plasmoid onListingCompleted: { if (!gridView.model && plasmoid.expanded) { diff --git a/containments/desktop/package/contents/ui/FolderViewLayer.qml b/containments/desktop/package/contents/ui/FolderViewLayer.qml --- a/containments/desktop/package/contents/ui/FolderViewLayer.qml +++ b/containments/desktop/package/contents/ui/FolderViewLayer.qml @@ -188,6 +188,18 @@ onPositionsChanged: { folderView.positions = plasmoid.configuration.positions; } + + onScreenMappingChanged: { + Folder.ScreenMapper.screenMapping = plasmoid.configuration.screenMapping; + } + } + + Connections { + target: Folder.ScreenMapper + + onScreenMappingChanged: { + plasmoid.configuration.screenMapping = Folder.ScreenMapper.screenMapping; + } } PlasmaCore.ColorScope { @@ -227,6 +239,7 @@ } Component.onCompleted: { + Folder.ScreenMapper.screenMapping = plasmoid.configuration.screenMapping; folderView.sortMode = plasmoid.configuration.sortMode; folderView.positions = plasmoid.configuration.positions; } diff --git a/containments/desktop/plugins/folder/CMakeLists.txt b/containments/desktop/plugins/folder/CMakeLists.txt --- a/containments/desktop/plugins/folder/CMakeLists.txt +++ b/containments/desktop/plugins/folder/CMakeLists.txt @@ -20,6 +20,7 @@ viewpropertiesmenu.cpp wheelinterceptor.cpp shortcut.cpp + screenmapper.cpp ) install(FILES qmldir DESTINATION ${QML_INSTALL_DIR}/org/kde/private/desktopcontainment/folder) diff --git a/containments/desktop/plugins/folder/autotests/CMakeLists.txt b/containments/desktop/plugins/folder/autotests/CMakeLists.txt --- a/containments/desktop/plugins/folder/autotests/CMakeLists.txt +++ b/containments/desktop/plugins/folder/autotests/CMakeLists.txt @@ -5,6 +5,7 @@ include_directories("..";${include_directories}) ecm_add_tests( + screenmappertest.cpp foldermodeltest.cpp positionertest.cpp viewpropertiesmenutest.cpp diff --git a/containments/desktop/plugins/folder/autotests/foldermodeltest.h b/containments/desktop/plugins/folder/autotests/foldermodeltest.h --- a/containments/desktop/plugins/folder/autotests/foldermodeltest.h +++ b/containments/desktop/plugins/folder/autotests/foldermodeltest.h @@ -32,9 +32,6 @@ Q_OBJECT private Q_SLOTS: - void initTestCase(); - void cleanupTestCase(); - void init(); void cleanup(); void tst_listing(); @@ -48,8 +45,13 @@ void tst_defaultValues(); void tst_actionMenu(); void tst_lockedChanged(); + void tst_multiScreen(); + void tst_multiScreenDifferenPath(); + private: + void createTestFolder(const QString &path); + FolderModel *m_folderModel; QTemporaryDir *m_folderDir; }; diff --git a/containments/desktop/plugins/folder/autotests/foldermodeltest.cpp b/containments/desktop/plugins/folder/autotests/foldermodeltest.cpp --- a/containments/desktop/plugins/folder/autotests/foldermodeltest.cpp +++ b/containments/desktop/plugins/folder/autotests/foldermodeltest.cpp @@ -21,6 +21,7 @@ #include "foldermodeltest.h" #include "foldermodel.h" +#include "screenmapper.h" #include #include @@ -30,38 +31,34 @@ static const QLatin1String desktop(QLatin1String("Desktop")); -void FolderModelTest::initTestCase() +void FolderModelTest::createTestFolder(const QString &path) { - m_folderDir = new QTemporaryDir(); - QDir dir(m_folderDir->path()); - dir.mkdir(desktop); - dir.cd(desktop); + dir.mkdir(path); + dir.cd(path); dir.mkdir("firstDir"); QFile f; for (int i = 1; i < 10; i++) { f.setFileName(QStringLiteral("%1/file%2.txt").arg(dir.path(), QString::number(i))); f.open(QFile::WriteOnly); f.close(); } - -} - -void FolderModelTest::cleanupTestCase() -{ - delete m_folderDir; } void FolderModelTest::init() { + m_folderDir = new QTemporaryDir(); + createTestFolder(desktop); m_folderModel = new FolderModel(this); m_folderModel->setUrl(m_folderDir->path() + QDir::separator() + desktop ); QSignalSpy s(m_folderModel, &FolderModel::listingCompleted); s.wait(1000); } void FolderModelTest::cleanup() { + delete m_folderDir; + m_folderDir = 0; delete m_folderModel; m_folderModel = nullptr; } @@ -265,3 +262,110 @@ m_folderModel->setLocked(true); QCOMPARE(s.count(), 2); } + +void FolderModelTest::tst_multiScreen() +{ + auto *screenMapper = ScreenMapper::instance(); + m_folderModel->setUsedByContainment(true); + m_folderModel->setScreenMapper(screenMapper); + m_folderModel->setScreen(0); + QSignalSpy s(m_folderModel, &FolderModel::listingCompleted); + s.wait(1000); + const auto count = m_folderModel->rowCount(); + for (int i = 0; i < count; i++) { + const auto index = m_folderModel->index(i, 0); + const auto name = index.data(FolderModel::UrlRole).toString(); + // all items are on the first screen by default + QCOMPARE(screenMapper->screenForItem(name), 0); + } + + // move one file to a new screen + const auto movedItem = m_folderModel->index(0, 0).data(FolderModel::UrlRole).toString(); + FolderModel secondFolderModel; + secondFolderModel.setUrl(m_folderDir->path() + QDir::separator() + desktop ); + secondFolderModel.setUsedByContainment(true); + secondFolderModel.setScreenMapper(screenMapper); + secondFolderModel.setScreen(1); + QSignalSpy s2(&secondFolderModel, &FolderModel::listingCompleted); + s2.wait(1000); + const auto count2 = secondFolderModel.rowCount(); + QCOMPARE(count2, 0); + + screenMapper->addMapping(movedItem, 1); + m_folderModel->invalidate(); + secondFolderModel.invalidate(); + s.wait(1000); + s2.wait(1000); + // we have one less item + QCOMPARE(m_folderModel->rowCount(), count - 1); + QCOMPARE(secondFolderModel.rowCount(), 1); + QCOMPARE(secondFolderModel.index(0,0).data(FolderModel::UrlRole).toString(), movedItem); + QCOMPARE(screenMapper->screenForItem(movedItem), 1); + + // remove extra screen, we have all items back + screenMapper->removeScreen(1, m_folderModel->url()); + s.wait(500); + QCOMPARE(m_folderModel->rowCount(), count); + QCOMPARE(secondFolderModel.rowCount(), 0); + QCOMPARE(screenMapper->screenForItem(movedItem), 0); + + // add back extra screen, the item is moved there + screenMapper->addScreen(1, m_folderModel->url()); + s.wait(500); + s2.wait(500); + QCOMPARE(m_folderModel->rowCount(), count - 1); + QCOMPARE(secondFolderModel.rowCount(), 1); + QCOMPARE(secondFolderModel.index(0,0).data(FolderModel::UrlRole).toString(), movedItem); + QCOMPARE(screenMapper->screenForItem(movedItem), 1); + + // create a new item, it appears on the first screen + QDir dir(m_folderDir->path()); + dir.cd(desktop); + dir.mkdir("secondDir"); + dir.cd("secondDir"); + s.wait(1000); + QCOMPARE(m_folderModel->rowCount(), count); + QCOMPARE(secondFolderModel.rowCount(), 1); + QCOMPARE(screenMapper->screenForItem("file://" + dir.path()), 0); +} + +void FolderModelTest::tst_multiScreenDifferenPath() +{ + auto *screenMapper = ScreenMapper::instance(); + m_folderModel->setUsedByContainment(true); + m_folderModel->setScreenMapper(screenMapper); + m_folderModel->setScreen(0); + QSignalSpy s(m_folderModel, &FolderModel::listingCompleted); + s.wait(1000); + const auto count = m_folderModel->rowCount(); + QCOMPARE(count, 10); + + const QLatin1String desktop2(QLatin1String("Desktop2")); + createTestFolder(desktop2); + FolderModel secondFolderModel; + secondFolderModel.setUsedByContainment(true); + secondFolderModel.setScreenMapper(screenMapper); + secondFolderModel.setUrl(m_folderDir->path() + QDir::separator() + desktop2 ); + secondFolderModel.setScreen(1); + QSignalSpy s2(&secondFolderModel, &FolderModel::listingCompleted); + s2.wait(1000); + const auto count2 = secondFolderModel.rowCount(); + QCOMPARE(count2, 10); + + // create a new item, it appears on the first screen + QDir dir(m_folderDir->path()); + dir.cd(desktop); + dir.mkdir("secondDir"); + s.wait(1000); + QCOMPARE(m_folderModel->rowCount(), count + 1); + QCOMPARE(secondFolderModel.rowCount(), count2); + + + // create a new item, it appears on the second screen + dir.cd(m_folderDir->path() + QDir::separator() + desktop2); + dir.mkdir("secondDir2"); + s.wait(1000); + QCOMPARE(m_folderModel->rowCount(), count + 1); + QCOMPARE(secondFolderModel.rowCount(), count2 + 1); +} + diff --git a/containments/desktop/plugins/folder/autotests/positionertest.h b/containments/desktop/plugins/folder/autotests/positionertest.h --- a/containments/desktop/plugins/folder/autotests/positionertest.h +++ b/containments/desktop/plugins/folder/autotests/positionertest.h @@ -52,8 +52,10 @@ void tst_defaultValues(); void tst_changeEnabledStatus(); void tst_changePerStripe(); + void tst_proxyMapping(); private: + QMap hash2map(const QHash &hash); void checkPositions(int perStripe); Positioner *m_positioner; diff --git a/containments/desktop/plugins/folder/autotests/positionertest.cpp b/containments/desktop/plugins/folder/autotests/positionertest.cpp --- a/containments/desktop/plugins/folder/autotests/positionertest.cpp +++ b/containments/desktop/plugins/folder/autotests/positionertest.cpp @@ -29,6 +29,7 @@ #include "foldermodel.h" #include "positioner.h" +#include "screenmapper.h" QTEST_MAIN(PositionerTest) @@ -58,6 +59,9 @@ void PositionerTest::init() { m_folderModel = new FolderModel(this); + m_folderModel->setScreen(0); + m_folderModel->setScreenMapper(ScreenMapper::instance()); + m_folderModel->setUsedByContainment(true); m_positioner = new Positioner(this); m_positioner->setEnabled(true); m_positioner->setFolderModel(m_folderModel); @@ -209,6 +213,98 @@ QCOMPARE(s.count(), 2); } +void PositionerTest::tst_proxyMapping() +{ + auto *screenMapper = ScreenMapper::instance(); + FolderModel secondFolderModel; + secondFolderModel.setUrl(m_folderDir->path() + QDir::separator() + desktop ); + secondFolderModel.setUsedByContainment(true); + secondFolderModel.setScreenMapper(screenMapper); + secondFolderModel.setScreen(1); + Positioner secondPositioner; + secondPositioner.setEnabled(true); + secondPositioner.setFolderModel(&secondFolderModel); + secondPositioner.setPerStripe(3); + QSignalSpy s2(&secondFolderModel, &FolderModel::listingCompleted); + s2.wait(1000); + QSignalSpy s(m_folderModel, &FolderModel::listingCompleted); + + QMap expectedSource2ProxyScreen0; + QMap expectedProxy2SourceScreen0; + QMap expectedProxy2SourceScreen1; + QMap expectedSource2ProxyScreen1; + + for (int i = 0; i < m_folderModel->rowCount(); i++) { + expectedSource2ProxyScreen0[i] = i; + expectedProxy2SourceScreen0[i] = i; + } + + // swap items 1 and 2 in the positioner + m_positioner->move({1, 2, 2, 1}); + expectedSource2ProxyScreen0[1] = 2; + expectedSource2ProxyScreen0[2] = 1; + expectedProxy2SourceScreen0[1] = 2; + expectedProxy2SourceScreen0[2] = 1; + + auto savedSource2ProxyScreen0 = expectedSource2ProxyScreen0; + auto savedProxy2SourceScreen0 = expectedProxy2SourceScreen0; + + QCOMPARE(hash2map(m_positioner->proxyToSourceMapping()), expectedSource2ProxyScreen0); + QCOMPARE(hash2map(m_positioner->sourceToProxyMapping()), expectedProxy2SourceScreen0); + QCOMPARE(hash2map(secondPositioner.proxyToSourceMapping()), expectedProxy2SourceScreen1); + QCOMPARE(hash2map(secondPositioner.sourceToProxyMapping()), expectedSource2ProxyScreen1); + + const auto movedItem = m_folderModel->index(1, 0).data(FolderModel::UrlRole).toString(); + + // move the item 1 from source (now in position 2) to the second screen + screenMapper->addMapping(movedItem, 1); + s.wait(1000); + s2.wait(1000); + + expectedProxy2SourceScreen1[0] = 0; + expectedSource2ProxyScreen1[0] = 0; + expectedSource2ProxyScreen0.clear(); + expectedProxy2SourceScreen0.clear(); + for (int i = 0; i < m_folderModel->rowCount(); i++) { + // as item 1 disappeared, the mapping of all items after that are shifted + auto proxyIndex = (i <= 1) ? i : i + 1; + expectedSource2ProxyScreen0[proxyIndex] = i; + expectedProxy2SourceScreen0[i] = proxyIndex; + } + + QCOMPARE(hash2map(m_positioner->proxyToSourceMapping()), expectedSource2ProxyScreen0); + QCOMPARE(hash2map(m_positioner->sourceToProxyMapping()), expectedProxy2SourceScreen0); + QCOMPARE(hash2map(secondPositioner.proxyToSourceMapping()), expectedProxy2SourceScreen1); + QCOMPARE(hash2map(secondPositioner.sourceToProxyMapping()), expectedSource2ProxyScreen1); + + // move back the same item to the first screen + screenMapper->addMapping(movedItem, 0); + s.wait(1000); + s2.wait(1000); + + // nothing on the second screen + expectedSource2ProxyScreen1.clear(); + expectedProxy2SourceScreen1.clear(); + // first screen should look like in the beginning + expectedSource2ProxyScreen0 = savedSource2ProxyScreen0; + expectedProxy2SourceScreen0 = savedProxy2SourceScreen0; + + QCOMPARE(hash2map(m_positioner->proxyToSourceMapping()), expectedSource2ProxyScreen0); + QCOMPARE(hash2map(m_positioner->sourceToProxyMapping()), expectedProxy2SourceScreen0); + QCOMPARE(hash2map(secondPositioner.proxyToSourceMapping()), expectedProxy2SourceScreen1); + QCOMPARE(hash2map(secondPositioner.sourceToProxyMapping()), expectedSource2ProxyScreen1); +} + +QMap PositionerTest::hash2map(const QHash &hash) +{ + QMap map; + for (auto it = hash.constBegin(); it != hash.constEnd(); ++it) { + map[it.key()] = it.value(); + } + + return map; +} + void PositionerTest::checkPositions(int perStripe) { QSignalSpy s(m_positioner, &Positioner::positionsChanged); diff --git a/containments/desktop/plugins/folder/autotests/foldermodeltest.h b/containments/desktop/plugins/folder/autotests/screenmappertest.h copy from containments/desktop/plugins/folder/autotests/foldermodeltest.h copy to containments/desktop/plugins/folder/autotests/screenmappertest.h --- a/containments/desktop/plugins/folder/autotests/foldermodeltest.h +++ b/containments/desktop/plugins/folder/autotests/screenmappertest.h @@ -2,6 +2,7 @@ * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company * * * * Author: Andras Mantia * + * Work sponsored by the LiMux project of the city of Munich. * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * @@ -19,39 +20,31 @@ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * ***************************************************************************/ -#ifndef FOLDERMODELTEST_H -#define FOLDERMODELTEST_H +#ifndef SCREENMAPPERTEST_H +#define SCREENMAPPERTEST_H #include -class QTemporaryDir; -class FolderModel; +class ScreenMapper; -class FolderModelTest : public QObject +class ScreenMapperTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); - void cleanupTestCase(); - void init(); - void cleanup(); - void tst_listing(); - void tst_listingDescending(); - void tst_listingFolderNotFirst(); - void tst_filterListing(); - void tst_cd(); - void tst_rename_data(); - void tst_rename(); - void tst_selection(); - void tst_defaultValues(); - void tst_actionMenu(); - void tst_lockedChanged(); - -private: - FolderModel *m_folderModel; - QTemporaryDir *m_folderDir; + + void tst_addScreens(); + void tst_removeScreens(); + void tst_addMapping(); + void tst_addRemoveScreenWithItems(); + void tst_addRemoveScreenDifferentPaths(); + +private: + void addScreens(const QString &path); + + ScreenMapper *m_screenMapper; }; -#endif // FOLDERMODELTEST_H +#endif // SCREENMAPPERTEST_H diff --git a/containments/desktop/plugins/folder/autotests/screenmappertest.cpp b/containments/desktop/plugins/folder/autotests/screenmappertest.cpp new file mode 100644 --- /dev/null +++ b/containments/desktop/plugins/folder/autotests/screenmappertest.cpp @@ -0,0 +1,159 @@ +/*************************************************************************** + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company * + * * + * Author: Andras Mantia * + * Work sponsored by the LiMux project of the city of Munich. * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ + +#include "screenmappertest.h" +#include "screenmapper.h" + +#include +#include + +QTEST_MAIN(ScreenMapperTest) + +void ScreenMapperTest::initTestCase() +{ + m_screenMapper = ScreenMapper::instance(); +} + +void ScreenMapperTest::init() +{ + m_screenMapper->cleanup(); +} + +void ScreenMapperTest::tst_addScreens() +{ + const auto path = QStringLiteral("desktop:/"); + QSignalSpy s(m_screenMapper, &ScreenMapper::screensChanged); + m_screenMapper->addScreen(-1, path); + QCOMPARE(s.count(), 0); + m_screenMapper->addScreen(1, path); + QCOMPARE(s.count(), 1); + m_screenMapper->addScreen(0, path); + QCOMPARE(s.count(), 2); + m_screenMapper->addScreen(1, path); + QCOMPARE(s.count(), 2); + QCOMPARE(m_screenMapper->firstAvailableScreen(path), 0); +} + +void ScreenMapperTest::tst_removeScreens() +{ + const auto path = QStringLiteral("desktop:/"); + addScreens(path); + QSignalSpy s(m_screenMapper, &ScreenMapper::screensChanged); + m_screenMapper->removeScreen(-1, path); + QCOMPARE(s.count(), 0); + m_screenMapper->removeScreen(1, path); + QCOMPARE(s.count(), 1); + QCOMPARE(m_screenMapper->firstAvailableScreen(path), 0); + m_screenMapper->removeScreen(1, path); + QCOMPARE(s.count(), 1); + m_screenMapper->addScreen(3, path); + QCOMPARE(s.count(), 2); + m_screenMapper->removeScreen(0, path); + QCOMPARE(s.count(), 3); + QCOMPARE(m_screenMapper->firstAvailableScreen(path), 2); +} + +void ScreenMapperTest::tst_addMapping() +{ + const auto path = QStringLiteral("desktop:/"); + addScreens(path); + QSignalSpy s(m_screenMapper, &ScreenMapper::screenMappingChanged); + QString file("desktop:/foo%1.txt"); + + for (int i = 0 ; i < 3; i++) { + const QString name = file.arg(i); + m_screenMapper->addMapping(name, i); + QCOMPARE(s.count(), i + 1); + QCOMPARE(m_screenMapper->screenForItem(name), i); + } +} + +void ScreenMapperTest::tst_addRemoveScreenWithItems() +{ + const auto path = QStringLiteral("desktop:/"); + addScreens(path); + QString file("desktop:/foo%1.txt"); + + for (int i = 0 ; i < 3; i++) { + const QString name = file.arg(i); + m_screenMapper->addMapping(name, i); + } + + // remove one screen + m_screenMapper->removeScreen(1, path); + QCOMPARE(m_screenMapper->screenForItem(file.arg(0)), 0); + QCOMPARE(m_screenMapper->screenForItem(file.arg(1)), -1); + QCOMPARE(m_screenMapper->screenForItem(file.arg(2)), 2); + + // add removed screen back, items screen is restored + m_screenMapper->addScreen(1, path); + QCOMPARE(m_screenMapper->screenForItem(file.arg(0)), 0); + QCOMPARE(m_screenMapper->screenForItem(file.arg(1)), 1); + QCOMPARE(m_screenMapper->screenForItem(file.arg(2)), 2); + + // remove all screens, firstAvailableScreen changes + m_screenMapper->removeScreen(0, path); + QCOMPARE(m_screenMapper->firstAvailableScreen(path), 1); + m_screenMapper->removeScreen(1, path); + QCOMPARE(m_screenMapper->firstAvailableScreen(path), 2); + m_screenMapper->removeScreen(2, path); + QCOMPARE(m_screenMapper->firstAvailableScreen(path), -1); + + + QCOMPARE(m_screenMapper->screenForItem(file.arg(0)), -1); + QCOMPARE(m_screenMapper->screenForItem(file.arg(1)), -1); + QCOMPARE(m_screenMapper->screenForItem(file.arg(2)), -1); + + // add all screens back, all item's screen is restored + addScreens(path); + QCOMPARE(m_screenMapper->screenForItem(file.arg(0)), 0); + QCOMPARE(m_screenMapper->screenForItem(file.arg(1)), 1); + QCOMPARE(m_screenMapper->screenForItem(file.arg(2)), 2); + + // remove one screen and move its item + const QString movedItem = file.arg(1); + m_screenMapper->removeScreen(1, path); + QCOMPARE(m_screenMapper->screenForItem(movedItem), -1); + m_screenMapper->addMapping(movedItem, 0); + QCOMPARE(m_screenMapper->screenForItem(movedItem), 0); + + // add back the screen, item goes back to the original place + m_screenMapper->addScreen(1, path); + QCOMPARE(m_screenMapper->screenForItem(movedItem), 1); +} + +void ScreenMapperTest::tst_addRemoveScreenDifferentPaths() +{ + const auto path = QStringLiteral("desktop:/Foo"); + const auto path2 = QStringLiteral("desktop:/Foo2"); + m_screenMapper->addScreen(0, path); + QCOMPARE(m_screenMapper->firstAvailableScreen(path), 0); + QCOMPARE(m_screenMapper->firstAvailableScreen(path2), -1); + +} + +void ScreenMapperTest::addScreens(const QString &path) +{ + m_screenMapper->addScreen(0, path); + m_screenMapper->addScreen(1, path); + m_screenMapper->addScreen(2, path); +} diff --git a/containments/desktop/plugins/folder/foldermodel.h b/containments/desktop/plugins/folder/foldermodel.h --- a/containments/desktop/plugins/folder/foldermodel.h +++ b/containments/desktop/plugins/folder/foldermodel.h @@ -56,6 +56,8 @@ class DropJob; } +class ScreenMapper; + class DirLister : public KDirLister { Q_OBJECT @@ -94,6 +96,8 @@ Q_PROPERTY(QString filterPattern READ filterPattern WRITE setFilterPattern NOTIFY filterPatternChanged) Q_PROPERTY(QStringList filterMimeTypes READ filterMimeTypes WRITE setFilterMimeTypes NOTIFY filterMimeTypesChanged) Q_PROPERTY(QObject* newMenu READ newMenu CONSTANT) + Q_PROPERTY(ScreenMapper* screenMapper READ screenMapper WRITE setScreenMapper NOTIFY screenMapperChanged) + Q_PROPERTY(QObject* appletInterface READ appletInterface WRITE setAppletInterface NOTIFY appletInterfaceChanged); public: enum DataRole { @@ -180,6 +184,12 @@ QStringList filterMimeTypes() const; void setFilterMimeTypes(const QStringList &mimeList); + ScreenMapper* screenMapper() const; + void setScreenMapper(ScreenMapper* screenMapper); + + QObject *appletInterface() const; + void setAppletInterface(QObject *appletInterface); + KFileItem rootItem() const; Q_INVOKABLE void up(); @@ -233,6 +243,8 @@ Q_INVOKABLE void undo(); Q_INVOKABLE void refresh(); + void setScreen(int screen); + Q_SIGNALS: void urlChanged() const; void listingCompleted() const; @@ -254,6 +266,9 @@ void filterModeChanged() const; void filterPatternChanged() const; void filterMimeTypesChanged() const; + void screenChanged() const; + void screenMapperChanged() const; + void appletInterfaceChanged() const; void requestRename() const; void move(int x, int y, QList urls); void popupMenuAboutToShow(KIO::DropJob *dropJob, QMimeData *mimeData, int x, int y); @@ -300,6 +315,10 @@ QPoint m_dragHotSpotScrollOffset; bool m_dragInProgress; bool m_urlChangedWhileDragging; + // target filename to target position of a drop event, note that this deliberately + // is not using the URL to easily support desktop:/ URL schemes + QHash m_dropTargetPositions; + QTimer *m_dropTargetPositionsCleanup; QPointer m_previewGenerator; QPointer m_viewAdapter; KActionCollection m_actionCollection; @@ -321,6 +340,9 @@ bool m_filterPatternMatchAll; QSet m_mimeSet; QList m_regExps; + int m_screen = -1; + ScreenMapper *m_screenMapper = nullptr; + QObject *m_appletInterface = nullptr; }; #endif diff --git a/containments/desktop/plugins/folder/foldermodel.cpp b/containments/desktop/plugins/folder/foldermodel.cpp --- a/containments/desktop/plugins/folder/foldermodel.cpp +++ b/containments/desktop/plugins/folder/foldermodel.cpp @@ -24,6 +24,7 @@ #include "foldermodel.h" #include "itemviewadapter.h" #include "positioner.h" +#include "screenmapper.h" #include #include @@ -39,6 +40,8 @@ #include #include #include +#include +#include #include #include @@ -70,10 +73,16 @@ #include #include +#include +#include +#include + #include #include #include +Q_LOGGING_CATEGORY(FOLDERMODEL, "plasma.containments.desktop.folder.foldermodel") + DirLister::DirLister(QObject *parent) : KDirLister(parent) { } @@ -96,6 +105,7 @@ m_dirWatch(nullptr), m_dragInProgress(false), m_urlChangedWhileDragging(false), + m_dropTargetPositionsCleanup(new QTimer(this)), m_previewGenerator(nullptr), m_viewAdapter(nullptr), m_actionCollection(this), @@ -137,6 +147,44 @@ m_dirModel->setDirLister(dirLister); m_dirModel->setDropsAllowed(KDirModel::DropOnDirectory | KDirModel::DropOnLocalExecutable); + /* + * position dropped items at the desired target position + * + * TODO: push this somehow into the Positioner + */ + connect(this, &QAbstractItemModel::rowsInserted, + this, [this](const QModelIndex &parent, int first, int last) { + for (int i = first; i <= last; ++i) { + const auto idx = index(i, 0, parent); + const auto url = itemForIndex(idx).url(); + auto it = m_dropTargetPositions.find(url.fileName()); + if (it != m_dropTargetPositions.end()) { + const auto pos = it.value(); + m_dropTargetPositions.erase(it); + setSortMode(-1); + emit move(pos.x(), pos.y(), {url}); + } + } + }); + /* + * Dropped files may not actually show up as new files, e.g. when we overwrite + * an existing file. Or files that fail to be listed by the dirLister, or... + * To ensure we don't grow the map indefinitely, clean it up periodically. + * The cleanup timer is (re)started whenever we modify the map. We use a quite + * high interval of 10s. This should ensure, that we don't accidentally wipe + * the mapping when we actually still want to use it. Since the time between + * adding an entry in the map and it showing up in the model should be + * small, this should rarely, if ever happen. + */ + m_dropTargetPositionsCleanup->setInterval(10000); + m_dropTargetPositionsCleanup->setSingleShot(true); + connect(m_dropTargetPositionsCleanup, &QTimer::timeout, this, [this]() { + if (!m_dropTargetPositions.isEmpty()) { + qCDebug(FOLDERMODEL) << "clearing drop target positions after timeout:" << m_dropTargetPositions; + m_dropTargetPositions.clear(); + } + }); + m_selectionModel = new QItemSelectionModel(this, this); connect(m_selectionModel, &QItemSelectionModel::selectionChanged, this, &FolderModel::selectionChanged); @@ -154,6 +202,12 @@ FolderModel::~FolderModel() { + if (m_screenMapper) { + // disconnect so we don't handle signals from the screen mapper when + // removeScreen is called + m_screenMapper->disconnect(this); + m_screenMapper->removeScreen(m_screen, url()); + } } QHash< int, QByteArray > FolderModel::roleNames() const @@ -194,6 +248,8 @@ return; } + const auto oldUrl = m_url; + beginResetModel(); m_url = url; m_isDirCache.clear(); @@ -225,6 +281,11 @@ } emit iconNameChanged(); + + if (m_screenMapper) { + m_screenMapper->removeScreen(m_screen, oldUrl); + m_screenMapper->addScreen(m_screen, url); + } } QUrl FolderModel::resolvedUrl() const @@ -518,6 +579,18 @@ } } +void FolderModel::setScreen(int screen) +{ + if (m_screen == screen) + return; + + m_screen = screen; + if (m_usedByContainment && m_screenMapper) { + m_screenMapper->addScreen(screen, url()); + } + emit screenChanged(); +} + KFileItem FolderModel::rootItem() const { return m_dirModel->dirLister()->rootItem(); @@ -762,7 +835,7 @@ { DragImage *image = m_dragImages.value(row); if (!image) { - return QPoint(-1, -1); + return QPoint(0, 0); } return image->cursorOffset; @@ -884,6 +957,15 @@ } } +static bool isDropBetweenSharedViews(const QList &urls, const QUrl &folderUrl) +{ + for (const auto &url : urls) { + if (!folderUrl.isParentOf(url)) + return false; + } + return true; +} + void FolderModel::drop(QQuickItem *target, QObject* dropEvent, int row) { QMimeData *mimeData = qobject_cast(dropEvent->property("mimeData").value()); @@ -894,6 +976,7 @@ const int x = dropEvent->property("x").toInt(); const int y = dropEvent->property("y").toInt(); + const QPoint dropPos = {x, y}; if (m_dragInProgress && row == -1 && !m_urlChangedWhileDragging) { if (m_locked || mimeData->urls().isEmpty()) { @@ -956,15 +1039,46 @@ return; } - QPoint pos = {x, y}; - pos = target->mapToScene(pos).toPoint(); - pos = target->window()->mapToGlobal(pos); + + auto dropTargetFolderUrl = dropTargetUrl; + if (dropTargetFolderUrl.fileName() == QLatin1String(".")) { + // the target URL for desktop:/ is e.g. 'file://home/user/Desktop/.' + dropTargetFolderUrl = dropTargetFolderUrl.adjusted(QUrl::RemoveFilename); + } + + // use dropTargetUrl to resolve desktop:/ to the actual file location which is also used by the mime data + if (isDropBetweenSharedViews(mimeData->urls(), dropTargetFolderUrl)) { + /* QMimeData operates on local URLs, but the dir lister and thus screen mapper and positioner may + * use a fancy scheme like desktop:/ instead. Ensure we always use the latter to properly map URLs, + * i.e. go from file:///home/user/Desktop/file to desktop:/file + */ + auto mappableUrl = [this, dropTargetFolderUrl](const QUrl &url) -> QString { + QString mappedUrl = url.toString(); + if (dropTargetFolderUrl != m_dirModel->dirLister()->url()) { + const auto local = dropTargetFolderUrl.toString(); + const auto internal = m_dirModel->dirLister()->url().toString(); + if (mappedUrl.startsWith(local)) { + mappedUrl.replace(0, local.size(), internal); + } + } + return mappedUrl; + }; + setSortMode(-1); + for (const auto &url : mimeData->urls()) { + m_dropTargetPositions.insert(url.fileName(), dropPos); + m_screenMapper->addMapping(mappableUrl(url), m_screen, ScreenMapper::DelayedSignal); + } + m_dropTargetPositionsCleanup->start(); + return; + } Qt::DropAction proposedAction((Qt::DropAction)dropEvent->property("proposedAction").toInt()); Qt::DropActions possibleActions(dropEvent->property("possibleActions").toInt()); Qt::MouseButtons buttons(dropEvent->property("buttons").toInt()); Qt::KeyboardModifiers modifiers(dropEvent->property("modifiers").toInt()); + auto pos = target->mapToScene(dropPos).toPoint(); + pos = target->window()->mapToGlobal(pos); QDropEvent ev(pos, possibleActions, mimeData, buttons, modifiers); ev.setDropAction(proposedAction); @@ -978,10 +1092,33 @@ mimeCopy->setData(format, mimeData->data(format)); } - connect(dropJob, static_cast(&KIO::DropJob::popupMenuAboutToShow), this, [this, mimeCopy, x, y, dropJob](const KFileItemListProperties &) { + connect(dropJob, &KIO::DropJob::popupMenuAboutToShow, this, [this, mimeCopy, x, y, dropJob](const KFileItemListProperties &) { emit popupMenuAboutToShow(dropJob, mimeCopy, x, y); mimeCopy->deleteLater(); }); + + /* + * Position files that come from a drag'n'drop event at the drop event + * target position. To do so, we first listen to copy job to figure out + * the target URL. Then we store the position of this drop event in the + * hash and eventually trigger a move request when we get notified about + * the new file event from the source model. + * + * TODO: move this somehow to the Positioner + */ + connect(dropJob, &KIO::DropJob::copyJobStarted, this, [this, dropPos](KIO::CopyJob* copyJob) { + auto map = [this, dropPos](const QUrl &targetUrl) { + m_dropTargetPositions.insert(targetUrl.fileName(), dropPos); + m_dropTargetPositionsCleanup->start(); + }; + // remember drop target position for target URL and forget about the source URL + connect(copyJob, &KIO::CopyJob::copyingDone, this, [this, map](KIO::Job *, const QUrl &src, const QUrl &targetUrl, const QDateTime &, bool, bool) { + map(targetUrl); + }); + connect(copyJob, &KIO::CopyJob::copyingLinkDone, this, [this, map](KIO::Job *, const QUrl &src, const QString &, const QUrl &targetUrl) { + map(targetUrl); + }); + }); } void FolderModel::dropCwd(QObject* dropEvent) @@ -1162,6 +1299,9 @@ void FolderModel::evictFromIsDirCache(const KFileItemList& items) { foreach (const KFileItem &item, items) { + if (m_screenMapper) { + m_screenMapper->removeFromMap(item.url().toString()); + } m_isDirCache.remove(item.url()); } } @@ -1283,13 +1423,34 @@ bool FolderModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { + const KDirModel *dirModel = static_cast(sourceModel()); + const KFileItem item = dirModel->itemForIndex(dirModel->index(sourceRow, KDirModel::Name, sourceParent)); + + if (m_usedByContainment) { + const QString name = item.url().toString(); + const int screen = m_screenMapper->screenForItem(name); + // don't do anything if the folderview is not associated with a screen + if (m_screen != -1) { + if (screen == -1) { + // The item is not associated with a screen, probably because this is the first + // time we see it or the folderview was previously used as a regular applet. + // Associated with this folderview if the view is on the first available screen + if (m_screen == m_screenMapper->firstAvailableScreen(url())) { + m_screenMapper->addMapping(name, m_screen, ScreenMapper::DelayedSignal); + } else { + return false; + } + } else if (m_screen != screen) { + // the item belongs to a different screen, filter it out + return false; + } + } + } + if (m_filterMode == NoFilter) { return true; } - const KDirModel *dirModel = static_cast(sourceModel()); - const KFileItem item = dirModel->itemForIndex(dirModel->index(sourceRow, KDirModel::Name, sourceParent)); - if (m_filterMode == FilterShowMatches) { return (matchPattern(item) && matchMimeType(item)); } else { @@ -1617,6 +1778,60 @@ m_dirModel->dirLister()->updateDirectory(m_dirModel->dirLister()->url()); } +ScreenMapper *FolderModel::screenMapper() const +{ + return m_screenMapper; +} + +void FolderModel::setScreenMapper(ScreenMapper *screenMapper) +{ + if (m_screenMapper == screenMapper) + return; + + Q_ASSERT(!m_screenMapper); + + m_screenMapper = screenMapper; + if (m_screenMapper) { + connect(m_screenMapper, &ScreenMapper::screensChanged, this, &FolderModel::invalidateFilter); + connect(m_screenMapper, &ScreenMapper::screenMappingChanged, this, &FolderModel::invalidateFilter); + } + emit screenMapperChanged(); +} + +QObject *FolderModel::appletInterface() const +{ + return m_appletInterface; +} + +void FolderModel::setAppletInterface(QObject *appletInterface) +{ + if (m_appletInterface != appletInterface) { + Q_ASSERT(!m_appletInterface); + + m_appletInterface = appletInterface; + + if (appletInterface) { + Plasma::Applet *applet = appletInterface->property("_plasma_applet").value(); + + if (applet) { + Plasma::Containment *containment = applet->containment(); + + if (containment) { + Plasma::Corona *corona = containment->corona(); + + if (corona) { + m_screenMapper->setCorona(corona); + } + setScreen(containment->screen()); + connect(containment, &Plasma::Containment::screenChanged, this, &FolderModel::setScreen); + } + } + } + + emit appletInterfaceChanged(); + } +} + void FolderModel::moveSelectedToTrash() { if (!m_selectionModel->hasSelection()) { diff --git a/containments/desktop/plugins/folder/folderplugin.cpp b/containments/desktop/plugins/folder/folderplugin.cpp --- a/containments/desktop/plugins/folder/folderplugin.cpp +++ b/containments/desktop/plugins/folder/folderplugin.cpp @@ -32,15 +32,28 @@ #include "viewpropertiesmenu.h" #include "wheelinterceptor.h" #include "shortcut.h" +#include "screenmapper.h" +#include +#include static QObject *menuHelperSingletonProvider(QQmlEngine *engine, QJSEngine *jsEngine) { Q_UNUSED(engine); Q_UNUSED(jsEngine); return new MenuHelper(); } +static QObject *screenMapperProvider(QQmlEngine *engine, QJSEngine *scriptEngine) +{ + Q_UNUSED(scriptEngine); + + QObject *mapper = ScreenMapper::instance(); + engine->setObjectOwnership(mapper, QQmlEngine::CppOwnership); + return mapper; +} + + void FolderPlugin::registerTypes(const char *uri) { Q_ASSERT(uri == QLatin1String("org.kde.private.desktopcontainment.folder")); @@ -58,5 +71,6 @@ qmlRegisterType(uri, 0, 1, "ViewPropertiesMenu"); qmlRegisterType(uri, 0, 1, "WheelInterceptor"); qmlRegisterType(uri, 0, 1, "ShortCut"); + qmlRegisterSingletonType(uri, 0, 1, "ScreenMapper", screenMapperProvider); } diff --git a/containments/desktop/plugins/folder/positioner.h b/containments/desktop/plugins/folder/positioner.h --- a/containments/desktop/plugins/folder/positioner.h +++ b/containments/desktop/plugins/folder/positioner.h @@ -76,6 +76,15 @@ int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; +#ifdef BUILD_TESTING + QHash proxyToSourceMapping() const { + return m_proxyToSource; + } + QHash sourceToProxyMapping() const { + return m_sourceToProxy; + } +#endif + Q_SIGNALS: void enabledChanged() const; void folderModelChanged() const; @@ -128,6 +137,7 @@ QHash m_proxyToSource; QHash m_sourceToProxy; + bool m_beginInsertRowsCalled = false; // used to sync the amount of begin/endInsertRows calls }; #endif diff --git a/containments/desktop/plugins/folder/positioner.cpp b/containments/desktop/plugins/folder/positioner.cpp --- a/containments/desktop/plugins/folder/positioner.cpp +++ b/containments/desktop/plugins/folder/positioner.cpp @@ -25,6 +25,15 @@ #include +void ensureUnique(const QHash mapping) +{ + auto values = mapping.values(); + qSort(values); + auto uniqueValues = values.toSet().toList(); + qSort(uniqueValues); + Q_ASSERT(uniqueValues == values); +} + Positioner::Positioner(QObject *parent): QAbstractItemModel(parent) , m_enabled(false) , m_folderModel(nullptr) @@ -406,6 +415,7 @@ if (!toIndices.contains(from)) { m_proxyToSource.remove(from); + ensureUnique(m_proxyToSource); } updateMaps(to, sourceRow); @@ -422,6 +432,10 @@ const int newCount = rowCount(); if (newCount > oldCount) { + if (m_beginInsertRowsCalled) { + endInsertRows(); + m_beginInsertRowsCalled = false; + } beginInsertRows(QModelIndex(), oldCount, newCount - 1); endInsertRows(); } @@ -444,11 +458,19 @@ QHashIterator it(m_proxyToSource); + QSet uniqueName; + QSet uniqueId; + while (it.hasNext()) { it.next(); + Q_ASSERT(!uniqueId.contains(it.value())); + uniqueId.insert(it.value()); + const QString &name = m_folderModel->data(m_folderModel->index(it.value(), 0), FolderModel::UrlRole).toString(); + Q_ASSERT(!uniqueName.contains(name)); + uniqueName.insert(name); if (name.isEmpty()) { qDebug() << this << it.value() << "Source model doesn't know this index!"; @@ -459,6 +481,7 @@ positions.append(name); positions.append(QString::number(qMax(0, it.key() / m_perStripe))); positions.append(QString::number(qMax(0, it.key() % m_perStripe))); + } } @@ -507,14 +530,28 @@ if (m_enabled) { if (m_proxyToSource.isEmpty()) { if (!m_pendingPositions) { - emit beginInsertRows(parent, start, end); + beginInsertRows(parent, start, end); + m_beginInsertRowsCalled = true; initMaps(end + 1); } return; } + // When new rows are inserted, they might go in the beginning or in the middle. + // In this case we must update first the existing proxy->source and source->proxy + // mapping, otherwise the proxy items will point to the wrong source item. + int count = end - start + 1; + m_sourceToProxy.clear(); + for (auto it = m_proxyToSource.begin(); it != m_proxyToSource.end(); ++it) { + int sourceIdx = *it; + if (sourceIdx >= start) { + *it += count; + } + m_sourceToProxy[*it] = it.key(); + } + int free = -1; int rest = -1; @@ -535,6 +572,7 @@ int remainder = (end - rest); beginInsertRows(parent, firstNew, firstNew + remainder); + m_beginInsertRowsCalled = true; for (int i = 0; i <= remainder; ++i) { updateMaps(firstNew + i, rest + i); @@ -544,6 +582,8 @@ } } else { emit beginInsertRows(parent, start, end); + beginInsertRows(parent, start, end); + m_beginInsertRowsCalled = true; } } @@ -563,6 +603,8 @@ m_proxyToSource.remove(proxyRow); m_pendingChanges << createIndex(proxyRow, 0); } + ensureUnique(m_proxyToSource); + ensureUnique(m_sourceToProxy); QHash newProxyToSource; QHash newSourceToProxy; @@ -582,7 +624,9 @@ } m_proxyToSource = newProxyToSource; + ensureUnique(m_proxyToSource); m_sourceToProxy = newSourceToProxy; + ensureUnique(m_sourceToProxy); m_lastRow = -1; int newLast = lastRow(); @@ -614,7 +658,10 @@ if (!m_ignoreNextTransaction) { if (!m_pendingPositions) { - emit endInsertRows(); + if (m_beginInsertRowsCalled) { + endInsertRows(); + m_beginInsertRowsCalled = false; + } } else { applyPositions(); } @@ -688,8 +735,20 @@ void Positioner::updateMaps(int proxyIndex, int sourceIndex) { + // ensure we don't get duplicate mappings + const auto oldSourceIndex = m_proxyToSource.value(proxyIndex, -1); + const auto oldProxyIndex = m_sourceToProxy.value(sourceIndex, -1); + if (oldSourceIndex != -1) { + m_sourceToProxy.remove(oldSourceIndex); + } + if (oldProxyIndex != -1) { + m_proxyToSource.remove(oldProxyIndex); + } + m_proxyToSource.insert(proxyIndex, sourceIndex); m_sourceToProxy.insert(sourceIndex, proxyIndex); + ensureUnique(m_proxyToSource); + ensureUnique(m_sourceToProxy); m_lastRow = -1; } diff --git a/containments/desktop/plugins/folder/screenmapper.h b/containments/desktop/plugins/folder/screenmapper.h new file mode 100644 --- /dev/null +++ b/containments/desktop/plugins/folder/screenmapper.h @@ -0,0 +1,83 @@ +/*************************************************************************** + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company * + * * + * Author: Andras Mantia * + * Work sponsored by the LiMux project of the city of Munich. * + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ +#ifndef SCREENMAPPER_H +#define SCREENMAPPER_H + +#include +#include +#include + +#include "folderplugin_private_export.h" + +class QTimer; + +namespace Plasma { + class Corona; +} + +class FOLDERPLUGIN_TESTS_EXPORT ScreenMapper : public QObject +{ + Q_OBJECT + Q_PROPERTY(QStringList screenMapping READ screenMapping WRITE setScreenMapping NOTIFY screenMappingChanged) + +public: + enum MappingSignalBehavior { + DelayedSignal = 0, + ImmediateSignal + }; + + static ScreenMapper *instance(); + ~ScreenMapper() override = default; + + QStringList screenMapping() const; + void setScreenMapping(const QStringList &mapping); + + int screenForItem(const QString &name) const; + void addMapping(const QString &name, int screen, MappingSignalBehavior behavior = ImmediateSignal); + void removeFromMap(const QString &name); + void setCorona(Plasma::Corona *corona); + + void addScreen(int screenId, const QString &path); + void removeScreen(int screenId, const QString &path); + int firstAvailableScreen(const QString &path) const; + +#ifdef BUILD_TESTING + void cleanup(); +#endif + +Q_SIGNALS: + void screenMappingChanged() const; + void screensChanged() const; + +private: + ScreenMapper(QObject *parent = nullptr); + + QHash m_screenItemMap; + QHash m_itemsOnDisabledScreensMap; + QHash m_firstScreenForPath; // first available screen for a path + QHash m_screensPerPath; // screen per registered path + QVector m_availableScreens; + Plasma::Corona *m_corona = nullptr; + QTimer *m_screenMappingChangedTimer = nullptr; +}; + +#endif // SCREENMAPPER_H diff --git a/containments/desktop/plugins/folder/screenmapper.cpp b/containments/desktop/plugins/folder/screenmapper.cpp new file mode 100644 --- /dev/null +++ b/containments/desktop/plugins/folder/screenmapper.cpp @@ -0,0 +1,229 @@ +/*************************************************************************** + * Copyright (C) 2017 Klarälvdalens Datakonsult AB, a KDAB Group company * + * * + * Author: Andras Mantia * + * Work sponsored by the LiMux project of the city of Munich. * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + * This program is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU General Public License for more details. * + * * + * You should have received a copy of the GNU General Public License * + * along with this program; if not, write to the * + * Free Software Foundation, Inc., * + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA . * + ***************************************************************************/ +#include "screenmapper.h" + +#include +#include + +#include + +ScreenMapper *ScreenMapper::instance() +{ + static ScreenMapper *s_instance = new ScreenMapper(); + return s_instance; +} + +ScreenMapper::ScreenMapper(QObject *parent) + : QObject(parent) + , m_screenMappingChangedTimer(new QTimer(this)) + +{ + connect(m_screenMappingChangedTimer, &QTimer::timeout, + this, &ScreenMapper::screenMappingChanged); + + // used to compress screenMappingChanged signals when addMapping is called multiple times, + // eg. from FolderModel::filterAcceptRows. The timer interval is an arbitrary number, + // that doesn't delay too much the signal, but still compresses as much as possible + m_screenMappingChangedTimer->setInterval(100); + m_screenMappingChangedTimer->setSingleShot(true); +} + +void ScreenMapper::removeScreen(int screenId, const QString &path) +{ + if (screenId < 0 || !m_availableScreens.contains(screenId)) + return; + + QUrl screenUrl = QUrl::fromUserInput(path, {}, QUrl::AssumeLocalFile); + const auto screenPathWithScheme = screenUrl.url(); + // store the original location for the items + auto it = m_screenItemMap.constBegin(); + while (it != m_screenItemMap.constEnd()) { + const auto name = it.key(); + if (it.value() == screenId && name.startsWith(screenPathWithScheme)) { + m_itemsOnDisabledScreensMap[screenId].append(name); + } + ++it; + } + + m_availableScreens.removeAll(screenId); + + const auto newFirstScreen = std::min_element(m_availableScreens.constBegin(), m_availableScreens.constEnd()); + auto pathIt = m_screensPerPath.find(path); + if (pathIt != m_screensPerPath.end() && pathIt.value() > 0) { + int firstScreen = m_firstScreenForPath.value(path, -1); + if (firstScreen == screenId) { + m_firstScreenForPath[path] = (newFirstScreen == m_availableScreens.constEnd()) ? -1 : *newFirstScreen; + } + *pathIt = pathIt.value() - 1; + } else if (path.isEmpty()) { + // The screen got completely removed, not only its path changed. + // If the removed screen was the first screen for a desktop path, the first screen for that path + // needs to be updated. + for (auto it = m_firstScreenForPath.begin(); it != m_firstScreenForPath.end(); ++it) { + if (*it == screenId) { + *it = *newFirstScreen; + + // we have now the path for the screen that was removed, so adjust it + pathIt = m_screensPerPath.find(it.key()); + if (pathIt != m_screensPerPath.end()) { + *pathIt = pathIt.value() - 1; + } + } + } + } + + emit screensChanged(); +} + +void ScreenMapper::addScreen(int screenId, const QString &path) +{ + if (screenId < 0 || m_availableScreens.contains(screenId)) + return; + + QUrl screenUrl = QUrl::fromUserInput(path, {}, QUrl::AssumeLocalFile); + const auto screenPathWithScheme = screenUrl.url(); + const bool isEmpty = (path.isEmpty() || screenUrl.path() == "/"); + // restore the stored locations + auto it = m_itemsOnDisabledScreensMap.find(screenId); + if (it != m_itemsOnDisabledScreensMap.end()) { + auto items = it.value(); + for (const auto &name: it.value()) { + // add the items to the new screen, if they are on a disabled screen and their + // location is below the new screen's path + if (isEmpty || name.startsWith(screenPathWithScheme)) { + addMapping(name, screenId, DelayedSignal); + items.removeAll(name); + } + } + if (items.isEmpty()) { + m_itemsOnDisabledScreensMap.erase(it); + } else { + *it = items; + } + } + + m_availableScreens.append(screenId); + + // path is empty when a new screen appears that has no folderview base path associated with + if (!path.isEmpty()) { + auto it = m_screensPerPath.find(path); + int firstScreen = m_firstScreenForPath.value(path, -1); + if (firstScreen == -1 || screenId < firstScreen) { + m_firstScreenForPath[path] = screenId; + } + if (it == m_screensPerPath.end()) { + m_screensPerPath[path] = 1; + } else { + *it = it.value() + 1; + } + } + + emit screensChanged(); +} + +void ScreenMapper::addMapping(const QString &name, int screen, MappingSignalBehavior behavior) +{ + m_screenItemMap[name] = screen; + if (behavior == DelayedSignal) { + m_screenMappingChangedTimer->start(); + } else { + emit screenMappingChanged(); + } +} + +void ScreenMapper::removeFromMap(const QString &name) +{ + m_screenItemMap.remove(name); + m_screenMappingChangedTimer->start(); +} + +int ScreenMapper::firstAvailableScreen(const QString &path) const +{ + return m_firstScreenForPath.value(path, -1); +} + +#ifdef BUILD_TESTING +void ScreenMapper::cleanup() +{ + m_screenItemMap.clear(); + m_itemsOnDisabledScreensMap.clear(); + m_firstScreenForPath.clear(); + m_screensPerPath.clear(); + m_availableScreens.clear(); +} +#endif + +void ScreenMapper::setCorona(Plasma::Corona *corona) +{ + if (m_corona != corona) { + Q_ASSERT(!m_corona); + + m_corona = corona; + if (m_corona) { + connect(m_corona, &Plasma::Corona::screenRemoved, this, [this] (int screenId) { + removeScreen(screenId, {}); + }); + connect(m_corona, &Plasma::Corona::screenAdded, this, [this] (int screenId) { + addScreen(screenId, {}); + }); + } + } +} + +QStringList ScreenMapper::screenMapping() const +{ + QStringList result; + result.reserve(m_screenItemMap.count() * 2); + auto it = m_screenItemMap.constBegin(); + while (it != m_screenItemMap.constEnd()) { + result.append(it.key()); + result.append(QString::number(it.value())); + ++it; + } + + return result; +} + +void ScreenMapper::setScreenMapping(const QStringList &mapping) +{ + QHash newMap; + const int count = mapping.count(); + newMap.reserve(count / 2); + for (int i = 0; i < count - 1; i += 2) { + if (i + 1 < count) { + newMap[mapping[i]] = mapping[i + 1].toInt(); + } + } + + if (m_screenItemMap != newMap) { + m_screenItemMap = newMap; + emit screenMappingChanged(); + } +} + +int ScreenMapper::screenForItem(const QString &name) const +{ + int screen = m_screenItemMap.value(name, -1); + if (!m_availableScreens.contains(screen)) + screen = -1; + + return screen; +}