diff --git a/docs/scripting-api.rst b/docs/scripting-api.rst index 1261538ddd..3975338a6f 100644 --- a/docs/scripting-api.rst +++ b/docs/scripting-api.rst @@ -1622,6 +1622,39 @@ unlike in GUI, where row numbers start from 1 by default. config("style", styleName) +.. js:function:: onItemsAdded() + + Called when items are added to a tab. + + The target tab is returned by `selectedTab()`. + + The new items can be accessed with `selectedItemsData()`, + `selectedItemData()`, `selectedItems()` and `ItemSelection().current()`. + +.. js:function:: onItemsRemoved() + + Called when items are being removed from a tab. + + The target tab is returned by `selectedTab()`. + + The removed items can be accessed with `selectedItemsData()`, + `selectedItemData()`, `selectedItems()` and `ItemSelection().current()`. + +.. js:function:: onSelectedTabChanged() + + Called currently selected tab changes. + + The newly selected tab is returned by `selectedTab()`. + + The changed items can be accessed with `selectedItemsData()`, + `selectedItemData()`, `selectedItems()` and `ItemSelection().current()`. + +.. js:function:: onItemsLoaded() + + Called when items a loaded to a tab. + + The target tab is returned by `selectedTab()`. + Types ----- diff --git a/src/gui/clipboardbrowser.cpp b/src/gui/clipboardbrowser.cpp index 9a9e509ea8..f51a555064 100644 --- a/src/gui/clipboardbrowser.cpp +++ b/src/gui/clipboardbrowser.cpp @@ -1671,6 +1671,7 @@ bool ClipboardBrowser::loadItems() return false; d.rowsInserted(QModelIndex(), 0, m.rowCount()); + emit itemsLoaded(this); if ( hasFocus() ) setCurrent(0); onItemCountChanged(); diff --git a/src/gui/clipboardbrowser.h b/src/gui/clipboardbrowser.h index 1010850605..c37ef88633 100644 --- a/src/gui/clipboardbrowser.h +++ b/src/gui/clipboardbrowser.h @@ -242,6 +242,8 @@ class ClipboardBrowser final : public QListView void itemsChanged(const ClipboardBrowser *self); + void itemsLoaded(const ClipboardBrowser *self); + void itemSelectionChanged(const ClipboardBrowser *self); void internalEditorStateChanged(const ClipboardBrowser *self); diff --git a/src/gui/commandcompleterdocumentation.h b/src/gui/commandcompleterdocumentation.h index 8727574154..0009889b18 100644 --- a/src/gui/commandcompleterdocumentation.h +++ b/src/gui/commandcompleterdocumentation.h @@ -118,6 +118,7 @@ void addDocumentation(AddDocumentationCallback addDocumentation) addDocumentation("open", "open(url, ...) -> bool", "Tries to open URLs in appropriate applications."); addDocumentation("execute", "execute(argument, ..., null, stdinData, ...) -> `FinishedCommand` or `undefined`", "Executes a command."); addDocumentation("currentWindowTitle", "String currentWindowTitle() -> string", "Returns window title of currently focused window."); + addDocumentation("currentClipboardOwner", "String currentClipboardOwner() -> string", "Returns name of the current clipboard owner."); addDocumentation("dialog", "dialog(...)", "Shows messages or asks user for input."); addDocumentation("menuItems", "menuItems(text...) -> string", "Opens menu with given items and returns selected item or an empty string."); addDocumentation("menuItems", "menuItems(items[]) -> int", "Opens menu with given items and returns index of selected item or -1."); @@ -171,6 +172,10 @@ void addDocumentation(AddDocumentationCallback addDocumentation) addDocumentation("hideDataNotification", "hideDataNotification()", "Hide notification for current data."); addDocumentation("setClipboardData", "setClipboardData()", "Sets clipboard data for menu commands."); addDocumentation("styles", "styles() -> array of strings", "List available styles for `style` option."); + addDocumentation("onItemsAdded", "onItemsAdded()", "Called when items are added to a tab."); + addDocumentation("onItemsRemoved", "onItemsRemoved()", "Called when items are being removed from a tab."); + addDocumentation("onSelectedTabChanged", "onSelectedTabChanged()", "Called currently selected tab changes."); + addDocumentation("onItemsLoaded", "onItemsLoaded()", "Called when items a loaded to a tab."); addDocumentation("ByteArray", "ByteArray", "Wrapper for QByteArray Qt class."); addDocumentation("File", "File", "Wrapper for QFile Qt class."); addDocumentation("Dir", "Dir", "Wrapper for QDir Qt class."); diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index ac0b8e0f88..5f6f2742cf 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -187,6 +187,18 @@ void disableActionWhenTabGroupSelected(WidgetOrAction *action, MainWindow *windo action, &WidgetOrAction::setDisabled ); } +void addSelectionData( + QVariantMap *result, + const QModelIndexList &selectedIndexes) +{ + QList selected; + selected.reserve(selectedIndexes.size()); + for (const auto &index : selectedIndexes) + selected.append(index); + std::sort(selected.begin(), selected.end()); + result->insert(mimeSelectedItems, QVariant::fromValue(selected)); +} + /// Adds information about current tab and selection if command is triggered by user. QVariantMap addSelectionData( const ClipboardBrowser &c, @@ -203,12 +215,7 @@ QVariantMap addSelectionData( } if ( !selectedIndexes.isEmpty() ) { - QList selected; - selected.reserve(selectedIndexes.size()); - for (const auto &index : selectedIndexes) - selected.append(index); - std::sort(selected.begin(), selected.end()); - result.insert(mimeSelectedItems, QVariant::fromValue(selected)); + addSelectionData(&result, selectedIndexes); } return result; @@ -1073,9 +1080,16 @@ void MainWindow::setScriptOverrides(const QVector &overrides) bool MainWindow::isScriptOverridden(int id) const { + waitCollectOverrides(); return std::binary_search(m_overrides.begin(), m_overrides.end(), id); } +void MainWindow::waitCollectOverrides() const +{ + if (m_actionCollectOverrides && m_actionCollectOverrides->isRunning()) + m_actionCollectOverrides->waitForFinished(); +} + void MainWindow::onAboutToQuit() { if (cm) @@ -1253,6 +1267,8 @@ void MainWindow::onBrowserCreated(ClipboardBrowser *browser) this, &MainWindow::onSearchShowRequest ); connect( browser, &ClipboardBrowser::itemWidgetCreated, this, &MainWindow::onItemWidgetCreated ); + connect( browser, &ClipboardBrowser::itemsLoaded, + this, &MainWindow::onBrowserItemsLoaded ); if (browserOrNull() == browser) { const int index = ui->tabWidget->currentIndex(); @@ -1268,6 +1284,34 @@ void MainWindow::onBrowserDestroyed(ClipboardBrowserPlaceholder *placeholder) } } +void MainWindow::onBrowserItemsLoaded(const ClipboardBrowser *browser) +{ + if (isScriptOverridden(ScriptOverrides::OnItemsLoaded)) { + runEventHandlerScript( + QStringLiteral("onItemsLoaded()"), + createDataMap(mimeCurrentTab, browser->tabName())); + } + + connect( browser->model(), &QAbstractItemModel::rowsAboutToBeRemoved, + browser, [this, browser](const QModelIndex &, int first, int last) { + if (isScriptOverridden(ScriptOverrides::OnItemsRemoved)) + runItemHandlerScript(QStringLiteral("onItemsRemoved()"), browser, first, last); + } ); + connect( browser->model(), &QAbstractItemModel::rowsInserted, + browser, [this, browser](const QModelIndex &, int first, int last) { + if (isScriptOverridden(ScriptOverrides::OnItemsAdded)) + runItemHandlerScript(QStringLiteral("onItemsAdded()"), browser, first, last); + } ); + connect( browser->model(), &QAbstractItemModel::dataChanged, + browser, [this, browser](const QModelIndex &topLeft, const QModelIndex &bottomRight) { + if (isScriptOverridden(ScriptOverrides::OnItemsChanged)) { + runItemHandlerScript( + QStringLiteral("onItemsChanged()"), + browser, topLeft.row(), bottomRight.row()); + } + } ); +} + void MainWindow::onItemSelectionChanged(const ClipboardBrowser *browser) { if (browser == browserOrNull()) { @@ -1346,7 +1390,7 @@ void MainWindow::runDisplayCommands() if ( !isInternalActionId(m_displayActionId) ) { m_currentDisplayItem = m_displayItemList.takeFirst(); - const auto action = runScript("runDisplayCommands()", m_currentDisplayItem.data()); + const auto action = runScript(QStringLiteral("runDisplayCommands()"), m_currentDisplayItem.data()); m_displayActionId = action->id(); } @@ -1613,7 +1657,7 @@ void MainWindow::runMenuCommandFilters(MenuMatchCommands *menuMatchCommands, QVa if (isRunning) { m_sharedData->actions->setActionData(menuMatchCommands->actionId, data); } else { - const auto act = runScript("runMenuCommandFilters()", data); + const auto act = runScript(QStringLiteral("runMenuCommandFilters()"), data); menuMatchCommands->actionId = act->id(); } @@ -1884,7 +1928,7 @@ void MainWindow::activateMenuItem(ClipboardBrowserPlaceholder *placeholder, cons if ( m_options.trayItemPaste && !omitPaste && canPaste() ) { if (isScriptOverridden(ScriptOverrides::Paste)) { COPYQ_LOG("Pasting item with paste()"); - runScript("paste()"); + runScript(QStringLiteral("paste()")); } else if (lastWindow) { COPYQ_LOG( QStringLiteral("Pasting item from tray menu to: %1") .arg(lastWindow->getTitle()) ); @@ -2348,7 +2392,8 @@ void MainWindow::updateCommands(QVector allCommands, bool forceSave) reloadBrowsers(); } - runScript("collectOverrides()"); + waitCollectOverrides(); + m_actionCollectOverrides = runScript(QStringLiteral("collectOverrides()")); updateContextMenu(contextMenuUpdateIntervalMsec); updateTrayMenuCommands(); @@ -2404,12 +2449,43 @@ const Theme &MainWindow::theme() const Action *MainWindow::runScript(const QString &script, const QVariantMap &data) { auto act = new Action(); - act->setCommand(QStringList() << "copyq" << "eval" << "--" << script); + act->setCommand( + {QStringLiteral("copyq"), QStringLiteral("eval"), QStringLiteral("--"), script}); act->setData(data); runInternalAction(act); return act; } +void MainWindow::runEventHandlerScript(const QString &script, const QVariantMap &data) +{ + if (m_maxEventHandlerScripts == 0) + return; + + --m_maxEventHandlerScripts; + if (m_maxEventHandlerScripts == 0) + log("Event handler maximum recursion reached", LogWarning); + + const auto action = runScript(script, data); + action->waitForFinished(); + ++m_maxEventHandlerScripts; +} + +void MainWindow::runItemHandlerScript( + const QString &script, const ClipboardBrowser *browser, int firstRow, int lastRow) +{ + QModelIndexList indexes; + indexes.reserve(lastRow - firstRow + 1); + for (int row = firstRow; row <= lastRow; ++row) { + const auto index = browser->model()->index(row, 0); + if (index.isValid()) + indexes.append(index); + } + + QVariantMap data = createDataMap(mimeCurrentTab, browser->tabName()); + addSelectionData(&data, indexes); + runEventHandlerScript(script, data); +} + int MainWindow::findTabIndex(const QString &name) { TabWidget *w = ui->tabWidget; @@ -2984,6 +3060,12 @@ void MainWindow::tabChanged(int current, int) } setTabOrder(ui->searchBar, c); + + if (isScriptOverridden(ScriptOverrides::OnSelectedTabChanged)) { + runEventHandlerScript( + QStringLiteral("onSelectedTabChanged()"), + createDataMap(mimeCurrentTab, c->tabName())); + } } } @@ -3333,7 +3415,7 @@ void MainWindow::activateCurrentItemHelper() if (paste) { if (isScriptOverridden(ScriptOverrides::Paste)) { COPYQ_LOG("Pasting item with paste()"); - runScript("paste()"); + runScript(QStringLiteral("paste()")); } else if (lastWindow) { COPYQ_LOG( QStringLiteral("Pasting item from main window to: %1") .arg(lastWindow->getTitle()) ); @@ -3366,7 +3448,7 @@ void MainWindow::disableClipboardStoring(bool disable) updateIcon(); - runScript("setTitle(); showDataNotification()"); + runScript(QStringLiteral("setTitle(); showDataNotification()")); COPYQ_LOG( QString("Clipboard monitoring %1.") .arg(m_clipboardStoringDisabled ? "disabled" : "enabled") ); diff --git a/src/gui/mainwindow.h b/src/gui/mainwindow.h index 7bf762f473..705013161d 100644 --- a/src/gui/mainwindow.h +++ b/src/gui/mainwindow.h @@ -414,6 +414,7 @@ class MainWindow final : public QMainWindow void setScriptOverrides(const QVector &overrides); bool isScriptOverridden(int id) const; + void waitCollectOverrides() const; signals: /** Request clipboard change. */ @@ -500,6 +501,7 @@ class MainWindow final : public QMainWindow void onBrowserCreated(ClipboardBrowser *browser); void onBrowserDestroyed(ClipboardBrowserPlaceholder *placeholder); + void onBrowserItemsLoaded(const ClipboardBrowser *browser); void onItemSelectionChanged(const ClipboardBrowser *browser); void onItemsChanged(const ClipboardBrowser *browser); @@ -630,6 +632,9 @@ class MainWindow final : public QMainWindow const Theme &theme() const; Action *runScript(const QString &script, const QVariantMap &data = QVariantMap()); + void runEventHandlerScript(const QString &script, const QVariantMap &data = QVariantMap()); + void runItemHandlerScript( + const QString &script, const ClipboardBrowser *browser, int firstRow, int lastRow); void activateCurrentItemHelper(); void onItemClicked(); @@ -699,6 +704,8 @@ class MainWindow final : public QMainWindow bool m_enteringSearchMode = false; QVector m_overrides; + int m_maxEventHandlerScripts = 10; + QPointer m_actionCollectOverrides; }; #endif // MAINWINDOW_H diff --git a/src/scriptable/scriptable.cpp b/src/scriptable/scriptable.cpp index 356d696579..f51d53a30d 100644 --- a/src/scriptable/scriptable.cpp +++ b/src/scriptable/scriptable.cpp @@ -2954,8 +2954,18 @@ void Scriptable::collectOverrides() QVector overrides; const auto pasteFn = globalObject.property("paste"); - if (pasteFn.property("_copyq").toInt() != 1) + if (pasteFn.property(QStringLiteral("_copyq")).toInt() != 1) overrides.append(ScriptOverrides::Paste); + if (globalObject.hasOwnProperty(QStringLiteral("onItemsAdded"))) + overrides.append(ScriptOverrides::OnItemsAdded); + if (globalObject.hasOwnProperty(QStringLiteral("onItemsRemoved"))) + overrides.append(ScriptOverrides::OnItemsRemoved); + if (globalObject.hasOwnProperty(QStringLiteral("onItemsChanged"))) + overrides.append(ScriptOverrides::OnItemsChanged); + if (globalObject.hasOwnProperty(QStringLiteral("onSelectedTabChanged"))) + overrides.append(ScriptOverrides::OnSelectedTabChanged); + if (globalObject.hasOwnProperty(QStringLiteral("onItemsLoaded"))) + overrides.append(ScriptOverrides::OnItemsLoaded); m_proxy->setScriptOverrides(overrides); } diff --git a/src/scriptable/scriptoverrides.h b/src/scriptable/scriptoverrides.h index 4197f630d0..0d9bc80f16 100644 --- a/src/scriptable/scriptoverrides.h +++ b/src/scriptable/scriptoverrides.h @@ -4,5 +4,10 @@ namespace ScriptOverrides { enum ScriptOverrides { Paste = 0, + OnItemsAdded = 1, + OnItemsRemoved = 2, + OnItemsChanged = 3, + OnSelectedTabChanged = 4, + OnItemsLoaded = 5, }; } diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp index 644840f30d..024fef4471 100644 --- a/src/tests/tests.cpp +++ b/src/tests/tests.cpp @@ -152,6 +152,8 @@ bool testStderr(const QByteArray &stderrData, TestInterface::ReadStderrFlag flag static const std::vector ignoreList{ regex(R"(CopyQ Note \[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}\] setEnv("COPYQ_TEST_THROW", "0"); } +void Tests::scriptPaste() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: 'global.paste = function() { add("PASTE") }' + }, + ]) + )"; + RUN(script, ""); + RUN("add(1)", ""); + RUN("keys" << clipboardBrowserId << "ENTER", ""); + WAIT_ON_OUTPUT("read(0)", "PASTE"); +} + +void Tests::scriptOnSelectedTabChanged() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: 'global.onSelectedTabChanged = function() { add(selectedTab()) }' + }, + ]) + )"; + RUN(script, ""); + RUN("add" << "END", ""); + + const auto tab1 = testTab(1); + const auto tab2 = testTab(2); + RUN("show" << tab1, ""); + WAIT_ON_OUTPUT("tab" << tab1 << "read(0)", tab1); + RUN("show" << tab2, ""); + WAIT_ON_OUTPUT("tab" << tab2 << "read(0)", tab2); +} + +void Tests::scriptOnItemsRemoved() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: ` + global.onItemsRemoved = function() { + items = ItemSelection().current().items(); + tab(tab()[0]); + add("R0:" + str(items[0][mimeText])); + add("R1:" + str(items[1][mimeText])); + } + ` + }, + ]) + )"; + RUN(script, ""); + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add(3,2,1,0)", ""); + RUN("tab" << tab1 << "remove(1,2)", ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2,)", "R1:2,R0:1,"); +} + +void Tests::scriptOnItemsAdded() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: ` + global.onItemsAdded = function() { + if (selectedTab() == tab()[0]) abort(); + items = ItemSelection().current().items(); + tab(tab()[0]); + add("A:" + str(items[0][mimeText])); + } + ` + }, + ]) + )"; + RUN(script, ""); + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add(1,0)", ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", "A:0,A:1,"); +} + +void Tests::scriptOnItemsChanged() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: ` + global.onItemsChanged = function() { + if (selectedTab() == tab()[0]) abort(); + items = ItemSelection().current().items(); + tab(tab()[0]); + add("C:" + str(items[0][mimeText])); + } + ` + }, + ]) + )"; + RUN(script, ""); + const auto tab1 = testTab(1); + RUN("tab" << tab1 << "add(0)", ""); + RUN("tab" << tab1 << "change(0, mimeText, 'A')", ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", "C:A,,"); + RUN("tab" << tab1 << "change(0, mimeText, 'B')", ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", "C:B,C:A,"); +} + +void Tests::scriptOnItemsLoaded() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: ` + global.onItemsLoaded = function() { + if (selectedTab() == tab()[0]) abort(); + tab(tab()[0]); + add(selectedTab()); + } + ` + }, + ]) + )"; + RUN(script, ""); + + const auto tab1 = testTab(1); + RUN("show" << tab1, ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", tab1 + ",,"); + + const auto tab2 = testTab(2); + RUN("show" << tab2, ""); + WAIT_ON_OUTPUT("separator" << "," << "read(0,1,2)", tab2 + "," + tab1 + ","); +} + +void Tests::scriptEventMaxRecursion() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: 'global.onItemsAdded = function() { add("A") }' + }, + ]) + )"; + RUN(script, ""); + RUN("add(1,0)", ""); + WAIT_ON_OUTPUT("length", "22\n"); + waitFor(200); + RUN("separator" << "," << "read(0,1,2)", "A,A,A"); + RUN("length", "22\n"); +} + void Tests::displayCommand() { const auto testMime = COPYQ_MIME_PREFIX "test"; diff --git a/src/tests/tests.h b/src/tests/tests.h index 41b98a2a9b..d53c453d38 100644 --- a/src/tests/tests.h +++ b/src/tests/tests.h @@ -250,6 +250,15 @@ private slots: void scriptCommandEnhanceFunction(); void scriptCommandEndingWithComment(); void scriptCommandWithError(); + + void scriptPaste(); + void scriptOnSelectedTabChanged(); + void scriptOnItemsRemoved(); + void scriptOnItemsAdded(); + void scriptOnItemsChanged(); + void scriptOnItemsLoaded(); + void scriptEventMaxRecursion(); + void displayCommand(); void displayCommandForMenu();