[client] clipboard: retain local Wayland offers

This commit is contained in:
Geoffrey McRae
2026-08-14 12:22:49 +10:00
parent 608e85fe5d
commit 20c63464a7
4 changed files with 52 additions and 35 deletions

View File

@@ -194,7 +194,6 @@ static bool hasImageMimetype(char ** mimetypes)
enum ClipboardInvalidateMode enum ClipboardInvalidateMode
{ {
CLIPBOARD_INVALIDATE_SILENT,
CLIPBOARD_INVALIDATE_CONDITIONAL, CLIPBOARD_INVALIDATE_CONDITIONAL,
CLIPBOARD_INVALIDATE_FORCE, CLIPBOARD_INVALIDATE_FORCE,
}; };
@@ -339,8 +338,9 @@ 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);
/* Publishing already retired the old offer, so a delayed self-copy echo /* A self-copy offer only echoes the source that we published. It must not
* must not invalidate a newer local selection. */ * release the provider or disturb the external offer for a generation
* which the guest may still request. */
if (!selfCopy) if (!selfCopy)
waylandCBInvalidateLocal(CLIPBOARD_INVALIDATE_FORCE); waylandCBInvalidateLocal(CLIPBOARD_INVALIDATE_FORCE);
wl_data_offer_destroy(offer); wl_data_offer_destroy(offer);
@@ -664,6 +664,12 @@ 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);
const bool remoteSelection = wlCb.selectionSource != NULL;
if (mode == CLIPBOARD_INVALIDATE_CONDITIONAL && remoteSelection)
{
LG_UNLOCK(wlCb.lock);
return;
}
struct ClipboardRead * read = clipboardReadTakeCurrentNL(); struct ClipboardRead * read = clipboardReadTakeCurrentNL();
struct wl_data_offer * offer = wlCb.offer; struct wl_data_offer * offer = wlCb.offer;
wlCb.offer = NULL; wlCb.offer = NULL;
@@ -1215,11 +1221,6 @@ 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)
{ {

View File

@@ -485,10 +485,6 @@ 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);
@@ -1226,10 +1222,6 @@ 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

@@ -627,7 +627,7 @@ static void testRequest(void)
lgClipboard_free(); lgClipboard_free();
} }
static void testRemoteReplacesLocal(void) static void testRemoteKeepsLocalRequest(void)
{ {
init(); init();
bind(&p); bind(&p);
@@ -651,17 +651,17 @@ static void testRemoteReplacesLocal(void)
notice(&q, types, 1); notice(&q, types, 1);
CHECK(d.notice == 1); CHECK(d.notice == 1);
/* Rebinding after a remote notice must not replay the superseded local /* The two clipboard directions have independent generations. A late
* offer back to the provider. */ * request can still refer to the local offer advertised before this remote
lgClipboard_setTransport(&plainOps, &r); * notice, so its type must remain serviceable. */
CHECK(r.attach == 1); CHECK(q.ev->request(q.evCtx, 43, LG_CLIPBOARD_DATA_TEXT));
CHECK(r.notice == 0); CHECK(d.request == 1);
CHECK(r.release == 1); CHECK(d.reqType[0] == LG_CLIPBOARD_DATA_TEXT);
lgClipboard_free(); lgClipboard_free();
} }
static void testPendingRemoteReplacesLocal(void) static void testPendingRemoteKeepsLocalRequest(void)
{ {
init(); init();
bind(&p); bind(&p);
@@ -677,10 +677,11 @@ static void testPendingRemoteReplacesLocal(void)
lgClipboard_setLocalAvailable(true); lgClipboard_setLocalAvailable(true);
CHECK(d.notice == 1); CHECK(d.notice == 1);
lgClipboard_setTransport(&plainOps, &q); /* Publishing a notice which arrived while the local clipboard was
CHECK(q.attach == 1); * unavailable must likewise retain the local request allow-list. */
CHECK(q.notice == 0); CHECK(p.ev->request(p.evCtx, 44, LG_CLIPBOARD_DATA_TEXT));
CHECK(q.release == 1); CHECK(d.request == 1);
CHECK(d.reqType[0] == LG_CLIPBOARD_DATA_TEXT);
lgClipboard_free(); lgClipboard_free();
} }
@@ -1129,8 +1130,8 @@ static const struct Test tests[] =
{ {
{ "preference", testPreference }, { "preference", testPreference },
{ "request" , testRequest }, { "request" , testRequest },
{ "remote-local", testRemoteReplacesLocal }, { "remote-local", testRemoteKeepsLocalRequest },
{ "pending-remote", testPendingRemoteReplacesLocal }, { "pending-remote", testPendingRemoteKeepsLocalRequest },
{ "invalid" , testInvalid }, { "invalid" , testInvalid },
{ "cancel" , testCancel }, { "cancel" , testCancel },
{ "generation", testGeneration }, { "generation", testGeneration },

View File

@@ -706,12 +706,22 @@ static void testSelfCopy(void)
waylandCBNotice(LG_CLIPBOARD_DATA_TEXT); waylandCBNotice(LG_CLIPBOARD_DATA_TEXT);
struct Proxy * source = &proto.source[0]; struct Proxy * source = &proto.source[0];
CHECK(wlCb.selectionSource == (struct wl_data_source *)source); CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
CHECK(wlCb.offer == (struct wl_data_offer *)local);
CHECK(!local->dead);
/* A remote notice and its self echo must not erase the Wayland offer for a
* local clipboard generation that Windows can still request. */
const uint8_t localData[] = "local clipboard";
proto.receiveData = localData;
proto.receiveSize = sizeof(localData) - 1;
/* Losing focus before the compositor echoes our source must not release /* Losing focus before the compositor echoes our source must not release
* the remote clipboard. */ * the remote clipboard. */
waylandCBInvalidate(); waylandCBInvalidate();
CHECK(rec.releaseN == 0); CHECK(rec.releaseN == 0);
CHECK(wlCb.selectionSource == (struct wl_data_source *)source); CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
CHECK(wlCb.offer == (struct wl_data_offer *)local);
CHECK(!local->dead);
struct Proxy * echo = newOffer(); struct Proxy * echo = newOffer();
offerMime(echo, "text/plain"); offerMime(echo, "text/plain");
@@ -721,17 +731,28 @@ static void testSelfCopy(void)
CHECK(rec.noticeN == 1); CHECK(rec.noticeN == 1);
CHECK(rec.releaseN == 0); CHECK(rec.releaseN == 0);
CHECK(wlCb.selectionSource == (struct wl_data_source *)source); CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
CHECK(!wlCb.offer); CHECK(wlCb.offer == (struct wl_data_offer *)local);
for (enum LG_ClipboardData i = 0; i < LG_CLIPBOARD_DATA_NONE; ++i) CHECK(wlCb.mimetypes[LG_CLIPBOARD_DATA_TEXT]);
CHECK(!wlCb.mimetypes[i]); CHECK(!local->dead);
CHECK(local->dead);
CHECK(echo->dead); CHECK(echo->dead);
CHECK(proto.offerDestroyN == 2); CHECK(proto.offerDestroyN == 1);
CHECK(!echo->user); CHECK(!echo->user);
waylandCBInvalidate(); waylandCBInvalidate();
CHECK(rec.releaseN == 0); CHECK(rec.releaseN == 0);
CHECK(wlCb.selectionSource == (struct wl_data_source *)source); CHECK(wlCb.selectionSource == (struct wl_data_source *)source);
CHECK(wlCb.offer == (struct wl_data_offer *)local);
waylandCBRequest(102, LG_CLIPBOARD_DATA_TEXT);
CHECK(proto.receiveN == 1);
CHECK(rec.pollN == 1);
pollFire(0, EPOLLIN);
pollFire(0, EPOLLIN);
CHECK(rec.streamData.request == 102);
CHECK(rec.streamData.size == sizeof(localData) - 1);
CHECK(rec.streamData.endN == 1);
CHECK(memcmp(rec.streamData.data, localData,
sizeof(localData) - 1) == 0);
int fds[2]; int fds[2];
CHECK(pipe(fds) == 0); CHECK(pipe(fds) == 0);
@@ -754,6 +775,8 @@ static void testSelfCopy(void)
selectOffer(NULL); selectOffer(NULL);
CHECK(rec.releaseN == 1); CHECK(rec.releaseN == 1);
CHECK(!wlCb.selectionSource); CHECK(!wlCb.selectionSource);
CHECK(local->dead);
CHECK(proto.offerDestroyN == 2);
CHECK(proto.dirtyDestroyN == 0); CHECK(proto.dirtyDestroyN == 0);
finish(); finish();