From bcc646cd8b75a13ca0a3eff43dad62e47033459d Mon Sep 17 00:00:00 2001 From: Lukas Holecek Date: Sun, 10 Dec 2023 07:48:03 +0100 Subject: [PATCH] Get clipboard owner from script Adds `currentClipboardOwner()` function to scripting which returns name of the current clipboard owner. This by default returns `currentWindowTitle()`. The value is used to set `mimeWindowTitle` format for the clipboard data in automatic commands and filtering by window title. --- docs/scripting-api.rst | 12 +++++ src/app/clipboardmonitor.cpp | 39 +++++++++++--- src/app/clipboardmonitor.h | 8 +++ src/app/clipboardownermonitor.cpp | 62 +++++++++++++++++------ src/app/clipboardownermonitor.h | 17 ++++--- src/platform/dummy/dummyclipboard.h | 7 +-- src/platform/mac/macclipboard.h | 2 - src/platform/mac/macclipboard.mm | 8 --- src/platform/platformclipboard.h | 4 +- src/platform/x11/x11platformclipboard.cpp | 11 ++-- src/platform/x11/x11platformclipboard.h | 8 ++- src/scriptable/scriptable.cpp | 37 +++++++++----- src/scriptable/scriptable.h | 2 + src/scriptable/scriptableproxy.cpp | 7 --- src/scriptable/scriptableproxy.h | 2 - src/tests/tests.cpp | 49 ++++++++++++++++++ src/tests/tests.h | 2 + 17 files changed, 202 insertions(+), 75 deletions(-) diff --git a/docs/scripting-api.rst b/docs/scripting-api.rst index 537a2870d8..f584bc57d1 100644 --- a/docs/scripting-api.rst +++ b/docs/scripting-api.rst @@ -1060,6 +1060,18 @@ unlike in GUI, where row numbers start from 1 by default. :returns: Current window title. :rtype: string +.. js:function:: String currentClipboardOwner() + + Returns name of the current clipboard owner. + + The default implementation returns `currentWindowTitle()`. + + This is used to set `mimeWindowTitle` format for the clipboard data in + automatic commands and filtering by window title. + + :returns: Current clipboard owner name. + :rtype: string + .. js:function:: dialog(...) Shows messages or asks user for input. diff --git a/src/app/clipboardmonitor.cpp b/src/app/clipboardmonitor.cpp index 3eec6789ed..da70623eb4 100644 --- a/src/app/clipboardmonitor.cpp +++ b/src/app/clipboardmonitor.cpp @@ -12,6 +12,7 @@ #include "platform/platformclipboard.h" #include +#include namespace { @@ -49,6 +50,7 @@ bool isClipboardDataHidden(const QVariantMap &data) ClipboardMonitor::ClipboardMonitor(const QStringList &formats) : m_clipboard(platformNativeInterface()->clipboard()) , m_formats(formats) + , m_ownerMonitor(this) { const AppConfig config; m_storeClipboard = config.option(); @@ -76,9 +78,33 @@ ClipboardMonitor::ClipboardMonitor(const QStringList &formats) void ClipboardMonitor::startMonitoring() { + setClipboardOwner(currentClipboardOwner()); + COPYQ_LOG( + QStringLiteral("Initial clipboard owner: %1") + .arg(m_clipboardOwner)); + + connect(QGuiApplication::clipboard(), &QClipboard::changed, + this, [this](){ m_ownerMonitor.update(); }); + m_clipboard->startMonitoring(m_formats); } +QString ClipboardMonitor::currentClipboardOwner() +{ + QString owner; + emit fetchCurrentClipboardOwner(&owner); + return owner; +} + +void ClipboardMonitor::setClipboardOwner(const QString &owner) +{ + if (m_clipboardOwner != owner) { + m_clipboardOwner = owner; + m_clipboard->setClipboardOwner(m_clipboardOwner); + COPYQ_LOG(QStringLiteral("Clipboard owner: %1").arg(owner)); + } +} + void ClipboardMonitor::onClipboardChanged(ClipboardMode mode) { QVariantMap data = m_clipboard->data(mode, m_formats); @@ -98,6 +124,12 @@ void ClipboardMonitor::onClipboardChanged(ClipboardMode mode) *clipboardData = data; + // add window title of clipboard owner + if ( !data.contains(mimeOwner) && !data.contains(mimeWindowTitle) ) { + if ( !m_clipboardOwner.isEmpty() ) + data.insert(mimeWindowTitle, m_clipboardOwner.toUtf8()); + } + COPYQ_LOG( QString("%1 changed, owner is \"%2\"") .arg(mode == ClipboardMode::Clipboard ? "Clipboard" : "Selection", getTextData(data, mimeOwner)) ); @@ -131,13 +163,6 @@ void ClipboardMonitor::onClipboardChanged(ClipboardMode mode) data.insert(mimeClipboardMode, modeName); } - // add window title of clipboard owner - if ( !data.contains(mimeOwner) && !data.contains(mimeWindowTitle) ) { - const QByteArray windowTitle = m_clipboard->clipboardOwner(); - if ( !windowTitle.isEmpty() ) - data.insert(mimeWindowTitle, windowTitle); - } - // run automatic commands if ( anySessionOwnsClipboardData(data) ) { emit clipboardChanged(data, ClipboardOwnership::Own); diff --git a/src/app/clipboardmonitor.h b/src/app/clipboardmonitor.h index 42ebdd303b..bf91c6218d 100644 --- a/src/app/clipboardmonitor.h +++ b/src/app/clipboardmonitor.h @@ -3,6 +3,7 @@ #ifndef CLIPBOARDMONITOR_H #define CLIPBOARDMONITOR_H +#include "app/clipboardownermonitor.h" #include "common/common.h" #include "platform/platformnativeinterface.h" #include "platform/platformclipboard.h" @@ -22,11 +23,14 @@ class ClipboardMonitor final : public QObject public: explicit ClipboardMonitor(const QStringList &formats); void startMonitoring(); + QString currentClipboardOwner(); + void setClipboardOwner(const QString &owner); signals: void clipboardChanged(const QVariantMap &data, ClipboardOwnership ownership); void clipboardUnchanged(const QVariantMap &data); void synchronizeSelection(ClipboardMode sourceMode, uint sourceTextHash, uint targetTextHash); + void fetchCurrentClipboardOwner(QString *title); private: void onClipboardChanged(ClipboardMode mode); @@ -40,12 +44,16 @@ class ClipboardMonitor final : public QObject QString m_clipboardTab; bool m_storeClipboard; + ClipboardOwnerMonitor m_ownerMonitor; + #ifdef HAS_MOUSE_SELECTIONS bool m_storeSelection; bool m_runSelection; bool m_clipboardToSelection; bool m_selectionToClipboard; #endif + + QString m_clipboardOwner; }; #endif // CLIPBOARDMONITOR_H diff --git a/src/app/clipboardownermonitor.cpp b/src/app/clipboardownermonitor.cpp index fc2fe485ef..8fcd70ca3e 100644 --- a/src/app/clipboardownermonitor.cpp +++ b/src/app/clipboardownermonitor.cpp @@ -2,28 +2,49 @@ #include "clipboardownermonitor.h" +#include "app/clipboardmonitor.h" #include "common/appconfig.h" -#include "platform/platformwindow.h" +#include "common/log.h" #include -ClipboardOwnerMonitor::ClipboardOwnerMonitor() +constexpr int updateAfterEventIntervalMs = 20; + +ClipboardOwnerMonitor::ClipboardOwnerMonitor(ClipboardMonitor *monitor) + : m_monitor(monitor) { qApp->installNativeEventFilter(this); - m_timer.setSingleShot(true); const int delay = AppConfig().option(); - m_timer.setInterval(delay); - QObject::connect( &m_timer, &QTimer::timeout, [this]() { - m_clipboardOwner = m_newClipboardOwner; - - PlatformWindowPtr currentWindow = platformNativeInterface()->getCurrentWindow(); - if (currentWindow) { - const auto currentWindowTitle = currentWindow->getTitle().toUtf8(); - if (m_newClipboardOwner != currentWindowTitle) { - m_newClipboardOwner = currentWindowTitle; - m_timer.start(); + m_timerSetOwner.setSingleShot(true); + m_timerSetOwner.setInterval(delay); + + m_timerUpdateAfterEvent.setSingleShot(true); + m_timerUpdateAfterEvent.setInterval(updateAfterEventIntervalMs); + + QObject::connect( + &m_timerSetOwner, &QTimer::timeout, + [this]() { + if (!m_nextClipboardOwners.isEmpty()) { + m_monitor->setClipboardOwner(m_nextClipboardOwners.takeFirst()); + if (!m_nextClipboardOwners.isEmpty()) + m_timerSetOwner.start(); } + }); + + QObject::connect( &m_timerUpdateAfterEvent, &QTimer::timeout, [this]() { + const QString title = m_monitor->currentClipboardOwner(); + if (m_lastClipboardOwner != title) { + m_lastClipboardOwner = title; + if ( m_timerSetOwner.interval() == 0 ) + m_nextClipboardOwners = QStringList{m_lastClipboardOwner}; + else + m_nextClipboardOwners.append(m_lastClipboardOwner); + + if (!m_timerSetOwner.isActive()) + m_timerSetOwner.start(); + + COPYQ_LOG(QStringLiteral("Next clipboard owner: %1").arg(title)); } }); } @@ -33,10 +54,21 @@ ClipboardOwnerMonitor::~ClipboardOwnerMonitor() qApp->removeNativeEventFilter(this); } +void ClipboardOwnerMonitor::update() +{ + if ( m_timerSetOwner.interval() == 0 ) { + m_lastClipboardOwner = m_monitor->currentClipboardOwner(); + m_nextClipboardOwners.clear(); + m_monitor->setClipboardOwner(m_lastClipboardOwner); + } else if ( !m_timerUpdateAfterEvent.isActive() ) { + m_timerUpdateAfterEvent.start(); + } +} + bool ClipboardOwnerMonitor::nativeEventFilter(const QByteArray &, void *, NativeEventResult *) { - if ( !m_timer.isActive() ) - m_timer.start(); + if ( !m_timerUpdateAfterEvent.isActive() ) + m_timerUpdateAfterEvent.start(); return false; } diff --git a/src/app/clipboardownermonitor.h b/src/app/clipboardownermonitor.h index 5a9883cd51..31b5cb8d1b 100644 --- a/src/app/clipboardownermonitor.h +++ b/src/app/clipboardownermonitor.h @@ -7,7 +7,7 @@ #include #include -#include "platform/platformnativeinterface.h" +class ClipboardMonitor; #if QT_VERSION >= QT_VERSION_CHECK(6,0,0) using NativeEventResult = qintptr; @@ -18,18 +18,21 @@ using NativeEventResult = long; class ClipboardOwnerMonitor final : public QAbstractNativeEventFilter { public: - ClipboardOwnerMonitor(); + explicit ClipboardOwnerMonitor(ClipboardMonitor *monitor); ~ClipboardOwnerMonitor(); - const QByteArray &clipboardOwner() const { return m_clipboardOwner; } - bool nativeEventFilter( const QByteArray &, void *message, NativeEventResult *result) override; + void update(); + void updateNow(); + private: - QByteArray m_clipboardOwner; - QByteArray m_newClipboardOwner; - QTimer m_timer; + ClipboardMonitor *m_monitor; + QString m_lastClipboardOwner; + QStringList m_nextClipboardOwners; + QTimer m_timerSetOwner; + QTimer m_timerUpdateAfterEvent; }; #endif // CLIPBOARDOWNERMONITOR_H diff --git a/src/platform/dummy/dummyclipboard.h b/src/platform/dummy/dummyclipboard.h index 3f10f829a7..bda2f08500 100644 --- a/src/platform/dummy/dummyclipboard.h +++ b/src/platform/dummy/dummyclipboard.h @@ -3,7 +3,6 @@ #ifndef DUMMYCLIPBOARD_H #define DUMMYCLIPBOARD_H -#include "app/clipboardownermonitor.h" #include "common/clipboardmode.h" #include "platform/platformclipboard.h" @@ -22,20 +21,18 @@ class DummyClipboard : public PlatformClipboard void setData(ClipboardMode mode, const QVariantMap &dataMap) override; - QByteArray clipboardOwner() override { return m_ownerMonitor.clipboardOwner(); } - const QMimeData *mimeData(ClipboardMode mode) const override; bool isSelectionSupported() const override { return false; } bool isHidden(const QMimeData &data) const override; + void setClipboardOwner(const QString &) override {} + protected: virtual const QMimeData *rawMimeData(ClipboardMode mode) const; virtual void onChanged(int mode); void onClipboardChanged(QClipboard::Mode mode); - - ClipboardOwnerMonitor m_ownerMonitor; }; #endif // DUMMYCLIPBOARD_H diff --git a/src/platform/mac/macclipboard.h b/src/platform/mac/macclipboard.h index 73434ed286..8374a878a9 100644 --- a/src/platform/mac/macclipboard.h +++ b/src/platform/mac/macclipboard.h @@ -11,8 +11,6 @@ class MacClipboard final : public DummyClipboard { void setData(ClipboardMode mode, const QVariantMap &dataMap) override; - QByteArray clipboardOwner() override; - bool isHidden(const QMimeData &data) const override; protected: diff --git a/src/platform/mac/macclipboard.mm b/src/platform/mac/macclipboard.mm index 7d6c291668..d7bf822e60 100644 --- a/src/platform/mac/macclipboard.mm +++ b/src/platform/mac/macclipboard.mm @@ -52,14 +52,6 @@ return DummyClipboard::setData(mode, dataMapForMac); } -QByteArray MacClipboard::clipboardOwner() -{ - PlatformWindowPtr currentWindow = platformNativeInterface()->getCurrentWindow(); - if (currentWindow) - return currentWindow->getTitle().toUtf8(); - return QByteArray(); -} - bool MacClipboard::isHidden(const QMimeData &data) const { return data.hasFormat( QStringLiteral("application/x-nspasteboard-concealed-type") ); diff --git a/src/platform/platformclipboard.h b/src/platform/platformclipboard.h index 35a1c0387b..33427296e5 100644 --- a/src/platform/platformclipboard.h +++ b/src/platform/platformclipboard.h @@ -34,14 +34,14 @@ class PlatformClipboard : public QObject */ virtual void setData(ClipboardMode mode, const QVariantMap &dataMap) = 0; - virtual QByteArray clipboardOwner() = 0; - virtual const QMimeData *mimeData(ClipboardMode mode) const = 0; virtual bool isSelectionSupported() const = 0; virtual bool isHidden(const QMimeData &data) const = 0; + virtual void setClipboardOwner(const QString &owner) = 0; + signals: /// Notifies about clipboard changes. void changed(ClipboardMode mode); diff --git a/src/platform/x11/x11platformclipboard.cpp b/src/platform/x11/x11platformclipboard.cpp index e9161d68ac..84707af6c6 100644 --- a/src/platform/x11/x11platformclipboard.cpp +++ b/src/platform/x11/x11platformclipboard.cpp @@ -13,6 +13,8 @@ #include "systemclipboard/waylandclipboard.h" +#include + #ifdef COPYQ_WITH_X11 # include # include @@ -136,7 +138,7 @@ QVariantMap X11PlatformClipboard::data(ClipboardMode mode, const QStringList &fo auto data = clipboardData.data; if ( !data.contains(mimeOwner) ) - data[mimeWindowTitle] = clipboardData.owner; + data[mimeWindowTitle] = clipboardData.owner.toUtf8(); return data; } @@ -174,14 +176,13 @@ void X11PlatformClipboard::onChanged(int mode) // Store the current window title right after the clipboard/selection changes. // This makes sure that the title points to the correct clipboard/selection // owner most of the times. - const auto currentWindowTitle = clipboardOwner(); - if (currentWindowTitle != clipboardData.newOwner) { + if (m_clipboardOwner != clipboardData.newOwner) { COPYQ_LOG( QString("New %1 owner: \"%2\"") .arg( QString::fromLatin1(mode == QClipboard::Clipboard ? "clipboard" : "selection"), - QString::fromUtf8(currentWindowTitle) + m_clipboardOwner ) ); - clipboardData.newOwner = currentWindowTitle; + clipboardData.newOwner = m_clipboardOwner; } if (mode == QClipboard::Selection) { diff --git a/src/platform/x11/x11platformclipboard.h b/src/platform/x11/x11platformclipboard.h index 510599652e..ffa2f3014c 100644 --- a/src/platform/x11/x11platformclipboard.h +++ b/src/platform/x11/x11platformclipboard.h @@ -26,6 +26,8 @@ class X11PlatformClipboard final : public DummyClipboard bool isSelectionSupported() const override { return m_selectionSupported; } + void setClipboardOwner(const QString &owner) override { m_clipboardOwner = owner; } + protected: const QMimeData *rawMimeData(ClipboardMode mode) const override; void onChanged(int mode) override; @@ -34,8 +36,8 @@ class X11PlatformClipboard final : public DummyClipboard struct ClipboardData { QVariantMap newData; QVariantMap data; - QByteArray owner; - QByteArray newOwner; + QString owner; + QString newOwner; QTimer timerEmitChange; QStringList formats; quint32 newDataTimestamp; @@ -59,6 +61,8 @@ class X11PlatformClipboard final : public DummyClipboard bool m_monitoring = false; bool m_selectionSupported = true; + + QString m_clipboardOwner; }; #endif // X11PLATFORMCLIPBOARD_H diff --git a/src/scriptable/scriptable.cpp b/src/scriptable/scriptable.cpp index 5f1e117c3b..ace086bff5 100644 --- a/src/scriptable/scriptable.cpp +++ b/src/scriptable/scriptable.cpp @@ -17,6 +17,7 @@ #include "item/itemfactory.h" #include "item/serialize.h" #include "platform/platformclipboard.h" +#include "platform/platformwindow.h" #include "scriptable/commandhelp.h" #include "scriptable/scriptablebytearray.h" #include "scriptable/scriptabledir.h" @@ -792,21 +793,16 @@ QByteArray Scriptable::makeByteArray(const QJSValue &value) const bool Scriptable::toItemData(const QJSValue &value, const QString &mime, QVariantMap *data) const { - if (mime == mimeItems) { - const QByteArray *itemData = getByteArray(value); - if (!itemData) - return false; - - return deserializeData(data, *itemData); + if (value.isUndefined()) { + data->insert( mime, QVariant() ); + return true; } - if (value.isUndefined()) - data->insert( mime, QVariant() ); - else if (!mime.startsWith("text/") && getByteArray(value) ) - data->insert( mime, *getByteArray(value) ); - else - data->insert( mime, toString(value).toUtf8() ); + const QByteArray *itemData = getByteArray(value); + if (mime == mimeItems) + return itemData && deserializeData(data, *itemData); + data->insert( mime, itemData ? *itemData : toString(value).toUtf8() ); return true; } @@ -2179,7 +2175,13 @@ QJSValue Scriptable::execute() QJSValue Scriptable::currentWindowTitle() { m_skipArguments = 0; - return m_proxy->currentWindowTitle(); + PlatformWindowPtr window = platformNativeInterface()->getCurrentWindow(); + return window ? window->getTitle() : QString(); +} + +QJSValue Scriptable::currentClipboardOwner() +{ + return eval("currentWindowTitle()"); } QJSValue Scriptable::dialog() @@ -2808,6 +2810,8 @@ void Scriptable::monitorClipboard() this, &Scriptable::onMonitorClipboardUnchanged ); connect( &monitor, &ClipboardMonitor::synchronizeSelection, this, &Scriptable::onSynchronizeSelection ); + connect( &monitor, &ClipboardMonitor::fetchCurrentClipboardOwner, + this, &Scriptable::onFetchCurrentClipboardOwner ); monitor.startMonitoring(); setClipboardMonitorRunning(true); @@ -2914,6 +2918,13 @@ void Scriptable::onSynchronizeSelection(ClipboardMode sourceMode, uint sourceTex #endif } +void Scriptable::onFetchCurrentClipboardOwner(QString *owner) +{ + const QJSValue result = eval("currentClipboardOwner()"); + if ( !result.isError() ) + *owner = toString(result); +} + bool Scriptable::sourceScriptCommands() { const auto commands = m_proxy->scriptCommands(); diff --git a/src/scriptable/scriptable.h b/src/scriptable/scriptable.h index 8c21f45c5e..0b41c789cb 100644 --- a/src/scriptable/scriptable.h +++ b/src/scriptable/scriptable.h @@ -297,6 +297,7 @@ public slots: QJSValue execute(); QJSValue currentWindowTitle(); + QJSValue currentClipboardOwner(); QJSValue dialog(); @@ -389,6 +390,7 @@ public slots: void onMonitorClipboardChanged(const QVariantMap &data, ClipboardOwnership ownership); void onMonitorClipboardUnchanged(const QVariantMap &data); void onSynchronizeSelection(ClipboardMode sourceMode, uint sourceTextHash, uint targetTextHash); + void onFetchCurrentClipboardOwner(QString *title); bool sourceScriptCommands(); void callDisplayFunctions(QJSValueList displayFunctions); diff --git a/src/scriptable/scriptableproxy.cpp b/src/scriptable/scriptableproxy.cpp index b5f27a26f9..285432e2c5 100644 --- a/src/scriptable/scriptableproxy.cpp +++ b/src/scriptable/scriptableproxy.cpp @@ -2074,13 +2074,6 @@ void ScriptableProxy::serverLog(const QString &text) log(text, LogAlways); } -QString ScriptableProxy::currentWindowTitle() -{ - INVOKE(currentWindowTitle, ()); - PlatformWindowPtr window = platformNativeInterface()->getCurrentWindow(); - return window ? window->getTitle() : QString(); -} - int ScriptableProxy::inputDialog(const NamedValueList &values) { INVOKE(inputDialog, (values)); diff --git a/src/scriptable/scriptableproxy.h b/src/scriptable/scriptableproxy.h index 3a94366eda..168325a8a7 100644 --- a/src/scriptable/scriptableproxy.h +++ b/src/scriptable/scriptableproxy.h @@ -228,8 +228,6 @@ public slots: void serverLog(const QString &text); - QString currentWindowTitle(); - int inputDialog(const NamedValueList &values); void setSelectedItemsData(const QString &mime, const QVariant &value); diff --git a/src/tests/tests.cpp b/src/tests/tests.cpp index 4a59273e80..ede18c3fe8 100644 --- a/src/tests/tests.cpp +++ b/src/tests/tests.cpp @@ -4480,6 +4480,55 @@ void Tests::avoidStoringPasswords() RUN("count", "0\n"); } +void Tests::currentClipboardOwner() +{ + const auto script = R"( + setCommands([ + { + isScript: true, + cmd: 'global.currentClipboardOwner = function() { return settings("clipboard_owner"); }' + }, + { + automatic: true, + input: mimeWindowTitle, + cmd: 'copyq: setData("application/x-copyq-owner-test", input())', + }, + { + automatic: true, + wndre: '.*IGNORE', + cmd: 'copyq ignore; copyq add IGNORED', + }, + ]) + )"; + RUN("settings" << "clipboard_owner" << "TEST1", ""); + RUN(script, ""); + + TEST( m_test->setClipboard("test1") ); + WAIT_ON_OUTPUT("read(0)", "test1"); + RUN("read('application/x-copyq-owner-test', 0)", "TEST1"); + + RUN("settings" << "clipboard_owner" << "TEST2", ""); + RUN("config" << "change_clipboard_owner_delay_ms" << "5000", "5000\n"); + TEST( m_test->setClipboard("test2") ); + WAIT_ON_OUTPUT("read(0)", "test2"); + RUN("read('application/x-copyq-owner-test', 0)", "TEST2"); + + RUN("settings" << "clipboard_owner" << "TEST3", ""); + TEST( m_test->setClipboard("test3") ); + WAIT_ON_OUTPUT("read(0)", "test3"); + RUN("read('application/x-copyq-owner-test', 0)", "TEST2"); + + RUN("settings" << "clipboard_owner" << "TEST4_IGNORE", ""); + RUN("config" << "change_clipboard_owner_delay_ms" << "0", "0\n"); + TEST( m_test->setClipboard("test4") ); + WAIT_ON_OUTPUT("read(0)", "IGNORED"); + + RUN("settings" << "clipboard_owner" << "TEST5", ""); + TEST( m_test->setClipboard("test5") ); + WAIT_ON_OUTPUT("read(0)", "test5"); + RUN("read('application/x-copyq-owner-test', 0)", "TEST5"); +} + 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 bf668600c0..43975f3b8f 100644 --- a/src/tests/tests.h +++ b/src/tests/tests.h @@ -293,6 +293,8 @@ private slots: void avoidStoringPasswords(); + void currentClipboardOwner(); + private: void clearServerErrors(); int run(const QStringList &arguments, QByteArray *stdoutData = nullptr,