From 222455cd643c128fa9730c9c527a7fdcadd0acfe Mon Sep 17 00:00:00 2001 From: Johan Klokkhammer Helsing Date: Tue, 17 Dec 2019 12:08:54 +0100 Subject: Client: Add basic support for tablet-unstable-v2 [ChangeLog][QPA plugin] Added support for drawing tablets through the tablet-unstable-v2 protocol. Adds support for the basic one tablet, one tool, no pads scenario. Besides the auto tests, I tested with a Huion Kamvas Pro 16 with a simple pressure sensitive pen with two buttons. As far as I can tell, it works the same way as on xcb on Sway and Gnome Shell. Also tested on a Wacom PTZ-630 with a mouse, art pen, airbrush and a stylus. Mapped the distance event to QTabletEvent::Z and slider to QTabletEvent::tangentialPressure. For now we send QTabletEvents even when there's a mouse or finger on the tablet. Those should ideally be sent as QMouseEvents/QTouchEvents, but that's out of scope for this patch, but at least we will send synthesized mouse events if the tablet events are not accepted. Change-Id: I93291ffa5f00fa2bb8533eddd8d873b84a3386b8 Reviewed-by: Shawn Rutledge --- src/3rdparty/protocol/qt_attribution.json | 18 + src/3rdparty/protocol/tablet-unstable-v2.xml | 1178 ++++++++++++++++++++++++++ src/client/client.pro | 3 + src/client/qwaylanddisplay.cpp | 3 + src/client/qwaylanddisplay_p.h | 3 + src/client/qwaylandinputdevice.cpp | 3 + src/client/qwaylandinputdevice_p.h | 5 + src/client/qwaylandintegration.cpp | 2 + src/client/qwaylandtabletv2.cpp | 332 ++++++++ src/client/qwaylandtabletv2_p.h | 191 +++++ sync.profile | 2 + tests/auto/client/client.pro | 1 + tests/auto/client/tabletv2/tabletv2.pro | 7 + tests/auto/client/tabletv2/tst_tabletv2.cpp | 918 ++++++++++++++++++++ 14 files changed, 2666 insertions(+) create mode 100644 src/3rdparty/protocol/tablet-unstable-v2.xml create mode 100644 src/client/qwaylandtabletv2.cpp create mode 100644 src/client/qwaylandtabletv2_p.h create mode 100644 tests/auto/client/tabletv2/tabletv2.pro create mode 100644 tests/auto/client/tabletv2/tst_tabletv2.cpp diff --git a/src/3rdparty/protocol/qt_attribution.json b/src/3rdparty/protocol/qt_attribution.json index c49ead4f3..dad8c4256 100644 --- a/src/3rdparty/protocol/qt_attribution.json +++ b/src/3rdparty/protocol/qt_attribution.json @@ -89,6 +89,24 @@ Copyright (c) 2013 BMW Car IT GmbH" "Copyright": "Copyright © 2013-2014 Collabora, Ltd." }, + { + "Id": "wayland-tablet-protocol", + "Name": "Wayland Tablet Protocol", + "QDocModule": "qtwaylandcompositor", + "QtUsage": "Used in the Qt Wayland platform plugin", + "Files": "tablet-unstable-v2.xml", + + "Description": "", + "Homepage": "https://wayland.freedesktop.org", + "Version": "unstable v2, version 1", + "DownloadLocation": "https://cgit.freedesktop.org/wayland/wayland-protocols/plain/unstable/tablet/tablet-unstable-v2.xml", + "LicenseId": "MIT", + "License": "MIT License", + "LicenseFile": "MIT_LICENSE.txt", + "Copyright": "Copyright 2014 © Stephen "Lyude" Chandler Paul +Copyright 2015-2016 © Red Hat, Inc." + }, + { "Id": "wayland-viewporter-protocol", "Name": "Wayland Viewporter Protocol", diff --git a/src/3rdparty/protocol/tablet-unstable-v2.xml b/src/3rdparty/protocol/tablet-unstable-v2.xml new file mode 100644 index 000000000..b286d964a --- /dev/null +++ b/src/3rdparty/protocol/tablet-unstable-v2.xml @@ -0,0 +1,1178 @@ + + + + + Copyright 2014 © Stephen "Lyude" Chandler Paul + Copyright 2015-2016 © Red Hat, Inc. + + Permission is hereby granted, free of charge, to any person + obtaining a copy of this software and associated documentation files + (the "Software"), to deal in the Software without restriction, + including without limitation the rights to use, copy, modify, merge, + publish, distribute, sublicense, and/or sell copies of the Software, + and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice (including the + next paragraph) shall be included in all copies or substantial + portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + + + + This description provides a high-level overview of the interplay between + the interfaces defined this protocol. For details, see the protocol + specification. + + More than one tablet may exist, and device-specifics matter. Tablets are + not represented by a single virtual device like wl_pointer. A client + binds to the tablet manager object which is just a proxy object. From + that, the client requests wp_tablet_manager.get_tablet_seat(wl_seat) + and that returns the actual interface that has all the tablets. With + this indirection, we can avoid merging wp_tablet into the actual Wayland + protocol, a long-term benefit. + + The wp_tablet_seat sends a "tablet added" event for each tablet + connected. That event is followed by descriptive events about the + hardware; currently that includes events for name, vid/pid and + a wp_tablet.path event that describes a local path. This path can be + used to uniquely identify a tablet or get more information through + libwacom. Emulated or nested tablets can skip any of those, e.g. a + virtual tablet may not have a vid/pid. The sequence of descriptive + events is terminated by a wp_tablet.done event to signal that a client + may now finalize any initialization for that tablet. + + Events from tablets require a tool in proximity. Tools are also managed + by the tablet seat; a "tool added" event is sent whenever a tool is new + to the compositor. That event is followed by a number of descriptive + events about the hardware; currently that includes capabilities, + hardware id and serial number, and tool type. Similar to the tablet + interface, a wp_tablet_tool.done event is sent to terminate that initial + sequence. + + Any event from a tool happens on the wp_tablet_tool interface. When the + tool gets into proximity of the tablet, a proximity_in event is sent on + the wp_tablet_tool interface, listing the tablet and the surface. That + event is followed by a motion event with the coordinates. After that, + it's the usual motion, axis, button, etc. events. The protocol's + serialisation means events are grouped by wp_tablet_tool.frame events. + + Two special events (that don't exist in X) are down and up. They signal + "tip touching the surface". For tablets without real proximity + detection, the sequence is: proximity_in, motion, down, frame. + + When the tool leaves proximity, a proximity_out event is sent. If any + button is still down, a button release event is sent before this + proximity event. These button events are sent in the same frame as the + proximity event to signal to the client that the buttons were held when + the tool left proximity. + + If the tool moves out of the surface but stays in proximity (i.e. + between windows), compositor-specific grab policies apply. This usually + means that the proximity-out is delayed until all buttons are released. + + Moving a tool physically from one tablet to the other has no real effect + on the protocol, since we already have the tool object from the "tool + added" event. All the information is already there and the proximity + events on both tablets are all a client needs to reconstruct what + happened. + + Some extra axes are normalized, i.e. the client knows the range as + specified in the protocol (e.g. [0, 65535]), the granularity however is + unknown. The current normalized axes are pressure, distance, and slider. + + Other extra axes are in physical units as specified in the protocol. + The current extra axes with physical units are tilt, rotation and + wheel rotation. + + Since tablets work independently of the pointer controlled by the mouse, + the focus handling is independent too and controlled by proximity. + The wp_tablet_tool.set_cursor request sets a tool-specific cursor. + This cursor surface may be the same as the mouse cursor, and it may be + the same across tools but it is possible to be more fine-grained. For + example, a client may set different cursors for the pen and eraser. + + Tools are generally independent of tablets and it is + compositor-specific policy when a tool can be removed. Common approaches + will likely include some form of removing a tool when all tablets the + tool was used on are removed. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + An object that provides access to the graphics tablets available on this + system. All tablets are associated with a seat, to get access to the + actual tablets, use wp_tablet_manager.get_tablet_seat. + + + + + Get the wp_tablet_seat object for the given seat. This object + provides access to all graphics tablets in this seat. + + + + + + + + Destroy the wp_tablet_manager object. Objects created from this + object are unaffected and should be destroyed separately. + + + + + + + An object that provides access to the graphics tablets available on this + seat. After binding to this interface, the compositor sends a set of + wp_tablet_seat.tablet_added and wp_tablet_seat.tool_added events. + + + + + Destroy the wp_tablet_seat object. Objects created from this + object are unaffected and should be destroyed separately. + + + + + + This event is sent whenever a new tablet becomes available on this + seat. This event only provides the object id of the tablet, any + static information about the tablet (device name, vid/pid, etc.) is + sent through the wp_tablet interface. + + + + + + + This event is sent whenever a tool that has not previously been used + with a tablet comes into use. This event only provides the object id + of the tool; any static information about the tool (capabilities, + type, etc.) is sent through the wp_tablet_tool interface. + + + + + + + This event is sent whenever a new pad is known to the system. Typically, + pads are physically attached to tablets and a pad_added event is + sent immediately after the wp_tablet_seat.tablet_added. + However, some standalone pad devices logically attach to tablets at + runtime, and the client must wait for wp_tablet_pad.enter to know + the tablet a pad is attached to. + + This event only provides the object id of the pad. All further + features (buttons, strips, rings) are sent through the wp_tablet_pad + interface. + + + + + + + + An object that represents a physical tool that has been, or is + currently in use with a tablet in this seat. Each wp_tablet_tool + object stays valid until the client destroys it; the compositor + reuses the wp_tablet_tool object to indicate that the object's + respective physical tool has come into proximity of a tablet again. + + A wp_tablet_tool object's relation to a physical tool depends on the + tablet's ability to report serial numbers. If the tablet supports + this capability, then the object represents a specific physical tool + and can be identified even when used on multiple tablets. + + A tablet tool has a number of static characteristics, e.g. tool type, + hardware_serial and capabilities. These capabilities are sent in an + event sequence after the wp_tablet_seat.tool_added event before any + actual events from this tool. This initial event sequence is + terminated by a wp_tablet_tool.done event. + + Tablet tool events are grouped by wp_tablet_tool.frame events. + Any events received before a wp_tablet_tool.frame event should be + considered part of the same hardware state change. + + + + + Sets the surface of the cursor used for this tool on the given + tablet. This request only takes effect if the tool is in proximity + of one of the requesting client's surfaces or the surface parameter + is the current pointer surface. If there was a previous surface set + with this request it is replaced. If surface is NULL, the cursor + image is hidden. + + The parameters hotspot_x and hotspot_y define the position of the + pointer surface relative to the pointer location. Its top-left corner + is always at (x, y) - (hotspot_x, hotspot_y), where (x, y) are the + coordinates of the pointer location, in surface-local coordinates. + + On surface.attach requests to the pointer surface, hotspot_x and + hotspot_y are decremented by the x and y parameters passed to the + request. Attach must be confirmed by wl_surface.commit as usual. + + The hotspot can also be updated by passing the currently set pointer + surface to this request with new values for hotspot_x and hotspot_y. + + The current and pending input regions of the wl_surface are cleared, + and wl_surface.set_input_region is ignored until the wl_surface is no + longer used as the cursor. When the use as a cursor ends, the current + and pending input regions become undefined, and the wl_surface is + unmapped. + + This request gives the surface the role of a wp_tablet_tool cursor. A + surface may only ever be used as the cursor surface for one + wp_tablet_tool. If the surface already has another role or has + previously been used as cursor surface for a different tool, a + protocol error is raised. + + + + + + + + + + This destroys the client's resource for this tool object. + + + + + + Describes the physical type of a tool. The physical type of a tool + generally defines its base usage. + + The mouse tool represents a mouse-shaped tool that is not a relative + device but bound to the tablet's surface, providing absolute + coordinates. + + The lens tool is a mouse-shaped tool with an attached lens to + provide precision focus. + + + + + + + + + + + + + + The tool type is the high-level type of the tool and usually decides + the interaction expected from this tool. + + This event is sent in the initial burst of events before the + wp_tablet_tool.done event. + + + + + + + If the physical tool can be identified by a unique 64-bit serial + number, this event notifies the client of this serial number. + + If multiple tablets are available in the same seat and the tool is + uniquely identifiable by the serial number, that tool may move + between tablets. + + Otherwise, if the tool has no serial number and this event is + missing, the tool is tied to the tablet it first comes into + proximity with. Even if the physical tool is used on multiple + tablets, separate wp_tablet_tool objects will be created, one per + tablet. + + This event is sent in the initial burst of events before the + wp_tablet_tool.done event. + + + + + + + + This event notifies the client of a hardware id available on this tool. + + The hardware id is a device-specific 64-bit id that provides extra + information about the tool in use, beyond the wl_tool.type + enumeration. The format of the id is specific to tablets made by + Wacom Inc. For example, the hardware id of a Wacom Grip + Pen (a stylus) is 0x802. + + This event is sent in the initial burst of events before the + wp_tablet_tool.done event. + + + + + + + + Describes extra capabilities on a tablet. + + Any tool must provide x and y values, extra axes are + device-specific. + + + + + + + + + + + + This event notifies the client of any capabilities of this tool, + beyond the main set of x/y axes and tip up/down detection. + + One event is sent for each extra capability available on this tool. + + This event is sent in the initial burst of events before the + wp_tablet_tool.done event. + + + + + + + This event signals the end of the initial burst of descriptive + events. A client may consider the static description of the tool to + be complete and finalize initialization of the tool. + + + + + + This event is sent when the tool is removed from the system and will + send no further events. Should the physical tool come back into + proximity later, a new wp_tablet_tool object will be created. + + It is compositor-dependent when a tool is removed. A compositor may + remove a tool on proximity out, tablet removal or any other reason. + A compositor may also keep a tool alive until shutdown. + + If the tool is currently in proximity, a proximity_out event will be + sent before the removed event. See wp_tablet_tool.proximity_out for + the handling of any buttons logically down. + + When this event is received, the client must wp_tablet_tool.destroy + the object. + + + + + + Notification that this tool is focused on a certain surface. + + This event can be received when the tool has moved from one surface to + another, or when the tool has come back into proximity above the + surface. + + If any button is logically down when the tool comes into proximity, + the respective button event is sent after the proximity_in event but + within the same frame as the proximity_in event. + + + + + + + + + Notification that this tool has either left proximity, or is no + longer focused on a certain surface. + + When the tablet tool leaves proximity of the tablet, button release + events are sent for each button that was held down at the time of + leaving proximity. These events are sent before the proximity_out + event but within the same wp_tablet.frame. + + If the tool stays within proximity of the tablet, but the focus + changes from one surface to another, a button release event may not + be sent until the button is actually released or the tool leaves the + proximity of the tablet. + + + + + + Sent whenever the tablet tool comes in contact with the surface of the + tablet. + + If the tool is already in contact with the tablet when entering the + input region, the client owning said region will receive a + wp_tablet.proximity_in event, followed by a wp_tablet.down + event and a wp_tablet.frame event. + + Note that this event describes logical contact, not physical + contact. On some devices, a compositor may not consider a tool in + logical contact until a minimum physical pressure threshold is + exceeded. + + + + + + + Sent whenever the tablet tool stops making contact with the surface of + the tablet, or when the tablet tool moves out of the input region + and the compositor grab (if any) is dismissed. + + If the tablet tool moves out of the input region while in contact + with the surface of the tablet and the compositor does not have an + ongoing grab on the surface, the client owning said region will + receive a wp_tablet.up event, followed by a wp_tablet.proximity_out + event and a wp_tablet.frame event. If the compositor has an ongoing + grab on this device, this event sequence is sent whenever the grab + is dismissed in the future. + + Note that this event describes logical contact, not physical + contact. On some devices, a compositor may not consider a tool out + of logical contact until physical pressure falls below a specific + threshold. + + + + + + Sent whenever a tablet tool moves. + + + + + + + + Sent whenever the pressure axis on a tool changes. The value of this + event is normalized to a value between 0 and 65535. + + Note that pressure may be nonzero even when a tool is not in logical + contact. See the down and up events for more details. + + + + + + + Sent whenever the distance axis on a tool changes. The value of this + event is normalized to a value between 0 and 65535. + + Note that distance may be nonzero even when a tool is not in logical + contact. See the down and up events for more details. + + + + + + + Sent whenever one or both of the tilt axes on a tool change. Each tilt + value is in degrees, relative to the z-axis of the tablet. + The angle is positive when the top of a tool tilts along the + positive x or y axis. + + + + + + + + Sent whenever the z-rotation axis on the tool changes. The + rotation value is in degrees clockwise from the tool's + logical neutral position. + + + + + + + Sent whenever the slider position on the tool changes. The + value is normalized between -65535 and 65535, with 0 as the logical + neutral position of the slider. + + The slider is available on e.g. the Wacom Airbrush tool. + + + + + + + Sent whenever the wheel on the tool emits an event. This event + contains two values for the same axis change. The degrees value is + in the same orientation as the wl_pointer.vertical_scroll axis. The + clicks value is in discrete logical clicks of the mouse wheel. This + value may be zero if the movement of the wheel was less + than one logical click. + + Clients should choose either value and avoid mixing degrees and + clicks. The compositor may accumulate values smaller than a logical + click and emulate click events when a certain threshold is met. + Thus, wl_tablet_tool.wheel events with non-zero clicks values may + have different degrees values. + + + + + + + + Describes the physical state of a button that produced the button event. + + + + + + + + Sent whenever a button on the tool is pressed or released. + + If a button is held down when the tool moves in or out of proximity, + button events are generated by the compositor. See + wp_tablet_tool.proximity_in and wp_tablet_tool.proximity_out for + details. + + + + + + + + + Marks the end of a series of axis and/or button updates from the + tablet. The Wayland protocol requires axis updates to be sent + sequentially, however all events within a frame should be considered + one hardware event. + + + + + + + + + + + + The wp_tablet interface represents one graphics tablet device. The + tablet interface itself does not generate events; all events are + generated by wp_tablet_tool objects when in proximity above a tablet. + + A tablet has a number of static characteristics, e.g. device name and + pid/vid. These capabilities are sent in an event sequence after the + wp_tablet_seat.tablet_added event. This initial event sequence is + terminated by a wp_tablet.done event. + + + + + This destroys the client's resource for this tablet object. + + + + + + This event is sent in the initial burst of events before the + wp_tablet.done event. + + + + + + + This event is sent in the initial burst of events before the + wp_tablet.done event. + + + + + + + + A system-specific device path that indicates which device is behind + this wp_tablet. This information may be used to gather additional + information about the device, e.g. through libwacom. + + A device may have more than one device path. If so, multiple + wp_tablet.path events are sent. A device may be emulated and not + have a device path, and in that case this event will not be sent. + + The format of the path is unspecified, it may be a device node, a + sysfs path, or some other identifier. It is up to the client to + identify the string provided. + + This event is sent in the initial burst of events before the + wp_tablet.done event. + + + + + + + This event is sent immediately to signal the end of the initial + burst of descriptive events. A client may consider the static + description of the tablet to be complete and finalize initialization + of the tablet. + + + + + + Sent when the tablet has been removed from the system. When a tablet + is removed, some tools may be removed. + + When this event is received, the client must wp_tablet.destroy + the object. + + + + + + + A circular interaction area, such as the touch ring on the Wacom Intuos + Pro series tablets. + + Events on a ring are logically grouped by the wl_tablet_pad_ring.frame + event. + + + + + Request that the compositor use the provided feedback string + associated with this ring. This request should be issued immediately + after a wp_tablet_pad_group.mode_switch event from the corresponding + group is received, or whenever the ring is mapped to a different + action. See wp_tablet_pad_group.mode_switch for more details. + + Clients are encouraged to provide context-aware descriptions for + the actions associated with the ring; compositors may use this + information to offer visual feedback about the button layout + (eg. on-screen displays). + + The provided string 'description' is a UTF-8 encoded string to be + associated with this ring, and is considered user-visible; general + internationalization rules apply. + + The serial argument will be that of the last + wp_tablet_pad_group.mode_switch event received for the group of this + ring. Requests providing other serials than the most recent one will be + ignored. + + + + + + + + This destroys the client's resource for this ring object. + + + + + + Describes the source types for ring events. This indicates to the + client how a ring event was physically generated; a client may + adjust the user interface accordingly. For example, events + from a "finger" source may trigger kinetic scrolling. + + + + + + + Source information for ring events. + + This event does not occur on its own. It is sent before a + wp_tablet_pad_ring.frame event and carries the source information + for all events within that frame. + + The source specifies how this event was generated. If the source is + wp_tablet_pad_ring.source.finger, a wp_tablet_pad_ring.stop event + will be sent when the user lifts the finger off the device. + + This event is optional. If the source is unknown for an interaction, + no event is sent. + + + + + + + Sent whenever the angle on a ring changes. + + The angle is provided in degrees clockwise from the logical + north of the ring in the pad's current rotation. + + + + + + + Stop notification for ring events. + + For some wp_tablet_pad_ring.source types, a wp_tablet_pad_ring.stop + event is sent to notify a client that the interaction with the ring + has terminated. This enables the client to implement kinetic scrolling. + See the wp_tablet_pad_ring.source documentation for information on + when this event may be generated. + + Any wp_tablet_pad_ring.angle events with the same source after this + event should be considered as the start of a new interaction. + + + + + + Indicates the end of a set of ring events that logically belong + together. A client is expected to accumulate the data in all events + within the frame before proceeding. + + All wp_tablet_pad_ring events before a wp_tablet_pad_ring.frame event belong + logically together. For example, on termination of a finger interaction + on a ring the compositor will send a wp_tablet_pad_ring.source event, + a wp_tablet_pad_ring.stop event and a wp_tablet_pad_ring.frame event. + + A wp_tablet_pad_ring.frame event is sent for every logical event + group, even if the group only contains a single wp_tablet_pad_ring + event. Specifically, a client may get a sequence: angle, frame, + angle, frame, etc. + + + + + + + + A linear interaction area, such as the strips found in Wacom Cintiq + models. + + Events on a strip are logically grouped by the wl_tablet_pad_strip.frame + event. + + + + + Requests the compositor to use the provided feedback string + associated with this strip. This request should be issued immediately + after a wp_tablet_pad_group.mode_switch event from the corresponding + group is received, or whenever the strip is mapped to a different + action. See wp_tablet_pad_group.mode_switch for more details. + + Clients are encouraged to provide context-aware descriptions for + the actions associated with the strip, and compositors may use this + information to offer visual feedback about the button layout + (eg. on-screen displays). + + The provided string 'description' is a UTF-8 encoded string to be + associated with this ring, and is considered user-visible; general + internationalization rules apply. + + The serial argument will be that of the last + wp_tablet_pad_group.mode_switch event received for the group of this + strip. Requests providing other serials than the most recent one will be + ignored. + + + + + + + + This destroys the client's resource for this strip object. + + + + + + Describes the source types for strip events. This indicates to the + client how a strip event was physically generated; a client may + adjust the user interface accordingly. For example, events + from a "finger" source may trigger kinetic scrolling. + + + + + + + Source information for strip events. + + This event does not occur on its own. It is sent before a + wp_tablet_pad_strip.frame event and carries the source information + for all events within that frame. + + The source specifies how this event was generated. If the source is + wp_tablet_pad_strip.source.finger, a wp_tablet_pad_strip.stop event + will be sent when the user lifts their finger off the device. + + This event is optional. If the source is unknown for an interaction, + no event is sent. + + + + + + + Sent whenever the position on a strip changes. + + The position is normalized to a range of [0, 65535], the 0-value + represents the top-most and/or left-most position of the strip in + the pad's current rotation. + + + + + + + Stop notification for strip events. + + For some wp_tablet_pad_strip.source types, a wp_tablet_pad_strip.stop + event is sent to notify a client that the interaction with the strip + has terminated. This enables the client to implement kinetic + scrolling. See the wp_tablet_pad_strip.source documentation for + information on when this event may be generated. + + Any wp_tablet_pad_strip.position events with the same source after this + event should be considered as the start of a new interaction. + + + + + + Indicates the end of a set of events that represent one logical + hardware strip event. A client is expected to accumulate the data + in all events within the frame before proceeding. + + All wp_tablet_pad_strip events before a wp_tablet_pad_strip.frame event belong + logically together. For example, on termination of a finger interaction + on a strip the compositor will send a wp_tablet_pad_strip.source event, + a wp_tablet_pad_strip.stop event and a wp_tablet_pad_strip.frame + event. + + A wp_tablet_pad_strip.frame event is sent for every logical event + group, even if the group only contains a single wp_tablet_pad_strip + event. Specifically, a client may get a sequence: position, frame, + position, frame, etc. + + + + + + + + A pad group describes a distinct (sub)set of buttons, rings and strips + present in the tablet. The criteria of this grouping is usually positional, + eg. if a tablet has buttons on the left and right side, 2 groups will be + presented. The physical arrangement of groups is undisclosed and may + change on the fly. + + Pad groups will announce their features during pad initialization. Between + the corresponding wp_tablet_pad.group event and wp_tablet_pad_group.done, the + pad group will announce the buttons, rings and strips contained in it, + plus the number of supported modes. + + Modes are a mechanism to allow multiple groups of actions for every element + in the pad group. The number of groups and available modes in each is + persistent across device plugs. The current mode is user-switchable, it + will be announced through the wp_tablet_pad_group.mode_switch event both + whenever it is switched, and after wp_tablet_pad.enter. + + The current mode logically applies to all elements in the pad group, + although it is at clients' discretion whether to actually perform different + actions, and/or issue the respective .set_feedback requests to notify the + compositor. See the wp_tablet_pad_group.mode_switch event for more details. + + + + + Destroy the wp_tablet_pad_group object. Objects created from this object + are unaffected and should be destroyed separately. + + + + + + Sent on wp_tablet_pad_group initialization to announce the available + buttons in the group. Button indices start at 0, a button may only be + in one group at a time. + + This event is first sent in the initial burst of events before the + wp_tablet_pad_group.done event. + + Some buttons are reserved by the compositor. These buttons may not be + assigned to any wp_tablet_pad_group. Compositors may broadcast this + event in the case of changes to the mapping of these reserved buttons. + If the compositor happens to reserve all buttons in a group, this event + will be sent with an empty array. + + + + + + + Sent on wp_tablet_pad_group initialization to announce available rings. + One event is sent for each ring available on this pad group. + + This event is sent in the initial burst of events before the + wp_tablet_pad_group.done event. + + + + + + + Sent on wp_tablet_pad initialization to announce available strips. + One event is sent for each strip available on this pad group. + + This event is sent in the initial burst of events before the + wp_tablet_pad_group.done event. + + + + + + + Sent on wp_tablet_pad_group initialization to announce that the pad + group may switch between modes. A client may use a mode to store a + specific configuration for buttons, rings and strips and use the + wl_tablet_pad_group.mode_switch event to toggle between these + configurations. Mode indices start at 0. + + Switching modes is compositor-dependent. See the + wp_tablet_pad_group.mode_switch event for more details. + + This event is sent in the initial burst of events before the + wp_tablet_pad_group.done event. This event is only sent when more than + more than one mode is available. + + + + + + + This event is sent immediately to signal the end of the initial + burst of descriptive events. A client may consider the static + description of the tablet to be complete and finalize initialization + of the tablet group. + + + + + + Notification that the mode was switched. + + A mode applies to all buttons, rings and strips in a group + simultaneously, but a client is not required to assign different actions + for each mode. For example, a client may have mode-specific button + mappings but map the ring to vertical scrolling in all modes. Mode + indices start at 0. + + Switching modes is compositor-dependent. The compositor may provide + visual cues to the client about the mode, e.g. by toggling LEDs on + the tablet device. Mode-switching may be software-controlled or + controlled by one or more physical buttons. For example, on a Wacom + Intuos Pro, the button inside the ring may be assigned to switch + between modes. + + The compositor will also send this event after wp_tablet_pad.enter on + each group in order to notify of the current mode. Groups that only + feature one mode will use mode=0 when emitting this event. + + If a button action in the new mode differs from the action in the + previous mode, the client should immediately issue a + wp_tablet_pad.set_feedback request for each changed button. + + If a ring or strip action in the new mode differs from the action + in the previous mode, the client should immediately issue a + wp_tablet_ring.set_feedback or wp_tablet_strip.set_feedback request + for each changed ring or strip. + + + + + + + + + + A pad device is a set of buttons, rings and strips + usually physically present on the tablet device itself. Some + exceptions exist where the pad device is physically detached, e.g. the + Wacom ExpressKey Remote. + + Pad devices have no axes that control the cursor and are generally + auxiliary devices to the tool devices used on the tablet surface. + + A pad device has a number of static characteristics, e.g. the number + of rings. These capabilities are sent in an event sequence after the + wp_tablet_seat.pad_added event before any actual events from this pad. + This initial event sequence is terminated by a wp_tablet_pad.done + event. + + All pad features (buttons, rings and strips) are logically divided into + groups and all pads have at least one group. The available groups are + notified through the wp_tablet_pad.group event; the compositor will + emit one event per group before emitting wp_tablet_pad.done. + + Groups may have multiple modes. Modes allow clients to map multiple + actions to a single pad feature. Only one mode can be active per group, + although different groups may have different active modes. + + + + + Requests the compositor to use the provided feedback string + associated with this button. This request should be issued immediately + after a wp_tablet_pad_group.mode_switch event from the corresponding + group is received, or whenever a button is mapped to a different + action. See wp_tablet_pad_group.mode_switch for more details. + + Clients are encouraged to provide context-aware descriptions for + the actions associated with each button, and compositors may use + this information to offer visual feedback on the button layout + (e.g. on-screen displays). + + Button indices start at 0. Setting the feedback string on a button + that is reserved by the compositor (i.e. not belonging to any + wp_tablet_pad_group) does not generate an error but the compositor + is free to ignore the request. + + The provided string 'description' is a UTF-8 encoded string to be + associated with this ring, and is considered user-visible; general + internationalization rules apply. + + The serial argument will be that of the last + wp_tablet_pad_group.mode_switch event received for the group of this + button. Requests providing other serials than the most recent one will + be ignored. + + + + + + + + + Destroy the wp_tablet_pad object. Objects created from this object + are unaffected and should be destroyed separately. + + + + + + Sent on wp_tablet_pad initialization to announce available groups. + One event is sent for each pad group available. + + This event is sent in the initial burst of events before the + wp_tablet_pad.done event. At least one group will be announced. + + + + + + + A system-specific device path that indicates which device is behind + this wp_tablet_pad. This information may be used to gather additional + information about the device, e.g. through libwacom. + + The format of the path is unspecified, it may be a device node, a + sysfs path, or some other identifier. It is up to the client to + identify the string provided. + + This event is sent in the initial burst of events before the + wp_tablet_pad.done event. + + + + + + + Sent on wp_tablet_pad initialization to announce the available + buttons. + + This event is sent in the initial burst of events before the + wp_tablet_pad.done event. This event is only sent when at least one + button is available. + + + + + + + This event signals the end of the initial burst of descriptive + events. A client may consider the static description of the pad to + be complete and finalize initialization of the pad. + + + + + + Describes the physical state of a button that caused the button + event. + + + + + + + + Sent whenever the physical state of a button changes. + + + + + + + + + Notification that this pad is focused on the specified surface. + + + + + + + + + Notification that this pad is no longer focused on the specified + surface. + + + + + + + + Sent when the pad has been removed from the system. When a tablet + is removed its pad(s) will be removed too. + + When this event is received, the client must destroy all rings, strips + and groups that were offered by this pad, and issue wp_tablet_pad.destroy + the pad itself. + + + + diff --git a/src/client/client.pro b/src/client/client.pro index 1b514eeec..793a44183 100644 --- a/src/client/client.pro +++ b/src/client/client.pro @@ -36,6 +36,7 @@ WAYLANDCLIENTSOURCES += \ ../extensions/qt-key-unstable-v1.xml \ ../extensions/qt-windowmanager.xml \ ../3rdparty/protocol/wp-primary-selection-unstable-v1.xml \ + ../3rdparty/protocol/tablet-unstable-v2.xml \ ../3rdparty/protocol/text-input-unstable-v2.xml \ ../3rdparty/protocol/xdg-output-unstable-v1.xml \ ../3rdparty/protocol/wayland.xml @@ -52,6 +53,7 @@ SOURCES += qwaylandintegration.cpp \ qwaylandextendedsurface.cpp \ qwaylandsubsurface.cpp \ qwaylandsurface.cpp \ + qwaylandtabletv2.cpp \ qwaylandtouch.cpp \ qwaylandqtkey.cpp \ ../shared/qwaylandmimehelper.cpp \ @@ -77,6 +79,7 @@ HEADERS += qwaylandintegration_p.h \ qwaylandextendedsurface_p.h \ qwaylandsubsurface_p.h \ qwaylandsurface_p.h \ + qwaylandtabletv2_p.h \ qwaylandtouch_p.h \ qwaylandqtkey_p.h \ qwaylandabstractdecoration_p.h \ diff --git a/src/client/qwaylanddisplay.cpp b/src/client/qwaylanddisplay.cpp index a8d19dc6c..ffcc72ff9 100644 --- a/src/client/qwaylanddisplay.cpp +++ b/src/client/qwaylanddisplay.cpp @@ -69,6 +69,7 @@ #include "qwaylandextendedsurface_p.h" #include "qwaylandsubsurface_p.h" #include "qwaylandtouch_p.h" +#include "qwaylandtabletv2_p.h" #include "qwaylandqtkey_p.h" #include @@ -330,6 +331,8 @@ void QWaylandDisplay::registry_global(uint32_t id, const QString &interface, uin mTouchExtension.reset(new QWaylandTouchExtension(this, id)); } else if (interface == QStringLiteral("zqt_key_v1")) { mQtKeyExtension.reset(new QWaylandQtKeyExtension(this, id)); + } else if (interface == QStringLiteral("zwp_tablet_manager_v2")) { + mTabletManager.reset(new QWaylandTabletManagerV2(this, id, qMin(1, int(version)))); #if QT_CONFIG(wayland_client_primary_selection) } else if (interface == QStringLiteral("zwp_primary_selection_device_manager_v1")) { mPrimarySelectionManager.reset(new QWaylandPrimarySelectionDeviceManagerV1(this, id, 1)); diff --git a/src/client/qwaylanddisplay_p.h b/src/client/qwaylanddisplay_p.h index cd845b44e..e99ec1983 100644 --- a/src/client/qwaylanddisplay_p.h +++ b/src/client/qwaylanddisplay_p.h @@ -97,6 +97,7 @@ class QWaylandDataDeviceManager; #if QT_CONFIG(wayland_client_primary_selection) class QWaylandPrimarySelectionDeviceManagerV1; #endif +class QWaylandTabletManagerV2; class QWaylandTouchExtension; class QWaylandQtKeyExtension; class QWaylandWindow; @@ -160,6 +161,7 @@ public: QWaylandPrimarySelectionDeviceManagerV1 *primarySelectionManager() const { return mPrimarySelectionManager.data(); } #endif QtWayland::qt_surface_extension *windowExtension() const { return mWindowExtension.data(); } + QWaylandTabletManagerV2 *tabletManager() const { return mTabletManager.data(); } QWaylandTouchExtension *touchExtension() const { return mTouchExtension.data(); } QtWayland::zwp_text_input_manager_v2 *textInputManager() const { return mTextInputManager.data(); } QWaylandHardwareIntegration *hardwareIntegration() const { return mHardwareIntegration.data(); } @@ -247,6 +249,7 @@ private: QScopedPointer mTouchExtension; QScopedPointer mQtKeyExtension; QScopedPointer mWindowManagerIntegration; + QScopedPointer mTabletManager; #if QT_CONFIG(wayland_client_primary_selection) QScopedPointer mPrimarySelectionManager; #endif diff --git a/src/client/qwaylandinputdevice.cpp b/src/client/qwaylandinputdevice.cpp index ce086ee4c..e0f0c6c8e 100644 --- a/src/client/qwaylandinputdevice.cpp +++ b/src/client/qwaylandinputdevice.cpp @@ -50,6 +50,7 @@ #if QT_CONFIG(wayland_client_primary_selection) #include "qwaylandprimaryselectionv1_p.h" #endif +#include "qwaylandtabletv2_p.h" #include "qwaylandtouch_p.h" #include "qwaylandscreen_p.h" #include "qwaylandcursor_p.h" @@ -418,6 +419,8 @@ QWaylandInputDevice::QWaylandInputDevice(QWaylandDisplay *display, int version, if (mQDisplay->textInputManager()) mTextInput.reset(new QWaylandTextInput(mQDisplay, mQDisplay->textInputManager()->get_text_input(wl_seat()))); + if (auto *tm = mQDisplay->tabletManager()) + mTabletSeat.reset(new QWaylandTabletSeatV2(tm, this)); } QWaylandInputDevice::~QWaylandInputDevice() diff --git a/src/client/qwaylandinputdevice_p.h b/src/client/qwaylandinputdevice_p.h index 87f8559c4..448d0fce5 100644 --- a/src/client/qwaylandinputdevice_p.h +++ b/src/client/qwaylandinputdevice_p.h @@ -89,6 +89,7 @@ class QWaylandDisplay; #if QT_CONFIG(wayland_client_primary_selection) class QWaylandPrimarySelectionDeviceV1; #endif +class QWaylandTabletSeatV2; class QWaylandTextInput; #if QT_CONFIG(cursor) class QWaylandCursorTheme; @@ -127,6 +128,9 @@ public: QWaylandPrimarySelectionDeviceV1 *primarySelectionDevice() const; #endif + void setTabletSeat(QWaylandTabletSeatV2 *tabletSeat); + QWaylandTabletSeatV2* tabletSeat() const; + void setTextInput(QWaylandTextInput *textInput); QWaylandTextInput *textInput() const; @@ -183,6 +187,7 @@ private: Touch *mTouch = nullptr; QScopedPointer mTextInput; + QScopedPointer mTabletSeat; uint32_t mTime = 0; uint32_t mSerial = 0; diff --git a/src/client/qwaylandintegration.cpp b/src/client/qwaylandintegration.cpp index 33e64c6e8..85c1990b6 100644 --- a/src/client/qwaylandintegration.cpp +++ b/src/client/qwaylandintegration.cpp @@ -425,6 +425,8 @@ void QWaylandIntegration::initializeShellIntegration() qCWarning(lcQpaWayland) << "Loading shell integration failed."; qCWarning(lcQpaWayland) << "Attempted to load the following shells" << preferredShells; } + + QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); } QWaylandInputDevice *QWaylandIntegration::createInputDevice(QWaylandDisplay *display, int version, uint32_t id) diff --git a/src/client/qwaylandtabletv2.cpp b/src/client/qwaylandtabletv2.cpp new file mode 100644 index 000000000..eb2e865f6 --- /dev/null +++ b/src/client/qwaylandtabletv2.cpp @@ -0,0 +1,332 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qwaylandtabletv2_p.h" +#include "qwaylandinputdevice_p.h" +#include "qwaylanddisplay_p.h" +#include "qwaylandsurface_p.h" + +QT_BEGIN_NAMESPACE + +namespace QtWaylandClient { + +QWaylandTabletManagerV2::QWaylandTabletManagerV2(QWaylandDisplay *display, uint id, uint version) + : zwp_tablet_manager_v2(display->wl_registry(), id, qMin(version, uint(1))) +{ + // Create tabletSeats for all seats. + // This only works if we get the manager after all seats + const auto seats = display->inputDevices(); + for (auto *seat : seats) + createTabletSeat(seat); +} + +QWaylandTabletSeatV2 *QWaylandTabletManagerV2::createTabletSeat(QWaylandInputDevice *seat) +{ + return new QWaylandTabletSeatV2(this, seat); +} + +QWaylandTabletSeatV2::QWaylandTabletSeatV2(QWaylandTabletManagerV2 *manager, QWaylandInputDevice *seat) + : QtWayland::zwp_tablet_seat_v2(manager->get_tablet_seat(seat->wl_seat())) +{ +} + +QWaylandTabletSeatV2::~QWaylandTabletSeatV2() +{ + for (auto *tablet : m_tablets) + tablet->destroy(); + for (auto *tool : m_tools) + tool->destroy(); + for (auto *pad : m_pads) + pad->destroy(); + destroy(); +} + +void QWaylandTabletSeatV2::zwp_tablet_seat_v2_tablet_added(zwp_tablet_v2 *id) +{ + auto *tablet = new QWaylandTabletV2(id); + m_tablets.push_back(tablet); + connect(tablet, &QWaylandTabletV2::destroyed, this, [this, tablet] { m_tablets.removeOne(tablet); }); +} + +void QWaylandTabletSeatV2::zwp_tablet_seat_v2_tool_added(zwp_tablet_tool_v2 *id) +{ + auto *tool = new QWaylandTabletToolV2(id); + m_tools.push_back(tool); + connect(tool, &QWaylandTabletToolV2::destroyed, this, [this, tool] { m_tools.removeOne(tool); }); +} + +void QWaylandTabletSeatV2::zwp_tablet_seat_v2_pad_added(zwp_tablet_pad_v2 *id) +{ + auto *pad = new QWaylandTabletPadV2(id); + m_pads.push_back(pad); + connect(pad, &QWaylandTabletPadV2::destroyed, this, [this, pad] { m_pads.removeOne(pad); }); +} + +QWaylandTabletV2::QWaylandTabletV2(::zwp_tablet_v2 *tablet) + : QtWayland::zwp_tablet_v2(tablet) +{ +} + +void QWaylandTabletV2::zwp_tablet_v2_removed() +{ + destroy(); + delete this; +} + +QWaylandTabletToolV2::QWaylandTabletToolV2(::zwp_tablet_tool_v2 *tool) + : QtWayland::zwp_tablet_tool_v2(tool) +{ +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_type(uint32_t tool_type) +{ + m_toolType = type(tool_type); +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_hardware_serial(uint32_t hardware_serial_hi, uint32_t hardware_serial_lo) +{ + m_uid = (quint64(hardware_serial_hi) << 32) + hardware_serial_lo; +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_capability(uint32_t capability) +{ + if (capability == capability_rotation) + m_hasRotation = true; +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_done() +{ + switch (m_toolType) { + case type::type_airbrush: + case type::type_brush: + case type::type_pencil: + case type::type_pen: + m_pointerType = QTabletEvent::PointerType::Pen; + break; + case type::type_eraser: + m_pointerType = QTabletEvent::PointerType::Eraser; + break; + case type::type_mouse: + case type::type_lens: + m_pointerType = QTabletEvent::PointerType::Cursor; + break; + case type::type_finger: + m_pointerType = QTabletEvent::PointerType::UnknownPointer; + break; + } + switch (m_toolType) { + case type::type_airbrush: + m_tabletDevice = QTabletEvent::TabletDevice::Airbrush; + break; + case type::type_brush: + case type::type_pencil: + case type::type_pen: + case type::type_eraser: + m_tabletDevice = m_hasRotation ? QTabletEvent::TabletDevice::RotationStylus : QTabletEvent::TabletDevice::Stylus; + break; + case type::type_lens: + m_tabletDevice = QTabletEvent::TabletDevice::Puck; + break; + case type::type_mouse: + case type::type_finger: + m_tabletDevice = QTabletEvent::TabletDevice::NoDevice; + break; + } +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_removed() +{ + destroy(); + delete this; +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_proximity_in(uint32_t serial, zwp_tablet_v2 *tablet, wl_surface *surface) +{ + Q_UNUSED(tablet); + Q_UNUSED(serial); + if (Q_UNLIKELY(!surface)) { + qCDebug(lcQpaWayland) << "Ignoring zwp_tablet_tool_v2_proximity_v2 with no surface"; + return; + } + m_pending.enteredSurface = true; + m_pending.proximitySurface = QWaylandSurface::fromWlSurface(surface); +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_proximity_out() +{ + m_pending.enteredSurface = false; + m_pending.proximitySurface = nullptr; +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_down(uint32_t serial) +{ + Q_UNUSED(serial); + m_pending.down = true; +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_up() +{ + m_pending.down = false; +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_motion(wl_fixed_t x, wl_fixed_t y) +{ + m_pending.surfacePosition = QPointF(wl_fixed_to_double(x), wl_fixed_to_double(y)); +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_pressure(uint32_t pressure) +{ + const int maxPressure = 65535; + m_pending.pressure = qreal(pressure)/maxPressure; +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_distance(uint32_t distance) +{ + m_pending.distance = distance; +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_tilt(wl_fixed_t tilt_x, wl_fixed_t tilt_y) +{ + m_pending.xTilt = wl_fixed_to_double(tilt_x); + m_pending.yTilt = wl_fixed_to_double(tilt_y); +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_rotation(wl_fixed_t degrees) +{ + m_pending.rotation = wl_fixed_to_double(degrees); +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_slider(int32_t position) +{ + m_pending.slider = qreal(position) / 65535; +} + +static Qt::MouseButton mouseButtonFromTablet(uint button) +{ + switch (button) { + case 0x110: return Qt::MouseButton::LeftButton; // BTN_LEFT + case 0x14b: return Qt::MouseButton::MiddleButton; // BTN_STYLUS + case 0x14c: return Qt::MouseButton::RightButton; // BTN_STYLUS2 + default: + return Qt::NoButton; + } +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_button(uint32_t serial, uint32_t button, uint32_t state) +{ + Q_UNUSED(serial); + Qt::MouseButton mouseButton = mouseButtonFromTablet(button); + if (state == button_state_pressed) + m_pending.buttons |= mouseButton; + else + m_pending.buttons &= ~mouseButton; +} + +void QWaylandTabletToolV2::zwp_tablet_tool_v2_frame(uint32_t time) +{ + if (m_pending.proximitySurface && !m_applied.proximitySurface) { + QWindowSystemInterface::handleTabletEnterProximityEvent(m_tabletDevice, m_pointerType, m_uid); + m_applied.proximitySurface = m_pending.proximitySurface; + } + + if (!(m_pending == m_applied) && m_pending.proximitySurface) { + if (!m_pending.proximitySurface) { + qCWarning(lcQpaWayland) << "Can't send tablet event with no proximity surface, ignoring"; + return; + } + QWaylandWindow *waylandWindow = QWaylandWindow::fromWlSurface(m_pending.proximitySurface->object()); + QWindow *window = waylandWindow->window(); + ulong timestamp = time; + const QPointF localPosition = waylandWindow->mapFromWlSurface(m_pending.surfacePosition); + + QPointF delta = localPosition - localPosition.toPoint(); + QPointF globalPosition = window->mapToGlobal(localPosition.toPoint()); + globalPosition += delta; + + Qt::MouseButtons buttons = m_pending.down ? Qt::MouseButton::LeftButton : Qt::MouseButton::NoButton; + buttons |= m_pending.buttons; + qreal pressure = m_pending.pressure; + int xTilt = int(m_pending.xTilt); + int yTilt = int(m_pending.yTilt); + qreal tangentialPressure = m_pending.slider; + qreal rotation = m_pending.rotation; + int z = int(m_pending.distance); + QWindowSystemInterface::handleTabletEvent(window, timestamp, localPosition, globalPosition, + m_tabletDevice, m_pointerType, buttons, pressure, + xTilt, yTilt, tangentialPressure, rotation, z, m_uid); + } + + if (!m_pending.proximitySurface && m_applied.enteredSurface) { + QWindowSystemInterface::handleTabletLeaveProximityEvent(m_tabletDevice, m_pointerType, m_uid); + m_pending = State(); // Don't leave pressure etc. lying around when we enter the next surface + } + + m_applied = m_pending; +} + +// TODO: delete when upgrading to c++20 +bool QWaylandTabletToolV2::State::operator==(const QWaylandTabletToolV2::State &o) const { + return + down == o.down && + proximitySurface.data() == o.proximitySurface.data() && + enteredSurface == o.enteredSurface && + surfacePosition == o.surfacePosition && + distance == o.distance && + pressure == o.pressure && + rotation == o.rotation && + xTilt == o.xTilt && + yTilt == o.yTilt && + slider == o.slider && + buttons == o.buttons; +} + +QWaylandTabletPadV2::QWaylandTabletPadV2(::zwp_tablet_pad_v2 *pad) + : QtWayland::zwp_tablet_pad_v2(pad) +{ +} + +void QWaylandTabletPadV2::zwp_tablet_pad_v2_removed() +{ + destroy(); + delete this; +} + +} // namespace QtWaylandClient + +QT_END_NAMESPACE diff --git a/src/client/qwaylandtabletv2_p.h b/src/client/qwaylandtabletv2_p.h new file mode 100644 index 000000000..b4daaf5db --- /dev/null +++ b/src/client/qwaylandtabletv2_p.h @@ -0,0 +1,191 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the plugins of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QWAYLANDTABLETV2_P_H +#define QWAYLANDTABLETV2_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include + +#include + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +namespace QtWaylandClient { + +class QWaylandDisplay; +class QWaylandInputDevice; +class QWaylandSurface; + +class QWaylandTabletSeatV2; +class QWaylandTabletV2; +class QWaylandTabletToolV2; +class QWaylandTabletPadV2; + +class Q_WAYLAND_CLIENT_EXPORT QWaylandTabletManagerV2 : public QtWayland::zwp_tablet_manager_v2 +{ +public: + explicit QWaylandTabletManagerV2(QWaylandDisplay *display, uint id, uint version); + QWaylandTabletSeatV2 *createTabletSeat(QWaylandInputDevice *seat); +}; + +class Q_WAYLAND_CLIENT_EXPORT QWaylandTabletSeatV2 : public QObject, public QtWayland::zwp_tablet_seat_v2 +{ + Q_OBJECT +public: + explicit QWaylandTabletSeatV2(QWaylandTabletManagerV2 *manager, QWaylandInputDevice *seat); + ~QWaylandTabletSeatV2() override; + +protected: + void zwp_tablet_seat_v2_tablet_added(struct ::zwp_tablet_v2 *id) override; + void zwp_tablet_seat_v2_tool_added(struct ::zwp_tablet_tool_v2 *id) override; + void zwp_tablet_seat_v2_pad_added(struct ::zwp_tablet_pad_v2 *id) override; + +private: + QVector m_tablets; + QVector m_tools; + QVector m_pads; +}; + +class Q_WAYLAND_CLIENT_EXPORT QWaylandTabletV2 : public QObject, public QtWayland::zwp_tablet_v2 +{ + Q_OBJECT +public: + explicit QWaylandTabletV2(::zwp_tablet_v2 *tablet); + +protected: +// void zwp_tablet_v2_name(const QString &name) override; +// void zwp_tablet_v2_id(uint32_t vid, uint32_t pid) override; +// void zwp_tablet_v2_path(const QString &path) override; +// void zwp_tablet_v2_done() override; + void zwp_tablet_v2_removed() override; +}; + +class Q_WAYLAND_CLIENT_EXPORT QWaylandTabletToolV2 : public QObject, public QtWayland::zwp_tablet_tool_v2 +{ + Q_OBJECT +public: + explicit QWaylandTabletToolV2(::zwp_tablet_tool_v2 *tool); + +protected: + void zwp_tablet_tool_v2_type(uint32_t tool_type) override; + void zwp_tablet_tool_v2_hardware_serial(uint32_t hardware_serial_hi, uint32_t hardware_serial_lo) override; +// void zwp_tablet_tool_v2_hardware_id_wacom(uint32_t hardware_id_hi, uint32_t hardware_id_lo) override; + void zwp_tablet_tool_v2_capability(uint32_t capability) override; + void zwp_tablet_tool_v2_done() override; + void zwp_tablet_tool_v2_removed() override; + void zwp_tablet_tool_v2_proximity_in(uint32_t serial, struct ::zwp_tablet_v2 *tablet, struct ::wl_surface *surface) override; + void zwp_tablet_tool_v2_proximity_out() override; + void zwp_tablet_tool_v2_down(uint32_t serial) override; + void zwp_tablet_tool_v2_up() override; + void zwp_tablet_tool_v2_motion(wl_fixed_t x, wl_fixed_t y) override; + void zwp_tablet_tool_v2_pressure(uint32_t pressure) override; + void zwp_tablet_tool_v2_distance(uint32_t distance) override; + void zwp_tablet_tool_v2_tilt(wl_fixed_t tilt_x, wl_fixed_t tilt_y) override; + void zwp_tablet_tool_v2_rotation(wl_fixed_t degrees) override; + void zwp_tablet_tool_v2_slider(int32_t position) override; +// void zwp_tablet_tool_v2_wheel(wl_fixed_t degrees, int32_t clicks) override; + void zwp_tablet_tool_v2_button(uint32_t serial, uint32_t button, uint32_t state) override; + void zwp_tablet_tool_v2_frame(uint32_t time) override; + +private: + + // Static state (sent before done event) + QTabletEvent::PointerType m_pointerType = QTabletEvent::PointerType::UnknownPointer; + QTabletEvent::TabletDevice m_tabletDevice = QTabletEvent::TabletDevice::NoDevice; + type m_toolType = type_pen; + bool m_hasRotation = false; + quint64 m_uid = 0; + + // Accumulated state (applied on frame event) + struct State { + bool down = false; + QPointer proximitySurface; + bool enteredSurface = false; // Not enough with just proximitySurface, if the surface is deleted, we still want to send a leave event + QPointF surfacePosition; + uint distance = 0; + qreal pressure = 0; + qreal rotation = 0; + qreal xTilt = 0; + qreal yTilt = 0; + qreal slider = 0; + Qt::MouseButtons buttons = Qt::MouseButton::NoButton; // Actual buttons, down state -> left mouse is mapped inside the frame handler + //auto operator<=>(const Point&) const = default; // TODO: use this when upgrading to C++20 + bool operator==(const State &o) const; + } m_pending, m_applied; +}; + +// We don't actually use this, but need to handle the "removed" event to comply with the protocol +class Q_WAYLAND_CLIENT_EXPORT QWaylandTabletPadV2 : public QObject, public QtWayland::zwp_tablet_pad_v2 +{ + Q_OBJECT +public: + explicit QWaylandTabletPadV2(::zwp_tablet_pad_v2 *pad); + +protected: +// void zwp_tablet_pad_v2_group(struct ::zwp_tablet_pad_group_v2 *pad_group) override; +// void zwp_tablet_pad_v2_path(const QString &path) override; +// void zwp_tablet_pad_v2_buttons(uint32_t buttons) override; +// void zwp_tablet_pad_v2_done() override; +// void zwp_tablet_pad_v2_button(uint32_t time, uint32_t button, uint32_t state) override; +// void zwp_tablet_pad_v2_enter(uint32_t serial, struct ::zwp_tablet_v2 *tablet, struct ::wl_surface *surface) override; +// void zwp_tablet_pad_v2_leave(uint32_t serial, struct ::wl_surface *surface) override; + void zwp_tablet_pad_v2_removed() override; +}; + +} // namespace QtWaylandClient + +QT_END_NAMESPACE + +#endif // QWAYLANDTABLETV2_P_H diff --git a/sync.profile b/sync.profile index 572213457..95ddb557c 100644 --- a/sync.profile +++ b/sync.profile @@ -24,6 +24,7 @@ "^qwayland-qt-key-unstable-v1.h" , "^qwayland-server-buffer-extension.h", "^qwayland-surface-extension.h", + "^qwayland-tablet-unstable-v2.h", "^qwayland-text-input-unstable-v2.h", "^qwayland-touch-extension.h", "^qwayland-wayland.h", @@ -34,6 +35,7 @@ "^wayland-qt-key-unstable-v1-client-protocol.h", "^wayland-server-buffer-extension-client-protocol.h", "^wayland-surface-extension-client-protocol.h", + "^wayland-tablet-unstable-v2-client-protocol.h", "^wayland-text-input-unstable-v2-client-protocol.h", "^wayland-touch-extension-client-protocol.h", "^wayland-wayland-client-protocol.h", diff --git a/tests/auto/client/client.pro b/tests/auto/client/client.pro index cda1765e3..46a3aa42e 100644 --- a/tests/auto/client/client.pro +++ b/tests/auto/client/client.pro @@ -11,6 +11,7 @@ SUBDIRS += \ seatv4 \ seatv5 \ surface \ + tabletv2 \ wl_connect \ xdgdecorationv1 \ xdgoutput \ diff --git a/tests/auto/client/tabletv2/tabletv2.pro b/tests/auto/client/tabletv2/tabletv2.pro new file mode 100644 index 000000000..9dc9636e9 --- /dev/null +++ b/tests/auto/client/tabletv2/tabletv2.pro @@ -0,0 +1,7 @@ +include (../shared/shared.pri) + +WAYLANDSERVERSOURCES += \ + $$PWD/../../../../src/3rdparty/protocol/tablet-unstable-v2.xml + +TARGET = tst_tabletv2 +SOURCES += tst_tabletv2.cpp diff --git a/tests/auto/client/tabletv2/tst_tabletv2.cpp b/tests/auto/client/tabletv2/tst_tabletv2.cpp new file mode 100644 index 000000000..2fe2ff420 --- /dev/null +++ b/tests/auto/client/tabletv2/tst_tabletv2.cpp @@ -0,0 +1,918 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "mockcompositor.h" + +#include + +#include + +using namespace MockCompositor; + +constexpr int tabletVersion = 1; // protocol VERSION, not the name suffix (_v2) + +class TabletManagerV2; +class TabletSeatV2; + +class TabletV2 : public QObject, public QtWaylandServer::zwp_tablet_v2 +{ + Q_OBJECT +public: + explicit TabletV2(TabletSeatV2 *tabletSeat) + : m_tabletSeat(tabletSeat) + { + } + + void send_removed() = delete; + void send_removed(struct ::wl_resource *resource) = delete; + void sendRemoved(); + + QPointer m_tabletSeat; // destroy order is not guaranteed +protected: + void zwp_tablet_v2_destroy(Resource *resource) override; +}; + +class TabletToolV2 : public QObject, public QtWaylandServer::zwp_tablet_tool_v2 +{ + Q_OBJECT +public: + using ToolType = QtWaylandServer::zwp_tablet_tool_v2::type; + explicit TabletToolV2(TabletSeatV2 *tabletSeat, ToolType toolType, quint64 hardwareSerial) + : m_tabletSeat(tabletSeat) + , m_toolType(toolType) + , m_hardwareSerial(hardwareSerial) + { + } + + wl_resource *toolResource() // for convenience + { + Q_ASSERT(resourceMap().size() == 1); + // Strictly speaking, there may be more than one resource for the tool, for intsance if + // if there are multiple clients, or a client has called get_tablet_seat multiple times. + // For now we'll pretend there can only be one resource. + return resourceMap().first()->handle; + } + + void send_removed() = delete; + void send_removed(struct ::wl_resource *resource) = delete; + void sendRemoved(); + + uint sendProximityIn(TabletV2 *tablet, Surface *surface); + void sendProximityOut(); + void sendMotion(QPointF position) + { + Q_ASSERT(m_proximitySurface); + for (auto *resource : resourceMap()) + send_motion(resource->handle, wl_fixed_from_double(position.x()), wl_fixed_from_double(position.y())); + } + uint sendDown(); + void sendUp() { send_up(toolResource()); } + void sendPressure(uint pressure); + void sendTilt(qreal tiltX, qreal tiltY) { send_tilt(toolResource(), wl_fixed_from_double(tiltX), wl_fixed_from_double(tiltY)); } + void sendRotation(qreal rotation) { send_rotation(toolResource(), wl_fixed_from_double(rotation)); } + uint sendButton(uint button, bool pressed); + uint sendFrame(); + + QPointer m_tabletSeat; // destruction order is not guaranteed + ToolType m_toolType = ToolType::type_pen; + quint64 m_hardwareSerial = 0; + QPointer m_proximitySurface; +protected: + void zwp_tablet_tool_v2_destroy(Resource *resource) override; +}; + +class TabletPadV2 : public QObject, public QtWaylandServer::zwp_tablet_pad_v2 +{ + Q_OBJECT +public: + explicit TabletPadV2(TabletSeatV2 *tabletSeat) + : m_tabletSeat(tabletSeat) + { + } + + void send_removed() = delete; + void send_removed(struct ::wl_resource *resource) = delete; + void sendRemoved(); + + QPointer m_tabletSeat; // destroy order is not guaranteed +protected: + void zwp_tablet_pad_v2_destroy(Resource *resource) override; +}; + +class TabletSeatV2 : public QObject, public QtWaylandServer::zwp_tablet_seat_v2 +{ + Q_OBJECT +public: + explicit TabletSeatV2(TabletManagerV2 *manager, Seat *seat) + : m_manager(manager) + , m_seat(seat) + {} + TabletV2 *addTablet() + { + auto *tablet = new TabletV2(this); + m_tablets.append(tablet); + for (auto *resource : resourceMap()) + sendTabletAdded(resource, tablet); + return tablet; + } + + void sendTabletAdded(Resource *resource, TabletV2 *tablet) + { + // Although, not necessarily correct, assuming just one tablet_seat per client + auto *tabletResource = tablet->add(resource->client(), resource->version()); + zwp_tablet_seat_v2::send_tablet_added(resource->handle, tabletResource->handle); + // TODO: send extra stuff before done? + tablet->send_done(tabletResource->handle); + } + + using ToolType = QtWaylandServer::zwp_tablet_tool_v2::type; + TabletToolV2 *addTool(ToolType toolType = ToolType::type_pen, quint64 hardwareSerial = 0) + { + auto *tool = new TabletToolV2(this, toolType, hardwareSerial); + m_tools.append(tool); + for (auto *resource : resourceMap()) + sendToolAdded(resource, tool); + return tool; + } + + void sendToolAdded(Resource *resource, TabletToolV2 *tool) + { + // Although, not necessarily correct, assuming just one tablet_seat per client + auto *toolResource = tool->add(resource->client(), resource->version())->handle; + zwp_tablet_seat_v2::send_tool_added(resource->handle, toolResource); + tool->send_type(toolResource, tool->m_toolType); + if (tool->m_hardwareSerial) { + const uint hi = tool->m_hardwareSerial >> 32; + const uint lo = tool->m_hardwareSerial & 0xffffffff; + tool->send_hardware_serial(toolResource, hi, lo); + } + tool->send_done(toolResource); + } + + TabletPadV2 *addPad() + { + auto *pad = new TabletPadV2(this); + m_pads.append(pad); + for (auto *resource : resourceMap()) + sendPadAdded(resource, pad); + return pad; + } + + void sendPadAdded(Resource *resource, TabletPadV2 *pad) + { + // Although, not necessarily correct, assuming just one tablet_seat per client + auto *padResource = pad->add(resource->client(), resource->version())->handle; + zwp_tablet_seat_v2::send_pad_added(resource->handle, padResource); + pad->send_done(padResource); + } + + void removeAll() + { + const auto tools = m_tools; + for (auto *tool : tools) + tool->sendRemoved(); + + const auto tablets = m_tablets; + for (auto *tablet : tablets) + tablet->sendRemoved(); + + const auto pads = m_pads; + for (auto *pad : pads) + pad->sendRemoved(); + } + + TabletManagerV2 *m_manager = nullptr; + Seat *m_seat = nullptr; + QVector m_tablets; + QVector m_tabletsWaitingForDestroy; + QVector m_tools; + QVector m_toolsWaitingForDestroy; + QVector m_pads; + QVector m_padsWaitingForDestroy; + +protected: + void zwp_tablet_seat_v2_bind_resource(Resource *resource) + { + for (auto *tablet : m_tablets) + sendTabletAdded(resource, tablet); + for (auto *tool : m_tools) + sendToolAdded(resource, tool); + for (auto *pad : m_pads) + sendPadAdded(resource, pad); + } +}; + +class TabletManagerV2 : public Global, public QtWaylandServer::zwp_tablet_manager_v2 +{ + Q_OBJECT +public: + explicit TabletManagerV2(CoreCompositor *compositor, int version = 1) + : QtWaylandServer::zwp_tablet_manager_v2(compositor->m_display, version) + , m_version(version) + {} + bool isClean() override + { + for (auto *seat : m_tabletSeats) { + if (!seat->m_tabletsWaitingForDestroy.empty()) + return false; + if (!seat->m_toolsWaitingForDestroy.empty()) + return false; + if (!seat->m_padsWaitingForDestroy.empty()) + return false; + } + return true; + } + + TabletSeatV2 *tabletSeatFor(Seat *seat) + { + Q_ASSERT(seat); + if (auto *tabletSeat = m_tabletSeats.value(seat, nullptr)) + return tabletSeat; + + auto *tabletSeat = new TabletSeatV2(this, seat); + m_tabletSeats[seat] = tabletSeat; + return tabletSeat; + } + + int m_version = 1; // TODO: Remove on libwayland upgrade + QMap m_tabletSeats; + +protected: + void zwp_tablet_manager_v2_destroy(Resource *resource) override + { + // tablet_seats created from this object are unaffected and should be destroyed separately. + wl_resource_destroy(resource->handle); + } + + void zwp_tablet_manager_v2_get_tablet_seat(Resource *resource, uint32_t id, ::wl_resource *seatResource) override + { + auto *seat = fromResource(seatResource); + QVERIFY(seat); + auto *tabletSeat = tabletSeatFor(seat); + tabletSeat->add(resource->client(), id, resource->version()); + } +}; + +void TabletV2::sendRemoved() +{ + for (auto *resource : resourceMap()) + zwp_tablet_v2_send_removed(resource->handle); + bool removed = m_tabletSeat->m_tablets.removeOne(this); + QVERIFY(removed); + m_tabletSeat->m_tabletsWaitingForDestroy.append(this); +} + +void TabletV2::zwp_tablet_v2_destroy(QtWaylandServer::zwp_tablet_v2::Resource *resource) +{ + Q_UNUSED(resource) + if (m_tabletSeat) { + bool removed = m_tabletSeat->m_tabletsWaitingForDestroy.removeOne(this); + QVERIFY(removed); + } + wl_resource_destroy(resource->handle); +} + +void TabletToolV2::sendRemoved() +{ + for (auto *resource : resourceMap()) + zwp_tablet_tool_v2_send_removed(resource->handle); + bool removed = m_tabletSeat->m_tools.removeOne(this); + QVERIFY(removed); + m_tabletSeat->m_toolsWaitingForDestroy.append(this); +} + +uint TabletToolV2::sendProximityIn(TabletV2 *tablet, Surface *surface) +{ + Q_ASSERT(!m_proximitySurface); + m_proximitySurface = surface; + uint serial = m_tabletSeat->m_seat->m_compositor->nextSerial(); + auto *client = surface->resource()->client(); + auto tabletResource = tablet->resourceMap().value(client)->handle; + send_proximity_in(toolResource(), serial, tabletResource, surface->resource()->handle); + return serial; +} + +void TabletToolV2::sendProximityOut() +{ + Q_ASSERT(m_proximitySurface); + send_proximity_out(toolResource()); + m_proximitySurface = nullptr; +} + +uint TabletToolV2::sendDown() +{ + uint serial = m_tabletSeat->m_seat->m_compositor->nextSerial(); + send_down(toolResource(), serial); + return serial; +} + +void TabletToolV2::sendPressure(uint pressure) +{ + Q_ASSERT(m_proximitySurface); + auto *client = m_proximitySurface->resource()->client(); + auto toolResource = resourceMap().value(client)->handle; + send_pressure(toolResource, pressure); +} + +uint TabletToolV2::sendButton(uint button, bool pressed) +{ + button_state state = pressed ? button_state_pressed : button_state_released; + uint serial = m_tabletSeat->m_seat->m_compositor->nextSerial(); + send_button(toolResource(), serial, button, state); + return serial; +} + +uint TabletToolV2::sendFrame() +{ + uint time = m_tabletSeat->m_seat->m_compositor->currentTimeMilliseconds(); + for (auto *resource : resourceMap()) + send_frame(resource->handle, time); + return time; +} + +void TabletToolV2::zwp_tablet_tool_v2_destroy(QtWaylandServer::zwp_tablet_tool_v2::Resource *resource) +{ + if (m_tabletSeat) { + bool removed = m_tabletSeat->m_toolsWaitingForDestroy.removeOne(this); + QVERIFY(removed); + } + wl_resource_destroy(resource->handle); +} + +void TabletPadV2::sendRemoved() +{ + for (auto *resource : resourceMap()) + zwp_tablet_pad_v2_send_removed(resource->handle); + bool removed = m_tabletSeat->m_pads.removeOne(this); + QVERIFY(removed); + m_tabletSeat->m_padsWaitingForDestroy.append(this); +} + +void TabletPadV2::zwp_tablet_pad_v2_destroy(QtWaylandServer::zwp_tablet_pad_v2::Resource *resource) +{ + if (m_tabletSeat) { + bool removed = m_tabletSeat->m_padsWaitingForDestroy.removeOne(this); + QVERIFY(removed); + } + wl_resource_destroy(resource->handle); +} + +class TabletCompositor : public DefaultCompositor { +public: + explicit TabletCompositor() + { + exec([this] { + m_config.autoConfigure = true; + add(tabletVersion); + }); + } + TabletSeatV2 *tabletSeat(int i = 0) + { + return get()->tabletSeatFor(get(i)); + } + TabletV2 *tablet(int i = 0, int iSeat = 0) + { + if (auto *ts = tabletSeat(iSeat)) + return ts->m_tablets.value(i, nullptr); + return nullptr; + } + TabletToolV2 *tabletTool(int i = 0, int iSeat = 0) + { + if (auto *ts = tabletSeat(iSeat)) + return ts->m_tools.value(i, nullptr); + return nullptr; + } + TabletPadV2 *tabletPad(int i = 0, int iSeat = 0) + { + if (auto *ts = tabletSeat(iSeat)) + return ts->m_pads.value(i, nullptr); + return nullptr; + } +}; + +Q_DECLARE_METATYPE(QtWaylandServer::zwp_tablet_tool_v2::type); +Q_DECLARE_METATYPE(QTabletEvent::PointerType); +Q_DECLARE_METATYPE(Qt::MouseButton); + +class tst_tabletv2 : public QObject, private TabletCompositor +{ + using ToolType = QtWaylandServer::zwp_tablet_tool_v2::type; + Q_OBJECT +private slots: + void cleanup(); + void bindsToManager(); + void createsTabletSeat(); + void destroysTablet(); + void destroysTool(); + void destroysPad(); + void proximityEvents(); + void moveEvent(); + void pointerType_data(); + void pointerType(); + void hardwareSerial(); + void buttons_data(); + void buttons(); + void tabletEvents(); +}; + +class ProximityFilter : public QObject { + Q_OBJECT +public: + ProximityFilter() { qApp->installEventFilter(this); } + ~ProximityFilter() override { qDeleteAll(m_events); } + QVector m_events; + + int nextEventIndex = 0; + int numEvents() const { return m_events.size() - nextEventIndex; } + QTabletEvent *popEvent() + { + auto *event = m_events.value(nextEventIndex, nullptr); + if (event) + ++nextEventIndex; + return event; + } + +protected: + bool eventFilter(QObject *object, QEvent *event) override + { + Q_UNUSED(object); + switch (event->type()) { + case QEvent::TabletEnterProximity: + case QEvent::TabletLeaveProximity: { + auto *e = static_cast(event); + auto *ev = new QTabletEvent(e->type(), e->posF(), e->globalPosF(), e->device(), + e->pointerType(), e->pressure(), e->xTilt(), e->yTilt(), + e->tangentialPressure(), e->rotation(), e->z(), + Qt::KeyboardModifier::NoModifier, e->uniqueId(), + e->button(), e->buttons()); + m_events << ev; + break; + } + default: + break; + } + return false; + } +}; + +void tst_tabletv2::cleanup() +{ + exec([&] { + tabletSeat()->removeAll(); + }); + QCOMPOSITOR_COMPARE(get()->m_tabletSeats.size(), 1); + QCOMPOSITOR_COMPARE(tabletSeat()->m_tablets.size(), 0); + QCOMPOSITOR_COMPARE(tabletSeat()->m_tools.size(), 0); + QCOMPOSITOR_COMPARE(tabletSeat()->m_pads.size(), 0); + + QTRY_VERIFY2(isClean(), qPrintable(dirtyMessage())); +} + +void tst_tabletv2::bindsToManager() +{ + QCOMPOSITOR_TRY_COMPARE(get()->resourceMap().size(), 1); + QCOMPOSITOR_TRY_COMPARE(get()->resourceMap().first()->version(), tabletVersion); +} + +void tst_tabletv2::createsTabletSeat() +{ + QCOMPOSITOR_TRY_VERIFY(tabletSeat()); + QCOMPOSITOR_TRY_VERIFY(tabletSeat()->resourceMap().contains(client())); + QCOMPOSITOR_TRY_COMPARE(tabletSeat()->resourceMap().value(client())->version(), tabletVersion); + //TODO: Maybe also assert some capability reported though qt APIs? +} + +void tst_tabletv2::destroysTablet() +{ + QCOMPOSITOR_TRY_VERIFY(tabletSeat()); + exec([&] { + tabletSeat()->addTablet(); + }); + QCOMPOSITOR_TRY_VERIFY(tablet()); + + exec([&] { + tablet()->sendRemoved(); + }); + + QCOMPOSITOR_TRY_VERIFY(!tablet()); + QCOMPOSITOR_TRY_VERIFY(tabletSeat()->m_tabletsWaitingForDestroy.empty()); +} + +void tst_tabletv2::destroysTool() +{ + QCOMPOSITOR_TRY_VERIFY(tabletSeat()); + exec([&] { + tabletSeat()->addTool(); + }); + QCOMPOSITOR_TRY_VERIFY(tabletTool()); + + exec([&] { + tabletTool()->sendRemoved(); + }); + + QCOMPOSITOR_TRY_VERIFY(!tabletTool()); + QCOMPOSITOR_TRY_VERIFY(tabletSeat()->m_toolsWaitingForDestroy.empty()); +} + +void tst_tabletv2::destroysPad() +{ + QCOMPOSITOR_TRY_VERIFY(tabletSeat()); + exec([&] { + tabletSeat()->addPad(); + }); + QCOMPOSITOR_TRY_VERIFY(tabletPad()); + + exec([&] { + tabletPad()->sendRemoved(); + }); + + QCOMPOSITOR_TRY_VERIFY(!tabletPad()); + QCOMPOSITOR_TRY_VERIFY(tabletSeat()->m_padsWaitingForDestroy.empty()); +} + +void tst_tabletv2::proximityEvents() +{ + ProximityFilter filter; + + QCOMPOSITOR_TRY_VERIFY(tabletSeat()); + exec([&] { + tabletSeat()->addTablet(); + tabletSeat()->addTool(); + }); + + QRasterWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + QCOMPOSITOR_TRY_VERIFY(tablet()); + exec([&] { + auto *surface = xdgSurface()->m_surface; + auto *tool = tabletTool(); + tool->sendProximityIn(tablet(), surface); + tool->sendFrame(); + }); + + QTRY_COMPARE(filter.numEvents(), 1); + QTabletEvent *enterEvent = filter.popEvent(); + QCOMPARE(enterEvent->type(), QEvent::TabletEnterProximity); + + exec([&] { + auto *tool = tabletTool(); + tool->sendProximityOut(); + tool->sendFrame(); + }); + + QTRY_COMPARE(filter.numEvents(), 1); + QTabletEvent *leaveEvent = filter.popEvent(); + QCOMPARE(leaveEvent->type(), QEvent::TabletLeaveProximity); +} + +class TabletWindow : public QRasterWindow { + Q_OBJECT +public: + ~TabletWindow() override { qDeleteAll(m_events); } + + void tabletEvent(QTabletEvent *e) override + { + m_events << new QTabletEvent(e->type(), e->posF(), e->globalPosF(), e->device(), + e->pointerType(), e->pressure(), e->xTilt(), e->yTilt(), + e->tangentialPressure(), e->rotation(), e->z(), + Qt::KeyboardModifier::NoModifier, e->uniqueId(), e->button(), + e->buttons()); + emit tabletEventReceived(m_events.last()); + } + int nextEventIndex = 0; + int numEvents() const { return m_events.size() - nextEventIndex; } + QTabletEvent *popEvent() + { + auto *event = m_events.value(nextEventIndex, nullptr); + if (event) + ++nextEventIndex; + return event; + } + +signals: + void tabletEventReceived(QTabletEvent *event); + +private: + QVector m_events; +}; + +void tst_tabletv2::moveEvent() +{ + QCOMPOSITOR_TRY_VERIFY(tabletSeat()); + exec([&] { + tabletSeat()->addTablet(); + tabletSeat()->addTool(); + }); + + TabletWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + QCOMPOSITOR_TRY_VERIFY(tablet()); + exec([&] { + auto *surface = xdgSurface()->m_surface; + auto *tool = tabletTool(); + tool->sendProximityIn(tablet(), surface); + QMargins margins = window.frameMargins(); + tool->sendMotion(QPointF(12 + margins.left(), 34 + margins.top())); + tool->sendFrame(); + }); + QTRY_VERIFY(window.numEvents()); + QTabletEvent *event = window.popEvent(); + QCOMPARE(event->type(), QEvent::TabletMove); + QCOMPARE(event->pressure(), 0); + QCOMPARE(event->posF(), QPointF(12, 34)); +} + +void tst_tabletv2::pointerType_data() +{ + QTest::addColumn("toolType"); + QTest::addColumn("pointerType"); + QTest::addColumn("tabletDevice"); + + QTest::newRow("pen") << ToolType::type_pen << QTabletEvent::PointerType::Pen << QTabletEvent::TabletDevice::Stylus; + QTest::newRow("eraser") << ToolType::type_eraser << QTabletEvent::PointerType::Eraser << QTabletEvent::TabletDevice::Stylus; + QTest::newRow("pencil") << ToolType::type_pencil << QTabletEvent::PointerType::Pen << QTabletEvent::TabletDevice::Stylus; + QTest::newRow("airbrush") << ToolType::type_airbrush << QTabletEvent::PointerType::Pen << QTabletEvent::TabletDevice::Airbrush; + QTest::newRow("brush") << ToolType::type_brush << QTabletEvent::PointerType::Pen << QTabletEvent::TabletDevice::Stylus; // TODO: is TabletDevice::Stylus the right thing? + QTest::newRow("lens") << ToolType::type_lens << QTabletEvent::PointerType::Cursor << QTabletEvent::TabletDevice::Puck; + // TODO: also add tests for FourDMouse and RotationStylus (also need to send capabilities) + + // TODO: should these rather be mapped to touch/mouse events? + QTest::newRow("finger") << ToolType::type_finger << QTabletEvent::PointerType::UnknownPointer << QTabletEvent::TabletDevice::NoDevice; + QTest::newRow("mouse") << ToolType::type_mouse << QTabletEvent::PointerType::Cursor << QTabletEvent::TabletDevice::NoDevice; +} + +void tst_tabletv2::pointerType() +{ + using ToolType = QtWaylandServer::zwp_tablet_tool_v2::type; + QFETCH(ToolType, toolType); + QFETCH(QTabletEvent::PointerType, pointerType); + QFETCH(QTabletEvent::TabletDevice, tabletDevice); + + ProximityFilter filter; + + QCOMPOSITOR_TRY_VERIFY(tabletSeat()); + exec([&] { + tabletSeat()->addTablet(); + tabletSeat()->addTool(toolType); + }); + + TabletWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + QCOMPOSITOR_TRY_VERIFY(tablet()); + exec([&] { + auto *surface = xdgSurface()->m_surface; + auto *tool = tabletTool(); + tool->sendProximityIn(tablet(), surface); + QMargins margins = window.frameMargins(); + tool->sendMotion(QPointF(12 + margins.left(), 34 + margins.top())); + tool->sendFrame(); + }); + + QTRY_COMPARE(filter.numEvents(), 1); + QTabletEvent *event = filter.popEvent(); + QCOMPARE(event->pointerType(), pointerType); + QCOMPARE(event->device(), tabletDevice); + + QTRY_VERIFY(window.numEvents()); + event = window.popEvent(); + QCOMPARE(event->pointerType(), pointerType); + QCOMPARE(event->device(), tabletDevice); + + exec([&] { + tabletTool()->sendProximityOut(); + tabletTool()->sendFrame(); + }); + + QTRY_VERIFY(filter.numEvents()); + event = filter.popEvent(); + QCOMPARE(event->pointerType(), pointerType); + QCOMPARE(event->device(), tabletDevice); +} + +void tst_tabletv2::hardwareSerial() +{ + ProximityFilter filter; + const quint64 uid = 0xbaba15dead15f00d; + + QCOMPOSITOR_TRY_VERIFY(tabletSeat()); + exec([&] { + tabletSeat()->addTablet(); + tabletSeat()->addTool(ToolType::type_pen, uid); + }); + + TabletWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + QCOMPOSITOR_TRY_VERIFY(tablet()); + exec([&] { + auto *surface = xdgSurface()->m_surface; + auto *tool = tabletTool(); + tool->sendProximityIn(tablet(), surface); + QMargins margins = window.frameMargins(); + tool->sendMotion(QPointF(12 + margins.left(), 34 + margins.top())); + tool->sendFrame(); + }); + + QTRY_COMPARE(filter.numEvents(), 1); + QTabletEvent *event = filter.popEvent(); + QCOMPARE(event->uniqueId(), uid); + + QTRY_VERIFY(window.numEvents()); + event = window.popEvent(); + QCOMPARE(event->uniqueId(), uid); + + exec([&] { + tabletTool()->sendProximityOut(); + tabletTool()->sendFrame(); + }); + + QTRY_VERIFY(filter.numEvents()); + event = filter.popEvent(); + QCOMPARE(event->uniqueId(), uid); +} + +// As defined in linux/input-event-codes.h +#ifndef BTN_STYLUS +#define BTN_STYLUS 0x14b +#endif +#ifndef BTN_STYLUS2 +#define BTN_STYLUS2 0x14c +#endif + +void tst_tabletv2::buttons_data() +{ + QTest::addColumn("tabletButton"); + QTest::addColumn("mouseButton"); + + QTest::newRow("BTN_STYLUS2") << uint(BTN_STYLUS2) << Qt::MouseButton::RightButton; + QTest::newRow("BTN_STYLUS") << uint(BTN_STYLUS) << Qt::MouseButton::MiddleButton; +} + +void tst_tabletv2::buttons() +{ + QFETCH(uint, tabletButton); + QFETCH(Qt::MouseButton, mouseButton); + + QCOMPOSITOR_TRY_VERIFY(tabletSeat()); + exec([&] { + tabletSeat()->addTablet(); + tabletSeat()->addTool(); + }); + + TabletWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + QCOMPOSITOR_TRY_VERIFY(tablet()); + exec([&] { + tabletTool()->sendProximityIn(tablet(), xdgSurface()->m_surface); + QMargins margins = window.frameMargins(); + tabletTool()->sendMotion(QPointF(12 + margins.left(), 34 + margins.top())); + tabletTool()->sendFrame(); + }); + + QTRY_VERIFY(window.numEvents()); + window.popEvent(); + + QCOMPOSITOR_TRY_VERIFY(tablet()); + exec([&] { + tabletTool()->sendButton(tabletButton, true); + tabletTool()->sendFrame(); + tabletTool()->sendButton(tabletButton, false); + tabletTool()->sendFrame(); + }); + + QTRY_VERIFY(window.numEvents()); + QTabletEvent *event = window.popEvent(); + QCOMPARE(event->buttons(), mouseButton); + + exec([&] { + tabletTool()->sendProximityOut(); + tabletTool()->sendFrame(); + }); +} + +void tst_tabletv2::tabletEvents() +{ + QCOMPOSITOR_TRY_VERIFY(tabletSeat()); + exec([&] { + tabletSeat()->addTablet(); + tabletSeat()->addTool(); + }); + + TabletWindow window; + window.resize(64, 64); + window.show(); + QCOMPOSITOR_TRY_VERIFY(xdgSurface() && xdgSurface()->m_committedConfigureSerial); + + const QPointF insideDecorations(window.frameMargins().left(), window.frameMargins().top()); + + QCOMPOSITOR_TRY_VERIFY(tablet()); + exec([&] { + auto *surface = xdgSurface()->m_surface; + auto *tool = tabletTool(); + // TODO: encapsulate this into a helper function? + tool->sendProximityIn(tablet(), surface); + tool->sendMotion(QPointF(12, 34) + insideDecorations); + tool->sendDown(); + tool->sendPressure(65535); + tool->sendFrame(); + }); + + QTRY_VERIFY(window.numEvents()); + QTabletEvent *event = window.popEvent(); + QCOMPARE(event->type(), QEvent::TabletPress); + QCOMPARE(event->pressure(), 1.0); + QCOMPARE(event->posF(), QPointF(12, 34)); + + // Values we didn't send should be 0 + QCOMPARE(event->rotation(), 0); + QCOMPARE(event->xTilt(), 0); + QCOMPARE(event->yTilt(), 0); + + exec([&] { + tabletTool()->sendMotion(QPointF(45, 56) + insideDecorations); + tabletTool()->sendPressure(65535/2); + tabletTool()->sendRotation(90); + tabletTool()->sendTilt(13, 37); + tabletTool()->sendFrame(); + }); + + QTRY_VERIFY(window.numEvents()); + event = window.popEvent(); + QCOMPARE(event->type(), QEvent::TabletMove); + QVERIFY(qAbs(event->pressure() - 0.5) < 0.01); + QVERIFY(qAbs(event->rotation() - 90) < 0.01); + QVERIFY(qAbs(event->xTilt() - 13) < 0.01); + QVERIFY(qAbs(event->yTilt() - 37) < 0.01); + QCOMPARE(event->posF(), QPointF(45, 56)); + + // Verify that the values stay the same if we don't update them + exec([&] { + tabletTool()->sendMotion(QPointF(10, 11) + insideDecorations); // Change position only + tabletTool()->sendFrame(); + }); + QTRY_VERIFY(window.numEvents()); + event = window.popEvent(); + QCOMPARE(event->type(), QEvent::TabletMove); + QVERIFY(qAbs(event->pressure() - 0.5) < 0.01); + QVERIFY(qAbs(event->rotation() - 90) < 0.01); + QVERIFY(qAbs(event->xTilt() - 13) < 0.01); + QVERIFY(qAbs(event->yTilt() - 37) < 0.01); + QCOMPARE(event->posF(), QPointF(10, 11)); + + exec([&] { + tabletTool()->sendPressure(0); + tabletTool()->sendUp(); + tabletTool()->sendFrame(); + + tabletTool()->sendProximityOut(); + tabletTool()->sendFrame(); + }); + + QTRY_VERIFY(window.numEvents()); + event = window.popEvent(); + QCOMPARE(event->type(), QEvent::TabletRelease); + QCOMPARE(event->pressure(), 0); + QCOMPARE(event->posF(), QPointF(10, 11)); +} + +QCOMPOSITOR_TEST_MAIN(tst_tabletv2) +#include "tst_tabletv2.moc" -- cgit v1.2.3