[client] clipboard: suppress Wayland self-copy feedback

This commit is contained in:
Geoffrey McRae
2026-08-14 11:46:39 +10:00
parent c868082a7f
commit dc2ab76c36
5 changed files with 213 additions and 19 deletions

View File

@@ -192,6 +192,15 @@ static bool hasImageMimetype(char ** mimetypes)
return false; return false;
} }
enum ClipboardInvalidateMode
{
CLIPBOARD_INVALIDATE_SILENT,
CLIPBOARD_INVALIDATE_CONDITIONAL,
CLIPBOARD_INVALIDATE_FORCE,
};
static void waylandCBInvalidateLocal(enum ClipboardInvalidateMode mode);
/* wlCb.lock must be held. */ /* wlCb.lock must be held. */
static struct ClipboardRead * clipboardReadTakeCurrentNL(void) static struct ClipboardRead * clipboardReadTakeCurrentNL(void)
{ {
@@ -312,14 +321,15 @@ static void dataDeviceHandleSelection(void * opaque,
LG_LOCK(wlCb.lock); LG_LOCK(wlCb.lock);
wlCb.selectionSource = NULL; wlCb.selectionSource = NULL;
LG_UNLOCK(wlCb.lock); LG_UNLOCK(wlCb.lock);
waylandCBInvalidate(); waylandCBInvalidateLocal(CLIPBOARD_INVALIDATE_FORCE);
return; return;
} }
struct DataOffer * extra = wl_data_offer_get_user_data(offer); struct DataOffer * extra = wl_data_offer_get_user_data(offer);
if (!hasAnyMimetype(extra->mimetypes) || extra->isSelfCopy) const bool selfCopy = extra->isSelfCopy;
if (!hasAnyMimetype(extra->mimetypes) || selfCopy)
{ {
if (!extra->isSelfCopy) if (!selfCopy)
{ {
LG_LOCK(wlCb.lock); LG_LOCK(wlCb.lock);
wlCb.selectionSource = NULL; wlCb.selectionSource = NULL;
@@ -329,7 +339,12 @@ static void dataDeviceHandleSelection(void * opaque,
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
free(extra->mimetypes[i]); free(extra->mimetypes[i]);
free(extra); free(extra);
waylandCBInvalidate(); if (!selfCopy)
waylandCBInvalidateLocal(CLIPBOARD_INVALIDATE_FORCE);
else
/* This is the compositor echoing the source we just installed. It is
* not a new local clipboard owner and must not send a provider CLEAR. */
waylandCBInvalidateLocal(CLIPBOARD_INVALIDATE_SILENT);
wl_data_offer_destroy(offer); wl_data_offer_destroy(offer);
return; return;
} }
@@ -647,7 +662,7 @@ static void clipboardReadCallback(uint32_t events, void * opaque)
} }
} }
void waylandCBInvalidate(void) static void waylandCBInvalidateLocal(enum ClipboardInvalidateMode mode)
{ {
char * mimetypes[LG_CLIPBOARD_DATA_NONE]; char * mimetypes[LG_CLIPBOARD_DATA_NONE];
LG_LOCK(wlCb.lock); LG_LOCK(wlCb.lock);
@@ -656,16 +671,27 @@ void waylandCBInvalidate(void)
wlCb.offer = NULL; wlCb.offer = NULL;
memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes)); memcpy(mimetypes, wlCb.mimetypes, sizeof(mimetypes));
memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes)); memset(wlCb.mimetypes, 0, sizeof(wlCb.mimetypes));
const bool hadLocal = read || offer || hasAnyMimetype(mimetypes);
const bool notifyProvider = mode == CLIPBOARD_INVALIDATE_FORCE ||
(mode == CLIPBOARD_INVALIDATE_CONDITIONAL && hadLocal);
const LG_ClipboardRequest request = read ? read->request :
LG_CLIPBOARD_REQUEST_INVALID;
if (read)
clipboardReadRetire(read);
/* Serialize the provider release with source publication. Clipboard
* provider operations may not synchronously deliver event callbacks, so
* calling this while holding wlCb.lock cannot re-enter the display server. */
if (notifyProvider)
{
if (read)
lgClipboard_abort(request);
lgClipboard_release();
}
LG_UNLOCK(wlCb.lock); LG_UNLOCK(wlCb.lock);
if (read) if (read && !notifyProvider)
{
const LG_ClipboardRequest request = read->request;
clipboardReadRetire(read);
lgClipboard_abort(request); lgClipboard_abort(request);
}
lgClipboard_release();
if (offer) if (offer)
wl_data_offer_destroy(offer); wl_data_offer_destroy(offer);
@@ -673,6 +699,11 @@ void waylandCBInvalidate(void)
free(mimetypes[i]); free(mimetypes[i]);
} }
void waylandCBInvalidate(void)
{
waylandCBInvalidateLocal(CLIPBOARD_INVALIDATE_CONDITIONAL);
}
void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type) void waylandCBRequest(LG_ClipboardRequest request, LG_ClipboardData type)
{ {
if (request == LG_CLIPBOARD_REQUEST_INVALID || if (request == LG_CLIPBOARD_REQUEST_INVALID ||
@@ -1186,6 +1217,11 @@ static void waylandCBPublish(LG_ClipboardData type)
wl_data_source_offer(source, *mimetype); wl_data_source_offer(source, *mimetype);
wl_data_source_offer(source, wlCb.lgMimetype); wl_data_source_offer(source, wlCb.lgMimetype);
/* Publishing a remote selection supersedes the old external offer. Retire
* it before set_selection so a simultaneous focus loss cannot release the
* provider while this remote source is active. */
waylandCBInvalidateLocal(CLIPBOARD_INVALIDATE_SILENT);
LG_LOCK(wlCb.lock); LG_LOCK(wlCb.lock);
if (wlCb.dataDevice) if (wlCb.dataDevice)
{ {
@@ -1194,13 +1230,13 @@ static void waylandCBPublish(LG_ClipboardData type)
wlCb.selectionSource = source; wlCb.selectionSource = source;
wl_data_device_set_selection(wlCb.dataDevice, source, wl_data_device_set_selection(wlCb.dataDevice, source,
wlWm.keyboardEnterSerial); wlWm.keyboardEnterSerial);
} LG_UNLOCK(wlCb.lock);
else return;
{
wl_data_source_destroy(source);
free(transfer);
} }
LG_UNLOCK(wlCb.lock); LG_UNLOCK(wlCb.lock);
wl_data_source_destroy(source);
free(transfer);
} }
void waylandCBNotice(LG_ClipboardData type) void waylandCBNotice(LG_ClipboardData type)

View File

@@ -485,6 +485,10 @@ static void eventNotice(void * opaque,
generation = clipboard.remoteGeneration; generation = clipboard.remoteGeneration;
transfer = clearRemoteRequestNL(); transfer = clearRemoteRequestNL();
notice = clipboard.localAvailable && g_params.clipboardToLocal; notice = clipboard.localAvailable && g_params.clipboardToLocal;
/* Once published, the remote notice replaces the local clipboard. Do not
* replay that stale local offer if the provider is rebound. */
if (notice)
clipboard.localTypeCount = 0;
LG_UNLOCK(clipboard.stateLock); LG_UNLOCK(clipboard.stateLock);
LG_UNLOCK(clipboard.requestLock); LG_UNLOCK(clipboard.requestLock);
@@ -1222,6 +1226,10 @@ void lgClipboard_setLocalAvailable(bool available)
notice = available && clipboard.remoteNotice && notice = available && clipboard.remoteNotice &&
g_params.clipboardToLocal; g_params.clipboardToLocal;
type = clipboard.remoteType; type = clipboard.remoteType;
/* A pending remote notice replaces the local clipboard when it becomes
* publishable. Do not replay the superseded local offer afterwards. */
if (notice)
clipboard.localTypeCount = 0;
if (!available) if (!available)
transfer = clearRemoteRequestNL(); transfer = clearRemoteRequestNL();
LG_UNLOCK(clipboard.stateLock); LG_UNLOCK(clipboard.stateLock);

View File

@@ -211,6 +211,8 @@ if(ENABLE_WAYLAND)
teardown teardown
teardown-external teardown-external
self-copy self-copy
ignored
invalidate-lock
dnd dnd
) )
foreach(name IN LISTS WAYLAND_CLIPBOARD_CASES) foreach(name IN LISTS WAYLAND_CLIPBOARD_CASES)
@@ -471,6 +473,8 @@ target_link_libraries(clipboard-tests
set(CLIPBOARD_CASES set(CLIPBOARD_CASES
preference preference
request request
remote-local
pending-remote
invalid invalid
cancel cancel
generation generation

View File

@@ -627,6 +627,64 @@ static void testRequest(void)
lgClipboard_free(); lgClipboard_free();
} }
static void testRemoteReplacesLocal(void)
{
init();
bind(&p);
const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT };
lgClipboard_notifyTypes(types, 1);
CHECK(p.notice == 1);
g_params.clipboardToLocal = false;
notice(&p, types, 1);
CHECK(d.notice == 0);
/* A notice which is not published locally must not discard the local
* clipboard advertised towards the guest. */
lgClipboard_setTransport(&plainOps, &q);
CHECK(q.attach == 1);
CHECK(q.notice == 1);
CHECK(q.release == 0);
g_params.clipboardToLocal = true;
notice(&q, types, 1);
CHECK(d.notice == 1);
/* Rebinding after a remote notice must not replay the superseded local
* offer back to the provider. */
lgClipboard_setTransport(&plainOps, &r);
CHECK(r.attach == 1);
CHECK(r.notice == 0);
CHECK(r.release == 1);
lgClipboard_free();
}
static void testPendingRemoteReplacesLocal(void)
{
init();
bind(&p);
const LG_ClipboardData types[] = { LG_CLIPBOARD_DATA_TEXT };
lgClipboard_notifyTypes(types, 1);
CHECK(p.notice == 1);
lgClipboard_setLocalAvailable(false);
notice(&p, types, 1);
CHECK(d.notice == 0);
lgClipboard_setLocalAvailable(true);
CHECK(d.notice == 1);
lgClipboard_setTransport(&plainOps, &q);
CHECK(q.attach == 1);
CHECK(q.notice == 0);
CHECK(q.release == 1);
lgClipboard_free();
}
static void testInvalid(void) static void testInvalid(void)
{ {
init(); init();
@@ -1071,6 +1129,8 @@ static const struct Test tests[] =
{ {
{ "preference", testPreference }, { "preference", testPreference },
{ "request" , testRequest }, { "request" , testRequest },
{ "remote-local", testRemoteReplacesLocal },
{ "pending-remote", testPendingRemoteReplacesLocal },
{ "invalid" , testInvalid }, { "invalid" , testInvalid },
{ "cancel" , testCancel }, { "cancel" , testCancel },
{ "generation", testGeneration }, { "generation", testGeneration },

View File

@@ -155,7 +155,9 @@ struct Log
bool requestOK; bool requestOK;
bool requestCancelSync; bool requestCancelSync;
bool requestBeginSync; bool requestBeginSync;
bool expectReleaseLocked;
bool firing; bool firing;
unsigned int releaseLockedN;
}; };
struct WaylandDSState wlWm; struct WaylandDSState wlWm;
@@ -440,6 +442,12 @@ void lgClipboard_notifyTypes(
void lgClipboard_release(void) void lgClipboard_release(void)
{ {
if (rec.expectReleaseLocked)
{
CHECK(atomic_flag_test_and_set_explicit(
&wlCb.lock, memory_order_acquire));
++rec.releaseLockedN;
}
++rec.releaseN; ++rec.releaseN;
} }
@@ -690,11 +698,72 @@ static void testMime(void)
} }
static void testSelfCopy(void) static void testSelfCopy(void)
{
start();
struct Proxy * local = textOffer();
CHECK(rec.noticeN == 1);
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
struct Proxy * source = &proto.source[0];
CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
/* Losing focus before the compositor echoes our source must not release
* the remote clipboard. */
waylandCBInvalidate();
CHECK(rec.releaseN == 0);
CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
struct Proxy * echo = newOffer();
offerMime(echo, "text/plain");
offerMime(echo, wlCb.lgMimetype);
selectOffer(echo);
CHECK(rec.noticeN == 1);
CHECK(rec.releaseN == 0);
CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
CHECK(!wlCb.offer);
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i)
CHECK(!wlCb.mimetypes[i]);
CHECK(local->dead);
CHECK(echo->dead);
CHECK(proto.offerDestroyN == 2);
CHECK(!echo->user);
waylandCBInvalidate();
CHECK(rec.releaseN == 0);
CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
int fds[2];
CHECK(pipe(fds) == 0);
sourceListener(source)->send(source->data,
(struct wl_data_source *)source, "text/plain", fds[1]);
CHECK(rec.requestN == 1);
CHECK(rec.requestType == LG_CLIPBOARD_DATA_TEXT);
CHECK(rec.requestStream);
CHECK(rec.requestOpaque);
CHECK(rec.requestStream->begin(rec.requestOpaque,
LG_CLIPBOARD_DATA_TEXT, LG_CLIPBOARD_SIZE_UNKNOWN) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
CHECK(rec.requestStream->end(rec.requestOpaque, 0) ==
LG_CLIPBOARD_RESULT_ACCEPTED);
uint8_t value;
CHECK(read(fds[0], &value, 1) == 0);
CHECK(close(fds[0]) == 0);
checkClosed(fds[1]);
selectOffer(NULL);
CHECK(rec.releaseN == 1);
CHECK(!wlCb.selectionSource);
CHECK(proto.dirtyDestroyN == 0);
finish();
}
static void testIgnoredOffer(void)
{ {
start(); start();
struct Proxy * offer = newOffer(); struct Proxy * offer = newOffer();
offerMime(offer, "text/plain"); offerMime(offer, "application/x-unsupported");
offerMime(offer, wlCb.lgMimetype);
selectOffer(offer); selectOffer(offer);
CHECK(rec.noticeN == 0); CHECK(rec.noticeN == 0);
@@ -706,6 +775,21 @@ static void testSelfCopy(void)
finish(); finish();
} }
static void testInvalidateSerialization(void)
{
start();
struct Proxy * offer = textOffer();
rec.expectReleaseLocked = true;
waylandCBInvalidate();
CHECK(rec.releaseN == 1);
CHECK(rec.releaseLockedN == 1);
CHECK(offer->dead);
CHECK(!wlCb.offer);
finish();
}
static void testReplace(void) static void testReplace(void)
{ {
start(); start();
@@ -1245,6 +1329,8 @@ static const struct Test tests[] =
{ "teardown" , testTeardown }, { "teardown" , testTeardown },
{ "teardown-external", testTeardownExternalOwner }, { "teardown-external", testTeardownExternalOwner },
{ "self-copy" , testSelfCopy }, { "self-copy" , testSelfCopy },
{ "ignored" , testIgnoredOffer },
{ "invalidate-lock", testInvalidateSerialization },
{ "dnd" , testDnd }, { "dnd" , testDnd },
}; };