From 609d85b833a668c69ff3781a195f2b0690ef673a Mon Sep 17 00:00:00 2001 From: Lukas Holecek Date: Mon, 27 Feb 2017 13:37:58 +0100 Subject: [PATCH] Pin/unpin items Fixes #545 Fixes #609 Fixes #652 --- plugins/CMakeLists.txt | 1 + plugins/itempinned/CMakeLists.txt | 8 + plugins/itempinned/itempinned.cpp | 329 +++++++++++++++++++ plugins/itempinned/itempinned.h | 133 ++++++++ plugins/itempinned/itempinned.pro | 15 + plugins/itempinned/itempinnedsettings.ui | 33 ++ plugins/itempinned/tests/itempinnedtests.cpp | 164 +++++++++ plugins/itempinned/tests/itempinnedtests.h | 55 ++++ plugins/plugins.pro | 1 + src/item/clipboardmodel.cpp | 29 ++ src/item/clipboardmodel.h | 15 + 11 files changed, 783 insertions(+) create mode 100644 plugins/itempinned/CMakeLists.txt create mode 100644 plugins/itempinned/itempinned.cpp create mode 100644 plugins/itempinned/itempinned.h create mode 100644 plugins/itempinned/itempinned.pro create mode 100644 plugins/itempinned/itempinnedsettings.ui create mode 100644 plugins/itempinned/tests/itempinnedtests.cpp create mode 100644 plugins/itempinned/tests/itempinnedtests.h diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 7f48b1154..1790b47c4 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -58,6 +58,7 @@ add_subdirectory("itemencrypted") add_subdirectory("itemfakevim") add_subdirectory("itemimage") add_subdirectory("itemnotes") +add_subdirectory("itempinned") add_subdirectory("itemtags") add_subdirectory("itemtext") add_subdirectory("itemsync") diff --git a/plugins/itempinned/CMakeLists.txt b/plugins/itempinned/CMakeLists.txt new file mode 100644 index 000000000..0b714a274 --- /dev/null +++ b/plugins/itempinned/CMakeLists.txt @@ -0,0 +1,8 @@ +set(copyq_plugin_itempinned_SOURCES + ../../src/common/common.cpp + ../../src/common/log.cpp + ../../src/common/mimetypes.cpp + ) + +copyq_add_plugin(itempinned) + diff --git a/plugins/itempinned/itempinned.cpp b/plugins/itempinned/itempinned.cpp new file mode 100644 index 000000000..67825b8a4 --- /dev/null +++ b/plugins/itempinned/itempinned.cpp @@ -0,0 +1,329 @@ +/* + Copyright (c) 2014, Lukas Holecek + + This file is part of CopyQ. + + CopyQ 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 3 of the License, or + (at your option) any later version. + + CopyQ 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 CopyQ. If not, see . +*/ + +#include "itempinned.h" +#include "ui_itempinnedsettings.h" + +#include "common/common.h" +#include "common/command.h" +#include "common/contenttype.h" + +#ifdef HAS_TESTS +# include "tests/itempinnedtests.h" +#endif + +#include +#include +#include +#include + +#include + +namespace { + +const char mimePinned[] = "application/x-copyq-item-pinned"; + +int pinnedRow(const QModelIndex &index) +{ + bool ok; + const auto dataMap = index.data(contentType::data).toMap(); + const auto pinnedRow = dataMap.value(mimePinned).toInt(&ok); + return ok ? pinnedRow : -1; +} + +bool isPinned(const QModelIndex &index) +{ + return pinnedRow(index) >= 0; +} + +Command dummyPinCommand() +{ + Command c; + c.icon = QString(QChar(IconThumbTack)); + c.inMenu = true; + c.shortcuts = QStringList() + << ItemPinnedLoader::tr("Ctrl+Shift+P", "Shortcut to pin and unpin items"); + return c; +} + +} // namespace + +ItemPinned::ItemPinned(ItemWidget *childItem, int pinnedRow) + : QWidget( childItem->widget()->parentWidget() ) + , ItemWidget(this) + , m_border(new QWidget(this)) + , m_childItem(childItem) + , m_pinnedRow(pinnedRow) +{ + m_childItem->widget()->setObjectName("item_child"); + m_childItem->widget()->setParent(this); + + m_border->setFixedWidth( pointsToPixels(8) ); + m_border->setStyleSheet("background: rgba(0,0,0,0.2)"); + + QBoxLayout *layout; + layout = new QHBoxLayout(this); + layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing( pointsToPixels(5) ); + + layout->addWidget(m_childItem->widget()); + layout->addWidget(m_border); +} + +void ItemPinned::highlight(const QRegExp &re, const QFont &highlightFont, const QPalette &highlightPalette) +{ + m_childItem->setHighlight(re, highlightFont, highlightPalette); +} + +QWidget *ItemPinned::createEditor(QWidget *parent) const +{ + return m_childItem->createEditor(parent); +} + +void ItemPinned::setEditorData(QWidget *editor, const QModelIndex &index) const +{ + return m_childItem->setEditorData(editor, index); +} + +void ItemPinned::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const +{ + return m_childItem->setModelData(editor, model, index); +} + +bool ItemPinned::hasChanges(QWidget *editor) const +{ + return m_childItem->hasChanges(editor); +} + +QObject *ItemPinned::createExternalEditor(const QModelIndex &index, QWidget *parent) const +{ + return m_childItem->createExternalEditor(index, parent); +} + +void ItemPinned::updateSize(const QSize &maximumSize, int idealWidth) +{ + const int childItemWidth = idealWidth - m_border->width() - layout()->spacing(); + m_childItem->updateSize(maximumSize, childItemWidth); +} + +ItemPinnedSaver::ItemPinnedSaver(QAbstractItemModel *model, QVariantMap &settings, const ItemSaverPtr &saver) + : m_model(model) + , m_settings(settings) + , m_saver(saver) +{ + connect( model, SIGNAL(rowsInserted(QModelIndex, int, int)), + SLOT(onRowsInserted(QModelIndex, int, int)) ); + connect( model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + SLOT(onRowsRemoved(QModelIndex,int,int)) ); +} + +bool ItemPinnedSaver::saveItems(const QAbstractItemModel &model, QIODevice *file) +{ + return m_saver->saveItems(model, file); +} + +bool ItemPinnedSaver::canRemoveItems(const QList &indexList, QString *error) +{ + const bool containsPinnedItems = std::any_of( + std::begin(indexList), std::end(indexList), isPinned); + + if (!containsPinnedItems) + return m_saver->canRemoveItems(indexList, error); + + if (error) { + *error = "Removing pinned items is not allowed (plugin.itempinned.unpin() items first)"; + return false; + } + + QMessageBox::information( + QApplication::activeWindow(), + ItemPinnedLoader::tr("Cannot Remove Pinned Items"), + ItemPinnedLoader::tr("Unpin items first to remove them.") ); + return false; +} + +bool ItemPinnedSaver::canMoveItems(const QList &indexList) +{ + return m_saver->canMoveItems(indexList); +} + +void ItemPinnedSaver::itemsRemovedByUser(const QList &indexList) +{ + m_saver->itemsRemovedByUser(indexList); +} + +QVariantMap ItemPinnedSaver::copyItem(const QAbstractItemModel &model, const QVariantMap &itemData) +{ + return m_saver->copyItem(model, itemData); +} + +void ItemPinnedSaver::onRowsInserted(const QModelIndex &, int start, int end) +{ + if (!m_model) + return; + + // Shift rows below inserted up. + const int rowCount = end - start + 1; + for (int row = end + 1; row < m_model->rowCount(); ++row) { + const auto index = m_model->index(row, 0); + if ( isPinned(index) ) + moveRow(row, row - rowCount); + } +} + +void ItemPinnedSaver::onRowsRemoved(const QModelIndex &, int start, int end) +{ + if (!m_model) + return; + + // Shift rows below removed down. + const int rowCount = end - start + 1; + for (int row = m_model->rowCount() - 1; row >= end; --row) { + const auto index = m_model->index(row, 0); + if ( isPinned(index) ) + moveRow(row, row + rowCount + 1); + } +} + +void ItemPinnedSaver::moveRow(int from, int to) +{ +#if QT_VERSION < 0x050000 + QMetaObject::invokeMethod(m_model, "moveRow", Q_ARG(int, from), Q_ARG(int, to)); +#else + m_model->moveRow(QModelIndex(), from, QModelIndex(), to); +#endif +} + +ItemPinnedLoader::ItemPinnedLoader() +{ +} + +ItemPinnedLoader::~ItemPinnedLoader() +{ +} + +QStringList ItemPinnedLoader::formatsToSave() const +{ + return QStringList() << mimePinned; +} + +QVariantMap ItemPinnedLoader::applySettings() +{ + return m_settings; +} + +QWidget *ItemPinnedLoader::createSettingsWidget(QWidget *parent) +{ + ui.reset(new Ui::ItemPinnedSettings); + QWidget *w = new QWidget(parent); + ui->setupUi(w); + + return w; +} + +ItemWidget *ItemPinnedLoader::transform(ItemWidget *itemWidget, const QModelIndex &index) +{ + const auto row = pinnedRow(index); + if (row < 0) + return nullptr; + + return new ItemPinned(itemWidget, row); +} + +ItemSaverPtr ItemPinnedLoader::transformSaver(const ItemSaverPtr &saver, QAbstractItemModel *model) +{ + return std::make_shared(model, m_settings, saver); +} + +QObject *ItemPinnedLoader::tests(const TestInterfacePtr &test) const +{ +#ifdef HAS_TESTS + QObject *tests = new ItemPinnedTests(test); + return tests; +#else + Q_UNUSED(test); + return nullptr; +#endif +} + +QString ItemPinnedLoader::script() const +{ + return "plugins." + id() + R"SCRIPT( = { + + mime: ')SCRIPT" + QString(mimePinned) + R"SCRIPT(', + + _pin: function(pin, args) { + var rows = Array.prototype.slice.call(args, 0) + if (rows.length == 0) + rows = selecteditems() + + for (var i in rows) { + var row = rows[i] + change(row, this.mime, pin ? str(row) : '') + } + }, + + isPinned: function(rows) { + var rows = Array.prototype.slice.call(arguments, 0) + if (rows.length == 0) + rows = selecteditems() + + for (var i in rows) { + var pinnedRowText = str(read(this.mime, rows[i])) + pinnedRow = parseInt(pinnedRowText) + if (pinnedRow >= 0) + return true + } + + return false + }, + + pin: function() { + this._pin(true, arguments) + }, + + unpin: function() { + this._pin(false, arguments) + }, + + })SCRIPT"; +} + +QList ItemPinnedLoader::commands() const +{ + QList commands; + + Command c; + + c = dummyPinCommand(); + c.name = tr("Pin"); + c.matchCmd = "copyq: plugins.itempinned.isPinned() && fail()"; + c.cmd = "copyq: plugins.itempinned.pin()"; + commands.append(c); + + c = dummyPinCommand(); + c.name = tr("Unpin"); + c.matchCmd = "copyq: plugins.itempinned.isPinned() || fail()"; + c.cmd = "copyq: plugins.itempinned.unpin()"; + commands.append(c); + + return commands; +} + +Q_EXPORT_PLUGIN2(itempinned, ItemPinnedLoader) diff --git a/plugins/itempinned/itempinned.h b/plugins/itempinned/itempinned.h new file mode 100644 index 000000000..c7339d8d7 --- /dev/null +++ b/plugins/itempinned/itempinned.h @@ -0,0 +1,133 @@ +/* + Copyright (c) 2014, Lukas Holecek + + This file is part of CopyQ. + + CopyQ 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 3 of the License, or + (at your option) any later version. + + CopyQ 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 CopyQ. If not, see . +*/ + +#ifndef ITEMPINNED_H +#define ITEMPINNED_H + +#include "gui/icons.h" +#include "item/itemwidget.h" + +#include + +#include + +namespace Ui { +class ItemPinnedSettings; +} + +class ItemPinned : public QWidget, public ItemWidget +{ + Q_OBJECT + +public: + ItemPinned(ItemWidget *childItem, int pinnedRow); + +protected: + void highlight(const QRegExp &re, const QFont &highlightFont, + const QPalette &highlightPalette) override; + + QWidget *createEditor(QWidget *parent) const override; + + void setEditorData(QWidget *editor, const QModelIndex &index) const override; + + void setModelData(QWidget *editor, QAbstractItemModel *model, + const QModelIndex &index) const override; + + bool hasChanges(QWidget *editor) const override; + + QObject *createExternalEditor(const QModelIndex &index, QWidget *parent) const override; + + void updateSize(const QSize &maximumSize, int idealWidth) override; + +private: + QWidget *m_border; + std::unique_ptr m_childItem; + int m_pinnedRow; +}; + +class ItemPinnedSaver : public QObject, public ItemSaverInterface +{ + Q_OBJECT + +public: + ItemPinnedSaver(QAbstractItemModel *model, QVariantMap &settings, const ItemSaverPtr &saver); + + bool saveItems(const QAbstractItemModel &model, QIODevice *file) override; + + bool canRemoveItems(const QList &indexList, QString *error) override; + + bool canMoveItems(const QList &indexList) override; + + void itemsRemovedByUser(const QList &indexList) override; + + QVariantMap copyItem(const QAbstractItemModel &model, const QVariantMap &itemData) override; + +private slots: + void onRowsInserted(const QModelIndex &parent, int first, int last); + void onRowsRemoved(const QModelIndex &parent, int first, int last); + +private: + void moveRow(int from, int to); + + QPointer m_model; + QVariantMap m_settings; + ItemSaverPtr m_saver; +}; + +class ItemPinnedLoader : public QObject, public ItemLoaderInterface +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID COPYQ_PLUGIN_ITEM_LOADER_ID) + Q_INTERFACES(ItemLoaderInterface) + +public: + ItemPinnedLoader(); + ~ItemPinnedLoader(); + + QString id() const override { return "itempinned"; } + QString name() const override { return tr("Pinned Items"); } + QString author() const override { return QString(); } + QString description() const override { return tr("Pin item to a row and avoid deleting it."); } + QVariant icon() const override { return QVariant(IconThumbTack); } + + QStringList formatsToSave() const override; + + QVariantMap applySettings() override; + + void loadSettings(const QVariantMap &settings) override { m_settings = settings; } + + QWidget *createSettingsWidget(QWidget *parent) override; + + ItemWidget *transform(ItemWidget *itemWidget, const QModelIndex &index) override; + + ItemSaverPtr transformSaver(const ItemSaverPtr &saver, QAbstractItemModel *model) override; + + QObject *tests(const TestInterfacePtr &test) const override; + + QString script() const override; + + QList commands() const override; + +private: + QVariantMap m_settings; + std::unique_ptr ui; + ItemLoaderPtr m_transformedLoader; +}; + +#endif // ITEMPINNED_H diff --git a/plugins/itempinned/itempinned.pro b/plugins/itempinned/itempinned.pro new file mode 100644 index 000000000..607efb669 --- /dev/null +++ b/plugins/itempinned/itempinned.pro @@ -0,0 +1,15 @@ +include(../plugins_common.pri) + +HEADERS += itempinned.h +SOURCES += itempinned.cpp \ + ../../src/common/common.cpp \ + ../../src/common/log.cpp \ + ../../src/common/mimetypes.cpp +FORMS += itempinnedsettings.ui +TARGET = $$qtLibraryTarget(itempinned) + +CONFIG(debug, debug|release) { + SOURCES += tests/itempinnedtests.cpp + HEADERS += tests/itempinnedtests.h +} + diff --git a/plugins/itempinned/itempinnedsettings.ui b/plugins/itempinned/itempinnedsettings.ui new file mode 100644 index 000000000..05d5d43f7 --- /dev/null +++ b/plugins/itempinned/itempinnedsettings.ui @@ -0,0 +1,33 @@ + + + ItemPinnedSettings + + + + + + This plugin allows you to pin selected items from menu. + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/plugins/itempinned/tests/itempinnedtests.cpp b/plugins/itempinned/tests/itempinnedtests.cpp new file mode 100644 index 000000000..1f007099a --- /dev/null +++ b/plugins/itempinned/tests/itempinnedtests.cpp @@ -0,0 +1,164 @@ +/* + Copyright (c) 2014, Lukas Holecek + + This file is part of CopyQ. + + CopyQ 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 3 of the License, or + (at your option) any later version. + + CopyQ 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 CopyQ. If not, see . +*/ + +#include "itempinnedtests.h" + +#include "tests/test_utils.h" + +ItemPinnedTests::ItemPinnedTests(const TestInterfacePtr &test, QObject *parent) + : QObject(parent) + , m_test(test) +{ +} + +void ItemPinnedTests::initTestCase() +{ + TEST(m_test->initTestCase()); +} + +void ItemPinnedTests::cleanupTestCase() +{ + TEST(m_test->cleanupTestCase()); +} + +void ItemPinnedTests::init() +{ + TEST(m_test->init()); +} + +void ItemPinnedTests::cleanup() +{ + TEST( m_test->cleanup() ); +} + +void ItemPinnedTests::isPinned() +{ + RUN("add" << "b" << "a", ""); + RUN("-e" << "plugins.itempinned.isPinned(0)", "false\n"); + RUN("-e" << "plugins.itempinned.isPinned(1)", "false\n"); +} + +void ItemPinnedTests::pin() +{ + RUN("add" << "b" << "a", ""); + RUN("-e" << "plugins.itempinned.pin(1)", ""); + RUN("-e" << "plugins.itempinned.isPinned(1)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(0)", "false\n"); + + RUN("-e" << "plugins.itempinned.pin(0)", ""); + RUN("-e" << "plugins.itempinned.isPinned(0)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(1)", "true\n"); +} + +void ItemPinnedTests::pinMultiple() +{ + RUN("add" << "d" << "c" << "b" << "a", ""); + RUN("-e" << "plugins.itempinned.pin(1, 2)", ""); + RUN("-e" << "plugins.itempinned.isPinned(0)", "false\n"); + RUN("-e" << "plugins.itempinned.isPinned(1)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(2)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(3)", "false\n"); + + RUN("-e" << "plugins.itempinned.pin(1, 2 ,3)", ""); + RUN("-e" << "plugins.itempinned.isPinned(0)", "false\n"); + RUN("-e" << "plugins.itempinned.isPinned(1)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(2)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(3)", "true\n"); +} + +void ItemPinnedTests::unpin() +{ + RUN("add" << "b" << "a", ""); + RUN("-e" << "plugins.itempinned.pin(0, 1)", ""); + RUN("-e" << "plugins.itempinned.isPinned(0)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(1)", "true\n"); + + RUN("-e" << "plugins.itempinned.unpin(0)", ""); + RUN("-e" << "plugins.itempinned.isPinned(0)", "false\n"); + RUN("-e" << "plugins.itempinned.isPinned(1)", "true\n"); + + RUN("-e" << "plugins.itempinned.unpin(1)", ""); + RUN("-e" << "plugins.itempinned.isPinned(0)", "false\n"); + RUN("-e" << "plugins.itempinned.isPinned(1)", "false\n"); +} + +void ItemPinnedTests::unpinMultiple() +{ + RUN("add" << "d" << "c" << "b" << "a", ""); + RUN("-e" << "plugins.itempinned.pin(0, 1, 2, 3)", ""); + RUN("-e" << "plugins.itempinned.isPinned(0)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(1)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(2)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(3)", "true\n"); + + RUN("-e" << "plugins.itempinned.unpin(1, 2)", ""); + RUN("-e" << "plugins.itempinned.isPinned(0)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(1)", "false\n"); + RUN("-e" << "plugins.itempinned.isPinned(2)", "false\n"); + RUN("-e" << "plugins.itempinned.isPinned(3)", "true\n"); + + RUN("-e" << "plugins.itempinned.unpin(2, 3)", ""); + RUN("-e" << "plugins.itempinned.isPinned(0)", "true\n"); + RUN("-e" << "plugins.itempinned.isPinned(1)", "false\n"); + RUN("-e" << "plugins.itempinned.isPinned(2)", "false\n"); + RUN("-e" << "plugins.itempinned.isPinned(3)", "false\n"); +} + +void ItemPinnedTests::removePinnedThrows() +{ + RUN("add" << "b" << "a", ""); + RUN("-e" << "plugins.itempinned.pin(0, 1)", ""); + + TEST( m_test->runClientWithError(Args() << "remove" << "0" << "1", 1) ); + RUN("separator" << " " << "read" << "0" << "1", "a b"); +} + +void ItemPinnedTests::pinToRow() +{ + const auto read = Args() << "separator" << " " << "read"; + + RUN("add" << "a", ""); + RUN("-e" << "plugins.itempinned.pin(0)", ""); + RUN("add" << "b", ""); + RUN(read << "0" << "1", "a b"); + + RUN("add" << "c", ""); + RUN(read << "0" << "1" << "2", "a c b"); + + RUN("-e" << "plugins.itempinned.pin(1)", ""); + RUN("add" << "d", ""); + RUN(read << "0" << "1" << "2" << "3", "a c d b"); + + RUN("-e" << "plugins.itempinned.pin(2)", ""); + RUN("-e" << "plugins.itempinned.unpin(1); remove(1)", ""); + RUN(read << "0" << "1" << "2", "a b d"); +} + +void ItemPinnedTests::fullTab() +{ + RUN("config" << "maxitems" << "3", "3\n"); + RUN("add" << "c" << "b" << "a", ""); + RUN("-e" << "plugins.itempinned.pin(0,1,2)", ""); + + // Tab is full and no items can be removed. + TEST( m_test->runClientWithError(Args() << "add" << "X", 1) ); + TEST( m_test->runClientWithError(Args() << "write" << "1" << "text/plain" << "X", 1) ); + RUN("separator" << " " << "read" << "0" << "1" << "2", "a b c"); + RUN("size", "3\n"); +} diff --git a/plugins/itempinned/tests/itempinnedtests.h b/plugins/itempinned/tests/itempinnedtests.h new file mode 100644 index 000000000..bb35a85b7 --- /dev/null +++ b/plugins/itempinned/tests/itempinnedtests.h @@ -0,0 +1,55 @@ +/* + Copyright (c) 2014, Lukas Holecek + + This file is part of CopyQ. + + CopyQ 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 3 of the License, or + (at your option) any later version. + + CopyQ 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 CopyQ. If not, see . +*/ + +#ifndef ITEMPINNEDTESTS_H +#define ITEMPINNEDTESTS_H + +#include "tests/testinterface.h" + +#include + +class ItemPinnedTests : public QObject +{ + Q_OBJECT +public: + explicit ItemPinnedTests(const TestInterfacePtr &test, QObject *parent = nullptr); + +private slots: + void initTestCase(); + void cleanupTestCase(); + void init(); + void cleanup(); + + void isPinned(); + void pin(); + void pinMultiple(); + void unpin(); + void unpinMultiple(); + + void removePinnedThrows(); + + void pinToRow(); + + void fullTab(); + +private: + TestInterfacePtr m_test; +}; + +#endif // ITEMPINNEDTESTS_H diff --git a/plugins/plugins.pro b/plugins/plugins.pro index 77d266016..82fb78018 100644 --- a/plugins/plugins.pro +++ b/plugins/plugins.pro @@ -7,6 +7,7 @@ SUBDIRS = \ itemfakevim \ itemimage \ itemnotes \ + itempinned \ itemtags \ itemtext \ itemsync \ diff --git a/src/item/clipboardmodel.cpp b/src/item/clipboardmodel.cpp index 2c6d2c32e..976d058d4 100644 --- a/src/item/clipboardmodel.cpp +++ b/src/item/clipboardmodel.cpp @@ -54,6 +54,14 @@ int topMostRow(const QList &indexList) } // namespace +void ClipboardItemList::move(int from, int count, int to) +{ + const auto start = std::begin(m_items) + from; + const auto end = start + count; + const auto dest = std::begin(m_items) + to + (to < from ? 1 : 0); + std::rotate(start, end, dest); +} + ClipboardModel::ClipboardModel(QObject *parent) : QAbstractListModel(parent) , m_max(100) @@ -162,6 +170,27 @@ bool ClipboardModel::removeRows(int position, int rows, const QModelIndex&) return true; } +bool ClipboardModel::moveRows( + const QModelIndex &sourceParent, int sourceRow, int rows, + const QModelIndex &destinationParent, int destinationRow) +{ + if (sourceParent.isValid() || destinationParent.isValid()) + return false; + + if (sourceRow < 0 || destinationRow < 0 || rows <= 0 || sourceRow + rows > rowCount()) + return false; + + const int last = sourceRow + rows - 1; + if (sourceRow <= destinationRow && destinationRow <= last) + return false; + + beginMoveRows(sourceParent, sourceRow, last, destinationParent, destinationRow); + m_clipboardList.move(sourceRow, rows, destinationRow); + endMoveRows(); + + return true; +} + int ClipboardModel::getRowNumber(int row, bool cycle) const { int n = rowCount(); diff --git a/src/item/clipboardmodel.h b/src/item/clipboardmodel.h index 035eb0acc..e22cc083a 100644 --- a/src/item/clipboardmodel.h +++ b/src/item/clipboardmodel.h @@ -71,6 +71,8 @@ class ClipboardItemList { m_items.insert(to, item); } + void move(int from, int count, int to); + void reserve(int maxItems) { m_items.reserve(maxItems); @@ -125,6 +127,13 @@ class ClipboardModel : public QAbstractListModel const QModelIndex &index = QModelIndex()) override; bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex()) override; + bool moveRows(const QModelIndex &sourceParent, int sourceRow, int rows, + const QModelIndex &destinationParent, int destinationRow) +#if QT_VERSION < 0x050000 + ; +#else + override; +#endif /** insert new item to model. */ void insertItem(const QVariantMap &data, int row); @@ -196,6 +205,12 @@ class ClipboardModel : public QAbstractListModel /** Emit unloaded() and unload (remove) all items. */ void unloadItems(); +public slots: +#if QT_VERSION < 0x050000 + void moveRow(int from, int to) { moveRows(QModelIndex(), from, 1, QModelIndex(), to); } +#endif + + signals: void unloaded(); void tabNameChanged(const QString &tabName);