diff --git a/docs/scripting-api.rst b/docs/scripting-api.rst index 28cf09dc979e05a2c9730d22fb90dff7d3805524..082410016fa5ad1bf6b3b915ee7130378d08efaf 100644 --- a/docs/scripting-api.rst +++ b/docs/scripting-api.rst @@ -232,6 +232,10 @@ omitted. copy(mimeText, 'Hello, World!', mimeHtml, '<p>Hello, World!</p>') +.. js:function:: copy(Item) + + Function override with an item argument. + .. js:function:: copy() Sends ``Ctrl+C`` to current window. @@ -324,11 +328,11 @@ omitted. Copies previous item from current tab to clipboard. -.. js:function:: add(text|item...) +.. js:function:: add(text|Item...) Same as ``insert(0, ...)``. -.. js:function:: insert(row, text|item...) +.. js:function:: insert(row, text|Item...) Inserts new items to current tab. @@ -366,12 +370,28 @@ omitted. Throws an exception if space for the items cannot be allocated. +.. js:function:: write(row, Item...) + + Function override with one or more item arguments. + +.. js:function:: write(row, Item[]) + + Function override with item list argument. + .. js:function:: change(row, mimeType, data, [mimeType, data]...) Changes data in item in current tab. If data is ``undefined`` the format is removed from item. +.. js:function:: change(row, Item...) + + Function override with one or more item arguments. + +.. js:function:: change(row, Item[]) + + Function override with item list argument. + .. js:function:: String separator() Returns item separator (used when concatenating item data). @@ -643,7 +663,7 @@ omitted. See `Selected Items`_. -.. js:function:: bool setSelectedItemData(index, item) +.. js:function:: bool setSelectedItemData(index, Item) Set data for given selected item. @@ -661,7 +681,7 @@ omitted. See `Selected Items`_. -.. js:function:: setSelectedItemsData(item[]) +.. js:function:: setSelectedItemsData(Item[]) Set data to all selected items. @@ -684,7 +704,7 @@ omitted. Returns deserialized object from serialized items. -.. js:function:: ByteArray pack(item) +.. js:function:: ByteArray pack(Item) Returns serialized item. @@ -692,7 +712,7 @@ omitted. Returns an item in current tab. -.. js:function:: setItem(row, text|item) +.. js:function:: setItem(row, text|Item) Inserts item to current tab. diff --git a/src/gui/commandcompleterdocumentation.h b/src/gui/commandcompleterdocumentation.h index 4414c443aa551c53dea0dbdc3aa922b02637ef2b..b24efa6f4b9c527a7b7e4b40acf6dfeca51ded57 100644 --- a/src/gui/commandcompleterdocumentation.h +++ b/src/gui/commandcompleterdocumentation.h @@ -32,6 +32,7 @@ void addDocumentation(AddDocumentationCallback addDocumentation) addDocumentation("isClipboard", "bool isClipboard()", "Returns true only in automatic command triggered by clipboard change."); addDocumentation("copy", "copy(text)", "Sets clipboard plain text."); addDocumentation("copy", "copy(mimeType, data, [mimeType, data]...)", "Sets clipboard data."); + addDocumentation("copy", "copy(Item)", "Function override with an item argument."); addDocumentation("copy", "copy()", "Sends `Ctrl+C` to current window."); addDocumentation("copySelection", "copySelection(...)", "Same as `copy(...)` for Linux/X11 mouse selection."); addDocumentation("paste", "paste()", "Pastes current clipboard."); @@ -47,15 +48,19 @@ void addDocumentation(AddDocumentationCallback addDocumentation) addDocumentation("select", "select(row)", "Copies item in the row to clipboard."); addDocumentation("next", "next()", "Copies next item from current tab to clipboard."); addDocumentation("previous", "previous()", "Copies previous item from current tab to clipboard."); - addDocumentation("add", "add(text|item...)", "Same as `insert(0, ...)`."); - addDocumentation("insert", "insert(row, text|item...)", "Inserts new items to current tab."); + addDocumentation("add", "add(text|Item...)", "Same as `insert(0, ...)`."); + addDocumentation("insert", "insert(row, text|Item...)", "Inserts new items to current tab."); addDocumentation("remove", "remove(row, ...)", "Removes items in current tab."); addDocumentation("move", "move(row)", "Moves selected items to given row in same tab."); addDocumentation("edit", "edit([row|text] ...)", "Edits items in current tab."); addDocumentation("read", "ByteArray read([mimeType])", "Same as `clipboard()`."); addDocumentation("read", "ByteArray read(mimeType, row, ...)", "Returns concatenated data from items, or clipboard if row is negative."); addDocumentation("write", "write(row, mimeType, data, [mimeType, data]...)", "Inserts new item to current tab."); + addDocumentation("write", "write(row, Item...)", "Function override with one or more item arguments."); + addDocumentation("write", "write(row, Item[])", "Function override with item list argument."); addDocumentation("change", "change(row, mimeType, data, [mimeType, data]...)", "Changes data in item in current tab."); + addDocumentation("change", "change(row, Item...)", "Function override with one or more item arguments."); + addDocumentation("change", "change(row, Item[])", "Function override with item list argument."); addDocumentation("separator", "String separator()", "Returns item separator (used when concatenating item data)."); addDocumentation("separator", "separator(separator)", "Sets item separator for concatenating item data."); addDocumentation("action", "action()", "Opens action dialog."); @@ -95,15 +100,15 @@ void addDocumentation(AddDocumentationCallback addDocumentation) addDocumentation("selectedTab", "String selectedTab()", "Returns tab that was selected when script was executed."); addDocumentation("selectedItems", "int[] selectedItems()", "Returns selected rows in current tab."); addDocumentation("selectedItemData", "Item selectedItemData(index)", "Returns data for given selected item."); - addDocumentation("setSelectedItemData", "bool setSelectedItemData(index, item)", "Set data for given selected item."); + addDocumentation("setSelectedItemData", "bool setSelectedItemData(index, Item)", "Set data for given selected item."); addDocumentation("selectedItemsData", "Item[] selectedItemsData()", "Returns data for all selected items."); - addDocumentation("setSelectedItemsData", "setSelectedItemsData(item[])", "Set data to all selected items."); + addDocumentation("setSelectedItemsData", "setSelectedItemsData(Item[])", "Set data to all selected items."); addDocumentation("currentItem", "int currentItem(), int index()", "Returns current row in current tab."); addDocumentation("escapeHtml", "String escapeHtml(text)", "Returns text with special HTML characters escaped."); addDocumentation("unpack", "Item unpack(data)", "Returns deserialized object from serialized items."); - addDocumentation("pack", "ByteArray pack(item)", "Returns serialized item."); + addDocumentation("pack", "ByteArray pack(Item)", "Returns serialized item."); addDocumentation("getItem", "Item getItem(row)", "Returns an item in current tab."); - addDocumentation("setItem", "setItem(row, text|item)", "Inserts item to current tab."); + addDocumentation("setItem", "setItem(row, text|Item)", "Inserts item to current tab."); addDocumentation("toBase64", "String toBase64(data)", "Returns base64-encoded data."); addDocumentation("fromBase64", "ByteArray fromBase64(base64String)", "Returns base64-decoded data."); addDocumentation("md5sum", "ByteArray md5sum(data)", "Returns MD5 checksum of data."); diff --git a/src/scriptable/scriptable.cpp b/src/scriptable/scriptable.cpp index 7c7d551733861bfe9e399cb62db5534598edcf94..4aa3e73bc802944b06b5073af7c45412211730a3 100644 --- a/src/scriptable/scriptable.cpp +++ b/src/scriptable/scriptable.cpp @@ -624,16 +624,19 @@ Scriptable::Scriptable( addScriptableClass(&ScriptableDir::staticMetaObject, "Dir", m_engine); } +QJSValue Scriptable::argumentsArray() const +{ + return m_engine->globalObject().property("_copyqArguments"); +} + int Scriptable::argumentCount() const { - const auto args = m_engine->globalObject().property("_copyqArguments"); - return args.property("length").toInt(); + return argumentsArray().property("length").toInt(); } QJSValue Scriptable::argument(int index) const { - const auto args = m_engine->globalObject().property("_copyqArguments"); - return args.property(static_cast<quint32>(index) ); + return argumentsArray().property(static_cast<quint32>(index) ); } QJSValue Scriptable::newByteArray(const QByteArray &bytes) const @@ -1306,16 +1309,16 @@ QJSValue Scriptable::read() return newByteArray(result); } -void Scriptable::write() +QJSValue Scriptable::write() { m_skipArguments = -1; - changeItem(true); + return changeItem(true); } -void Scriptable::change() +QJSValue Scriptable::change() { m_skipArguments = -1; - changeItem(false); + return changeItem(false); } void Scriptable::separator() @@ -2931,16 +2934,77 @@ QVector<int> Scriptable::getRows() const return rows; } +QVector<QVariantMap> Scriptable::getItemArguments(int begin, int end, QString *error) +{ + if (begin == end) { + *error = QLatin1String("Expected item arguments"); + return {}; + } + + const auto firstArg = argument(begin); + if (firstArg.isArray()) { + if (end - begin != 1) { + *error = QLatin1String("Unexpected multiple item list arguments"); + return {}; + } + return getItemList(0, firstArg.property("length").toInt(), firstArg); + } + + QVector<QVariantMap> items; + if (firstArg.toVariant().canConvert<QVariantMap>()) { + for (int i = begin; i < end; ++i) + items.append( fromScriptValue<QVariantMap>(argument(i), this) ); + } else if (end - begin == 1) { + QVariantMap data; + QJSValue value = argument(begin); + setTextData( &data, toString(value) ); + items.append(data); + } else { + if ((end - begin) % 2 != 0) { + *error = QLatin1String("Unexpected uneven number of mimeType/data arguments"); + return {}; + } + QVariantMap data; + for (int i = begin; i < end; i += 2) { + // MIME + const QString mime = toString(argument(i)); + // DATA + toItemData( argument(i + 1), mime, &data ); + } + items.append(data); + } + + return items; +} + +QVector<QVariantMap> Scriptable::getItemList(int begin, int end, const QJSValue &arguments) +{ + if (end < begin) + return {}; + + QVector<QVariantMap> items; + items.reserve(end - begin); + + for (int i = begin; i < end; ++i) { + const auto arg = arguments.property( static_cast<quint32>(i) ); + if ( arg.isObject() && getByteArray(arg) == nullptr && !arg.isArray() ) + items.append( fromScriptValue<QVariantMap>(arg, this) ); + else + items.append( createDataMap(mimeText, toString(arg)) ); + } + + return items; +} + QJSValue Scriptable::copy(ClipboardMode mode) { const int args = argumentCount(); - QVariantMap data; if (args == 0) { // Reset clipboard first. const QString mime = COPYQ_MIME_PREFIX "invalid"; const QByteArray value = "invalid"; - data.insert(mime, value); + const QVariantMap data = createDataMap(mime, value); m_proxy->setClipboard(data, mode); m_proxy->copyFromCurrentWindow(); @@ -2955,27 +3019,16 @@ QJSValue Scriptable::copy(ClipboardMode mode) return throwError( tr("Failed to copy to clipboard!") ); } - if (args == 1) { - QJSValue value = argument(0); - setTextData( &data, toString(value) ); - m_proxy->setClipboard(data, mode); - return true; - } - - if (args % 2 == 0) { - for (int i = 0; i < args; ++i) { - // MIME - QString mime = toString(argument(i)); + QString error; + const QVector<QVariantMap> items = getItemArguments(0, args, &error); + if ( !error.isEmpty() ) + return throwError(error); - // DATA - toItemData(argument(++i), mime, &data); - } + if (items.size() != 1) + return throwError(QLatin1String("Expected single item")); - m_proxy->setClipboard(data, mode); - return true; - } - - return throwError(argumentError()); + m_proxy->setClipboard(items[0], mode); + return true; } void Scriptable::abortEvaluation(Abort abort) @@ -2985,7 +3038,7 @@ void Scriptable::abortEvaluation(Abort abort) emit finished(); } -void Scriptable::changeItem(bool create) +QJSValue Scriptable::changeItem(bool create) { int row; int args = argumentCount(); @@ -2993,37 +3046,27 @@ void Scriptable::changeItem(bool create) // [ROW] if ( toInt(argument(0), &row) ) { - if (args < 3 || args % 2 != 1 ) { - throwError(argumentError()); - return; - } i = 1; } else { - if (args < 2 || args % 2 != 0 ) { - throwError(argumentError()); - return; - } row = 0; i = 0; } - QVariantMap data; - - for (; i < args; i += 2) { - // MIME - const QString mime = toString(argument(i)); - // DATA - toItemData( argument(i + 1), mime, &data ); - } + QString error; + const QVector<QVariantMap> items = getItemArguments(i, args, &error); + if ( !error.isEmpty() ) + return throwError(error); if (create) { - QVector<QVariantMap> items(1, data); - const auto error = m_proxy->browserInsert(m_tabName, row, items); + error = m_proxy->browserInsert(m_tabName, row, items); if ( !error.isEmpty() ) - throwError(error); + return throwError(error); } else { - m_proxy->browserChange(m_tabName, data, row); + error = m_proxy->browserChange(m_tabName, row, items); + if ( !error.isEmpty() ) + return throwError(error); } + return QJSValue(); } void Scriptable::nextToClipboard(int where) @@ -3325,17 +3368,7 @@ void Scriptable::insert(int row, int argumentsBegin, int argumentsEnd) { m_skipArguments = argumentsEnd; - QVector<QVariantMap> items; - items.reserve(argumentsEnd - argumentsBegin); - - for (int i = argumentsBegin; i < argumentsEnd; ++i) { - const auto arg = argument(i); - if ( arg.isObject() && getByteArray(arg) == nullptr && !arg.isArray() ) - items.append( fromScriptValue<QVariantMap>(arg, this) ); - else - items.append( createDataMap(mimeText, toString(arg)) ); - } - + const QVector<QVariantMap> items = getItemList(argumentsBegin, argumentsEnd, argumentsArray()); const auto error = m_proxy->browserInsert(m_tabName, row, items); if ( !error.isEmpty() ) throwError(error); diff --git a/src/scriptable/scriptable.h b/src/scriptable/scriptable.h index 527438ceb9c04de2c5e96b7ebc48747c0f5aba26..93bf07da55b34ebcfafe7aaba4f4487f01f48d5c 100644 --- a/src/scriptable/scriptable.h +++ b/src/scriptable/scriptable.h @@ -84,6 +84,7 @@ public: AllEvaluations, }; + QJSValue argumentsArray() const; int argumentCount() const; QJSValue argument(int index) const; @@ -218,8 +219,8 @@ public slots: QJSValue move(); QJSValue read(); - void write(); - void change(); + QJSValue write(); + QJSValue change(); void separator(); void action(); @@ -400,8 +401,19 @@ private: void processUncaughtException(const QString &cmd); void showExceptionMessage(const QString &message); QVector<int> getRows() const; + + /** + * Parses arguments as one of these or raises an argument error: + * - item... + * - mimeType, data, [mimeType, data]... + * - list of items + * - text + */ + QVector<QVariantMap> getItemArguments(int begin, int end, QString *error); + QVector<QVariantMap> getItemList(int begin, int end, const QJSValue &arguments); + QJSValue copy(ClipboardMode mode); - void changeItem(bool create); + QJSValue changeItem(bool create); void nextToClipboard(int where); QJSValue screenshot(bool select); QByteArray serialize(const QJSValue &value); diff --git a/src/scriptable/scriptableproxy.cpp b/src/scriptable/scriptableproxy.cpp index 4ceec39763790fd93c262b77fa30b4254583542e..d3a6fcc5201d4d08ce6f93ac64b0255bb913be78 100644 --- a/src/scriptable/scriptableproxy.cpp +++ b/src/scriptable/scriptableproxy.cpp @@ -1496,36 +1496,42 @@ QString ScriptableProxy::browserInsert(const QString &tabName, int row, const QV ClipboardBrowser *c = fetchBrowser(tabName); if (!c) - return "Invalid tab"; + return QLatin1String("Invalid tab"); if ( !c->allocateSpaceForNewItems(items.size()) ) - return "Tab is full (cannot remove any items)"; + return QLatin1String("Tab is full (cannot remove any items)"); for (const auto &item : items) { if ( !c->add(item, row) ) - return "Failed to new add items"; + return QLatin1String("Failed to new add items"); } return QString(); } -bool ScriptableProxy::browserChange(const QString &tabName, const QVariantMap &data, int row) +QString ScriptableProxy::browserChange(const QString &tabName, int row, const QVector<QVariantMap> &items) { - INVOKE(browserChange, (tabName, data, row)); + INVOKE(browserChange, (tabName, row, items)); + ClipboardBrowser *c = fetchBrowser(tabName); if (!c) - return false; + return QLatin1String("Invalid tab"); - const auto index = c->index(row); - QVariantMap itemData = c->model()->data(index, contentType::data).toMap(); - for (auto it = data.constBegin(); it != data.constEnd(); ++it) { - if ( it.value().isValid() ) - itemData.insert( it.key(), it.value() ); - else - itemData.remove( it.key() ); + int currentRow = row; + for (const auto &data : items) { + const auto index = c->index(currentRow); + QVariantMap itemData = c->model()->data(index, contentType::data).toMap(); + for (auto it = data.constBegin(); it != data.constEnd(); ++it) { + if ( it.value().isValid() ) + itemData.insert( it.key(), it.value() ); + else + itemData.remove( it.key() ); + } + c->model()->setData(index, itemData, contentType::data); + ++currentRow; } - return c->model()->setData(index, itemData, contentType::data); + return QString(); } QByteArray ScriptableProxy::browserItemData(const QString &tabName, int arg1, const QString &arg2) diff --git a/src/scriptable/scriptableproxy.h b/src/scriptable/scriptableproxy.h index b57d3a4a5e2d1e77821f5e8b32d26599f022bc61..305ec800de9740d9b1b5e34a8e22c8f189486dbc 100644 --- a/src/scriptable/scriptableproxy.h +++ b/src/scriptable/scriptableproxy.h @@ -169,7 +169,7 @@ public slots: bool browserOpenEditor(const QString &tabName, const QByteArray &arg1, bool changeClipboard); QString browserInsert(const QString &tabName, int row, const QVector<QVariantMap> &items); - bool browserChange(const QString &tabName, const QVariantMap &data, int row); + QString browserChange(const QString &tabName, int row, const QVector<QVariantMap> &items); QByteArray browserItemData(const QString &tabName, int arg1, const QString &arg2); QVariantMap browserItemData(const QString &tabName, int arg1); diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp index 6ec02a371abd780635334b5ed6f24044ad147427..23ad4dc6ca1318908a616bd73d5c4737bba5bfc8 100644 --- a/src/tests/tests.cpp +++ b/src/tests/tests.cpp @@ -1104,6 +1104,25 @@ void Tests::commandsWriteRead() RUN("read" << COPYQ_MIME_PREFIX "test1" << "0", arg1.toLatin1()); RUN("read" << COPYQ_MIME_PREFIX "test2" << "0", input); RUN("read" << COPYQ_MIME_PREFIX "test3" << "0", arg2.toLatin1()); + + RUN("write(1, {'text/plain': 'A'}, {'text/plain': 'B'})", ""); + RUN("read(mimeText, 0, 1, 2, 3)", "\nB\nA\n"); + + RUN("write(0, [{'text/plain': 'C'}, {'text/plain': 'D'}])", ""); + RUN("read(mimeText, 0, 1, 2, 3)", "D\nC\n\nB"); + + RUN("write(0, ['E', 'F'])", ""); + RUN("read(mimeText, 0, 1, 2, 3)", "F\nE\nD\nC"); + + RUN_EXPECT_ERROR_WITH_STDERR( + "write(0, [{}], [{}])", + CommandException, "Unexpected multiple item list arguments"); + RUN_EXPECT_ERROR_WITH_STDERR( + "write(0)", + CommandException, "Expected item arguments"); + RUN_EXPECT_ERROR_WITH_STDERR( + "write(0, '1', '2', '3')", + CommandException, "Unexpected uneven number of mimeType/data arguments"); } void Tests::commandChange() @@ -1471,6 +1490,17 @@ void Tests::commandCopy() , "true\n" ); WAIT_FOR_CLIPBOARD2("C", "DATA3"); WAIT_FOR_CLIPBOARD2("D", "DATA4"); + + RUN( "copy({'DATA1': 1, 'DATA2': 2})", "true\n" ); + WAIT_FOR_CLIPBOARD2("1", "DATA1"); + WAIT_FOR_CLIPBOARD2("2", "DATA2"); + + RUN_EXPECT_ERROR_WITH_STDERR( + "copy({}, {})", + CommandException, "Expected single item"); + RUN_EXPECT_ERROR_WITH_STDERR( + "copy([{}, {}])", + CommandException, "Expected single item"); } void Tests::commandClipboard()