diff --git a/plugins/itemencrypted/itemencrypted.cpp b/plugins/itemencrypted/itemencrypted.cpp index 260caf533d..04297ec6df 100644 --- a/plugins/itemencrypted/itemencrypted.cpp +++ b/plugins/itemencrypted/itemencrypted.cpp @@ -38,7 +38,7 @@ const QLatin1String dataFileHeaderV2("CopyQ_encrypted_tab v2"); const QLatin1String configEncryptTabs("encrypt_tabs"); -const int maxItemCount = 10000; +const int maxItemCount = 100'000; bool waitOrTerminate(QProcess *p, int timeoutMs) { diff --git a/plugins/itemfakevim/itemfakevim.cpp b/plugins/itemfakevim/itemfakevim.cpp index 9f026e0bc1..16fd37aeb3 100644 --- a/plugins/itemfakevim/itemfakevim.cpp +++ b/plugins/itemfakevim/itemfakevim.cpp @@ -754,7 +754,7 @@ QVariant ItemFakeVimLoader::icon() const void ItemFakeVimLoader::setEnabled(bool enabled) { - m_enabled = enabled; + ItemLoaderInterface::setEnabled(enabled); updateCurrentlyEnabledState(); } @@ -811,7 +811,7 @@ void ItemFakeVimLoader::updateCurrentlyEnabledState() if ( qobject_cast(qApp) == nullptr ) return; - const bool enable = m_enabled && m_reallyEnabled; + const bool enable = isEnabled() && m_reallyEnabled; if (m_currentlyEnabled == enable) return; diff --git a/plugins/itemfakevim/itemfakevim.h b/plugins/itemfakevim/itemfakevim.h index 1e0eec1f38..67aa5215b7 100644 --- a/plugins/itemfakevim/itemfakevim.h +++ b/plugins/itemfakevim/itemfakevim.h @@ -49,7 +49,6 @@ class ItemFakeVimLoader final : public QObject, public ItemLoaderInterface void wrapEditWidget(QObject *obj); - bool m_enabled = true; bool m_reallyEnabled = false; bool m_currentlyEnabled = false; QString m_sourceFileName; diff --git a/plugins/itemsync/filewatcher.cpp b/plugins/itemsync/filewatcher.cpp index 0afa2cf740..760de2a916 100644 --- a/plugins/itemsync/filewatcher.cpp +++ b/plugins/itemsync/filewatcher.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -29,6 +30,77 @@ const QLatin1String mimePrivatePrefix(COPYQ_MIME_PREFIX_ITEMSYNC_PRIVATE); const QLatin1String mimeOldBaseName(COPYQ_MIME_PREFIX_ITEMSYNC_PRIVATE "old-basename"); const QLatin1String mimeHashPrefix(COPYQ_MIME_PREFIX_ITEMSYNC_PRIVATE "hash"); +constexpr int itemDataTreshold = 10'000; + +class SyncDataFile { +public: + SyncDataFile() {} + + explicit SyncDataFile(const QString &path, const QString &format = QString()) + : m_path(path) + , m_format(format) + {} + + const QString &path() const { return m_path; } + void setPath(const QString &path) { m_path = path; } + + const QString &format() const { return m_format; } + void setFormat(const QString &format) { m_format = format; } + + QByteArray readAll() const + { + COPYQ_LOG( QStringLiteral("ItemSync: Reading data file: %1").arg(m_path) ); + + QFile f(m_path); + if ( !f.open(QIODevice::ReadOnly) ) + return QByteArray(); + + if ( m_format.isEmpty() ) + return f.readAll(); + + QDataStream stream(&f); + QVariantMap dataMap; + if ( !deserializeData(&stream, &dataMap) ) { + log( QStringLiteral("ItemSync: Failed to read data file \"%1\": %2") + .arg(m_path, f.errorString()), + LogError ); + return QByteArray(); + } + + return dataMap.value(m_format).toByteArray(); + } + +private: + QString m_path; + QString m_format; +}; +Q_DECLARE_METATYPE(SyncDataFile) + +QDataStream &operator<<(QDataStream &out, SyncDataFile value) +{ + return out << value.path() << value.format(); +} + +QDataStream &operator>>(QDataStream &in, SyncDataFile &value) +{ + QString path; + QString format; + in >> path >> format; + value.setPath(path); + value.setFormat(format); + return in; +} + +void registerSyncDataFileConverter() +{ + QMetaType::registerConverter(&SyncDataFile::readAll); +#if QT_VERSION < QT_VERSION_CHECK(6,0,0) + qRegisterMetaTypeStreamOperators("SyncDataFile"); +#else + qRegisterMetaType("SyncDataFile"); +#endif +} + struct Ext { Ext() : extension(), format() {} @@ -61,7 +133,7 @@ const QLatin1String noteFileSuffix("_note.txt"); const int defaultUpdateFocusItemsIntervalMs = 10000; const int batchItemUpdateIntervalMs = 100; -const qint64 sizeLimit = 10 << 20; +const qint64 sizeLimit = 50'000'000; FileFormat getFormatSettingsFromFileName(const QString &fileName, const QList &formatSettings, @@ -943,18 +1015,34 @@ void FileWatcher::updateDataAndWatchFile(const QDir &dir, const BaseNameExtensio const QString fileName = basePath + ext.extension; - QFile f( dir.absoluteFilePath(fileName) ); + const QString path = dir.absoluteFilePath(fileName); + QFile f(path); if ( !f.open(QIODevice::ReadOnly) ) continue; if ( ext.extension == dataFileSuffix ) { QDataStream stream(&f); - if ( deserializeData(&stream, dataMap) ) + QVariantMap dataMap2; + if ( deserializeData(&stream, &dataMap2) ) { + for (auto it = dataMap2.constBegin(); it != dataMap2.constEnd(); ++it) { + if (it.value().toByteArray().length() > itemDataTreshold) { + const QVariant value = QVariant::fromValue(SyncDataFile(path, it.key())); + dataMap->insert(it.key(), value); + } else { + dataMap->insert(it.key(), it.value()); + } + } + mimeToExtension->insert(mimeUnknownFormats, dataFileSuffix); + } } else if ( f.size() > sizeLimit || ext.format.startsWith(mimeNoFormat) || dataMap->contains(ext.format) ) { mimeToExtension->insert(mimeNoFormat + ext.extension, ext.extension); + } else if ( f.size() > itemDataTreshold ) { + const QVariant value = QVariant::fromValue(SyncDataFile(path)); + dataMap->insert(ext.format, value); + mimeToExtension->insert(ext.format, ext.extension); } else { dataMap->insert(ext.format, f.readAll()); mimeToExtension->insert(ext.format, ext.extension); diff --git a/plugins/itemsync/filewatcher.h b/plugins/itemsync/filewatcher.h index 99aea6c544..f6af420055 100644 --- a/plugins/itemsync/filewatcher.h +++ b/plugins/itemsync/filewatcher.h @@ -46,6 +46,11 @@ using BaseNameExtensionsList = QList; using Hash = QByteArray; +class SyncDataFile; +QDataStream &operator<<(QDataStream &out, SyncDataFile value); +QDataStream &operator>>(QDataStream &in, SyncDataFile &value); +void registerSyncDataFileConverter(); + class FileWatcher final : public QObject { public: static QString getBaseName(const QModelIndex &index); diff --git a/plugins/itemsync/itemsync.cpp b/plugins/itemsync/itemsync.cpp index 93cbfe655a..7b390a611a 100644 --- a/plugins/itemsync/itemsync.cpp +++ b/plugins/itemsync/itemsync.cpp @@ -531,6 +531,7 @@ QString ItemSyncScriptable::selectedTabPath() ItemSyncLoader::ItemSyncLoader() { + registerSyncDataFileConverter(); } ItemSyncLoader::~ItemSyncLoader() = default; diff --git a/plugins/itemsync/tests/itemsynctests.cpp b/plugins/itemsync/tests/itemsynctests.cpp index f4649b962a..3be7cdaac5 100644 --- a/plugins/itemsync/tests/itemsynctests.cpp +++ b/plugins/itemsync/tests/itemsynctests.cpp @@ -3,6 +3,7 @@ #include "itemsynctests.h" #include "common/mimetypes.h" +#include "common/sleeptimer.h" #include "tests/test_utils.h" #include @@ -768,3 +769,43 @@ void ItemSyncTests::avoidDuplicateItemsAddedFromClipboard() TEST( m_test->setClipboard("one") ); WAIT_ON_OUTPUT(args << "read(0,1,2,3)", "one,two,,"); } + +void ItemSyncTests::saveLargeItem() +{ + const auto tab = testTab(1); + const auto args = Args("tab") << tab; + + const auto script = R"( + write(0, [{ + 'text/plain': '1234567890'.repeat(10000), + 'application/x-copyq-test-data': 'abcdefghijklmnopqrstuvwxyz'.repeat(10000), + }]) + )"; + RUN(args << script, ""); + + for (int i = 0; i < 2; ++i) { + RUN(args << "read(0).left(20)", "12345678901234567890"); + RUN(args << "read(0).length", "100000\n"); + RUN(args << "getItem(0)[mimeText].left(20)", "12345678901234567890"); + RUN(args << "getItem(0)[mimeText].length", "100000\n"); + RUN(args << "getItem(0)['application/x-copyq-test-data'].left(26)", "abcdefghijklmnopqrstuvwxyz"); + RUN(args << "getItem(0)['application/x-copyq-test-data'].length", "260000\n"); + RUN("unload" << tab, tab + "\n"); + } + + RUN("show" << tab, ""); + RUN("keys" << clipboardBrowserId << keyNameFor(QKeySequence::Copy), ""); + WAIT_ON_OUTPUT("clipboard().left(20)", "12345678901234567890"); + RUN("clipboard('application/x-copyq-test-data').left(26)", "abcdefghijklmnopqrstuvwxyz"); + RUN("clipboard('application/x-copyq-test-data').length", "260000\n"); + + const auto tab2 = testTab(2); + const auto args2 = Args("tab") << tab2; + RUN("show" << tab2, ""); + waitFor(waitMsPasteClipboard); + RUN("keys" << clipboardBrowserId << keyNameFor(QKeySequence::Paste), ""); + RUN(args2 << "read(0).left(20)", "12345678901234567890"); + RUN(args2 << "read(0).length", "100000\n"); + RUN(args << "getItem(0)['application/x-copyq-test-data'].left(26)", "abcdefghijklmnopqrstuvwxyz"); + RUN(args << "getItem(0)['application/x-copyq-test-data'].length", "260000\n"); +} diff --git a/plugins/itemsync/tests/itemsynctests.h b/plugins/itemsync/tests/itemsynctests.h index 2b470c55b1..a1bc83aa1a 100644 --- a/plugins/itemsync/tests/itemsynctests.h +++ b/plugins/itemsync/tests/itemsynctests.h @@ -52,6 +52,8 @@ private slots: void avoidDuplicateItemsAddedFromClipboard(); + void saveLargeItem(); + private: TestInterfacePtr m_test; }; diff --git a/src/app/app.cpp b/src/app/app.cpp index 66e7796721..342f892217 100644 --- a/src/app/app.cpp +++ b/src/app/app.cpp @@ -7,6 +7,7 @@ #include "common/log.h" #include "common/settings.h" #include "common/textdata.h" +#include "item/serialize.h" #include "platform/platformnativeinterface.h" #ifdef Q_OS_UNIX # include "platform/unix/unixsignalhandler.h" @@ -120,6 +121,8 @@ App::App(QCoreApplication *application, , m_started(false) , m_closed(false) { + registerDataFileConverter(); + QObject::connect(m_app, &QCoreApplication::aboutToQuit, [this]() { exit(); }); #ifdef Q_OS_UNIX diff --git a/src/app/clipboardclient.cpp b/src/app/clipboardclient.cpp index 2b6098ef22..d0a560f759 100644 --- a/src/app/clipboardclient.cpp +++ b/src/app/clipboardclient.cpp @@ -9,6 +9,7 @@ #include "common/common.h" #include "common/log.h" #include "common/textdata.h" +#include "item/itemfactory.h" #include "platform/platformnativeinterface.h" #include "scriptable/scriptable.h" #include "scriptable/scriptableproxy.h" @@ -114,9 +115,14 @@ void ClipboardClient::onConnectionFailed() void ClipboardClient::start(const QStringList &arguments) { + ItemFactory itemFactory; + itemFactory.loadPlugins(); + QSettings settings; + itemFactory.loadItemFactorySettings(&settings); + QJSEngine engine; ScriptableProxy scriptableProxy(nullptr, nullptr); - Scriptable scriptable(&engine, &scriptableProxy); + Scriptable scriptable(&engine, &scriptableProxy, &itemFactory); const auto serverName = clipboardServerName(); ClientSocket socket(serverName); diff --git a/src/app/clipboardserver.cpp b/src/app/clipboardserver.cpp index 426e3c746e..fded12dab5 100644 --- a/src/app/clipboardserver.cpp +++ b/src/app/clipboardserver.cpp @@ -23,6 +23,7 @@ #include "gui/notificationbutton.h" #include "gui/notificationdaemon.h" #include "item/itemfactory.h" +#include "item/itemstore.h" #include "item/serialize.h" #include "scriptable/scriptableproxy.h" @@ -185,6 +186,8 @@ ClipboardServer::ClipboardServer(QApplication *app, const QString &sessionName) m_actionDataToSend.clear(); }); + initSingleShotTimer(&m_timerCleanItemFiles, 120000, this, &ClipboardServer::cleanDataFiles); + initSingleShotTimer(&m_updateThemeTimer, 1000, this, [this](){ AppConfig appConfig; loadSettings(&appConfig); @@ -294,6 +297,7 @@ void ClipboardServer::onAboutToQuit() terminateClients(5000); m_wnd->saveTabs(); + cleanDataFiles(); } void ClipboardServer::onCommitData(QSessionManager &sessionManager) @@ -464,6 +468,12 @@ void ClipboardServer::sendActionData(int actionId, const QByteArray &bytes) } } +void ClipboardServer::cleanDataFiles() +{ + COPYQ_LOG(QStringLiteral("Cleaning unused item files")); + ::cleanDataFiles( m_wnd->tabs() ); +} + void ClipboardServer::onClientNewConnection(const ClientSocketPtr &client) { auto proxy = new ScriptableProxy(m_wnd); @@ -632,6 +642,7 @@ bool ClipboardServer::eventFilter(QObject *object, QEvent *ev) if (state != Qt::ApplicationActive) { COPYQ_LOG( QString("Saving items on application state change (%1)").arg(state) ); m_wnd->saveTabs(); + m_timerCleanItemFiles.start(); } } else if (type == QEvent::ThemeChange) { if ( !m_updateThemeTimer.isActive() ) diff --git a/src/app/clipboardserver.h b/src/app/clipboardserver.h index 978d1a6d3f..141d76ff1f 100644 --- a/src/app/clipboardserver.h +++ b/src/app/clipboardserver.h @@ -118,6 +118,8 @@ class ClipboardServer final : public QObject, public App void sendActionData(int actionId, const QByteArray &bytes); + void cleanDataFiles(); + Server *m_server = nullptr; MainWindow* m_wnd = nullptr; QPointer m_monitor; @@ -135,6 +137,7 @@ class ClipboardServer final : public QObject, public App QMap m_actionDataToSend; QTimer m_timerClearUnsentActionData; + QTimer m_timerCleanItemFiles; struct ClientData { ClientData() = default; diff --git a/src/common/appconfig.h b/src/common/appconfig.h index 25076b979e..9e55c31ad4 100644 --- a/src/common/appconfig.h +++ b/src/common/appconfig.h @@ -13,7 +13,7 @@ QString defaultClipboardTabName(); namespace Config { -const int maxItems = 10000; +const int maxItems = 100'000; template struct Config { diff --git a/src/gui/configurationmanager.cpp b/src/gui/configurationmanager.cpp index a8da85ef21..b94ff4cbe8 100644 --- a/src/gui/configurationmanager.cpp +++ b/src/gui/configurationmanager.cpp @@ -175,7 +175,7 @@ void ConfigurationManager::initPluginWidgets(ItemFactory *itemFactory) for ( const auto &loader : itemFactory->loaders() ) { ItemOrderList::ItemPtr pluginItem(new PluginItem(loader)); const QIcon icon = getIcon(loader->icon()); - const auto state = itemFactory->isLoaderEnabled(loader) + const auto state = loader->isEnabled() ? ItemOrderList::Checked : ItemOrderList::Unchecked; m_tabItems->appendItem( loader->name(), icon, pluginItem, state ); @@ -542,7 +542,8 @@ void ConfigurationManager::apply(AppConfig *appConfig) for (int i = 0; i < m_tabItems->itemCount(); ++i) { const QString loaderId = m_tabItems->data(i).toString(); - Q_ASSERT(!loaderId.isEmpty()); + if ( loaderId.isEmpty() ) + continue; pluginPriority.append(loaderId); diff --git a/src/item/itemfactory.cpp b/src/item/itemfactory.cpp index ad45e13438..a8d3a7035b 100644 --- a/src/item/itemfactory.cpp +++ b/src/item/itemfactory.cpp @@ -29,6 +29,8 @@ namespace { +constexpr int itemDataTreshold = 10'000; + bool findPluginDir(QDir *pluginsDir) { #ifdef COPYQ_PLUGIN_PREFIX @@ -191,18 +193,22 @@ class DummySaver final : public ItemSaverInterface public: bool saveItems(const QString & /* tabName */, const QAbstractItemModel &model, QIODevice *file) override { - return serializeData(model, file); + return serializeData(model, file, itemDataTreshold); } }; class DummyLoader final : public ItemLoaderInterface { public: + int priority() const override { return std::numeric_limits::min(); } + QString id() const override { return QString(); } QString name() const override { return QString(); } QString author() const override { return QString(); } QString description() const override { return QString(); } + void setEnabled(bool) override {} + ItemWidget *create(const QVariantMap &data, QWidget *parent, bool preview) const override { return new DummyItem(data, parent, preview); @@ -254,7 +260,7 @@ ItemSaverPtr transformSaver( ItemSaverPtr newSaver = saverToTransform; for ( auto &loader : loaders ) { - if (loader != currentLoader) + if (loader != currentLoader && loader->isEnabled()) newSaver = loader->transformSaver(newSaver, model); } @@ -271,7 +277,7 @@ ItemSaverPtr saveWithOther( ItemLoaderPtr newLoader; for ( auto &loader : loaders ) { - if ( loader->canSaveItems(tabName) ) { + if ( loader->isEnabled() && loader->canSaveItems(tabName) ) { newLoader = loader; break; } @@ -294,14 +300,12 @@ ItemSaverPtr saveWithOther( return newSaver; } - } // namespace ItemFactory::ItemFactory(QObject *parent) : QObject(parent) , m_loaders() , m_dummyLoader(std::make_shared()) - , m_disabledLoaders() , m_loaderChildren() { } @@ -344,13 +348,16 @@ ItemWidget *ItemFactory::createItem( ItemWidget *ItemFactory::createItem( const QVariantMap &data, QWidget *parent, bool antialiasing, bool transform, bool preview) { - for ( auto &loader : enabledLoaders() ) { + for ( auto &loader : m_loaders ) { + if ( !loader->isEnabled() ) + continue; + ItemWidget *item = createItem(loader, data, parent, antialiasing, transform, preview); if (item != nullptr) return item; } - return nullptr; + return createSimpleItem(data, parent, antialiasing); } ItemWidget *ItemFactory::createSimpleItem( @@ -363,7 +370,10 @@ QStringList ItemFactory::formatsToSave() const { QStringList formats; - for ( const auto &loader : enabledLoaders() ) { + for (const auto &loader : m_loaders) { + if ( !loader->isEnabled() ) + continue; + for ( const auto &format : loader->formatsToSave() ) { if ( !formats.contains(format) ) formats.append(format); @@ -385,30 +395,17 @@ QStringList ItemFactory::formatsToSave() const void ItemFactory::setPluginPriority(const QStringList &pluginNames) { + m_loaders.removeOne(m_dummyLoader); std::sort( m_loaders.begin(), m_loaders.end(), PluginSorter(pluginNames) ); -} - -void ItemFactory::setLoaderEnabled(const ItemLoaderPtr &loader, bool enabled) -{ - if ( isLoaderEnabled(loader) != enabled ) { - if (enabled) - m_disabledLoaders.remove( m_disabledLoaders.indexOf(loader) ); - else - m_disabledLoaders.append(loader); - - loader->setEnabled(enabled); - } -} - -bool ItemFactory::isLoaderEnabled(const ItemLoaderPtr &loader) const -{ - return !m_disabledLoaders.contains(loader); + m_loaders.append(m_dummyLoader); } ItemSaverPtr ItemFactory::loadItems(const QString &tabName, QAbstractItemModel *model, QIODevice *file, int maxItems) { - auto loaders = enabledLoaders(); - for ( auto &loader : loaders ) { + for (auto &loader : m_loaders) { + if ( !loader->isEnabled() ) + continue; + file->seek(0); if ( loader->canLoadItems(file) ) { file->seek(0); @@ -416,8 +413,8 @@ ItemSaverPtr ItemFactory::loadItems(const QString &tabName, QAbstractItemModel * if (!saver) return nullptr; file->close(); - saver = saveWithOther(tabName, model, saver, &loader, loaders, maxItems); - return transformSaver(model, saver, loader, loaders); + saver = saveWithOther(tabName, model, saver, &loader, m_loaders, maxItems); + return transformSaver(model, saver, loader, m_loaders); } } @@ -431,11 +428,10 @@ ItemSaverPtr ItemFactory::loadItems(const QString &tabName, QAbstractItemModel * ItemSaverPtr ItemFactory::initializeTab(const QString &tabName, QAbstractItemModel *model, int maxItems) { - const auto loaders = enabledLoaders(); - for ( auto &loader : loaders ) { - if ( loader->canSaveItems(tabName) ) { + for (const auto &loader : m_loaders) { + if ( loader->isEnabled() && loader->canSaveItems(tabName) ) { const auto saver = loader->initializeTab(tabName, model, maxItems); - return saver ? transformSaver(model, saver, loader, loaders) : nullptr; + return saver ? transformSaver(model, saver, loader, m_loaders) : nullptr; } } @@ -447,8 +443,8 @@ bool ItemFactory::matches(const QModelIndex &index, const ItemFilter &filter) co if ( filter.matchesIndex(index) ) return true; - for ( const auto &loader : enabledLoaders() ) { - if ( isLoaderEnabled(loader) && loader->matches(index, filter) ) + for (const auto &loader : m_loaders) { + if ( loader->isEnabled() && loader->matches(index, filter) ) return true; } @@ -457,42 +453,21 @@ bool ItemFactory::matches(const QModelIndex &index, const ItemFilter &filter) co ItemScriptable *ItemFactory::scriptableObject(const QString &name) const { - QDir pluginsDir; - if ( !findPluginDir(&pluginsDir) ) - return nullptr; - - const QStringList nameFilters( QString::fromLatin1("*%1*").arg(name) ); - for (const auto &fileName : pluginsDir.entryList(nameFilters, QDir::Files)) { - const QString path = pluginsDir.absoluteFilePath(fileName); - auto loader = loadPlugin(path, name); - if (loader) { - QSettings settings; - settings.beginGroup("Plugins"); - loadItemFactorySettings(loader, &settings); - return loader->scriptableObject(); - } + for (const auto &loader : m_loaders) { + if (loader->id() == name) + return loader->isEnabled() ? loader->scriptableObject() : nullptr; } - return nullptr; } QVector ItemFactory::commands(bool enabled) const { -#ifdef HAS_TESTS - const QString id = qApp->property("CopyQ_test_id").toString(); - if ( !id.isEmpty() ) { - for ( const auto &loader : enabledLoaders(enabled) ) { - if (loader->id() == id) - return loader->commands(); - } - return QVector(); - } -#endif - QVector commands; - for ( const auto &loader : enabledLoaders(enabled) ) - commands << loader->commands(); + for (const auto &loader : m_loaders) { + if (loader->isEnabled() == enabled) + commands << loader->commands(); + } return commands; } @@ -514,6 +489,13 @@ bool ItemFactory::loadPlugins() if ( !findPluginDir(&pluginsDir) ) return false; + // Plugins can be loaded only once. + static bool pluginsLoaded = false; + Q_ASSERT(!pluginsLoaded); + if (pluginsLoaded) + return true; + pluginsLoaded = true; + for (const auto &fileName : pluginsDir.entryList(QDir::Files)) { const QString path = pluginsDir.absoluteFilePath(fileName); auto loader = loadPlugin(path, QString()); @@ -522,6 +504,7 @@ bool ItemFactory::loadPlugins() } std::sort(m_loaders.begin(), m_loaders.end(), priorityLessThan); + addLoader(m_dummyLoader); return true; } @@ -530,9 +513,9 @@ void ItemFactory::loadItemFactorySettings(QSettings *settings) { // load settings for each plugin settings->beginGroup("Plugins"); - for ( auto &loader : loaders() ) { + for (const auto &loader : m_loaders) { const bool enabled = loadItemFactorySettings(loader, settings); - setLoaderEnabled(loader, enabled); + loader->setEnabled(enabled); } settings->endGroup(); @@ -544,7 +527,10 @@ void ItemFactory::loadItemFactorySettings(QSettings *settings) QObject *ItemFactory::createExternalEditor(const QModelIndex &index, const QVariantMap &data, QWidget *parent) const { - for ( auto &loader : enabledLoaders() ) { + for (const auto &loader : m_loaders) { + if ( !loader->isEnabled() ) + continue; + QObject *editor = loader->createExternalEditor(index, data, parent); if (editor != nullptr) return editor; @@ -556,8 +542,8 @@ QObject *ItemFactory::createExternalEditor(const QModelIndex &index, const QVari QVariantMap ItemFactory::data(const QModelIndex &index) const { QVariantMap data = index.data(contentType::data).toMap(); - for (auto &loader : enabledLoaders()) { - if ( !loader->data(&data, index) ) + for (const auto &loader : m_loaders) { + if ( loader->isEnabled() && !loader->data(&data, index) ) return QVariantMap(); } return data; @@ -565,37 +551,23 @@ QVariantMap ItemFactory::data(const QModelIndex &index) const bool ItemFactory::setData(const QVariantMap &data, const QModelIndex &index, QAbstractItemModel *model) const { - for (auto &loader : enabledLoaders()) { - if ( loader->setData(data, index, model) ) + for (const auto &loader : m_loaders) { + if ( loader->isEnabled() && loader->setData(data, index, model) ) return true; } return model->setData(index, data, contentType::updateData); } -ItemLoaderList ItemFactory::enabledLoaders(bool enabled) const -{ - ItemLoaderList loaders; - - for (auto &loader : m_loaders) { - if ( isLoaderEnabled(loader) == enabled ) - loaders.append(loader); - } - - if (enabled) - loaders.append(m_dummyLoader); - - return loaders; -} - ItemWidget *ItemFactory::transformItem(ItemWidget *item, const QVariantMap &data) { - for (auto &loader : m_loaders) { - if ( isLoaderEnabled(loader) ) { - ItemWidget *newItem = loader->transform(item, data); - if (newItem != nullptr) - item = newItem; - } + for (const auto &loader : m_loaders) { + if ( !loader->isEnabled() ) + continue; + + ItemWidget *newItem = loader->transform(item, data); + if (newItem != nullptr) + item = newItem; } return item; @@ -642,6 +614,9 @@ ItemLoaderPtr ItemFactory::loadPlugin(const QString &path, const QString &id) co bool ItemFactory::loadItemFactorySettings(const ItemLoaderPtr &loader, QSettings *settings) const { + if (loader == m_dummyLoader) + return true; + settings->beginGroup(loader->id()); const auto enabled = settings->value("enabled", true).toBool(); @@ -649,5 +624,11 @@ bool ItemFactory::loadItemFactorySettings(const ItemLoaderPtr &loader, QSettings settings->endGroup(); +#ifdef HAS_TESTS + // For tests, enable only the tested plugin. + const QString testId = qApp->property("CopyQ_test_id").toString(); + if ( !testId.isEmpty() ) + return loader->id() == testId; +#endif return enabled; } diff --git a/src/item/itemfactory.h b/src/item/itemfactory.h index f96eecd2a6..455886cd41 100644 --- a/src/item/itemfactory.h +++ b/src/item/itemfactory.h @@ -76,16 +76,6 @@ class ItemFactory final : public QObject */ void setPluginPriority(const QStringList &pluginNames); - /** - * Enable or disable instantiation of ItemWidget objects using @a loader. - */ - void setLoaderEnabled(const ItemLoaderPtr &loader, bool enabled); - - /** - * Return true if @a loader is enabled. - */ - bool isLoaderEnabled(const ItemLoaderPtr &loader) const; - /** * Return true if no plugins were loaded. */ @@ -135,9 +125,6 @@ class ItemFactory final : public QObject /** Called if child ItemWidget destroyed. **/ void loaderChildDestroyed(QObject *obj); - /** Return enabled plugins with dummy item loader. */ - ItemLoaderList enabledLoaders(bool enabled = true) const; - /** Calls ItemLoaderInterface::transform() for all plugins in reverse order. */ ItemWidget *transformItem(ItemWidget *item, const QVariantMap &data); @@ -149,7 +136,6 @@ class ItemFactory final : public QObject ItemLoaderList m_loaders; ItemLoaderPtr m_dummyLoader; - ItemLoaderList m_disabledLoaders; QMap m_loaderChildren; }; diff --git a/src/item/itemstore.cpp b/src/item/itemstore.cpp index d361dec363..663c5f84dd 100644 --- a/src/item/itemstore.cpp +++ b/src/item/itemstore.cpp @@ -6,11 +6,14 @@ #include "common/log.h" #include "common/textdata.h" #include "item/itemfactory.h" +#include "item/serialize.h" #include #include #include #include +#include +#include namespace { @@ -65,6 +68,27 @@ ItemSaverPtr createTab( return saver; } +bool itemDataFiles(const QString &tabName, QStringList *files) +{ + const QString tabFileName = itemFileName(tabName); + if ( !QFile::exists(tabFileName) ) + return true; + + QFile tabFile(tabFileName); + if ( !tabFile.open(QIODevice::ReadOnly) ) { + printItemFileError("read tab", tabName, tabFile); + return false; + } + + return itemDataFiles(&tabFile, files); +} + +void cleanDataDir(QDir *dir) +{ + if ( dir->isEmpty() ) + QDir().rmdir( dir->absolutePath() ); +} + } // namespace ItemSaverPtr loadItems(const QString &tabName, QAbstractItemModel &model, ItemFactory *itemFactory, int maxItems) @@ -151,3 +175,44 @@ bool moveItems(const QString &oldId, const QString &newId) return false; } + +void cleanDataFiles(const QStringList &tabNames) +{ + QDir dir( QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) ); + if ( !dir.cd(QStringLiteral("items")) ) + return; + + QStringList files; + for (const QString &tabName : tabNames) { + if ( !itemDataFiles(tabName, &files) ) { + COPYQ_LOG( QStringLiteral("Stopping cleanup due to corrupted file: %1") + .arg(tabName) ); + return; + } + } + +#if QT_VERSION >= QT_VERSION_CHECK(5,14,0) + const QSet fileSet(files.constBegin(), files.constEnd()); +#else + const QSet fileSet = files.toSet(); +#endif + for ( const auto &i1 : dir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot) ) { + QDir d1(i1.absoluteFilePath()); + for ( const auto &i2 : d1.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot) ) { + QDir d2(i2.absoluteFilePath()); + for ( const auto &i3 : d2.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot) ) { + QDir d3(i3.absoluteFilePath()); + for ( const auto &f : d3.entryInfoList(QDir::Files) ) { + const QString path = f.absoluteFilePath(); + if ( !fileSet.contains(path) ) { + COPYQ_LOG( QStringLiteral("Removing: %1").arg(path) ); + QFile::remove(path); + } + } + cleanDataDir(&d3); + } + cleanDataDir(&d2); + } + cleanDataDir(&d1); + } +} diff --git a/src/item/itemstore.h b/src/item/itemstore.h index 86f1464125..eca3f0c41c 100644 --- a/src/item/itemstore.h +++ b/src/item/itemstore.h @@ -26,4 +26,6 @@ bool moveItems( const QString &newId //!< See ClipboardBrowser::getID(). ); +void cleanDataFiles(const QStringList &tabNames); + #endif // ITEMSTORE_H diff --git a/src/item/itemwidget.h b/src/item/itemwidget.h index 840ff563ea..d7dfe288ee 100644 --- a/src/item/itemwidget.h +++ b/src/item/itemwidget.h @@ -292,7 +292,8 @@ class ItemLoaderInterface */ virtual void applySettings(QSettings &) {} - virtual void setEnabled(bool) {} + virtual void setEnabled(bool enabled) { m_enabled = enabled; } + bool isEnabled() const { return m_enabled; } /** * Load stored configuration values. @@ -390,6 +391,9 @@ class ItemLoaderInterface ItemLoaderInterface(const ItemLoaderInterface &) = delete; ItemLoaderInterface &operator=(const ItemLoaderInterface &) = delete; + +private: + bool m_enabled = true; }; Q_DECLARE_INTERFACE(ItemLoaderInterface, COPYQ_PLUGIN_ITEM_LOADER_ID) diff --git a/src/item/serialize.cpp b/src/item/serialize.cpp index abb7eb8029..811328c73f 100644 --- a/src/item/serialize.cpp +++ b/src/item/serialize.cpp @@ -8,16 +8,63 @@ #include #include +#include #include +#include #include #include #include +#include +#include #include #include +class DataFile { +public: + DataFile() {} + + explicit DataFile(const QString &path) + : m_path(path) + {} + + const QString &path() const { return m_path; } + void setPath(const QString &path) { m_path = path; } + + QByteArray readAll() const + { + QFile f(m_path); + if ( !f.open(QIODevice::ReadOnly) ) { + log( QStringLiteral("Failed to read data file \"%1\": %2") + .arg(m_path, f.errorString()), + LogError ); + return QByteArray(); + } + return f.readAll(); + } + +private: + QString m_path; +}; +Q_DECLARE_METATYPE(DataFile) + +QDataStream &operator<<(QDataStream &out, DataFile value) +{ + return out << value.path(); +} + +QDataStream &operator>>(QDataStream &in, DataFile &value) +{ + QString path; + in >> path; + value.setPath(path); + return in; +} + namespace { +const QLatin1String mimeFilePrefix("FILE:"); + template bool readOrError(QDataStream *out, T *value, const char *error) { @@ -99,13 +146,15 @@ bool deserializeDataV2(QDataStream *out, QVariantMap *data) return false; QByteArray tmpBytes; - bool compress; + bool compress = false; for (qint32 i = 0; i < size; ++i) { - const QString mime = decompressMime(out); + QString mime = decompressMime(out); if ( out->status() != QDataStream::Ok ) return false; - if ( !readOrError(out, &compress, "Failed to read compression flag (v2)") ) + const bool hasDataFile = mime.startsWith(mimeFilePrefix); + + if ( !hasDataFile && !readOrError(out, &compress, "Failed to read compression flag (v2)") ) return false; if ( !readOrError(out, &tmpBytes, "Failed to read item data (v2)") ) @@ -119,15 +168,57 @@ bool deserializeDataV2(QDataStream *out, QVariantMap *data) return false; } } - data->insert(mime, tmpBytes); + + if (hasDataFile) { + mime = mime.mid( mimeFilePrefix.size() ); + const QString path = QString::fromUtf8(tmpBytes); + const QVariant value = QVariant::fromValue(DataFile(path)); + Q_ASSERT(value.canConvert()); + Q_ASSERT(value.value().path() == path); + data->insert(mime, value); + } else { + data->insert(mime, tmpBytes); + } } return out->status() == QDataStream::Ok; } +QString dataFilePath(const QByteArray &bytes, bool create = false) +{ + QDir dir( QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) ); + QCryptographicHash hash(QCryptographicHash::Sha256); + hash.addData(QByteArrayLiteral("copyq_salt")); + hash.addData(bytes); + const QString sha = QString::fromUtf8( hash.result().toHex() ); + const QString subpath = QStringLiteral("items/%1/%2/%3").arg( + sha.mid(0, 16), + sha.mid(16, 16), + sha.mid(32, 16) + ); + if (create && !dir.mkpath(subpath)) { + log( QStringLiteral("Failed to create data directory: %1") + .arg(dir.absoluteFilePath(subpath)), + LogError ); + return QString(); + } + return dir.absoluteFilePath( + QStringLiteral("%1/%2.dat").arg(subpath, sha.mid(48)) ); +} + } // namespace -void serializeData(QDataStream *stream, const QVariantMap &data) +void registerDataFileConverter() +{ + QMetaType::registerConverter(&DataFile::readAll); +#if QT_VERSION < QT_VERSION_CHECK(6,0,0) + qRegisterMetaTypeStreamOperators("DataFile"); +#else + qRegisterMetaType("DataFile"); +#endif +} + +void serializeData(QDataStream *stream, const QVariantMap &data, int itemDataTreshold) { *stream << static_cast(-2); @@ -138,9 +229,35 @@ void serializeData(QDataStream *stream, const QVariantMap &data) for (auto it = data.constBegin(); it != data.constEnd(); ++it) { const auto &mime = it.key(); bytes = data[mime].toByteArray(); - *stream << compressMime(mime) - << /* compressData = */ false - << bytes; + if ( (itemDataTreshold >= 0 && bytes.length() > itemDataTreshold) || mime.startsWith(mimeFilePrefix) ) { + const QString path = dataFilePath(bytes, true); + if ( path.isEmpty() ) { + stream->setStatus(QDataStream::WriteFailed); + return; + } + + if ( !QFile::exists(path) ) { + QSaveFile f(path); + f.setDirectWriteFallback(false); + if ( !f.open(QIODevice::WriteOnly) || !f.write(bytes) || !f.commit() ) { + log( QStringLiteral("Failed to create data file \"%1\": %2") + .arg(path, f.errorString()), + LogError ); + stream->setStatus(QDataStream::WriteFailed); + f.cancelWriting(); + return; + } + } + + if ( mime.startsWith(mimeFilePrefix) ) + *stream << compressMime(mime) << path.toUtf8(); + else + *stream << compressMime(mimeFilePrefix + mime) << path.toUtf8(); + } else { + *stream << compressMime(mime) + << /* compressData = */ false + << bytes; + } } } @@ -204,13 +321,15 @@ bool deserializeData(QVariantMap *data, const QByteArray &bytes) return deserializeData(&out, data); } -bool serializeData(const QAbstractItemModel &model, QDataStream *stream) +bool serializeData(const QAbstractItemModel &model, QDataStream *stream, int itemDataTreshold) { qint32 length = model.rowCount(); *stream << length; - for(qint32 i = 0; i < length && stream->status() == QDataStream::Ok; ++i) - serializeData( stream, model.data(model.index(i, 0), contentType::data).toMap() ); + for(qint32 i = 0; i < length && stream->status() == QDataStream::Ok; ++i) { + const QVariantMap data = model.data(model.index(i, 0), contentType::data).toMap(); + serializeData(stream, data, itemDataTreshold); + } return stream->status() == QDataStream::Ok; } @@ -248,11 +367,11 @@ bool deserializeData(QAbstractItemModel *model, QDataStream *stream, int maxItem return stream->status() == QDataStream::Ok; } -bool serializeData(const QAbstractItemModel &model, QIODevice *file) +bool serializeData(const QAbstractItemModel &model, QIODevice *file, int itemDataTreshold) { QDataStream stream(file); stream.setVersion(QDataStream::Qt_4_7); - return serializeData(model, &stream); + return serializeData(model, &stream, itemDataTreshold); } bool deserializeData(QAbstractItemModel *model, QIODevice *file, int maxItems) @@ -261,3 +380,54 @@ bool deserializeData(QAbstractItemModel *model, QIODevice *file, int maxItems) stream.setVersion(QDataStream::Qt_4_7); return deserializeData(model, &stream, maxItems); } + +bool itemDataFiles(QIODevice *file, QStringList *files) +{ + QDataStream out(file); + out.setVersion(QDataStream::Qt_4_7); + + qint32 length; + if ( !readOrError(&out, &length, "Failed to read length") ) + return false; + + if (length < 0) { + log("Corrupted data: Invalid length", LogError); + return false; + } + + for(qint32 i = 0; i < length; ++i) { + qint32 version; + if ( !readOrError(&out, &version, "Failed to read version") ) + return false; + + if (version != -2) + return true; + + qint32 size; + if ( !readOrError(&out, &size, "Failed to read size (v2)") ) + return false; + + QByteArray tmpBytes; + bool compress; + for (qint32 j = 0; j < size; ++j) { + QString mime = decompressMime(&out); + if ( out.status() != QDataStream::Ok ) + return false; + + const bool hasDataFile = mime.startsWith(mimeFilePrefix); + + if ( !hasDataFile && !readOrError(&out, &compress, "Failed to read compression flag (v2)") ) + return false; + + if ( !readOrError(&out, &tmpBytes, "Failed to read item data (v2)") ) + return false; + + if (hasDataFile) { + const QString path = QString::fromUtf8(tmpBytes); + files->append(path); + } + } + } + + return out.status() == QDataStream::Ok; +} diff --git a/src/item/serialize.h b/src/item/serialize.h index f27a905369..775a423217 100644 --- a/src/item/serialize.h +++ b/src/item/serialize.h @@ -10,14 +10,21 @@ class QByteArray; class QDataStream; class QIODevice; -void serializeData(QDataStream *stream, const QVariantMap &data); +class DataFile; +QDataStream &operator<<(QDataStream &out, DataFile value); +QDataStream &operator>>(QDataStream &in, DataFile &value); +void registerDataFileConverter(); + +void serializeData(QDataStream *stream, const QVariantMap &data, int itemDataTreshold = -1); bool deserializeData(QDataStream *stream, QVariantMap *data); QByteArray serializeData(const QVariantMap &data); bool deserializeData(QVariantMap *data, const QByteArray &bytes); -bool serializeData(const QAbstractItemModel &model, QDataStream *stream); +bool serializeData(const QAbstractItemModel &model, QDataStream *stream, int itemDataTreshold = -1); bool deserializeData(QAbstractItemModel *model, QDataStream *stream, int maxItems); -bool serializeData(const QAbstractItemModel &model, QIODevice *file); +bool serializeData(const QAbstractItemModel &model, QIODevice *file, int itemDataTreshold = -1); bool deserializeData(QAbstractItemModel *model, QIODevice *file, int maxItems); +bool itemDataFiles(QIODevice *file, QStringList *files); + #endif // SERIALIZE_H diff --git a/src/main.cpp b/src/main.cpp index b7d770d0f4..d662686b68 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -39,7 +39,7 @@ int evaluate( setLogLabel("Prompt"); QJSEngine engine; - Scriptable scriptable(&engine, nullptr); + Scriptable scriptable(&engine, nullptr, nullptr); QJSValue function = engine.globalObject().property(functionName); QJSValueList functionArguments; diff --git a/src/scriptable/scriptable.cpp b/src/scriptable/scriptable.cpp index af61985cbd..41bbf52211 100644 --- a/src/scriptable/scriptable.cpp +++ b/src/scriptable/scriptable.cpp @@ -225,7 +225,9 @@ struct ScriptValueFactory { QJSValueIterator it(value); while ( it.hasNext() ) { it.next(); - const auto itemValue = ::fromScriptValue( it.value(), scriptable ); + auto itemValue = ::fromScriptValue( it.value(), scriptable ); + if (itemValue.type() == QVariant::String) + itemValue = itemValue.toString().toUtf8(); result.insert(it.name(), itemValue); } return result; @@ -416,6 +418,9 @@ struct ScriptValueFactory { if (variant.canConvert()) return ::toScriptValue(variant.value(), scriptable); + if (variant.canConvert()) + return ::toScriptValue(variant.toByteArray(), scriptable); + return scriptable->engine()->toScriptValue(variant); } @@ -648,10 +653,12 @@ QJSValue fromUnicode(const QString &text, const QJSValue &codecName, Scriptable Scriptable::Scriptable( QJSEngine *engine, ScriptableProxy *proxy, + ItemFactory *factory, QObject *parent) : QObject(parent) , m_proxy(proxy) , m_engine(engine) + , m_factory(factory) , m_inputSeparator("\n") , m_input() { @@ -890,9 +897,9 @@ void Scriptable::setUncaughtException(const QJSValue &exc) QJSValue Scriptable::getPlugins() { // Load plugins on demand. - if ( m_plugins.isUndefined() ) { + if ( m_plugins.isUndefined() && m_factory ) { #if QT_VERSION >= QT_VERSION_CHECK(5,10,0) - m_plugins = m_engine->newQObject(new ScriptablePlugins(this)); + m_plugins = m_engine->newQObject(new ScriptablePlugins(this, m_factory)); m_engine->globalObject().setProperty(QStringLiteral("_copyqPlugins"), m_plugins); m_plugins = evaluateStrict(m_engine, QStringLiteral( "new Proxy({}, { get: function(_, name, _) { return _copyqPlugins.load(name); } });" @@ -900,11 +907,7 @@ QJSValue Scriptable::getPlugins() #else m_plugins = m_engine->newObject(); m_engine->globalObject().setProperty(QStringLiteral("_copyqPlugins"), m_plugins); - ItemFactory factory; - QSettings settings; - factory.loadPlugins(); - factory.loadItemFactorySettings(&settings); - for (const ItemLoaderPtr &loader : factory.loaders()) { + for (const ItemLoaderPtr &loader : m_factory->loaders()) { const auto obj = loader->scriptableObject(); if (!obj) continue; @@ -2821,13 +2824,10 @@ void Scriptable::provideSelection() QJSValue Scriptable::clipboardFormatsToSave() { - ItemFactory factory; - factory.loadPlugins(); - - QSettings settings; - factory.loadItemFactorySettings(&settings); + if (!m_factory) + return toScriptValue(QStringList(), this); - QStringList formats = factory.formatsToSave(); + QStringList formats = m_factory->formatsToSave(); COPYQ_LOG( "Clipboard formats to save: " + formats.join(", ") ); for (const auto &command : m_proxy->automaticCommands()) { @@ -3931,9 +3931,10 @@ QJSValue NetworkReply::toScriptValue() return m_self; } -ScriptablePlugins::ScriptablePlugins(Scriptable *scriptable) +ScriptablePlugins::ScriptablePlugins(Scriptable *scriptable, ItemFactory *factory) : QObject(scriptable) , m_scriptable(scriptable) + , m_factory(factory) { } @@ -3943,9 +3944,6 @@ QJSValue ScriptablePlugins::load(const QString &name) if (it != std::end(m_plugins)) return it.value(); - if (!m_factory) - m_factory = new ItemFactory(this); - auto obj = m_factory->scriptableObject(name); if (!obj) { m_scriptable->throwError( diff --git a/src/scriptable/scriptable.h b/src/scriptable/scriptable.h index 41fcd7fcd0..cd5962f331 100644 --- a/src/scriptable/scriptable.h +++ b/src/scriptable/scriptable.h @@ -58,9 +58,10 @@ class Scriptable final : public QObject Q_PROPERTY(QJSValue _initItemSelection READ getUndefined WRITE initItemSelection) public: - explicit Scriptable( + Scriptable( QJSEngine *engine, ScriptableProxy *proxy, + ItemFactory *factory = nullptr, QObject *parent = nullptr); enum class Abort { @@ -451,6 +452,7 @@ public slots: ScriptableProxy *m_proxy; QJSEngine *m_engine; + ItemFactory *m_factory; QJSValue m_temporaryFileClass; QString m_inputSeparator; QJSValue m_input; @@ -532,14 +534,14 @@ class ScriptablePlugins final : public QObject { Q_OBJECT public: - explicit ScriptablePlugins(Scriptable *scriptable); + ScriptablePlugins(Scriptable *scriptable, ItemFactory *factory); public slots: QJSValue load(const QString &name); private: - ItemFactory *m_factory = nullptr; Scriptable *m_scriptable; + ItemFactory *m_factory; QMap m_plugins; }; diff --git a/src/tests/test_utils.h b/src/tests/test_utils.h index f634b281f5..858ceda95e 100644 --- a/src/tests/test_utils.h +++ b/src/tests/test_utils.h @@ -6,6 +6,7 @@ #include "common/commandstatus.h" #include +#include #include #include #include @@ -92,4 +93,9 @@ inline QString testTab(int i) return "Tab_&" + QString::number(i); } +inline QString keyNameFor(QKeySequence::StandardKey standardKey) +{ + return QKeySequence(standardKey).toString(); +} + #endif // TEST_UTILS_H diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp index 3c6dc89399..80910da7e0 100644 --- a/src/tests/tests.cpp +++ b/src/tests/tests.cpp @@ -768,11 +768,6 @@ class TestInterfaceImpl final : public TestInterface { PlatformClipboardPtr m_clipboard; }; -QString keyNameFor(QKeySequence::StandardKey standardKey) -{ - return QKeySequence(standardKey).toString(); -} - int count(const QStringList &items, const QString &pattern) { int from = -1; @@ -4436,6 +4431,46 @@ void Tests::avoidStoringPasswords() RUN("count", "0\n"); } +void Tests::saveLargeItem() +{ + const auto tab = testTab(1); + const auto args = Args("tab") << tab; + + const auto script = R"( + write(0, [{ + 'text/plain': '1234567890'.repeat(10000), + 'application/x-copyq-test-data': 'abcdefghijklmnopqrstuvwxyz'.repeat(10000), + }]) + )"; + RUN(args << script, ""); + + for (int i = 0; i < 2; ++i) { + RUN(args << "read(0).left(20)", "12345678901234567890"); + RUN(args << "read(0).length", "100000\n"); + RUN(args << "getItem(0)[mimeText].length", "100000\n"); + RUN(args << "getItem(0)[mimeText].left(20)", "12345678901234567890"); + RUN(args << "getItem(0)['application/x-copyq-test-data'].left(26)", "abcdefghijklmnopqrstuvwxyz"); + RUN(args << "getItem(0)['application/x-copyq-test-data'].length", "260000\n"); + RUN("unload" << tab, tab + "\n"); + } + + RUN("show" << tab, ""); + RUN("keys" << clipboardBrowserId << keyNameFor(QKeySequence::Copy), ""); + WAIT_ON_OUTPUT("clipboard().left(20)", "12345678901234567890"); + RUN("clipboard('application/x-copyq-test-data').left(26)", "abcdefghijklmnopqrstuvwxyz"); + RUN("clipboard('application/x-copyq-test-data').length", "260000\n"); + + const auto tab2 = testTab(2); + const auto args2 = Args("tab") << tab2; + RUN("show" << tab2, ""); + waitFor(waitMsPasteClipboard); + RUN("keys" << clipboardBrowserId << keyNameFor(QKeySequence::Paste), ""); + RUN(args2 << "read(0).left(20)", "12345678901234567890"); + RUN(args2 << "read(0).length", "100000\n"); + RUN(args << "getItem(0)['application/x-copyq-test-data'].left(26)", "abcdefghijklmnopqrstuvwxyz"); + RUN(args << "getItem(0)['application/x-copyq-test-data'].length", "260000\n"); +} + int Tests::run( const QStringList &arguments, QByteArray *stdoutData, QByteArray *stderrData, const QByteArray &in, const QStringList &environment) diff --git a/src/tests/tests.h b/src/tests/tests.h index fcb8e9a70f..06c6efaf51 100644 --- a/src/tests/tests.h +++ b/src/tests/tests.h @@ -292,6 +292,8 @@ private slots: void avoidStoringPasswords(); + void saveLargeItem(); + private: void clearServerErrors(); int run(const QStringList &arguments, QByteArray *stdoutData = nullptr, diff --git a/src/ui/configtabhistory.ui b/src/ui/configtabhistory.ui index d810329224..2d15535543 100644 --- a/src/ui/configtabhistory.ui +++ b/src/ui/configtabhistory.ui @@ -77,7 +77,7 @@ Maximum number of items in each tab - 10000 + 100000 200 diff --git a/src/ui/tabpropertieswidget.ui b/src/ui/tabpropertieswidget.ui index 995f41a8d8..b3a8e82bbf 100644 --- a/src/ui/tabpropertieswidget.ui +++ b/src/ui/tabpropertieswidget.ui @@ -56,7 +56,7 @@ default - 10000 + 100000 200