Skip to content

Commit

Permalink
Get clipboard owner from script
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
hluk committed Dec 10, 2023
1 parent 8cf4dab commit 06b79d6
Show file tree
Hide file tree
Showing 19 changed files with 253 additions and 106 deletions.
18 changes: 18 additions & 0 deletions docs/scripting-api.rst
Expand Up @@ -1060,6 +1060,24 @@ 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.

Depending on the current system, option `update_clipboard_owner_delay_ms`
can introduce a delay before any new owner value return by this function is
used. The reason is to avoid using an incorrect clipboard owner from the
current window title if the real clipboard owner set the clipboard after or
just before hiding its window (like with some password managers).

:returns: Current clipboard owner name.
:rtype: string

.. js:function:: dialog(...)

Shows messages or asks user for input.
Expand Down
1 change: 1 addition & 0 deletions plugins/itemsync/tests/itemsynctests.cpp
Expand Up @@ -758,6 +758,7 @@ void ItemSyncTests::avoidDuplicateItemsAddedFromClipboard()
const Args args = Args() << "separator" << "," << "tab" << tab1;

RUN("config" << "clipboard_tab" << tab1, tab1 + "\n");
WAIT_ON_OUTPUT("isClipboardMonitorRunning", "true\n");

TEST( m_test->setClipboard("one") );
WAIT_ON_OUTPUT(args << "read(0,1,2,3)", "one,,,");
Expand Down
55 changes: 48 additions & 7 deletions src/app/clipboardmonitor.cpp
Expand Up @@ -11,7 +11,12 @@
#include "item/serialize.h"
#include "platform/platformclipboard.h"

#ifdef COPYQ_WS_X11
# include "platform/x11/x11info.h"
#endif

#include <QApplication>
#include <QClipboard>

namespace {

Expand Down Expand Up @@ -44,16 +49,30 @@ bool isClipboardDataHidden(const QVariantMap &data)
return data.value(mimeHidden).toByteArray() == "1";
}

int defaultOwnerUpdateInterval()
{
#ifdef COPYQ_WS_X11
if ( X11Info::isPlatformX11() )
return 150;
#endif
return 0;
}

} // namespace

ClipboardMonitor::ClipboardMonitor(const QStringList &formats)
: m_clipboard(platformNativeInterface()->clipboard())
, m_formats(formats)
, m_ownerMonitor(this)
{
const AppConfig config;
m_storeClipboard = config.option<Config::check_clipboard>();
m_clipboardTab = config.option<Config::clipboard_tab>();

const int ownerUpdateInterval = config.option<Config::update_clipboard_owner_delay_ms>();
m_ownerMonitor.setUpdateInterval(
ownerUpdateInterval < 0 ? defaultOwnerUpdateInterval() : ownerUpdateInterval);

m_formats.append({mimeOwner, mimeWindowTitle, mimeItemNotes, mimeHidden});
m_formats.removeDuplicates();

Expand All @@ -76,11 +95,33 @@ ClipboardMonitor::ClipboardMonitor(const QStringList &formats)

void ClipboardMonitor::startMonitoring()
{
setClipboardOwner(currentClipboardOwner());
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)
{
m_ownerMonitor.update();

QVariantMap data = m_clipboard->data(mode, m_formats);
auto clipboardData = mode == ClipboardMode::Clipboard
? &m_clipboardData : &m_selectionData;
Expand All @@ -98,6 +139,13 @@ void ClipboardMonitor::onClipboardChanged(ClipboardMode mode)

*clipboardData = data;

if ( !data.contains(mimeOwner)
&& !data.contains(mimeWindowTitle)
&& !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)) );
Expand Down Expand Up @@ -131,13 +179,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);
Expand Down
8 changes: 8 additions & 0 deletions src/app/clipboardmonitor.h
Expand Up @@ -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"
Expand All @@ -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);
Expand All @@ -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
63 changes: 46 additions & 17 deletions src/app/clipboardownermonitor.cpp
Expand Up @@ -2,28 +2,46 @@

#include "clipboardownermonitor.h"

#include "common/appconfig.h"
#include "platform/platformwindow.h"
#include "app/clipboardmonitor.h"
#include "common/log.h"

#include <QCoreApplication>

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<Config::change_clipboard_owner_delay_ms>();
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_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));
}
});
}
Expand All @@ -33,10 +51,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;
}
18 changes: 11 additions & 7 deletions src/app/clipboardownermonitor.h
Expand Up @@ -7,7 +7,7 @@
#include <QByteArray>
#include <QTimer>

#include "platform/platformnativeinterface.h"
class ClipboardMonitor;

#if QT_VERSION >= QT_VERSION_CHECK(6,0,0)
using NativeEventResult = qintptr;
Expand All @@ -18,18 +18,22 @@ 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 setUpdateInterval(int ms) { m_timerSetOwner.setInterval(ms); }

void update();

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
1 change: 1 addition & 0 deletions src/app/clipboardserver.cpp
Expand Up @@ -216,6 +216,7 @@ void ClipboardServer::stopMonitoring()
return;

COPYQ_LOG("Terminating monitor");
setClipboardMonitorRunning(false);

const auto client = findClient(m_monitor->id());
if (client)
Expand Down
9 changes: 5 additions & 4 deletions src/common/appconfig.h
Expand Up @@ -486,11 +486,12 @@ struct window_wait_for_modifier_released_ms : Config<int> {
}
};

struct change_clipboard_owner_delay_ms : Config<int> {
static QString name() { return "change_clipboard_owner_delay_ms"; }
static Value defaultValue() { return 150; }
struct update_clipboard_owner_delay_ms : Config<int> {
static QString name() { return "update_clipboard_owner_delay_ms"; }
static Value defaultValue() { return -1; }
static const char *description() {
return "Delay to save new clipboard owner window title";
return "Delay to update new clipboard owner window title"
" (use negative value for the default interval)";
}
};

Expand Down
2 changes: 1 addition & 1 deletion src/gui/configurationmanager.cpp
Expand Up @@ -327,7 +327,7 @@ void ConfigurationManager::initOptions()
bind<Config::window_key_press_time_ms>();
bind<Config::window_wait_for_modifier_released_ms>();

bind<Config::change_clipboard_owner_delay_ms>();
bind<Config::update_clipboard_owner_delay_ms>();

bind<Config::style>();

Expand Down
7 changes: 2 additions & 5 deletions src/platform/dummy/dummyclipboard.h
Expand Up @@ -3,7 +3,6 @@
#ifndef DUMMYCLIPBOARD_H
#define DUMMYCLIPBOARD_H

#include "app/clipboardownermonitor.h"
#include "common/clipboardmode.h"
#include "platform/platformclipboard.h"

Expand All @@ -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
2 changes: 0 additions & 2 deletions src/platform/mac/macclipboard.h
Expand Up @@ -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:
Expand Down
8 changes: 0 additions & 8 deletions src/platform/mac/macclipboard.mm
Expand Up @@ -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") );
Expand Down

0 comments on commit 06b79d6

Please sign in to comment.