From 112c4d4fbea77d5bf0209634e49a6e0153650663 Mon Sep 17 00:00:00 2001 From: oldosfan Date: Thu, 15 Sep 2022 02:08:55 +0000 Subject: [PATCH] Various improvements to primary selections and resize handling * 12to11.man: Document new environment variables. * Imakefile ($(OBJS)): Also depend on protocol headers. * README: Update implementation status. * compositor.h: Update prototypes. * data_device.c (DataOfferSetActions): Fix error sent when the offer is invalid. * dnd.c: Remove out of date comment. * primary_selection.c (struct _PDataSource): Move declaration to compositor.h. New field `atom_types'. (Offer): Record atom types as well. (HandleSourceResourceDestroy): Maybe disown the primary selection, and free offered MIME types. (XLResourceFromPDataSource, XLPDataSourceHasAtomTarget) (XLPDataSourceHasTarget, XLPDataSourceTargetCount) (XLPDataSourceGetTargets): New functions. (UpdateForSingleReference): Handle foreign selections correctly. (SetSelection): Try to own the X selection. (XLSetForeignPrimary, XLClearForeignPrimary): New functions. * seat.c (XLSeatResizeInProgress): New function. * xdata.c (struct _ReadTargetsData): New field `selection'. (struct _DataConversion): Pass send function and source resource to conversion function. (last_primary_time): New timestamp variable. (x_primary_targets, num_x_primary_targets, primary_data_source): New variables. (HasSelectionTarget, FindTranslationForMimeType): Accept arg `is_primary', meaning that the primary selection data should be used instead. (ReceiveBody): Extract generic code from Receive. (Receive): Replace with above macro. (Finish, SetActions): Correctly post errors on DND-specific actions. (ReceivePrimary): New function. (primary_offer_impl): New interface implementation. (CreatePrimaryOffer): New function. (SendOffers1): Extract generic code from SendOffers. (SendOffers, SendPrimaryOffers): Reimplement in terms of SendOffers1. (HandleNewSelection): Handle PRIMARY selection changes. (TargetsFinishCallback): Pass selection to HandleNewSelection. (NoticeClipboardChanged): Set TargetsReadData selection. (NoticePrimaryChanged): New function. (NoticeClipboardCleared): Free unused data. (NoticePrimaryCleared): New function. (HandleSelectionNotify): Handle notifications for both CLIPBOARD and PRIMARY. (MimeTypeFromTarget, TypeFromTarget): Accept argument `primary', meaning to use the primary selection when determining whether or not to call a conversion callback. (GetClipboardCallback): Pass resource and functions to the clipboard callback. (GetConversionCallback): Update to use provided functions and resource. (GetDragCallback, XLNoteSourceDestroyed): Fix typos in debug string. (XLNotePrimaryDestroyed): New function. (NoteLocalSelectionBody, NoteLocalSelectionFooter): Extract selection-generic code from XLNoteLocalSelection. (XLNoteLocalSelection): Reimplement in terms of the above two macros. (XLNoteLocalPrimary, GetPrimaryCallback): New functions. (XLInitXData): Select for selection input from XA_PRIMARY as well. * xdg_surface.c (AckConfigure): Improve debug code. (Commit): Clear that flag. (NoteBounds): If that flag is set, return. (HandleFreeze): Assume a configure event will arrive after a sync request; set flags accordingly. (XLGetXdgSurface): Add HandleFreeze. (XLXdgRoleSendConfigure): Clear some flags and add new state flag `StateWaitingForAckCommit'. Set it. It means that no commit has yet happened after ack_configure. (XLXdgRoleNoteRejectedConfigure): New function. Clear flags if the configure event following _NET_WM_SYNC_REQUEST was rejected. * xdg_toplevel.c (struct _XdgToplevel): New state flags StatePendingResize, StateConfigureSize, StatePendingConfigureStates. New field `configuration_timer'. (batch_state_changes): New flag. (DestroyBacking): Clear configuration timer. (NoteConfigureTime): New function; apply queued state changes. (HandleWmStateChange, HandleConfigureEvent): By default, queue state changes and configure events so they can be applied in one go. (Unmap): Clear delayed configuration timer. (Commit, CommitInsideFrame, PostResize): Improve movement delta adjustment for min_width and min_height, and queue pending motion for after ack_configure. (XLInitXdgToplevels): Set `batch_state_changes' depending on the DIRECT_STATE_CHANGES' environment variable. --- 12to11.man | 77 ++++++ Imakefile | 3 +- README | 4 +- compositor.h | 15 ++ data_device.c | 2 +- dnd.c | 3 - primary_selection.c | 191 +++++++++++++- seat.c | 6 + xdata.c | 600 ++++++++++++++++++++++++++++++++++---------- xdg_surface.c | 74 +++++- xdg_toplevel.c | 267 +++++++++++++++++--- 11 files changed, 1065 insertions(+), 177 deletions(-) diff --git a/12to11.man b/12to11.man index e651a01..4bc7a68 100644 --- a/12to11.man +++ b/12to11.man @@ -9,6 +9,83 @@ starts a Wayland compositor on the next available socket; Wayland programs will then be displayed through the X server. .SH OPTIONS None. +.SH ENVIRONMENT +Several environment variables exist that modify the behavior of the +protocol translator in one way or another. Most of these are used for +debugging, but some may be relevant to users as well. +.PP +The +.B USE_BUILTIN_RESIZE +environment variable, if set, forces the use of the built-in +drag-to-resize support provided by the protocol translator, even if +the X window manager provides its own. +.PP +The +.B DEBUG_REFRESH_PREDICTION +environment variable, if set, forces the frame clock to predict the +presentation deadline of the X compositing manager. This is used to +debug code that is otherwise not run without subsurfaces being +present. +.PP +The +.B DISABLE_FRAME_SYNCHRONIZATION +environment variable, if set, disables frame synchronization with the +X compositing manager. Setting this variable is probably not a good +idea. +.PP +The +.B SYNCHRONIZE +environment variable, if set, causes the X library to check for errors +immediately after issuing a request. The resulting backtraces from +the error handler then reflect the protocol request that actually +caused the error, instead of some unpredictable request in the future. +.PP +The +.B APPLY_STATE_WORKAROUND +environment variable, if set, causes the protocol translator to handle +toplevel window state changes differently. If Wayland programs do not +make the transition between fullscreen and maximized states correctly, +try setting this variable. +.PP +The +.B GLOBAL_SCALE +environment variable, if set to an integer, overrides the global +program scale factor normally provided by the +.I Gdk/WindowScalingFactor +X setting. +.PP +The +.B OUTPUT_SCALE +environment variable, if set to an integer, overrides the output scale +factor that is otherwise set to the global program scale factor. +Setting this option is only useful to debug Wayland program +downscaling. +.PP +The +.B DIRECT_STATE_CHANGES +variable, if set, forces ConfigureNotify events from the window +manager to be handled directly, without waiting some time for a +corresponding _NET_WM_STATE event to arrive. This is only +useful for debugging the window resizing logic. +.SH BUGS +There is a hard to catch bug where Wayland programs leaving the +fullscreen or maximized state may abruptly return to their maximized +size. Setting the +.B APPLY_STATE_WORKAROUND +environment variable may help. +.PP +Mozilla Firefox also does not work correctly. Resizing the Firefox +window leads to screen tearing, and Firefox often resizes its toplevel +windows outside their normal boundaries, causing invalid screen +contents to be displayed over other applications. +.PP +Using this protocol translator under a window manager that does not at +least support the +.B _NET_WM_SYNC_REQUEST +and +.B _NET_WM_STATE +window manager hints will result in Wayland programs running +incorrectly. .SH "SEE ALSO" X(1), Xorg(1) .SH AUTHOR diff --git a/Imakefile b/Imakefile index 8c89e6a..7315a2c 100644 --- a/Imakefile +++ b/Imakefile @@ -68,7 +68,8 @@ transfer_atoms.h: short_types.txt mime0.awk mime1.awk mime2.awk mime3.awk \ awk -f mime3.awk short_types.txt >> $@ awk -f mime4.awk short_types.txt >> $@ -$(OBJS): transfer_atoms.h +$(OBJS): transfer_atoms.h primary-selection-unstable-v1.h \ + linux-dmabuf-unstable-v1.h xdg-shell.h linux-dmabuf-unstable-v1.h: linux-dmabuf-unstable-v1.xml $(WAYLAND_SCANNER) server-header $< $@ diff --git a/README b/README index b4ba266..a70619a 100644 --- a/README +++ b/README @@ -2,8 +2,8 @@ This is a tool for running Wayland applications on an X server, preferably with a compositing manager running. It is not yet complete. What is not yet implemented includes support -for the primary selection, touchscreens, input methods, device -switching in dmabuf feedback, and the viewporter protocol extension. +for touchscreens, input methods, device switching in dmabuf feedback, +and the viewporter protocol extension. There are also problems with output reporting in subsurfaces. diff --git a/compositor.h b/compositor.h index afa50bc..49f40fc 100644 --- a/compositor.h +++ b/compositor.h @@ -86,6 +86,10 @@ struct _Compositor typedef struct _Seat Seat; +/* Forward declarations from primary_selection.c. */ + +typedef struct _PDataSource PDataSource; + /* Defined in 12to11.c. */ extern Compositor compositor; @@ -703,6 +707,7 @@ extern void *XLXdgRoleRunOnReconstrain (Role *, void (*) (void *, XEvent *), extern void XLXdgRoleCancelReconstrainCallback (void *); extern void XLXdgRoleReconstrain (Role *, XEvent *); extern void XLXdgRoleMoveBy (Role *, int, int); +extern void XLXdgRoleNoteRejectedConfigure (Role *); extern Window XLWindowFromXdgRole (Role *); extern Subcompositor *XLSubcompositorFromXdgRole (Role *); @@ -871,6 +876,7 @@ extern void *XLSeatAddModifierCallback (Seat *, void (*) (unsigned int, void *), void *); extern void XLSeatRemoveModifierCallback (void *); extern unsigned int XLSeatGetEffectiveModifiers (Seat *); +extern Bool XLSeatResizeInProgress (Seat *); extern Cursor InitDefaultCursor (void); @@ -933,6 +939,7 @@ extern void XLNoteSourceDestroyed (DataSource *); extern Bool XLNoteLocalSelection (Seat *, DataSource *); extern void XLReceiveDataFromSelection (Time, Atom, Atom, int); extern Bool XLOwnDragSelection (Time, DataSource *); +extern void XLNotePrimaryDestroyed (PDataSource *); extern void XLInitXData (void); @@ -968,7 +975,15 @@ extern Bool XLIsWindowIconSurface (Window); /* Defined in primary_selection.c. */ extern void XLInitPrimarySelection (void); +extern void XLSetForeignPrimary (Time, CreateOfferFuncs); +extern void XLClearForeignPrimary (Time); +extern Bool XLNoteLocalPrimary (Seat *, PDataSource *); extern void XLPrimarySelectionHandleFocusChange (Seat *); +extern struct wl_resource *XLResourceFromPDataSource (PDataSource *); +extern Bool XLPDataSourceHasAtomTarget (PDataSource *, Atom); +extern Bool XLPDataSourceHasTarget (PDataSource *, const char *); +extern int XLPDataSourceTargetCount (PDataSource *); +extern void XLPDataSourceGetTargets (PDataSource *, Atom *); /* Utility functions that don't belong in a specific file. */ diff --git a/data_device.c b/data_device.c index 82b3b83..d708b26 100644 --- a/data_device.c +++ b/data_device.c @@ -452,7 +452,7 @@ DataOfferSetActions (struct wl_client *client, struct wl_resource *resource, if (!(offer->state & IsDragAndDrop)) { - wl_resource_post_error (resource, WL_DATA_OFFER_ERROR_INVALID_FINISH, + wl_resource_post_error (resource, WL_DATA_OFFER_ERROR_INVALID_OFFER, "trying to finish non-drag-and-drop data offer"); return; } diff --git a/dnd.c b/dnd.c index 5628303..5045952 100644 --- a/dnd.c +++ b/dnd.c @@ -30,9 +30,6 @@ along with 12to11. If not, see . */ Drags between Wayland clients are implemented in seat.c and data_device.c instead. */ -/* TODO: Handle XdndActionAsk and allow using keyboard modifiers to - change the selected action. Then, update README. */ - enum { XdndProtocolVersion = 5, diff --git a/primary_selection.c b/primary_selection.c index 291c52f..466ac49 100644 --- a/primary_selection.c +++ b/primary_selection.c @@ -24,7 +24,6 @@ along with 12to11. If not, see . */ #include "primary-selection-unstable-v1.h" typedef struct _PDataDevice PDataDevice; -typedef struct _PDataSource PDataSource; typedef struct _PDataOffer PDataOffer; enum @@ -74,6 +73,9 @@ struct _PDataSource /* List of all MIME types provided by this source. */ XLList *mime_types; + /* List of atoms provided by this source. */ + XIDList *atom_types; + /* Number of MIME types provided by this source. */ int n_mime_types; }; @@ -90,6 +92,17 @@ static uint32_t last_change_serial; /* All data devices. */ static PDataDevice all_devices; +/* A special PDataSource meaning that the primary selection is a + foreign selection. */ +static PDataSource foreign_selection_key; + +/* Functions to call to obtain zwp_primary_selection_v1_offer + resources and send associated data for foreign selections. */ +static CreateOfferFuncs foreign_selection_functions; + +/* The time the foreign selection was set. */ +static Time foreign_selection_time; + /* Forward declaration. */ static void NoticeChanged (void); @@ -224,6 +237,8 @@ Offer (struct wl_client *client, struct wl_resource *resource, this source. */ source->mime_types = XLListPrepend (source->mime_types, XLStrdup (mime_type)); + source->atom_types = XIDListPrepend (source->atom_types, + InternAtom (mime_type)); source->n_mime_types++; } @@ -259,6 +274,10 @@ HandleSourceResourceDestroy (struct wl_resource *resource) /* And free the MIME types offered by the source. */ XLListFree (source->mime_types, XLFree); + XIDListFree (source->atom_types, NULL); + + /* Maybe disown the primary selection. */ + XLNotePrimaryDestroyed (source); /* If source is the primary selection, clear it and send the change to all clients. */ @@ -272,10 +291,92 @@ HandleSourceResourceDestroy (struct wl_resource *resource) XLFree (source); } +struct wl_resource * +XLResourceFromPDataSource (PDataSource *source) +{ + return source->resource; +} + +Bool +XLPDataSourceHasAtomTarget (PDataSource *source, Atom target) +{ + XIDList *list; + + for (list = source->atom_types; list; list = list->next) + { + if (list->data == target) + return True; + } + + return False; +} + +Bool +XLPDataSourceHasTarget (PDataSource *source, const char *mime_type) +{ + XLList *list; + + for (list = source->mime_types; list; list = list->next) + { + if (!strcmp (list->data, mime_type)) + return True; + } + + return False; +} + +int +XLPDataSourceTargetCount (PDataSource *source) +{ + return source->n_mime_types; +} + +void +XLPDataSourceGetTargets (PDataSource *source, Atom *targets) +{ + int i; + XIDList *list; + + list = source->atom_types; + + for (i = 0; i < source->n_mime_types; ++i) + { + targets[i] = list->data; + list = list->next; + } +} /* Device implementation. */ +static void +UpdateSingleReferenceWithForeignOffer (struct wl_client *client, + PDataDevice *reference) +{ + struct wl_resource *scratch, *resource; + Time time; + + time = foreign_selection_time; + resource = foreign_selection_functions.create_offer (client, time); + scratch = reference->resource; + + if (!resource) + return; + + /* Make the data offer known to the client. */ + zwp_primary_selection_device_v1_send_data_offer (scratch, resource); + + /* Tell the foreign selection provider to send supported resources + to the client. */ + foreign_selection_functions.send_offers (resource, time); + + /* Finally, tell the client that the offer is a selection. */ + zwp_primary_selection_device_v1_send_selection (scratch, resource); + + /* Set WasSentOffer since an offer was sent. */ + reference->flags |= WasSentOffer; +} + static void UpdateForSingleReference (PDataDevice *reference) { @@ -302,6 +403,15 @@ UpdateForSingleReference (PDataDevice *reference) return; } + /* If the primary selection is foreign, call the foreign selection + functions. */ + if (primary_selection == &foreign_selection_key) + { + UpdateSingleReferenceWithForeignOffer (client, reference); + + return; + } + /* Otherwise, create a data offer for that client. */ offer = AddDataOffer (client, primary_selection); @@ -392,8 +502,16 @@ static void SetSelection (struct wl_client *client, struct wl_resource *resource, struct wl_resource *source_resource, uint32_t serial) { + PDataDevice *device; + PDataSource *source; struct wl_resource *scratch; + device = wl_resource_get_user_data (resource); + + if (!device->seat) + /* This device is inert, since the seat has been deleted. */ + return; + if (serial < last_change_serial) /* This change is out of date. Do nothing. */ return; @@ -401,19 +519,34 @@ SetSelection (struct wl_client *client, struct wl_resource *resource, /* Otherwise, set the primary selection. */ last_change_serial = serial; + if (source_resource) + source = wl_resource_get_user_data (source_resource); + else + source = NULL; + + /* Try to own the X primary selection. If it fails, refrain from + changing the current selection data. */ + if (!XLNoteLocalPrimary (device->seat, source)) + return; + if (primary_selection) { /* The primary selection already exists. Send the cancelled event and clear the primary selection variable. */ scratch = primary_selection->resource; - primary_selection = NULL; - zwp_primary_selection_source_v1_send_cancelled (scratch); + if (primary_selection != &foreign_selection_key) + /* The special foreign selection has no associated + wl_resource. */ + zwp_primary_selection_source_v1_send_cancelled (scratch); + + /* Clear primary_selection. */ + primary_selection = NULL; } if (source_resource) /* Make source_resource the new primary selection. */ - primary_selection = wl_resource_get_user_data (source_resource); + primary_selection = source; /* Now, send the updated primary selection to clients. */ NoticeChanged (); @@ -585,6 +718,56 @@ HandleBind (struct wl_client *client, void *data, uint32_t version, wl_resource_set_implementation (resource, &manager_impl, NULL, NULL); } + + +void +XLSetForeignPrimary (Time time, CreateOfferFuncs functions) +{ + uint32_t serial; + struct wl_resource *scratch; + + if (time < foreign_selection_time) + return; + + serial = wl_display_next_serial (compositor.wl_display); + + /* Use this serial to prevent clients from changing the selection + again until the next event is sent. */ + last_change_serial = serial; + + foreign_selection_time = time; + foreign_selection_functions = functions; + + if (primary_selection + && primary_selection != &foreign_selection_key) + { + scratch = primary_selection->resource; + zwp_primary_selection_source_v1_send_cancelled (scratch); + } + + /* Use a special value of primary_selection to mean that foreign + selections are in use. */ + primary_selection = &foreign_selection_key; + + /* Send new data offers to current clients. */ + NoticeChanged (); +} + +void +XLClearForeignPrimary (Time time) +{ + if (time < foreign_selection_time) + return; + + if (primary_selection == &foreign_selection_key) + { + primary_selection = NULL; + NoticeChanged (); + } + + foreign_selection_time = time; +} + void XLInitPrimarySelection (void) { diff --git a/seat.c b/seat.c index 713e027..dac20c0 100644 --- a/seat.c +++ b/seat.c @@ -5129,3 +5129,9 @@ XLSeatGetEffectiveModifiers (Seat *seat) { return seat->base | seat->locked | seat->latched; } + +Bool +XLSeatResizeInProgress (Seat *seat) +{ + return seat->resize_in_progress; +} diff --git a/xdata.c b/xdata.c index ee20847..e9aa089 100644 --- a/xdata.c +++ b/xdata.c @@ -30,10 +30,11 @@ along with 12to11. If not, see . */ #include #include "compositor.h" +#include "primary-selection-unstable-v1.h" #include -/* X11 data source for Wayland clients. */ +/* X11 data transfer to and from Wayland clients. */ typedef struct _ReadTargetsData ReadTargetsData; typedef struct _TargetMapping TargetMapping; @@ -51,6 +52,9 @@ struct _ReadTargetsData /* Number of atoms read. */ int n_atoms; + + /* What selection is being read from. */ + Atom selection; }; struct _TargetMapping @@ -88,7 +92,11 @@ struct _DataConversion Atom atom; /* An alternative GetClipboardCallback, if any. */ - GetDataFunc (*clipboard_callback) (WriteTransfer *, Atom, Atom *); + GetDataFunc (*clipboard_callback) (WriteTransfer *, Atom, Atom *, + struct wl_resource *, + void (*) (struct wl_resource *, + const char *, int), + Bool); }; enum @@ -243,12 +251,21 @@ static Time last_x_selection_change; time any client did that. */ static Time last_clipboard_time, last_clipboard_change; +/* The last time ownership over PRIMARY changed. */ +static Time last_primary_time; + /* The currently supported selection targets. */ static Atom *x_selection_targets; /* The number of targets in that array. */ static int num_x_selection_targets; +/* The currently supported primary selection targets. */ +static Atom *x_primary_targets; + +/* The number of targets in that array. */ +static int num_x_primary_targets; + /* Data source currently being used to provide the X clipboard. */ static DataSource *selection_data_source; @@ -256,6 +273,10 @@ static DataSource *selection_data_source; selection. */ static DataSource *drag_data_source; +/* Data source currently being used to provide the primary + selection. */ +static PDataSource *primary_data_source; + #ifdef DEBUG static void __attribute__ ((__format__ (gnu_printf, 1, 2))) @@ -327,10 +348,23 @@ SetupMappingTable (void) } static Bool -HasSelectionTarget (Atom atom) +HasSelectionTarget (Atom atom, Bool is_primary) { int i; + if (is_primary) + { + /* Do this for the primary selection instead. */ + + for (i = 0; i < num_x_primary_targets; ++i) + { + if (x_primary_targets[i] == atom) + return True; + } + + return False; + } + for (i = 0; i < num_x_selection_targets; ++i) { if (x_selection_targets[i] == atom) @@ -341,7 +375,7 @@ HasSelectionTarget (Atom atom) } static TargetMapping * -FindTranslationForMimeType (const char *mime_type) +FindTranslationForMimeType (const char *mime_type, Bool is_primary) { unsigned short *buckets, i; unsigned int idx; @@ -349,12 +383,21 @@ FindTranslationForMimeType (const char *mime_type) idx = HashMimeString (mime_type) % 32; buckets = mapping_table.buckets[idx]; + DebugPrint ("Looking for translation of MIME type %s\n", + mime_type); + for (i = 0; i < mapping_table.n_elements[idx]; ++i) { if (!strcmp (direct_transfer[buckets[i]].mime_type, mime_type) - && HasSelectionTarget (MappingAtom (&direct_transfer[buckets[i]]))) - return &direct_transfer[buckets[i]]; + && HasSelectionTarget (MappingAtom (&direct_transfer[buckets[i]]), + is_primary)) + { + DebugPrint ("Found translation for MIME type %s\n", + mime_type); + + return &direct_transfer[buckets[i]]; + } } return NULL; @@ -644,34 +687,40 @@ PostReceiveDirect (Time time, Atom selection, Atom target, int fd) static void PostReceiveConversion (Time, Atom, Atom, int); +#define ReceiveBody(selection, primary) \ + Time time; \ + TargetMapping *translation; \ + \ + DebugPrint ("Receiving %s from X " #selection " \n", \ + mime_type); \ + \ + /* Cast to intptr_t to silence warnings when the pointer type is \ + larger than long. */ \ + time = (Time) (intptr_t) wl_resource_get_user_data (resource); \ + \ + /* Find which selection target corresponds to MIME_TYPE. */ \ + translation = FindTranslationForMimeType (mime_type, primary); \ + \ + if (translation) \ + { \ + if (!translation->translation_func) \ + /* If a corresponding target exists, ask to receive it. */ \ + PostReceiveDirect (time, selection, \ + MappingAtom (translation), fd); \ + else \ + /* Otherwise, use the translation function. */ \ + translation->translation_func (time, selection, \ + MappingAtom (translation), \ + fd); \ + } \ + else \ + close (fd) + static void Receive (struct wl_client *client, struct wl_resource *resource, const char *mime_type, int fd) { - Time time; - TargetMapping *translation; - - /* Cast to intptr_t to silence warnings when the pointer type is - larger than long. */ - time = (Time) (intptr_t) wl_resource_get_user_data (resource); - - /* Find which selection target corresponds to MIME_TYPE. */ - translation = FindTranslationForMimeType (mime_type); - - if (translation) - { - if (!translation->translation_func) - /* If a corresponding target exists, ask to receive it. */ - PostReceiveDirect (time, CLIPBOARD, - MappingAtom (translation), fd); - else - /* Otherwise, use the translation function. */ - translation->translation_func (time, CLIPBOARD, - MappingAtom (translation), - fd); - } - else - close (fd); + ReceiveBody (CLIPBOARD, False); } static void @@ -683,14 +732,16 @@ Destroy (struct wl_client *client, struct wl_resource *resource) static void Finish (struct wl_client *client, struct wl_resource *resource) { - /* TODO... */ + wl_resource_post_error (resource, WL_DATA_OFFER_ERROR_INVALID_FINISH, + "trying to finish foreign data offer"); } static void SetActions (struct wl_client *client, struct wl_resource *resource, uint32_t dnd_actions, uint32_t preferred_action) { - /* TODO... */ + wl_resource_post_error (resource, WL_DATA_OFFER_ERROR_INVALID_OFFER, + "trying to finish non-drag-and-drop data offer"); } static const struct wl_data_offer_interface wl_data_offer_impl = @@ -721,6 +772,38 @@ CreateOffer (struct wl_client *client, Time time) return resource; } +static void +ReceivePrimary (struct wl_client *client, struct wl_resource *resource, + const char *mime_type, int fd) +{ + ReceiveBody (XA_PRIMARY, True); +} + +static struct zwp_primary_selection_offer_v1_interface primary_offer_impl = + { + .receive = ReceivePrimary, + .destroy = Destroy, + }; + +static struct wl_resource * +CreatePrimaryOffer (struct wl_client *client, Time time) +{ + struct wl_resource *resource; + + resource = wl_resource_create (client, + &zwp_primary_selection_offer_v1_interface, + 1, 0); + if (!resource) + /* If allocating the resource failed, return NULL. */ + return NULL; + + /* Otherwise, set the user_data to the time of the selection + change. */ + wl_resource_set_implementation (resource, &primary_offer_impl, + (void *) time, NULL); + return resource; +} + static Bool CheckDuplicate (unsigned short index, Atom a) { @@ -765,20 +848,17 @@ CheckDuplicate (unsigned short index, Atom a) } static void -SendOffers (struct wl_resource *resource, Time time) +SendOffers1 (struct wl_resource *resource, int ntargets, Atom *targets, + void (*send_offer_func) (struct wl_resource *, const char *)) { int i, j; unsigned int idx; unsigned short *buckets; - if (time < last_x_selection_change) - /* This offer is out of date. */ - return; - - for (i = 0; i < num_x_selection_targets; ++i) + for (i = 0; i < ntargets; ++i) { /* Offer each type corresponding to this target. */ - idx = x_selection_targets[i] % 16; + idx = targets[i] % 16; /* N.B. that duplicates do appear in the atom buckets, which is intentional. */ @@ -787,13 +867,12 @@ SendOffers (struct wl_resource *resource, Time time) for (j = 0; j < mapping_table.n_atom_elements[idx]; ++j) { if (MappingIs (&direct_transfer[buckets[j]], - x_selection_targets[i]) - && CheckDuplicate (buckets[j], - x_selection_targets[i])) + targets[i]) + && CheckDuplicate (buckets[j], targets[i])) /* If it exists and was not previously offered, offer it to the client. */ - wl_data_offer_send_offer (resource, - direct_transfer[buckets[j]].mime_type); + send_offer_func (resource, + direct_transfer[buckets[j]].mime_type); } } @@ -804,10 +883,62 @@ SendOffers (struct wl_resource *resource, Time time) } static void -HandleNewSelection (Time time, Atom *targets, int ntargets) +SendOffers (struct wl_resource *resource, Time time) +{ + if (time < last_x_selection_change) + /* This offer is out of date. */ + return; + + SendOffers1 (resource, num_x_selection_targets, + x_selection_targets, wl_data_offer_send_offer); +} + +static void +SendPrimaryOffers (struct wl_resource *resource, Time time) +{ + if (time < last_primary_time) + /* This offer is out of date. */ + return; + + SendOffers1 (resource, num_x_primary_targets, + x_primary_targets, + zwp_primary_selection_offer_v1_send_offer); +} + +static void +HandleNewSelection (Time time, Atom selection, Atom *targets, + int ntargets) { CreateOfferFuncs funcs; + if (selection == XA_PRIMARY) + { + /* The primary selection changed, and now has the given + targets. */ + + if (time < last_primary_time) + { + XLFree (targets); + return; + } + + /* Set up the new primary targets. */ + XLFree (x_primary_targets); + x_primary_targets = targets; + num_x_primary_targets = ntargets; + last_primary_time = time; + + /* Add the right functions and set them as the foreign primary + selection handler at TIME. */ + funcs.create_offer = CreatePrimaryOffer; + funcs.send_offers = SendPrimaryOffers; + + XLSetForeignPrimary (time, funcs); + return; + } + + /* Else, the selection that changed is CLIPBOARD. */ + /* Ignore outdated selection changes. */ if (time < last_x_selection_change) { @@ -888,13 +1019,14 @@ TargetsFinishCallback (ReadTransfer *transfer, Bool success) data = GetTransferData (transfer); if (success) - DebugPrint ("Received targets from CLIPBOARD\n"); + DebugPrint ("Received targets from %lu\n", data->selection); else - DebugPrint ("Failed to obtain targets from CLIPBOARD\n"); + DebugPrint ("Failed to obtain targets\n"); if (success) HandleNewSelection (GetTransferTime (transfer), - data->atoms, data->n_atoms); + data->selection, data->atoms, + data->n_atoms); else /* HandleNewSelection keeps data->atoms around for a while. */ XLFree (data->atoms); @@ -913,12 +1045,28 @@ NoticeClipboardChanged (Time time) ReadTargetsData *data; data = XLCalloc (1, sizeof *data); + data->selection = CLIPBOARD; ConvertSelectionFuncs (CLIPBOARD, TARGETS, time, data, NULL, TargetsReadCallback, TargetsFinishCallback); } +/* The same, but for the primary selection. */ + +static void +NoticePrimaryChanged (Time time) +{ + ReadTargetsData *data; + + data = XLCalloc (1, sizeof *data); + data->selection = XA_PRIMARY; + + ConvertSelectionFuncs (XA_PRIMARY, TARGETS, time, data, + NULL, TargetsReadCallback, + TargetsFinishCallback); +} + static void NoticeClipboardCleared (Time time) { @@ -926,8 +1074,33 @@ NoticeClipboardCleared (Time time) if (time < last_x_selection_change) return; + DebugPrint ("CLIPBOARD was cleared at %lu\n", time); + last_x_selection_change = time; XLClearForeignSelection (time); + + /* Free data that is no longer used. */ + XLFree (x_selection_targets); + x_selection_targets = NULL; + num_x_selection_targets = 0; +} + +static void +NoticePrimaryCleared (Time time) +{ + /* Ignore outdated events. */ + if (time < last_primary_time) + return; + + DebugPrint ("PRIMARY was cleared at %lu\n", time); + + last_primary_time = time; + XLClearForeignPrimary (time); + + /* Free data that is no longer used. */ + XLFree (x_primary_targets); + x_primary_targets = NULL; + num_x_primary_targets = 0; } static void @@ -943,11 +1116,20 @@ HandleSelectionNotify (XFixesSelectionNotifyEvent *event) disowning the selection were successful. */ last_clipboard_change = event->selection_timestamp; + if (event->selection == XA_PRIMARY + && event->selection_timestamp > last_primary_time) + last_primary_time = event->selection_timestamp; + if (event->owner != None && event->selection == CLIPBOARD) NoticeClipboardChanged (event->timestamp); - else + else if (event->selection == CLIPBOARD) NoticeClipboardCleared (event->timestamp); + else if (event->owner != None + && event->selection == XA_PRIMARY) + NoticePrimaryChanged (event->timestamp); + else if (event->selection == XA_PRIMARY) + NoticePrimaryCleared (event->timestamp); } Bool @@ -1001,17 +1183,25 @@ GetDataConversion (Atom target) } static const char * -MimeTypeFromTarget (Atom target) +MimeTypeFromTarget (Atom target, Bool primary) { DataConversion *conversion; static char *string; + Bool missing_type; /* TODO: replace XGetAtomName with something more efficient. */ if (string) XFree (string); string = NULL; - if (!XLDataSourceHasAtomTarget (selection_data_source, target)) + if (!primary) + missing_type = !XLDataSourceHasAtomTarget (selection_data_source, + target); + else + missing_type = !XLPDataSourceHasAtomTarget (primary_data_source, + target); + + if (missing_type) { /* If the data source does not itself provide the specified target, then a conversion function is in use. */ @@ -1032,11 +1222,19 @@ MimeTypeFromTarget (Atom target) } static Atom -TypeFromTarget (Atom target) +TypeFromTarget (Atom target, Bool primary) { DataConversion *conversion; + Bool missing_type; - if (!XLDataSourceHasAtomTarget (selection_data_source, target)) + if (!primary) + missing_type = !XLDataSourceHasAtomTarget (selection_data_source, + target); + else + missing_type = !XLPDataSourceHasAtomTarget (primary_data_source, + target); + + if (missing_type) { /* If the data source does not itself provide the specified target, then a conversion function is in use. Use the type @@ -1183,11 +1381,14 @@ GetClipboardCallback (WriteTransfer *transfer, Atom target, WriteInfo *info; int pipe[2], rc; DataConversion *conversion; + struct wl_resource *resource; /* If the selection data source is destroyed, the selection will be disowned. */ XLAssert (selection_data_source != NULL); + resource = XLResourceFromDataSource (selection_data_source); + if (!XLDataSourceHasAtomTarget (selection_data_source, target)) { /* If the data source does not itself provide the specified @@ -1202,7 +1403,9 @@ GetClipboardCallback (WriteTransfer *transfer, Atom target, if (conversion->clipboard_callback) return conversion->clipboard_callback (transfer, target, - type); + type, resource, + wl_data_source_send_send, + False); /* Otherwise, use the default callback. */ DebugPrint ("Conversion to type %lu specified with default callback\n", @@ -1223,12 +1426,13 @@ GetClipboardCallback (WriteTransfer *transfer, Atom target, DebugPrint ("Created pipe (%d, %d)\n", pipe[0], pipe[1]); /* Send the write end of the pipe to the source. */ - wl_data_source_send_send (XLResourceFromDataSource (selection_data_source), - MimeTypeFromTarget (target), pipe[1]); + wl_data_source_send_send (resource, + MimeTypeFromTarget (target, False), + pipe[1]); close (pipe[1]); /* And set the prop type appropriately. */ - *type = TypeFromTarget (target); + *type = TypeFromTarget (target, False); /* Create the transfer info structure. */ info = XLMalloc (sizeof *info); @@ -1416,8 +1620,11 @@ ConversionReadFunc (WriteTransfer *transfer, unsigned char *buffer, } static GetDataFunc -GetConversionCallback (WriteTransfer *transfer, Atom target, - Atom *type) +GetConversionCallback (WriteTransfer *transfer, Atom target, Atom *type, + struct wl_resource *source, + void (*send_send) (struct wl_resource *, const char *, + int), + Bool is_primary) { ConversionWriteInfo *info; int pipe[2], rc; @@ -1441,8 +1648,7 @@ GetConversionCallback (WriteTransfer *transfer, Atom target, return NULL; /* Then, send the write end of the pipe to the data source. */ - wl_data_source_send_send (XLResourceFromDataSource (selection_data_source), - "text/plain;charset=utf-8", pipe[1]); + send_send (source, "text/plain;charset=utf-8", pipe[1]); close (pipe[1]); /* Following that, set the property type correctly. */ @@ -1677,7 +1883,7 @@ GetDragCallback (WriteTransfer *transfer, Atom target, be disowned. */ XLAssert (drag_data_source != NULL); - DebugPrint ("Entered GetClipboardCallback; target is %lu\n", + DebugPrint ("Entered GetDragCallback; target is %lu\n", target); /* First, create a non-blocking pipe. We will give the write end to @@ -1757,13 +1963,26 @@ XLNoteSourceDestroyed (DataSource *source) if (source == drag_data_source) { DebugPrint ("Disowning XdndSelection\nThis is being done in response" - "to the data source being destroyed.\n"); + " to the data source being destroyed.\n"); /* Disown the selection. */ DisownSelection (XdndSelection); } } +void +XLNotePrimaryDestroyed (PDataSource *source) +{ + if (source == primary_data_source) + { + DebugPrint ("Disowning PRIMARY\nThis is being done in response" + " to the data source being destroyed.\n"); + + /* Disown the selection. */ + DisownSelection (XA_PRIMARY); + } +} + static Bool FindTargetInArray (Atom *targets, int ntargets, Atom atom) { @@ -1778,52 +1997,96 @@ FindTargetInArray (Atom *targets, int ntargets, Atom atom) return False; } +/* FIXME: this should really be something other than two giant + macros... */ + +#define NoteLocalSelectionBody(callback, time_1, time_2, atom, field, foreign, n_foreign) \ + Time time; \ + Atom *targets; \ + int ntargets, i, n_data_conversions; \ + Bool rc; \ + \ + if (source == NULL) \ + { \ + DebugPrint ("Disowning " #atom " at %lu (vs. last change %lu)\n", \ + time_1, time_2); \ + \ + /* Disown the selection. */ \ + DisownSelection (atom); \ + field = NULL; \ + \ + /* Return whether or not the selection was actually \ + disowned. */ \ + return time_1 >= time_2; \ + } \ + \ + time = XLSeatGetLastUserTime (seat); \ + \ + DebugPrint ("Acquiring ownership of " #atom " at %lu\n", time); \ + \ + if (!time) \ + /* Nothing has yet happened on the seat. */ \ + return False; \ + \ + if (time < time_1 || time < time_2) \ + /* TIME is out of date. */ \ + return False; \ + \ + DebugPrint ("Setting callback function for " #atom "\n"); \ + \ + /* Since the local selection is now set, free foreign selection \ + data. */ \ + XLFree (foreign); \ + n_foreign = 0; \ + foreign = NULL; \ + \ + /* Otherwise, set the clipboard change time to TIME. */ \ + time_1 = time; \ + time_2 = time; \ + \ + +#define NoteLocalSelectionFooter(callback, atom, field, has_target) \ + /* Now, look to see what standard X targets the client doesn't \ + offer, and add them to the targets array. \ + \ + Most functioning Wayland clients will also offer the typical X \ + selection targets such as STRING and UTF8_STRING in addition to \ + MIME types; when they do, they perform the data conversion \ + conversion better than us. */ \ + \ + for (i = 0; i < ArrayElements (data_conversions); ++i) \ + { \ + /* If the source provides MIME typed-data for a format we know \ + how to convert, announce the associated selection conversion \ + targets. */ \ + \ + if (has_target (source, \ + data_conversions[i].mime_type) \ + && !FindTargetInArray (targets, ntargets, \ + data_conversions[i].type)) \ + { \ + DebugPrint ("Client doesn't provide standard X conversion" \ + " target for %s; adding it\n", \ + data_conversions[i].mime_type); \ + \ + targets[ntargets++] = data_conversions[i].type; \ + } \ + } \ + \ + /* And own the selection. */ \ + field = source; \ + \ + rc = OwnSelection (time, atom, callback, targets, ntargets); \ + XLFree (targets); \ + return rc \ + Bool XLNoteLocalSelection (Seat *seat, DataSource *source) { - Time time; - Atom *targets; - int ntargets, i, n_data_conversions; - Bool rc; - - if (source == NULL) - { - DebugPrint ("Disowning CLIPBOARD at %lu (vs. last change %lu)\n", - last_clipboard_time, last_x_selection_change); - - /* Disown the selection. */ - DisownSelection (CLIPBOARD); - selection_data_source = NULL; - - /* Return whether or not the selection was actually - disowned. */ - return last_clipboard_time >= last_x_selection_change; - } - - time = XLSeatGetLastUserTime (seat); - - DebugPrint ("Acquiring ownership of CLIPBOARD at %lu\n", time); - - if (!time) - /* Nothing has yet happened on the seat. */ - return False; - - if (time < last_clipboard_time - || time < last_clipboard_change) - /* TIME is out of date. */ - return False; - - DebugPrint ("Setting callback function for CLIPBOARD\n"); - - /* Since the local selection is now set, free foreign selection - data. */ - XLFree (x_selection_targets); - num_x_selection_targets = 0; - x_selection_targets = NULL; - - /* Otherwise, set the clipboard change time to TIME. */ - last_clipboard_time = time; - last_clipboard_change = time; + NoteLocalSelectionBody (GetClipboardCallback, last_clipboard_time, + last_x_selection_change, CLIPBOARD, + selection_data_source, x_selection_targets, + num_x_selection_targets); /* And copy the targets from the data source. */ ntargets = XLDataSourceTargetCount (source); @@ -1831,41 +2094,113 @@ XLNoteLocalSelection (Seat *seat, DataSource *source) targets = XLMalloc (sizeof *targets * (ntargets + n_data_conversions)); XLDataSourceGetTargets (source, targets); - /* Now, look to see what standard X targets the client doesn't - offer, and add them to the targets array. + NoteLocalSelectionFooter (GetClipboardCallback, CLIPBOARD, + selection_data_source, + XLDataSourceHasTarget); +} - Most functioning Wayland clients will also offer the typical X - selection targets such as STRING and UTF8_STRING in addition to - MIME types; when they do, they perform the data conversion - conversion better than us. */ +static GetDataFunc +GetPrimaryCallback (WriteTransfer *transfer, Atom target, + Atom *type) +{ + WriteInfo *info; + int pipe[2], rc; + DataConversion *conversion; + struct wl_resource *resource; - for (i = 0; i < ArrayElements (data_conversions); ++i) + /* If the selection data source is destroyed, the selection will be + disowned. */ + XLAssert (primary_data_source != NULL); + + resource = XLResourceFromPDataSource (primary_data_source); + + if (!XLPDataSourceHasAtomTarget (primary_data_source, target)) { - /* If the source provides MIME typed-data for a format we know - how to convert, announce the associated selection conversion - targets. */ + /* If the data source does not itself provide the specified + target, then a conversion function is in use. Call the + clipboard callback specified in the conversion table entry, + if it exists. */ + conversion = GetDataConversion (target); - if (XLDataSourceHasTarget (source, - data_conversions[i].mime_type) - && !FindTargetInArray (targets, ntargets, - data_conversions[i].type)) - { - DebugPrint ("Client doesn't provide standard X conversion" - " target for %s; adding it\n", - data_conversions[i].mime_type); + if (!conversion) + /* There must be a data conversion here. */ + abort (); - targets[ntargets++] = data_conversions[i].type; - } + /* This is ugly but the only reasonable way to fit the following + function in 80 columns. */ + +#define Function zwp_primary_selection_source_v1_send_send + + if (conversion->clipboard_callback) + return conversion->clipboard_callback (transfer, target, + type, resource, + Function, True); + +#undef Function + + /* Otherwise, use the default callback. */ + DebugPrint ("Conversion to type %lu specified with default callback\n", + target); } - /* And own the selection. */ - selection_data_source = source; + DebugPrint ("Entered GetPrimaryCallback; target is %lu\n", + target); - rc = OwnSelection (time, CLIPBOARD, GetClipboardCallback, - targets, ntargets); - XLFree (targets); + /* First, create a non-blocking pipe. We will give the write end to + the client. */ + rc = pipe2 (pipe, O_NONBLOCK); - return rc; + if (rc) + /* Creating the pipe failed. */ + return NULL; + + DebugPrint ("Created pipe (%d, %d)\n", pipe[0], pipe[1]); + + /* Send the write end of the pipe to the source. */ + zwp_primary_selection_source_v1_send_send (resource, + MimeTypeFromTarget (target, True), + pipe[1]); + close (pipe[1]); + + /* And set the prop type appropriately. */ + *type = TypeFromTarget (target, True); + + /* Create the transfer info structure. */ + info = XLMalloc (sizeof *info); + info->fd = pipe[0]; + info->flags = 0; + + DebugPrint ("Adding the read callback\n"); + + /* Wait for info to become readable. */ + info->read_callback = XLAddReadFd (info->fd, transfer, + NoticeTransferReadable); + + /* Set info as the transfer data. */ + SetWriteTransferData (transfer, info); + + return ClipboardReadFunc; +} + +Bool +XLNoteLocalPrimary (Seat *seat, PDataSource *source) +{ + /* I forgot why there are two variables for tracking the clipboard + time, so just use one here. */ + NoteLocalSelectionBody (GetPrimaryCallback, last_primary_time, + last_primary_time, XA_PRIMARY, + primary_data_source, x_primary_targets, + num_x_primary_targets); + + /* And copy the targets from the data source. */ + ntargets = XLPDataSourceTargetCount (source); + n_data_conversions = ArrayElements (data_conversions); + targets = XLMalloc (sizeof *targets * (ntargets + n_data_conversions)); + XLPDataSourceGetTargets (source, targets); + + NoteLocalSelectionFooter (GetPrimaryCallback, XA_PRIMARY, + primary_data_source, + XLPDataSourceHasTarget); } void @@ -1896,6 +2231,7 @@ XLInitXData (void) } SelectSelectionInput (CLIPBOARD); + SelectSelectionInput (XA_PRIMARY); /* Initialize atoms used in the direct transfer table. */ direct_transfer[1].atom_flag = UTF8_STRING | Duplicate; diff --git a/xdg_surface.c b/xdg_surface.c index b149558..c5218d8 100644 --- a/xdg_surface.c +++ b/xdg_surface.c @@ -35,7 +35,9 @@ enum StateLateFrame = (1 << 1), StatePendingWindowGeometry = (1 << 2), StateWaitingForAckConfigure = (1 << 3), - StateLateFrameAcked = (1 << 4), + StateWaitingForAckCommit = (1 << 4), + StateLateFrameAcked = (1 << 5), + StateMaybeConfigure = (1 << 6), }; typedef struct _XdgRole XdgRole; @@ -599,6 +601,10 @@ AckConfigure (struct wl_client *client, struct wl_resource *resource, if (XLFrameClockIsFrozen (xdg_role->clock) && xdg_role->role.surface) RunFrameCallbacksConditionally (xdg_role); + +#ifdef DEBUG_GEOMETRY_CALCULATION + fprintf (stderr, "Client acknowledged configuration\n"); +#endif } if (xdg_role->impl) @@ -656,6 +662,10 @@ Commit (Surface *surface, Role *role) xdg_role->state &= ~StatePendingWindowGeometry; } + /* This flag means no commit has happened after an + ack_configure. */ + xdg_role->state &= ~StateWaitingForAckCommit; + /* A frame is already in progress, so instead say that an urgent update is needed immediately after the frame completes. In any case, don't run frame callbacks upon buffer release anymore. */ @@ -1062,6 +1072,13 @@ NoteBounds (void *data, int min_x, int min_y, Don't resize the window until it's acknowledged. */ return; + if (role->state & StateWaitingForAckCommit) + /* Don't resize the window until all configure events are + acknowledged. We wait for a commit on the xdg_toplevel to do + this, because Firefox updates subsurfaces while the old size is + still in effect. */ + return; + /* Avoid resizing the window should its actual size not have changed. */ @@ -1071,6 +1088,10 @@ NoteBounds (void *data, int min_x, int min_y, if (role->bounds_width != bounds_width || role->bounds_height != bounds_height) { +#ifdef DEBUG_GEOMETRY_CALCULATION + fprintf (stderr, "Resizing to: %d %d\n", bounds_width, bounds_height); +#endif + if (role->impl->funcs.note_window_pre_resize) role->impl->funcs.note_window_pre_resize (&role->role, role->impl, @@ -1225,6 +1246,24 @@ WriteRedirectProperty (XdgRole *role) (unsigned char *) &bypass_compositor, 1); } +static void +HandleFreeze (void *data) +{ + XdgRole *role; + + role = data; + + /* _NET_WM_SYNC_REQUEST events should be succeeded by a + ConfigureNotify event. */ + role->state |= StateWaitingForAckConfigure; + role->state |= StateWaitingForAckCommit; + + /* This flag means the WaitingForAckConfigure was caused by a + _NET_WM_SYNC_REQUEST, and the following ConfigureNotify event + might not lead to a configure event being sent. */ + role->state |= StateMaybeConfigure; +} + void XLGetXdgSurface (struct wl_client *client, struct wl_resource *resource, uint32_t id, struct wl_resource *surface_resource) @@ -1331,6 +1370,8 @@ XLGetXdgSurface (struct wl_client *client, struct wl_resource *resource, role->subcompositor = MakeSubcompositor (); role->clock = XLMakeFrameClockForWindow (role->window); + XLFrameClockSetFreezeCallback (role->clock, HandleFreeze, role); + SubcompositorSetTarget (role->subcompositor, role->picture); SubcompositorSetInputCallback (role->subcompositor, InputRegionChanged, role); @@ -1412,6 +1453,16 @@ XLXdgRoleSendConfigure (Role *role, uint32_t serial) xdg_role = XdgRoleFromRole (role); xdg_role->conf_serial = serial; xdg_role->state |= StateWaitingForAckConfigure; + xdg_role->state |= StateWaitingForAckCommit; + + /* We know know that the ConfigureNotify event following any + _NET_WM_SYNC_REQUEST event was accepted, so clear the maybe + configure flag. */ + xdg_role->state &= ~StateMaybeConfigure; + +#ifdef DEBUG_GEOMETRY_CALCULATION + fprintf (stderr, "Waiting for ack_configure...\n"); +#endif xdg_surface_send_configure (role->resource, serial); } @@ -1767,3 +1818,24 @@ XLLookUpXdgPopup (Window window) return role->impl; } + +void +XLXdgRoleNoteRejectedConfigure (Role *role) +{ + XdgRole *xdg_role; + + xdg_role = XdgRoleFromRole (role); + + if (xdg_role->state & StateMaybeConfigure) + { + /* A configure event immediately following _NET_WM_SYNC_REQUEST + was rejected, meaning that we do not have to change anything + before unfreezing the frame clock. */ + xdg_role->state &= ~StateWaitingForAckConfigure; + xdg_role->state &= ~StateWaitingForAckCommit; + xdg_role->state &= ~StateMaybeConfigure; + + /* Unfreeze the frame clock now. */ + XLFrameClockUnfreeze (xdg_role->clock); + } +} diff --git a/xdg_toplevel.c b/xdg_toplevel.c index d85f2e9..664bb07 100644 --- a/xdg_toplevel.c +++ b/xdg_toplevel.c @@ -37,11 +37,14 @@ typedef enum _How How; enum { - StateIsMapped = 1, - StateMissingState = (1 << 1), - StatePendingMaxSize = (1 << 2), - StatePendingMinSize = (1 << 3), - StatePendingAckMovement = (1 << 4), + StateIsMapped = 1, + StateMissingState = (1 << 1), + StatePendingMaxSize = (1 << 2), + StatePendingMinSize = (1 << 3), + StatePendingAckMovement = (1 << 4), + StatePendingResize = (1 << 5), + StatePendingConfigureSize = (1 << 6), + StatePendingConfigureStates = (1 << 7), }; enum @@ -195,10 +198,26 @@ struct _XdgToplevel int width01, height01, width10, height10; int width00, height00, width11, height11; + /* The width, height, west and north motion of the next resize. */ + int resize_width, resize_height, resize_west, resize_north; + /* Mask of what this toplevel is allowed to do. It is first set based on _NET_SUPPORTED upon toplevel creation, and then _NET_WM_ALLOWED_ACTIONS. */ int supported; + + /* Timer for completing window state changes. The order of + _NET_WM_STATE changes and ConfigureNotify events is not + predictable, so we batch up both kinds of events with a 0.01 + second delay by default, before sending the resulting + ConfigureNotify event. However, if drag-to-resize is in + progress, no such delay is effected. */ +#define DefaultStateDelayNanoseconds 10000000 + Timer *configuration_timer; + + /* The width and height used by that timer if + StatePendingConfigureSize is set. */ + int configure_width, configure_height; }; /* iconv context used to convert between UTF-8 and Latin-1. */ @@ -208,6 +227,10 @@ static iconv_t latin_1_cd; with configure events. */ static Bool apply_state_workaround; +/* Whether or not to batch together state and size changes that arrive + at almost the same time. */ +static Bool batch_state_changes; + static XdgUnmapCallback * RunOnUnmap (XdgToplevel *toplevel, void (*unmap) (void *), void *data) @@ -290,6 +313,10 @@ DestroyBacking (XdgToplevel *toplevel) if (--toplevel->refcount) return; + /* If there is a pending configuration timer, remove it. */ + if (toplevel->configuration_timer) + RemoveTimer (toplevel->configuration_timer); + if (toplevel->parent_callback) CancelUnmapCallback (toplevel->parent_callback); @@ -325,6 +352,109 @@ SendConfigure (XdgToplevel *toplevel, unsigned int width, toplevel->conf_serial = serial; } +/* Forward declaration. */ + +static void SendStates (XdgToplevel *); +static void WriteStates (XdgToplevel *); + +static void +NoteConfigureTime (Timer *timer, void *data, struct timespec time) +{ + XdgToplevel *toplevel; + int width, height, effective_width, effective_height; + + toplevel = data; + + /* If only the window state changed, call SendStates. */ + if (!(toplevel->state & StatePendingConfigureSize)) + SendStates (toplevel); + else + { + /* If the states changed, write them. */ + if (toplevel->state & StatePendingConfigureStates) + WriteStates (toplevel); + + effective_width = toplevel->configure_width / global_scale_factor; + effective_height = toplevel->configure_height / global_scale_factor; + + /* Compute the geometry for the configure event based on the + current size of the toplevel. */ + XLXdgRoleCalcNewWindowSize (toplevel->role, + effective_width, + effective_height, + &width, &height); + + /* Send the configure event. */ + SendConfigure (toplevel, width, height); + + /* Mark the state has having been calculated if some state + transition has occured. */ + if (toplevel->toplevel_state.fullscreen + || toplevel->toplevel_state.maximized) + toplevel->state &= ~StateMissingState; + } + + /* Clear the pending size and state flags. */ + toplevel->state &= ~StatePendingConfigureSize; + toplevel->state &= ~StatePendingConfigureStates; + + /* Cancel and clear the timer. */ + RemoveTimer (timer); + toplevel->configuration_timer = NULL; +} + +static void +FlushConfigurationTimer (XdgToplevel *toplevel) +{ + if (!toplevel->configuration_timer) + return; + + /* Cancel the configuration timer and flush pending state to the + state array. It is assumed that a configure event will be sent + immediately afterwards. */ + + if (toplevel->state & StatePendingConfigureStates) + WriteStates (toplevel); + + /* Clear the pending size and state flags. */ + toplevel->state &= ~StatePendingConfigureSize; + toplevel->state &= ~StatePendingConfigureStates; + + /* Cancel and clear the timer. */ + RemoveTimer (toplevel->configuration_timer); + toplevel->configuration_timer = NULL; +} + +static Bool +MaybePostDelayedConfigure (XdgToplevel *toplevel, int flag) +{ + XLList *tem; + + if (!batch_state_changes) + return False; + + toplevel->state |= flag; + + if (toplevel->configuration_timer) + { + /* The timer is already ticking... */ + RetimeTimer (toplevel->configuration_timer); + return True; + } + + /* If some seat is being resized, return False. */ + for (tem = live_seats; tem; tem = tem->next) + { + if (XLSeatResizeInProgress (tem->data)) + return False; + } + + toplevel->configuration_timer + = AddTimer (NoteConfigureTime, toplevel, + MakeTimespec (0, DefaultStateDelayNanoseconds)); + return True; +} + static void WriteStates (XdgToplevel *toplevel) { @@ -477,7 +607,9 @@ HandleWmStateChange (XdgToplevel *toplevel) state->maximized = True; } - if (memcmp (&old, &state, sizeof *state)) + if (memcmp (&old, &state, sizeof *state) + && !MaybePostDelayedConfigure (toplevel, + StatePendingConfigureStates)) /* Finally, send states if they changed. */ SendStates (toplevel); @@ -826,6 +958,11 @@ Unmap (XdgToplevel *toplevel) memset (&toplevel->state, 0, sizeof toplevel->states); + /* If there is a pending configure timer, remove it. */ + if (toplevel->configuration_timer) + RemoveTimer (toplevel->configuration_timer); + toplevel->configuration_timer = NULL; + XLListFree (toplevel->resize_callbacks, XLSeatCancelResizeCallback); toplevel->resize_callbacks = NULL; @@ -909,6 +1046,7 @@ Commit (Role *role, Surface *surface, XdgRoleImplementation *impl) if (toplevel->state & StateIsMapped) Unmap (toplevel); + FlushConfigurationTimer (toplevel); SendConfigure (toplevel, 0, 0); } else if (!toplevel->conf_reply) @@ -919,6 +1057,67 @@ Commit (Role *role, Surface *surface, XdgRoleImplementation *impl) } } +static void +PostResize1 (XdgToplevel *toplevel, int west_motion, int north_motion, + int new_width, int new_height) +{ + /* FIXME: the two computations below are still not completely + right. */ + + if (new_width < toplevel->min_width) + { + west_motion += toplevel->min_width - new_width; + + /* Don't move too far west. */ + if (west_motion > 0) + west_motion = 0; + + new_width = toplevel->min_width; + } + + if (new_height < toplevel->min_height) + { + north_motion += toplevel->min_height - new_height; + + /* Don't move too far north. */ + if (north_motion > 0) + north_motion = 0; + + new_height = toplevel->min_height; + } + + if (!(toplevel->state & StatePendingAckMovement)) + { + FlushConfigurationTimer (toplevel); + SendConfigure (toplevel, new_width, new_height); + + toplevel->ack_west += west_motion; + toplevel->ack_north += north_motion; + toplevel->state |= StatePendingAckMovement; + + /* Clear extra state flags that are no longer useful. */ + toplevel->state &= ~StatePendingResize; + toplevel->resize_west = 0; + toplevel->resize_north = 0; + toplevel->resize_width = 0; + toplevel->resize_height = 0; + } + else + { + /* A configure event has been posted but has not yet been fully + processed. Accumulate the new width, height, west and north + values, and send another configure event once it really does + arrive, and the previous changes have been committed. */ + + toplevel->state |= StatePendingResize; + + toplevel->resize_west += west_motion; + toplevel->resize_north += north_motion; + toplevel->resize_width = new_width; + toplevel->resize_height = new_height; + } +} + static void CommitInsideFrame (Role *role, XdgRoleImplementation *impl) { @@ -935,6 +1134,14 @@ CommitInsideFrame (Role *role, XdgRoleImplementation *impl) toplevel->ack_west = 0; toplevel->ack_north = 0; toplevel->state &= ~StatePendingAckMovement; + + /* This pending movement has completed. Apply postponed state, + if there is any. */ + if (toplevel->state & StatePendingResize) + PostResize1 (toplevel, toplevel->resize_west, + toplevel->resize_north, + toplevel->resize_width, + toplevel->resize_height); } } @@ -994,7 +1201,14 @@ HandleConfigureEvent (XdgToplevel *toplevel, XEvent *event) if (event->xconfigure.width == toplevel->width && event->xconfigure.height == toplevel->height) - return True; + { + if (!toplevel->configuration_timer) + /* No configure event will be generated in the future. + Unfreeze the frame clock. */ + XLXdgRoleNoteRejectedConfigure (toplevel->role); + + return True; + } /* Try to guess if the window state was restored to some earlier value, and set it now, to avoid race conditions when some clients @@ -1005,16 +1219,21 @@ HandleConfigureEvent (XdgToplevel *toplevel, XEvent *event) event->xconfigure.height)) WriteStates (toplevel); - XLXdgRoleCalcNewWindowSize (toplevel->role, - ConfigureWidth (event), - ConfigureHeight (event), - &width, &height); + if (!MaybePostDelayedConfigure (toplevel, StatePendingConfigureSize)) + { + XLXdgRoleCalcNewWindowSize (toplevel->role, + ConfigureWidth (event), + ConfigureHeight (event), + &width, &height); - SendConfigure (toplevel, width, height); + SendConfigure (toplevel, width, height); + } /* Set toplevel->width and toplevel->height correctly. */ toplevel->width = event->xconfigure.width; toplevel->height = event->xconfigure.height; + toplevel->configure_width = toplevel->width; + toplevel->configure_height = toplevel->height; /* Also set the bounds width and height to avoid resizing the window. */ @@ -1177,27 +1396,8 @@ PostResize (Role *role, XdgRoleImplementation *impl, int west_motion, XdgToplevel *toplevel; toplevel = ToplevelFromRoleImpl (impl); - - if (new_width < toplevel->min_width) - { - new_width = toplevel->min_width; - - /* FIXME: this computation is not correct, just "good - enough". */ - west_motion = 0; - } - - if (new_height < toplevel->min_height) - { - new_height = toplevel->min_height; - north_motion = 0; - } - - SendConfigure (toplevel, new_width, new_height); - - toplevel->ack_west += west_motion; - toplevel->ack_north += north_motion; - toplevel->state |= StatePendingAckMovement; + PostResize1 (toplevel, west_motion, north_motion, + new_width, new_height); } static void @@ -1858,6 +2058,7 @@ XLInitXdgToplevels (void) { latin_1_cd = iconv_open ("ISO-8859-1", "UTF-8"); apply_state_workaround = (getenv ("APPLY_STATE_WORKAROUND") != NULL); + batch_state_changes = !getenv ("DIRECT_STATE_CHANGES"); } Bool