diff options
| author | Matthias Melcher <github@matthiasm.com> | 2025-11-17 21:10:01 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-11-17 21:10:01 +0100 |
| commit | fa65cd63214030011d7ac0a18818c644aff05750 (patch) | |
| tree | 8d5b7c129955a258897a14816cda802778c36910 | |
| parent | d623ad08a9e7d91e8b0599189bdb57e6ee1dcc94 (diff) | |
Add pen/stylus/tablet API and driver for macOS (#1326)
* define the pen/tablet support API
* add pen event handler stub as a fallback
* add pen device test "penpal".
* Add macOS pen/stylus/tablet driver.
* Add Oxygen documentation.
| -rw-r--r-- | FL/Fl.H | 2 | ||||
| -rw-r--r-- | FL/Fl_Widget_Tracker.H | 15 | ||||
| -rw-r--r-- | FL/core/events.H | 2 | ||||
| -rw-r--r-- | FL/core/pen_events.H | 465 | ||||
| -rw-r--r-- | FL/names.h | 83 | ||||
| -rw-r--r-- | src/CMakeLists.txt | 10 | ||||
| -rw-r--r-- | src/Fl_Menu.cxx | 3 | ||||
| -rw-r--r-- | src/Fl_cocoa.mm | 58 | ||||
| -rw-r--r-- | src/Fl_grab.cxx | 1 | ||||
| -rw-r--r-- | src/drivers/Cocoa/Fl_Cocoa_Pen_Events.mm | 530 | ||||
| -rw-r--r-- | src/drivers/Stubs/Fl_Stubs_Pen_Events.cxx | 73 | ||||
| -rw-r--r-- | test/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | test/penpal.cxx | 314 |
13 files changed, 1514 insertions, 43 deletions
@@ -28,6 +28,7 @@ #include <FL/core/function_types.H> // widget callbacks and services #include <FL/core/events.H> // global event handling #include <FL/core/options.H> // system and application setting +#include <FL/core/pen_events.H> // pen and tablet events #include <FL/Fl_Widget_Tracker.H> // historically included here #ifdef FLTK_HAVE_CAIRO @@ -109,7 +110,6 @@ FL_EXPORT extern bool idle(); FL_EXPORT extern const char* scheme_; FL_EXPORT extern Fl_Image* scheme_bg_; -//FL_EXPORT extern int e_original_keysym; // late addition FL_EXPORT extern int scrollbar_size_; FL_EXPORT extern int menu_linespacing_; // STR #2927 #endif diff --git a/FL/Fl_Widget_Tracker.H b/FL/Fl_Widget_Tracker.H index d0aad86c0..fb2182e5f 100644 --- a/FL/Fl_Widget_Tracker.H +++ b/FL/Fl_Widget_Tracker.H @@ -76,9 +76,20 @@ class FL_EXPORT Fl_Widget_Tracker { public: Fl_Widget_Tracker(Fl_Widget *wi); + // Rule of five. Note that we *can* implement these when we refactor widget + // tracking with a C++11 map or unordered_map, for example. + Fl_Widget_Tracker(const Fl_Widget_Tracker&) = delete; + Fl_Widget_Tracker(Fl_Widget_Tracker&&) = delete; + Fl_Widget_Tracker& operator=(const Fl_Widget_Tracker&) = delete; + Fl_Widget_Tracker& operator=(Fl_Widget_Tracker&&) = delete; ~Fl_Widget_Tracker(); /** + Clear the widget pointer. + */ + void clear() { wp_ = nullptr; } + + /** Returns a pointer to the watched widget. \return nullptr if the widget was deleted. */ @@ -88,13 +99,13 @@ public: Check if the widget was deleted since the tracker was created. \return 1 if the watched widget has been deleted, otherwise 0 */ - int deleted() {return wp_ == 0;} + int deleted() {return wp_ == nullptr;} /** Check if the widget exists and was not deleted since the tracker was created. \return 1 if the watched widget exists, otherwise 0 */ - int exists() { return wp_ != 0; } + int exists() { return wp_ != nullptr; } }; diff --git a/FL/core/events.H b/FL/core/events.H index 88d73c7e3..01c0fd7ed 100644 --- a/FL/core/events.H +++ b/FL/core/events.H @@ -71,6 +71,8 @@ FL_EXPORT extern Fl_Widget* belowmouse_; ///< Widget under mouse FL_EXPORT extern Fl_Widget* pushed_; ///< Widget receiving drag events FL_EXPORT extern Fl_Widget* focus_; ///< Widget with keyboard focus +// Event variables should be private, but would harm back compatibility. + #endif // FL_DOXYGEN diff --git a/FL/core/pen_events.H b/FL/core/pen_events.H new file mode 100644 index 000000000..c20cfa438 --- /dev/null +++ b/FL/core/pen_events.H @@ -0,0 +1,465 @@ +// +// Pen event header file for the Fast Light Tool Kit (FLTK). +// +// Copyright 2025 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +/** + \file FL/core/pen_events.H + \brief Pen event handling variables and functions. +*/ + +#ifndef Fl_core_pen_events_H +#define Fl_core_pen_events_H + +#include <FL/fl_config.h> // build configuration +#include <FL/Fl_Export.H> // for FL_EXPORT +#include <FL/core/function_types.H> // widget callbacks and services + +#include <cstdint> + +class Fl_Widget; + +namespace Fl { + +/** FLTK Pen/Stylus/Tablet input driver API. */ +namespace Pen { + +/** + \defgroup fl_pen_events Pen and tablet event handling + \ingroup fl_events + \brief This chapter documents the Fl::Pen namespace API, declared in <FL/core/pen_events.H> + + The FL::Pen namespace contains everything needed to work with a pen type input + device, either in connection with an external tablet, or as a stylus for + drawing directly onto a screen. + + To receive pen input, call Fl::Pen::subscribe() for one or more widgets. The + widget will receive a Fl::Pen::ENTER event when the stylus enters the widget + area. By returning 1 to Fl::Pen::ENTER, all further pen events are sent to + this widget, and no mouse events are generated until Fl::Pen::LEAVE. + + Returning 0 Fl::Pen::ENTER tells FLTK to suppress further pen events until + Fl::Pen::LEAVE, and convert them into mouse events instead. + + Pen events also set Fl::event_x(), Fl::event_y(), Fl::event_x_root(), + Fl::event_y_root(), Fl::event_is_click(), and Fl::event_clicks(). + + @{ + */ + +/** + \brief Bitfield of traits. + This is used in Fl::Pen::driver_traits() and Fl::Pen::pen_traits(). + */ +enum class Trait : uint32_t { + /// No bits set + NONE = 0x0000, + /// Set if FLTK supports tablets and pens on this platform + DRIVER_AVAILABLE = 0x0001, + /// Set after the system detected a pen, stylus, or tablet. This bit may not be + /// set until a pen is brought into proximity of the tablet. + DETECTED = 0x0002, + /// If set, this is a digitizer for a display; if clear, this is a standalone tablet + DISPLAY = 0x0004, + /// Driver provides different device IDs for different pens + PEN_ID = 0x0008, + /// Pen may have an eraser tip + ERASER = 0x0010, + /// Pen returns a pressure value + PRESSURE = 0x0020, + /// Pen returns a barrel pressure value (tangential pressure) + BARREL_PRESSURE = 0x0040, + /// Pen returns tilt in X direction + TILT_X = 0x0080, + /// Pen returns tilt in Y direction + TILT_Y = 0x0100, + /// Pen returns a twist value + TWIST = 0x0200, + /// Pen returns a proximity value + PROXIMITY = 0x0400, +}; + +/** + \brief Bitwise OR operator for Trait enum. + \param lhs Left-hand side trait flags + \param rhs Right-hand side trait flags + \return Combined trait flags + */ +inline constexpr Trait operator|(Trait lhs, Trait rhs) { + return static_cast<Trait>(static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs)); +} + +/** + \brief Bitwise AND operator for Trait enum. + \param lhs Left-hand side trait flags + \param rhs Right-hand side trait flags + \return Intersection of trait flags + */ +inline constexpr Trait operator&(Trait lhs, Trait rhs) { + return static_cast<Trait>(static_cast<uint32_t>(lhs) & static_cast<uint32_t>(rhs)); +} + +/** + \brief Bitwise OR assignment operator for Trait enum. + \param lhs Left-hand side trait flags (modified in place) + \param rhs Right-hand side trait flags + \return Reference to modified lhs + */ +inline Trait& operator|=(Trait& lhs, Trait rhs) { + lhs = lhs | rhs; + return lhs; +} + + +/** + \brief Bitfield of pen state flags. + \see event_state(), event_trigger() + */ +enum class State : uint32_t { + /// No button pressed + NONE = 0x0000, + /// The tip hovers over the surface but does not touch it + TIP_HOVERS = 0x0001, + /// The tip touches the surface + TIP_DOWN = 0x0002, + /// The eraser hovers over the surface but does not touch it + ERASER_HOVERS = 0x0004, + /// The eraser touches the surface + ERASER_DOWN = 0x0008, + /// Barrel button 0, usually the lower button on a pen, is pressed + BUTTON0 = 0x0100, + /// Barrel button 1, usually the upper button on a pen, is pressed + BUTTON1 = 0x0200, + /// Barrel button 2 is pressed + BUTTON2 = 0x0400, + /// Barrel button 3 is pressed + BUTTON3 = 0x0800, + /// Mask for all buttons, tip, and eraser down + ANY_DOWN = BUTTON0 | BUTTON1 | BUTTON2 | BUTTON3 | TIP_DOWN | ERASER_DOWN, +}; + +/** + \brief Bitwise OR operator for State enum. + \param lhs Left-hand side state flags + \param rhs Right-hand side state flags + \return Combined state flags + */ +inline constexpr State operator|(State lhs, State rhs) { + return static_cast<State>(static_cast<uint32_t>(lhs) | static_cast<uint32_t>(rhs)); +} + +/** + \brief Bitwise AND operator for State enum. + \param lhs Left-hand side state flags + \param rhs Right-hand side state flags + \return Intersection of state flags + */ +inline constexpr State operator&(State lhs, State rhs) { + return static_cast<State>(static_cast<uint32_t>(lhs) & static_cast<uint32_t>(rhs)); +} + +/** + \brief Bitwise OR assignment operator for State enum. + \param lhs Left-hand side state flags (modified in place) + \param rhs Right-hand side state flags + \return Reference to modified lhs + */ +inline State& operator|=(State& lhs, State rhs) { + lhs = lhs | rhs; + return lhs; +} + + +/** + \brief List of pen events. + These events extend the standard Fl_Event enumeration. + \see enum Fl_Event + */ +enum Event { + /** + Pen entered the proximity of the tablet with a new pen. + */ + DETECTED = 0x1000, + + /** + Pen entered the proximity of the tablet with a known, but changed pen. + User changed to a different pen (event_id() > 0) or the pen or tablet + was disconnected (event_id() == -1). Pen IDs, if supported, are assigned by + the tablet manufacturer. + */ + CHANGED, + + /** + Pen entered the proximity of the tablet with a known pen. + */ + IN_RANGE, + + /** + Pen left the proximity of the tablet. + */ + OUT_OF_RANGE, + + /** + Pen entered the widget area, either by moving in x/y, or by + a proximity change (pen gets closer to the surface). + event_trigger() returns 0, TIP_HOVERS, or ERASER_HOVERS. + */ + ENTER, + + /** + If no button is pressed, indicates that the pen left the widget area. + While any pen button is held down, or the pen touches the surface, + Fl::pushed() is set, and the pushed widgets receives DRAG events, even + if the pen leaves the widget area. If all buttons are released outside the + widget area, a LEAVE event is sent as well as LIFT or BUTTON_RELEASE. + */ + LEAVE, + + /** + Pen went from hovering to touching the surface. + event_trigger() returns TIP_DOWN or ERASER_DOWN. + */ + TOUCH, + + /** + Pen went from touching to hovering over the surface. + event_trigger() returns TIP_HOVERS or ERASER_HOVERS. + */ + LIFT, + + /** Pen moved without touching the surface and no button is pressed. */ + HOVER, + + /** Pen moved while touching the surface, or any button is pressed. */ + DRAW, + + /** + A pen button was pushed. + event_trigger() returns BUTTON0, BUTTON1, BUTTON2, or BUTTON3. + */ + BUTTON_PUSH, + + /** + A pen button was released. + event_trigger() returns BUTTON0, BUTTON1, BUTTON2, or BUTTON3. + */ + BUTTON_RELEASE + +}; + +/** + \brief Query the traits supported by the pen/tablet driver. + + This function returns a bitfield of traits that are supported by the FLTK driver + for this platform. If a trait is not supported, the corresponding event value + will not return a useful value. Note that even if the FLTK driver support a + trait, the underlying pen device or driver may not. Fl::Pen will return a + known default for those event values. + + The bitfield returned is static. + + \return a bitfield of supported traits + \see pen_traits() + */ +FL_EXPORT extern Trait driver_traits(); + +/** + \brief Return true if the corresponding bit is set in the driver traits. + \param[in] bits check for one or more trait bits + \return true if any bit is set + */ +inline bool driver_traits(Trait bits) { + return ((driver_traits() & bits) != Trait::NONE); +} + +/** + \brief Query traits of the current pen or stylus. + The value returned by this function may change when pens change or when more + information becomes known about the currently used pen. + \param[in] pen_id a now pen ID as returned from event_pen_id(), + or 0 for the current pen + \return a bitfield of supported traits + */ +FL_EXPORT extern Trait pen_traits(int pen_id = 0); + +/** + \brief Return true if the corresponding bit is set in the pen traits. + \param[in] bits check for one or more trait bits + \param[in] pen_id a now pen ID as returned from event_pen_id(), + or 0 for the current pen + \return true if any bit is set + */ +inline bool pen_traits(Trait bits, int pen_id = 0) { + return ((pen_traits() & bits) != Trait::NONE); +} + +/** + \brief Receive a Pen::ENTER event when the pen moves inside this widget. + Multiple widgets can subscribe to pen events, but every widget must only + subscribe once. + \param widget Widget to subscribe to pen events + */ +FL_EXPORT extern void subscribe(Fl_Widget* widget); + +/** + \brief Stop receiving Pen::ENTER for this widget. + Deleting a widget will automatically unsubscribe it. + \param widget Widget to unsubscribe from pen events + */ +FL_EXPORT extern void unsubscribe(Fl_Widget* widget); + +/** + Clear the "pushed" state and forward pen events as mouse events. + Call this if another window is popped up during pen event handling, so + mouse event handling can resume normal. + */ +FL_EXPORT extern void release(); + +/// \name Query values during event handling +/// @{ + +/** + \brief Returns the pen x and y position inside the handling widget as doubles. + These functions provide high-precision pen coordinates relative to the widget + that received the pen event. For integer coordinates, use Fl::event_x() and + Fl::event_y() instead. + \return Pen position as floating-point coordinate, defaults to 0.0 + \see Fl::event_x(), Fl::event_y() + */ +FL_EXPORT extern double event_x(); +/** \brief Returns pen Y coordinate in widget space, see event_x(). */ +FL_EXPORT extern double event_y(); + +/** + \brief Returns the pen x and y position in global coordinates as doubles. + For integer coordinates, use Fl::event_x_root() and Fl::event_y_root(). + \return Pen position as floating-point coordinate in screen space, defaults to 0.0 + \see Fl::event_x_root(), Fl::event_y_root() + */ +FL_EXPORT extern double event_x_root(); +/** \brief Returns pen Y coordinate in screen space, see event_x_root(). */ +FL_EXPORT extern double event_y_root(); + +/** + \brief Returns the ID of the pen used in the last event. + \return Unique pen identifier, or -1 if pen was removed, defaults to 0 + \see Trait::PEN_ID + */ +FL_EXPORT extern int event_pen_id(); + +/** + \brief Returns the pressure between the tip or eraser and the surface. + \return pressure value from 0.0 (no pressure) to 1.0 (maximum pressure), + defaults to 1.0. + \see Trait::PRESSURE + */ +FL_EXPORT extern double event_pressure(); + +/** + \brief Returns barrel pressure or tangential pressure. + \return Pressure value from -1.0 to 1.0 , defaults to 0.0 . + \see Trait::BARREL_PRESSURE + */ +FL_EXPORT extern double event_barrel_pressure(); + +/** + \brief Returns the tilt of the pen in the x and y directions between -1 and 1. + + X-axis tilt returns -1.0 when the pen tilts all the way to the left, 0.0 when + it is perfectly vertical, and 1.0 all the way to the right. Most pens seem to + return a maximum range of -0.7 to 0.7. + + Y-axis tilt returns -1.0 when the pen tilts away from the user, and 1.0 when + it tilts toward the user. + + \return Tilt value from -1.0 to 1.0, defaults to 0.0 + \see Trait::TILT_X, Trait::TILT_Y + */ +FL_EXPORT extern double event_tilt_x(); +/** \brief Returns pen Y-axis tilt, see event_tilt_x() */ +FL_EXPORT extern double event_tilt_y(); + +/** + \brief Returns the pens axial rotation in degrees. + \return Twist angle in degrees, defaults to 0.0 . + \see Trait::TWIST + */ +FL_EXPORT extern double event_twist(); + +/** + \brief Returns the proximity of the pen to the surface between 0 and 1. + A proximity of 0 is closest to the surface, 1 is farthest away. + \return Proximity value from 0.0 (touching) to 1.0 (far away), defaults to 0.0 . + \see Trait::PROXIMITY + */ +FL_EXPORT extern double event_proximity(); + +/** + \brief Returns the state of the various buttons and tips. + \return Current state flags (combination of State values) + */ +FL_EXPORT extern State event_state(); + +/** + \brief Return true if the corresponding bit is set in the event state. + \param[in] bits check for one or more event state bits + \return true if any bit is set + */ +inline bool event_state(State bits) { + return ((event_state() & bits) != State::NONE); +} + +/** + \brief Returns the state change that triggered the event. + \return a state with one bit set for the action that triggered this event + */ +FL_EXPORT extern State event_trigger(); + +/** @} */ // group fl_pen_events + + +} // namespace Pen + +} // namespace Fl + + +/* + Resources: + + Windows: + 1. Legacy WinTab API (Win2k), Wintab32.dll, wintab.h + https://developer.wacom.com/en-us/developer-dashboard/downloads + 2. Windows Ink API (Modern, Win10), Windows.UI.Input.Inking (WinRT API), InkCanvas(), etc. + https://learn.microsoft.com/windows/uwp/design/input/windows-ink + 3. Pointer Input / WM_POINTER API (Win8), WM_POINTERUPDATE, GetPointerPenInfo + https://learn.microsoft.com/windows/win32/inputmsg/wm-pointerupdate + return WTInfo(0, 0, NULL) > 0; // Wintab check + + Linux: + 1. Low-level: evdev, /dev/input/event*, libevdev, + https://www.kernel.org/doc/html/latest/input/event-codes.html + 2. Mid-level: XInput2 (for X11), XI_Motion, XI_ButtonPress + https://www.x.org/releases/current/doc/inputproto/XI2proto.txt + https://www.freedesktop.org/wiki/Software/libevdev/ + 3. Mid-level: Wayland tablet protocol, tablet-v2 protocol, + zwp_tablet_tool_v2_listener, zwp_tablet_v2, zwp_tablet_seat_v2 + https://wayland.app/protocols/tablet-v2 + + SDL3: + https://github.com/libsdl-org/SDL/blob/main/include/SDL3/SDL_pen.h + https://wiki.libsdl.org/SDL3/CategoryPen + */ + + +#endif // !Fl_core_pen_events_H diff --git a/FL/names.h b/FL/names.h index 90ef231e7..3587b3d8d 100644 --- a/FL/names.h +++ b/FL/names.h @@ -24,6 +24,9 @@ #ifndef FL_NAMES_H #define FL_NAMES_H +#include <FL/Fl.H> // for event constants +#include <map> + /** \defgroup fl_events Events handling functions @{ */ @@ -43,43 +46,53 @@ } \endcode */ -const char * const fl_eventnames[] = -{ - "FL_NO_EVENT", - "FL_PUSH", - "FL_RELEASE", - "FL_ENTER", - "FL_LEAVE", - "FL_DRAG", - "FL_FOCUS", - "FL_UNFOCUS", - "FL_KEYDOWN", - "FL_KEYUP", - "FL_CLOSE", - "FL_MOVE", - "FL_SHORTCUT", - "FL_DEACTIVATE", - "FL_ACTIVATE", - "FL_HIDE", - "FL_SHOW", - "FL_PASTE", - "FL_SELECTIONCLEAR", - "FL_MOUSEWHEEL", - "FL_DND_ENTER", - "FL_DND_DRAG", - "FL_DND_LEAVE", - "FL_DND_RELEASE", - "FL_SCREEN_CONFIGURATION_CHANGED", - "FL_FULLSCREEN", - "FL_ZOOM_GESTURE", - "FL_ZOOM_EVENT", - "FL_BEFORE_TOOLTIP", - "FL_BEFORE_MENU", - "FL_EVENT_30", // not yet defined, just in case it /will/ be defined ... - "FL_EVENT_31", // not yet defined, just in case it /will/ be defined ... - "FL_EVENT_32" // not yet defined, just in case it /will/ be defined ... +std::map<int, const char*> fl_eventnames = { + { FL_NO_EVENT, "FL_NO_EVENT" }, + { FL_PUSH, "FL_PUSH" }, + { FL_RELEASE, "FL_RELEASE" }, + { FL_ENTER, "FL_ENTER" }, + { FL_LEAVE, "FL_LEAVE" }, + { FL_DRAG, "FL_DRAG" }, + { FL_FOCUS, "FL_FOCUS" }, + { FL_UNFOCUS, "FL_UNFOCUS" }, + { FL_KEYDOWN, "FL_KEYDOWN" }, + { FL_KEYUP, "FL_KEYUP" }, + { FL_CLOSE, "FL_CLOSE" }, + { FL_MOVE, "FL_MOVE" }, + { FL_SHORTCUT, "FL_SHORTCUT" }, + { FL_DEACTIVATE, "FL_DEACTIVATE" }, + { FL_ACTIVATE, "FL_ACTIVATE" }, + { FL_HIDE, "FL_HIDE" }, + { FL_SHOW, "FL_SHOW" }, + { FL_PASTE, "FL_PASTE" }, + { FL_SELECTIONCLEAR, "FL_SELECTIONCLEAR" }, + { FL_MOUSEWHEEL, "FL_MOUSEWHEEL" }, + { FL_DND_ENTER, "FL_DND_ENTER" }, + { FL_DND_DRAG, "FL_DND_DRAG" }, + { FL_DND_LEAVE, "FL_DND_LEAVE" }, + { FL_DND_RELEASE, "FL_DND_RELEASE" }, + { FL_SCREEN_CONFIGURATION_CHANGED, "FL_SCREEN_CONFIGURATION_CHANGED" }, + { FL_FULLSCREEN, "FL_FULLSCREEN" }, + { FL_ZOOM_GESTURE, "FL_ZOOM_GESTURE" }, + { FL_ZOOM_EVENT, "FL_ZOOM_EVENT" }, + { FL_BEFORE_TOOLTIP, "FL_BEFORE_TOOLTIP" }, + { FL_BEFORE_MENU, "FL_BEFORE_MENU" }, + { /*FL_EVENT_*/ 30, "FL_EVENT_30" }, // not yet defined, just in case it /will/ be defined ... + { /*FL_EVENT_*/ 31, "FL_EVENT_31" }, // not yet defined, just in case it /will/ be defined ... + { /*FL_EVENT_*/ 32, "FL_EVENT_32" }, // not yet defined, just in case it /will/ be defined ... + { Fl::Pen::DETECTED, "Fl::Pen::DETECTED" }, + { Fl::Pen::CHANGED, "Fl::Pen::CHANGED" }, + { Fl::Pen::ENTER, "Fl::Pen::ENTER" }, + { Fl::Pen::LEAVE, "Fl::Pen::LEAVE" }, + { Fl::Pen::TOUCH, "Fl::Pen::TOUCH" }, + { Fl::Pen::LIFT, "Fl::Pen::LIFT" }, + { Fl::Pen::HOVER, "Fl::Pen::HOVER" }, + { Fl::Pen::DRAW, "Fl::Pen::DRAW" }, + { Fl::Pen::BUTTON_PUSH, "Fl::Pen::BUTTON_PUSH" }, + { Fl::Pen::BUTTON_RELEASE, "Fl::Pen::BUTTON_RELEASE" } }; + /** This is an array of font names you can use to convert font numbers into names. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 63445bf48..914d79e09 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -202,6 +202,12 @@ file(GLOB "*.[hH]" ) +# find all private header files in source directory "src/..." +file(GLOB + PRIVATE_HEADER_FILES + "*.[hH]" +) + # add generated header files in build directory list(APPEND HEADER_FILES ${CMAKE_CURRENT_BINARY_DIR}/../FL/fl_config.h @@ -235,6 +241,7 @@ if(FLTK_USE_X11 AND NOT FLTK_USE_WAYLAND) drivers/Xlib/Fl_Xlib_Copy_Surface_Driver.cxx drivers/Xlib/Fl_Xlib_Image_Surface_Driver.cxx drivers/X11/fl_X11_platform_init.cxx + drivers/Stubs/Fl_Stubs_Pen_Events.cxx Fl_x.cxx fl_dnd_x.cxx Fl_Native_File_Chooser_FLTK.cxx @@ -311,6 +318,7 @@ elseif(FLTK_USE_WAYLAND) drivers/Wayland/fl_wayland_clipboard_dnd.cxx drivers/Wayland/fl_wayland_platform_init.cxx drivers/Cairo/Fl_Cairo_Graphics_Driver.cxx + drivers/Stubs/Fl_Stubs_Pen_Events.cxx Fl_Native_File_Chooser_FLTK.cxx Fl_Native_File_Chooser_GTK.cxx ) @@ -397,6 +405,7 @@ else() drivers/GDI/Fl_GDI_Graphics_Driver_vertex.cxx drivers/GDI/Fl_GDI_Copy_Surface_Driver.cxx drivers/GDI/Fl_GDI_Image_Surface_Driver.cxx + drivers/Stubs/Fl_Stubs_Pen_Events.cxx Fl_win32.cxx fl_dnd_win32.cxx Fl_Native_File_Chooser_WIN32.cxx @@ -617,6 +626,7 @@ if(APPLE AND NOT FLTK_BACKEND_X11) set(MMFILES Fl_cocoa.mm drivers/Cocoa/Fl_Cocoa_Printer_Driver.mm + drivers/Cocoa/Fl_Cocoa_Pen_Events.mm Fl_Native_File_Chooser_MAC.mm Fl_MacOS_Sys_Menu_Bar.mm ) diff --git a/src/Fl_Menu.cxx b/src/Fl_Menu.cxx index a91666ad3..f938f50d3 100644 --- a/src/Fl_Menu.cxx +++ b/src/Fl_Menu.cxx @@ -886,7 +886,8 @@ int menuwindow::handle_part1(int e) { int item; const Fl_Menu_Item* m = mw.menu->find_shortcut(&item); if (m) { setitem(m, mymenu, item); - if (!m->submenu()) pp.state = DONE_STATE; + if (!m->submenu()) + pp.state = DONE_STATE; return 1; } } diff --git a/src/Fl_cocoa.mm b/src/Fl_cocoa.mm index 59b8829f5..348007aa3 100644 --- a/src/Fl_cocoa.mm +++ b/src/Fl_cocoa.mm @@ -72,6 +72,10 @@ extern int fl_send_system_handlers(void *e); // converting cr lf converter function static void createAppleMenu(void); static void cocoaMouseHandler(NSEvent *theEvent); +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_9 +static bool cocoaTabletHandler(NSEvent *theEvent, bool lock); +extern bool fl_cocoa_tablet_handler(NSEvent*, Fl_Window*); +#endif static void clipboard_check(void); static NSBitmapImageRep* rect_to_NSBitmapImageRep(Fl_Window *win, int x, int y, int w, int h); static NSBitmapImageRep* rect_to_NSBitmapImageRep_subwins(Fl_Window *win, int x, int y, int w, int h, bool capture_subwins); @@ -627,6 +631,10 @@ void Fl_Cocoa_Screen_Driver::breakMacEventLoop() endedAtPoint:(NSPoint)screenPoint operation:(NSDragOperation)operation; #endif - (BOOL)did_view_resolution_change; +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_9 +- (void)tabletProximity:(NSEvent *)theEvent; +- (void)tabletPoint:(NSEvent *)theEvent; +#endif #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_14 - (void)create_aux_bitmap:(CGContextRef)gc retina:(BOOL)r; - (void)reset_aux_bitmap; @@ -1049,21 +1057,52 @@ static void cocoaMagnifyHandler(NSEvent *theEvent) #endif } + +static bool cocoaTabletHandler(NSEvent *theEvent, bool lock) +{ + if (lock) fl_lock_function(); + auto theWindow = (Fl_Window*)[(FLWindow*)[theEvent window] getFl_Window]; + auto ret = fl_cocoa_tablet_handler(theEvent, theWindow); + if (lock) fl_unlock_function(); + return ret; +} + +namespace Fl { +// Global mouse position at mouse down event +int e_x_down { 0 }; +int e_y_down { 0 }; +}; + /* * Cocoa Mouse Button Handler */ static void cocoaMouseHandler(NSEvent *theEvent) { static int keysym[] = { 0, FL_Button+1, FL_Button+3, FL_Button+2, FL_Button+4, FL_Button+5 }; - static int px, py; fl_lock_function(); + // Handle tablet proximity and point subevents + if ( ([theEvent type] != NSEventTypeMouseEntered) // does not have a subtype + && ([theEvent type] != NSEventTypeMouseExited) ) // does not have a subtype + { + if ( ([theEvent subtype] == NSEventSubtypeTabletPoint) + || ([theEvent subtype] == NSEventSubtypeTabletProximity) ) + { + if (cocoaTabletHandler(theEvent, false)) { + fl_unlock_function(); + return; + } + // else fall through into mouse event handling + } + } + Fl_Window *window = (Fl_Window*)[(FLWindow*)[theEvent window] getFl_Window]; if (!window || !window->shown() ) { fl_unlock_function(); return; } + NSPoint pos = [theEvent locationInWindow]; float s = Fl::screen_driver()->scale(0); pos.x /= s; pos.y /= s; @@ -1096,7 +1135,8 @@ static void cocoaMouseHandler(NSEvent *theEvent) case NSEventTypeOtherMouseDown: sendEvent = FL_PUSH; Fl::e_is_click = 1; - px = (int)pos.x; py = (int)pos.y; + Fl::e_x_down = (int)pos.x; + Fl::e_y_down = (int)pos.y; if ([theEvent clickCount] > 1) Fl::e_clicks++; else @@ -1121,7 +1161,8 @@ static void cocoaMouseHandler(NSEvent *theEvent) case NSEventTypeOtherMouseDragged: { if ( !sendEvent ) { sendEvent = FL_MOVE; // Fl::handle will convert into FL_DRAG - if (fabs(pos.x-px)>5 || fabs(pos.y-py)>5) + if ( (fabs(pos.x - Fl::e_x_down) > 5) || + (fabs(pos.y - Fl::e_y_down) > 5)) Fl::e_is_click = 0; } mods_to_e_state( mods ); @@ -1158,6 +1199,7 @@ static void cocoaMouseHandler(NSEvent *theEvent) return; } + @interface FLTextView : NSTextView // this subclass is only needed under OS X < 10.6 { BOOL isActive; @@ -1847,7 +1889,7 @@ void Fl_Darwin_System_Driver::open_callback(void (*cb)(const char *)) { // still needed for the system menu. [[NSApp keyWindow] sendEvent:theEvent]; return; - } + } [NSApp sendEvent:theEvent]; } @end @@ -2589,6 +2631,14 @@ static FLTextInputContext* fltextinputcontext_instance = nil; - (void)mouseExited:(NSEvent *)theEvent { cocoaMouseHandler(theEvent); } +#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_9 +- (void)tabletProximity:(NSEvent *)theEvent { + cocoaTabletHandler(theEvent, true); +} +- (void)tabletPoint:(NSEvent *)theEvent { + cocoaTabletHandler(theEvent, true); +} +#endif #if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_5 - (void)updateTrackingAreas { if (fl_mac_os_version >= 100500) { diff --git a/src/Fl_grab.cxx b/src/Fl_grab.cxx index 2913a1642..d9f47c445 100644 --- a/src/Fl_grab.cxx +++ b/src/Fl_grab.cxx @@ -27,5 +27,6 @@ void Fl::grab(Fl_Window *win) { + Fl::Pen::release(); screen_driver()->grab(win); } diff --git a/src/drivers/Cocoa/Fl_Cocoa_Pen_Events.mm b/src/drivers/Cocoa/Fl_Cocoa_Pen_Events.mm new file mode 100644 index 000000000..e77e191bb --- /dev/null +++ b/src/drivers/Cocoa/Fl_Cocoa_Pen_Events.mm @@ -0,0 +1,530 @@ +// +// Definition of macOS Cocoa Pen/Tablet event driver. +// +// Copyright 2025 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + + +#include <config.h> +#include <FL/platform.H> +#include <FL/core/pen_events.H> +#include <FL/Fl.H> +#include <FL/Fl_Window.H> +#include <FL/Fl_Tooltip.H> +#include <FL/Fl_Widget_Tracker.H> +#include "../../Fl_Screen_Driver.H" + +#import <Cocoa/Cocoa.h> + +#include <map> +#include <memory> + +extern Fl_Window *fl_xmousewin; + +/* + Widgets and windows must subscribe to pen events. This is to reduce the amount + of events sent into the widget hierarchy. + + Usually there is a pretty small number of subscribers, so looping through the + subscriber list should not be an issue. + + All subscribers track their widget. If a widget is deleted while subscribed, + including during event handling, the driver will remove the subscription. + There is no need to explicitly unsubscribe. + */ +class Subscriber : public Fl_Widget_Tracker { +public: + Subscriber(Fl_Widget *w) : Fl_Widget_Tracker(w) { } +}; + + +/* + Manage a list of subscribers. + */ +class SubscriberList : public std::map<Fl_Widget*, std::shared_ptr<Subscriber>> { +public: + SubscriberList() = default; + /* Remove subscribers that have a nullptr as a widget */ + void cleanup() { + for (auto it = begin(); it != end(); ) { + if (!it->second->widget()) { + it = erase(it); + } else { + ++it; + } + } + } + /* Add a new subscriber, or return an existing one. */ + std::shared_ptr<Subscriber> add(Fl_Widget *w) { + cleanup(); + auto it = find(w); + if (it == end()) { + auto sub = std::make_shared<Subscriber>(w); + insert(std::make_pair(w, sub)); + return sub; + } else { + return it->second; + } + } + /* Remove a subscriber form the list. */ + void remove(Fl_Widget *w) { + auto it = find(w); + if (it != end()) { + it->second->clear(); + erase(it); + } + } +}; + +static SubscriberList subscriber_list_; +static std::shared_ptr<Subscriber> pushed_; +static std::shared_ptr<Subscriber> below_pen_; +static NSPointingDeviceType device_type_ { NSPointingDeviceTypePen }; + +// The trait list keeps track of traits for every pen ID that appears while +// handling events. +// AppKit does not tell us what traits are available per pen or tablet, so +// we use the first 5 motion events to discover event values that are not +// the default value, and enter that knowledge into the traits database. +typedef std::map<int, Fl::Pen::Trait> TraitList; +static TraitList trait_list_; +static int trait_countdown_ { 5 }; +static int current_pen_id_ { -1 }; +static Fl::Pen::Trait current_pen_trait_ { Fl::Pen::Trait::DRIVER_AVAILABLE }; +static Fl::Pen::Trait driver_traits_ { + Fl::Pen::Trait::DRIVER_AVAILABLE | Fl::Pen::Trait::PEN_ID | + Fl::Pen::Trait::ERASER | Fl::Pen::Trait::PRESSURE | + Fl::Pen::Trait::BARREL_PRESSURE | Fl::Pen::Trait::TILT_X | + Fl::Pen::Trait::TILT_Y | Fl::Pen::Trait::TWIST + // Notably missing: PROXIMITY +}; + +struct EventData { + double x { 0.0 }; + double y { 0.0 }; + double rx { 0.0 }; + double ry { 0.0 }; + double tilt_x { 0.0 }; + double tilt_y { 0.0 }; + double pressure { 1.0 }; + double barrel_pressure { 0.0 }; + double twist { 0.0 }; + int pen_id { 0 }; + Fl::Pen::State state { (Fl::Pen::State)0 }; + Fl::Pen::State trigger { (Fl::Pen::State)0 }; +}; + +// Temporary storage of event data for the driver; +static struct EventData ev; + + +namespace Fl { + +// Global mouse position at mouse down event +extern int e_x_down; +extern int e_y_down; + +namespace Pen { + +// The event data that is made available to the user during event handling +struct EventData e; + +} // namespace Pen + +} // namespace Fl + + +using namespace Fl::Pen; + + +// Return a bit for everything that AppKit could return. +Trait Fl::Pen::driver_traits() { + return driver_traits_; +} + +Trait Fl::Pen::pen_traits(int pen_id) { + auto it = trait_list_.find(pen_id); + if (pen_id == 0) + return current_pen_trait_; + if (it == trait_list_.end()) { + return Trait::DRIVER_AVAILABLE; + } else { + return it->second; + } +} + +void Fl::Pen::subscribe(Fl_Widget* widget) { + if (widget == nullptr) return; + subscriber_list_.add(widget); +} + +void Fl::Pen::unsubscribe(Fl_Widget* widget) { + if (widget == nullptr) return; + subscriber_list_.remove(widget); +} + +void Fl::Pen::release() { + pushed_ = nullptr; + below_pen_ = nullptr; +} + +double Fl::Pen::event_x() { return e.x; } + +double Fl::Pen::event_y() { return e.y; } + +double Fl::Pen::event_x_root() { return e.rx; } + +double Fl::Pen::event_y_root() { return e.ry; } + +int Fl::Pen::event_pen_id() { return e.pen_id; } + +double Fl::Pen::event_pressure() { return e.pressure; } + +double Fl::Pen::event_barrel_pressure() { return e.barrel_pressure; } + +double Fl::Pen::event_tilt_x() { return e.tilt_x; } + +double Fl::Pen::event_tilt_y() { return e.tilt_y; } + +double Fl::Pen::event_twist() { return e.twist; } + +// Not supported in AppKit NSEvent +double Fl::Pen::event_proximity() { return 0.0; } + +State Fl::Pen::event_state() { return e.state; } + +State Fl::Pen::event_trigger() { return e.trigger; } + +/** + Copy the event state. + */ +static void copy_state() { + Fl::Pen::State tr = (Fl::Pen::State)((uint32_t)Fl::Pen::e.state ^ (uint32_t)ev.state); + Fl::Pen::e = ev; + Fl::Pen::e.trigger = tr; + Fl::e_x = (int)ev.x; + Fl::e_y = (int)ev.y; + Fl::e_x_root = (int)ev.rx; + Fl::e_y_root = (int)ev.ry; +} + +/** + Offset coordinates for subwindows and subsubwindows. + */ +static void offset_subwindow_event(Fl_Widget *w, double &x, double &y) { + Fl_Widget *p = w, *q; + while (p) { + q = p->parent(); + if (p->as_window() && q) { + x -= p->x(); + y -= p->y(); + } + p = q; + }; +} + +/* + Check if coordinates are within the widget box. + Coordinates are in top_window space. We iterate up the hierarchy to ensure + that we handle subwindows correctly. + */ +static bool event_inside(Fl_Widget *w, double x, double y) { + offset_subwindow_event(w, x, y); + if (w->as_window()) { + return ((x >= 0) && (y >= 0) && (x < w->w()) && (y < w->h())); + } else { + return ((x >= w->x()) && (y >= w->y()) && (x < w->x() + w->w()) && (y < w->y() + w->h())); + } +} + +/* + Find the widget under the pen event. + Search the subscriber list for widgets that are inside the same top window, + are visible, and are within the give coordinates. Subwindow aware. + */ +static Fl_Widget *find_below_pen(Fl_Window *win, double x, double y) { + for (auto &sub: subscriber_list_) { + Fl_Widget *candidate = sub.second->widget(); + if (candidate && (candidate->top_window() == win)) { + if (candidate->visible() && event_inside(candidate, x, y)) { + return candidate; + } + } + } + return nullptr; +} + +/* + Send the current event and event data to a widget. + Note: we will get the wrong coordinates if the widget is not a child of + the current event window (LEAVE events between windows). + */ +static int pen_send(Fl_Widget *w, int event, State trigger, bool &copied) { + // Copy most event data only once + if (!copied) { + copy_state(); + copied = true; + } + // Copy the top_window coordinates again as they may change when w changes + e.x = ev.x; + e.y = ev.y; + offset_subwindow_event(w, e.x, e.y); + Fl::e_x = e.x; + Fl::e_y = e.y; + // Send the event. + e.trigger = trigger; + return w->handle(event); +} + +/* + Send an event to all subscribers. + */ +static int pen_send_all(int event, State trigger) { + bool copied = false; + // use local value because handler may still change ev values + for (auto &it: subscriber_list_) { + auto w = it.second->widget(); + if (w) + pen_send(w, event, trigger, copied); + } +} + +/* + Convert the NSEvent button number to Fl::Pen::State, + */ +static State button_to_trigger(NSInteger button, bool down) +{ + switch (button) { + case 0: + if ( (ev.state & (State::ERASER_DOWN | State::ERASER_HOVERS)) != State::NONE ) { + return down ? State::ERASER_DOWN : State::ERASER_HOVERS; + } else { + return down ? State::TIP_DOWN : State::TIP_HOVERS; + } + case 1: return State::BUTTON0; + case 2: return State::BUTTON1; + case 3: return State::BUTTON2; + case 4: return State::BUTTON3; + default: return State::NONE; + } +} + +/* + Handle events coming from Cocoa. + TODO: clickCount: store in Fl::event_clicks() + capabilityMask is useless, because it is vendor defined + If a modal window is open, AppKit will send window specific events only there. + */ +bool fl_cocoa_tablet_handler(NSEvent *event, Fl_Window *eventWindow) +{ + // Quick access to the main type. + auto type = [event type]; + + // There seems nothing useful here. Ignore for now. + if ((type == NSEventTypeMouseEntered) || (type == NSEventTypeMouseExited)) { + return false; + } + + // Sort out tablet-only events and mouse plus tablet events. + bool is_mouse = ((type != NSEventTypeTabletPoint) && (type != NSEventTypeTabletProximity)); + + // Set the subtype if one is available. Only NSEventSubtypeTabletPoint and + // NSEventSubtypeTabletProximity matter in this context + NSEventSubtype subtype = is_mouse ? [event subtype] : NSEventSubtypeMouseEvent; + + // Is this a change in proximity event? + bool is_proximity = ((type == NSEventTypeTabletProximity) || (subtype == NSEventSubtypeTabletProximity)); + + // Is this a pen pointer event? + bool is_point = ((type == NSEventTypeTabletPoint) || (subtype == NSEventSubtypeTabletPoint)); + + // Check if any of the pen down, move, drag, or up events was triggered. + bool is_down = ((type == NSEventTypeLeftMouseDown) || (type == NSEventTypeRightMouseDown) || (type == NSEventTypeOtherMouseDown)); + bool is_up = ((type == NSEventTypeLeftMouseUp) || (type == NSEventTypeRightMouseUp) || (type == NSEventTypeOtherMouseUp)); + bool is_drag = ((type == NSEventTypeLeftMouseDragged) || (type == NSEventTypeRightMouseDragged) || (type == NSEventTypeOtherMouseDragged)); + bool is_motion = is_drag || (type == NSEventTypeMouseMoved); + + // Find out if we can get the pen position + bool has_position = (eventWindow != nullptr) && (is_up || is_down || is_motion || is_proximity || is_point); + + // Event has extended pen data set: + if (has_position) { + // Get the position data. + auto pt = [event locationInWindow]; + double s = Fl::screen_driver()->scale(0); + ev.x = pt.x/s; + ev.y = eventWindow->h() - pt.y/s; + ev.rx = ev.x + eventWindow->x(); + ev.ry = ev.y + eventWindow->y(); + if (!is_proximity) { + // Get the pressure data. + ev.pressure = [event pressure]; + ev.barrel_pressure = [event tangentialPressure]; + // Get the tilt + auto tilt = [event tilt]; + ev.tilt_x = -tilt.x; + ev.tilt_y = tilt.y; + // Other stuff + ev.twist = [event rotation]; // TODO: untested + // ev.proximity = [event proximity]; // not supported in AppKit + } + if (device_type_ == NSPointingDeviceTypeEraser) { + if ([event buttonMask] & 1) + ev.state = State::ERASER_DOWN; + else + ev.state = State::ERASER_HOVERS; + } else { + if ([event buttonMask] & 1) + ev.state = State::TIP_DOWN; + else + ev.state = State::TIP_HOVERS; + } + if ([event buttonMask] & 0x0002) ev.state |= State::BUTTON0; + if ([event buttonMask] & 0x0004) ev.state |= State::BUTTON1; + if ([event buttonMask] & 0x0008) ev.state |= State::BUTTON2; + if ([event buttonMask] & 0x0010) ev.state |= State::BUTTON3; + // printf("0x%08x\n", [event buttonMask]); + } + if (is_proximity) { + ev.pen_id = (int)[event vendorID]; + device_type_ = [event pointingDeviceType]; + } + if (type == NSEventTypeTabletProximity) { + if ([event isEnteringProximity]) { + // Check if this is the first time we see this pen, or if the pen changed + if (current_pen_id_ != ev.pen_id) { + current_pen_id_ = ev.pen_id; + auto it = trait_list_.find(current_pen_id_); + if (it == trait_list_.end()) { // not found, create a new entry + trait_list_[current_pen_id_] = Trait::DRIVER_AVAILABLE; + trait_countdown_ = 5; + pen_send_all(Fl::Pen::DETECTED, State::NONE); + // printf("IN RANGE, NEW PEN\n"); + } else { + pen_send_all(Fl::Pen::CHANGED, State::NONE); + // printf("IN RANGE, CHANGED PEN\n"); + } + trait_list_[0] = trait_list_[current_pen_id_]; // set current pen traits + } else { + pen_send_all(Fl::Pen::IN_RANGE, State::NONE); + // printf("IN RANGE\n"); + } + } else { + pen_send_all(Fl::Pen::OUT_OF_RANGE, State::NONE); + // printf("OUT OF RANGE\n"); + } + } + + Fl_Widget *receiver = nullptr; + bool pushed = false; + bool event_data_copied = false; + + if (has_position) { + if (trait_countdown_) { + trait_countdown_--; + if (ev.tilt_x != 0.0) current_pen_trait_ |= Trait::TILT_X; + if (ev.tilt_y != 0.0) current_pen_trait_ |= Trait::TILT_Y; + if (ev.pressure != 1.0) current_pen_trait_ |= Trait::PRESSURE; + if (ev.barrel_pressure != 0.0) current_pen_trait_ |= Trait::BARREL_PRESSURE; + if (ev.pen_id != 0) current_pen_trait_ |= Trait::PEN_ID; + if (ev.twist != 0.0) current_pen_trait_ |= Trait::TWIST; + //if (ev.proximity != 0) current_pen_trait_ |= Trait::PROXIMITY; + trait_list_[current_pen_id_] = current_pen_trait_; + } + fl_xmousewin = eventWindow; + if (pushed_ && pushed_->widget() && (Fl::pushed() == pushed_->widget())) { + receiver = pushed_->widget(); + if (Fl::grab() && (Fl::grab() != receiver->top_window())) + return 0; + if (Fl::modal() && (Fl::modal() != receiver->top_window())) + return 0; + pushed = true; + } else { + if (Fl::grab() && (Fl::grab() != eventWindow)) + return 0; + if (Fl::modal() && (Fl::modal() != eventWindow)) + return 0; + auto bpen = below_pen_ ? below_pen_->widget() : nullptr; + auto bmouse = Fl::belowmouse(); + auto bpen_old = bmouse && (bmouse == bpen) ? bpen : nullptr; + auto bpen_now = find_below_pen(eventWindow, ev.x, ev.y); + + if (bpen_now != bpen_old) { + if (bpen_old) { + pen_send(bpen_old, Fl::Pen::LEAVE, State::NONE, event_data_copied); + } + below_pen_ = nullptr; + if (bpen_now) { + State state = (device_type_ == NSPointingDeviceTypeEraser) ? State::ERASER_HOVERS : State::TIP_HOVERS; + if (pen_send(bpen_now, Fl::Pen::ENTER, state, event_data_copied)) { + below_pen_ = subscriber_list_[bpen_now]; + Fl::belowmouse(bpen_now); + } + } + } + + receiver = below_pen_ ? below_pen_->widget() : nullptr; + if (!receiver) + return 0; + } + } else { + // Anything to do here? + } + + if (!receiver) + return 0; + + int ret = 0; + if (is_down) { + if (!pushed) { + pushed_ = subscriber_list_[receiver]; + Fl::pushed(receiver); + } + State trigger = button_to_trigger([event buttonNumber], true); + if ([event buttonNumber] == 0) { + Fl::e_is_click = 1; + Fl::e_x_down = (int)ev.x; + Fl::e_y_down = (int)ev.y; + if ([event clickCount] > 1) + Fl::e_clicks++; + else + Fl::e_clicks = 0; + ret = pen_send(receiver, Fl::Pen::TOUCH, trigger, event_data_copied); + } else { + ret = pen_send(receiver, Fl::Pen::BUTTON_PUSH, trigger, event_data_copied); + } + } else if (is_up) { + if ( (ev.state & State::ANY_DOWN) == State::NONE ) { + Fl::pushed(nullptr); + pushed_ = nullptr; + } + State trigger = button_to_trigger([event buttonNumber], true); + if ([event buttonNumber] == 0) + ret = pen_send(receiver, Fl::Pen::LIFT, trigger, event_data_copied); + else + ret = pen_send(receiver, Fl::Pen::BUTTON_RELEASE, trigger, event_data_copied); + } else if (is_motion) { + if ( Fl::e_is_click && + ( (fabs((int)ev.x - Fl::e_x_down) > 5) || + (fabs((int)ev.y - Fl::e_y_down) > 5) ) ) + Fl::e_is_click = 0; + if (pushed) { + ret = pen_send(receiver, Fl::Pen::DRAW, State::NONE, event_data_copied); + } else { + ret = pen_send(receiver, Fl::Pen::HOVER, State::NONE, event_data_copied); + } + } + // Always return 1 because at this point, we capture pen events and don't + // want mouse events anymore! + return 1; +} diff --git a/src/drivers/Stubs/Fl_Stubs_Pen_Events.cxx b/src/drivers/Stubs/Fl_Stubs_Pen_Events.cxx new file mode 100644 index 000000000..6b3512023 --- /dev/null +++ b/src/drivers/Stubs/Fl_Stubs_Pen_Events.cxx @@ -0,0 +1,73 @@ +// +// Definition of default Pen/Tablet event driver. +// +// Copyright 2025 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + + +#include <config.h> +#include <FL/platform.H> +#include <FL/core/pen_events.H> +#include <FL/Fl.H> + +class Fl_Widget; + +namespace Fl { + +namespace Pen { + +// double e_pressure_; + +} // namespace Pen + +} // namespace Fl + + +using namespace Fl::Pen; + + +Trait Fl::Pen::driver_traits() { return Trait::NONE; } + +Trait Fl::Pen::pen_traits(int pen_id) { return Trait::NONE; } + +void Fl::Pen::subscribe(Fl_Widget* widget) { } + +void Fl::Pen::unsubscribe(Fl_Widget* widget) { } + +void Fl::Pen::release() { } + +double Fl::Pen::event_x() { return 0.0; } + +double Fl::Pen::event_y() { return 0.0; } + +double Fl::Pen::event_x_root() { return 0.0; } + +double Fl::Pen::event_y_root() { return 0.0; } + +int Fl::Pen::event_pen_id() { return 0; } + +double Fl::Pen::event_pressure() { return 1.0; } + +double Fl::Pen::event_barrel_pressure() { return 0.0; } + +double Fl::Pen::event_tilt_x() { return 0.0; } + +double Fl::Pen::event_tilt_y() { return 0.0; } + +double Fl::Pen::event_twist() { return 0.0; } + +double Fl::Pen::event_proximity() { return 0.0; } + +State Fl::Pen::event_state() { return Fl::Pen::State::NONE; } + +State Fl::Pen::event_trigger() { return Fl::Pen::State::NONE; } diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 261ff969b..81d0c42fb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -183,6 +183,7 @@ fl_create_example(navigation navigation.cxx fltk::fltk) fl_create_example(output output.cxx fltk::fltk) fl_create_example(overlay overlay.cxx fltk::fltk) fl_create_example(pack pack.cxx fltk::fltk) +fl_create_example(penpal penpal.cxx fltk::fltk) fl_create_example(pixmap pixmap.cxx fltk::images) fl_create_example(pixmap_browser pixmap_browser.cxx fltk::images) fl_create_example(preferences preferences.fl fltk::fltk) diff --git a/test/penpal.cxx b/test/penpal.cxx new file mode 100644 index 000000000..4ace5cbe9 --- /dev/null +++ b/test/penpal.cxx @@ -0,0 +1,314 @@ +// +// Penpal pen/stylus/tablet test program for the Fast Light Tool Kit (FLTK). +// +// Copyright 2025 by Bill Spitzak and others. +// +// This library is free software. Distribution and use rights are outlined in +// the file "COPYING" which should have been included with this file. If this +// file is missing or damaged, see the license at: +// +// https://www.fltk.org/COPYING.php +// +// Please see the following page on how to report bugs and issues: +// +// https://www.fltk.org/bugs.php +// + +// The Penpal test app is here to test pen/stylus/tablet event distribution +// in the Fl::Pen driver. Our main window has three canvases for drawing. +// The first canvas is a child of the main window. The second canvas is +// inside a group. The third canvas is a subwindow inside the main window. +// A second application window is itself yet another canvas. + +// We can test if the events are delivered to the right receiver, if the +// mouse and pen offsets are correct. The pen implementation also reacts +// to pen pressure and angles. If handle() returns 1 when receiving +// Fl::Pen::ENTER, the event handler should not send any mouse events until +// Fl::Pen::LEAVE. + +#include <FL/Fl.H> +#include <FL/Fl_Window.H> +#include <FL/Fl_Box.H> +#include <FL/Fl_Menu_Item.H> +#include <FL/platform.H> +#include <FL/fl_draw.H> +#include <FL/fl_message.H> +#include <FL/names.h> + +extern Fl_Menu_Item app_menu[]; +extern int popup_app_menu(); +Fl_Widget *cv1 { nullptr }; +Fl_Window *cvwin { nullptr }; + +// +// The canvas interface implements incremental drawing and handles draw events. +// It also implement pressure sensitive drawing with a pen or stylus. +// And it implements an overlay plane that visualizes pen event data. +// +class CanvasInterface { + Fl_Widget *widget_ { nullptr }; + bool in_window_ { false }; + bool first_draw_ { true }; + Fl_Offscreen offscreen_ { 0 }; + Fl_Color color_ { 1 }; + enum { NONE, HOVER, DRAW, PEN_HOVER, PEN_DRAW } overlay_ { NONE }; + int ov_x_ { 0 }; + int ov_y_ { 0 }; +public: + CanvasInterface(Fl_Widget *w) : widget_(w) { } + CanvasInterface(Fl_Window *w) : widget_(w), in_window_(true) { } + ~CanvasInterface() { + if (offscreen_) fl_delete_offscreen(offscreen_); + } + int cv_handle(int event); + void cv_draw(); + void cv_paint(); + void cv_pen_paint(); +}; + + +// +// Handle mouse and pen events. +// +int CanvasInterface::cv_handle(int event) +{ + switch (event) + { + // Event handling for pen events: + case Fl::Pen::ENTER: // Return 1 to receive all pen events and suppress mouse events + // Pen entered the widget area. + color_++; + if (color_ > 6) color_ = 1; + /* fall through */ + case Fl::Pen::HOVER: + // Pen move over the surface without touching it. + overlay_ = PEN_HOVER; + ov_x_ = Fl::event_x(); + ov_y_ = Fl::event_y(); + widget_->redraw(); + return 1; + case Fl::Pen::TOUCH: + // Pen tip or eraser just touched the surface. + if (Fl::event_state(FL_CTRL) || Fl::Pen::event_state(Fl::Pen::State::BUTTON0)) + return popup_app_menu(); + /* fall through */ + case Fl::Pen::DRAW: + // Pen is dragged over the surface, or hovers with a button pressed. + overlay_ = PEN_DRAW; + ov_x_ = Fl::event_x(); + ov_y_ = Fl::event_y(); + cv_pen_paint(); + widget_->redraw(); + return 1; + case Fl::Pen::LIFT: + // Pen was just lifted from the surface and is now hovering + return 1; + case Fl::Pen::LEAVE: + // The pen left the drawing area. + overlay_ = NONE; + widget_->redraw(); + return 1; + + // Event handling for mouse events: + case FL_ENTER: + color_++; + if (color_ > 6) color_ = 1; + /* fall through */ + case FL_MOVE: + overlay_ = HOVER; + ov_x_ = Fl::event_x(); + ov_y_ = Fl::event_y(); + widget_->redraw(); + return 1; + case FL_PUSH: + if (Fl::event_state(FL_CTRL) || Fl::event_button() == FL_RIGHT_MOUSE) + return popup_app_menu(); + /* fall through */ + case FL_DRAG: + overlay_ = DRAW; + ov_x_ = Fl::event_x(); + ov_y_ = Fl::event_y(); + cv_paint(); + widget_->redraw(); + return 1; + case FL_RELEASE: + return 1; + case FL_LEAVE: + overlay_ = NONE; + widget_->redraw(); + return 1; + } + return 0; +} + +// +// Canvas drawing copies the offscreen bitmap and then draws the overlays. +// +void CanvasInterface::cv_draw() +{ + if (first_draw_) { + first_draw_ = false; + offscreen_ = fl_create_offscreen(widget_->w(), widget_->h()); + fl_begin_offscreen(offscreen_); + fl_color(FL_WHITE); + fl_rectf(0, 0, widget_->w(), widget_->h()); + fl_end_offscreen(); + } + int dx = in_window_ ? 0 : widget_->x(), dy = in_window_ ? 0 : widget_->y(); + fl_copy_offscreen(dx, dy, widget_->w(), widget_->h(), offscreen_, 0, 0); + + // Preset values for overlay + int r = 10; + if (overlay_ == PEN_DRAW) + r = static_cast<int>(32.0 * Fl::Pen::event_pressure()); + fl_color(FL_BLACK); + switch (overlay_) { + case NONE: break; + case PEN_HOVER: + fl_color(FL_RED); + /* fall through */ + case HOVER: + fl_xyline(ov_x_-10, ov_y_, ov_x_+10); + fl_yxline(ov_x_, ov_y_-10, ov_y_+10); + break; + case PEN_DRAW: + fl_color(FL_RED); + /* fall through */ + case DRAW: + fl_arc(ov_x_-r, ov_y_-r, 2*r, 2*r, 0, 360); + fl_arc(ov_x_-r/2-40*Fl::Pen::event_tilt_x(), + ov_y_-r/2-40*Fl::Pen::event_tilt_y(), r, r, 0, 360); + break; + } +} + +// +// Paint a circle with mouse events. +// +void CanvasInterface::cv_paint() { + if (!offscreen_) + return; + int dx = in_window_ ? 0 : widget_->x(), dy = in_window_ ? 0 : widget_->y(); + fl_begin_offscreen(offscreen_); + fl_draw_circle(Fl::event_x()-dx-12, Fl::event_y()-dy-12, 24, color_); + fl_end_offscreen(); +} + +// +// Paint a circle with pen events. If the eraser is touching the surface, +// draw a white circle. +// +void CanvasInterface::cv_pen_paint() { + if (!offscreen_) + return; + int r = static_cast<int>(32.0 * (Fl::Pen::event_pressure()*Fl::Pen::event_pressure())); + int dx = in_window_ ? 0 : widget_->x(), dy = in_window_ ? 0 : widget_->y(); + Fl_Color cc = Fl::Pen::event_state(Fl::Pen::State::ERASER_DOWN) ? FL_WHITE : color_; + fl_begin_offscreen(offscreen_); + fl_draw_circle(Fl::event_x()-dx-r, Fl::event_y()-dy-r, 2*r, cc); + fl_end_offscreen(); +} + + +// +// A drawing canvas, based on a minimal widget. +// +class CanvasWidget : public Fl_Widget, CanvasInterface { +public: + CanvasWidget(int x, int y, int w, int h, const char *l=nullptr) + : Fl_Widget(x, y, w, h, l), CanvasInterface(this) { } + ~CanvasWidget() override { } + int handle(int event) override { + auto ret = cv_handle(event); + return ret ? ret : Fl_Widget::handle(event); + } + void draw() override { return cv_draw(); } +}; + +// +// A drawing canvas based on a window. Can be used as a standalone window +// and also as a subwindow inside another window. +// +class CanvasWindow : public Fl_Window, CanvasInterface { +public: + CanvasWindow(int x, int y, int w, int h, const char *l=nullptr) + : Fl_Window(x, y, w, h, l), CanvasInterface(this) { } + ~CanvasWindow() override { } + int handle(int event) override { + auto ret = cv_handle(event); + return ret ? ret : Fl_Window::handle(event); + } + void draw() override { return cv_draw(); } +}; + +// A popup menu with a few test tasks. +Fl_Menu_Item app_menu[] = { + { "with modal window", 0, [](Fl_Widget*, void*) { + fl_message("None of the canvas areas should receive\n" + "pen events while this window is open."); + } }, + { "with non-modal window", 0, [](Fl_Widget*, void*) { + auto w = new Fl_Window(400, 32, "Toolbox"); + w->set_non_modal(); + w->show(); + } }, + { "unsubscribe middle canvas", 0, [](Fl_Widget*, void*) { + if (cv1) Fl::Pen::unsubscribe(cv1); + } }, + { "resubscribe middle canvas", 0, [](Fl_Widget*, void*) { + if (cv1) Fl::Pen::subscribe(cv1); + } }, + { "delete middle canvas", 0, [](Fl_Widget*, void*) { + if (cv1) { cv1->top_window()->redraw(); delete cv1; cv1 = nullptr; } + } }, + { nullptr } +}; + +// +// Show the menu and run the callback. +// +int popup_app_menu() { + auto mi = app_menu->popup(Fl::event_x(), Fl::event_y(), "Tests"); + if (mi) mi->do_callback((Fl_Widget*)mi); + return 1; +} + +// +// Main app entry point +// +int main(int argc, char **argv) +{ + // Create our main app window + auto window = new Fl_Window(100, 100, 640, 220, "FLTK Pen/Stylus/Tablet test, Ctrl-Tap for menu"); + + // One testing canvas is just a regular child widget of the window + auto canvas_widget_0 = new CanvasWidget( 10, 10, 200, 200, "CV0"); + + // The second canvas is inside a group + auto cv1_group = new Fl_Group(215, 5, 210, 210); + cv1_group->box(FL_FRAME_BOX); + auto canvas_widget_1 = cv1 = new CanvasWidget(220, 10, 200, 200, "CV1"); + cv1_group->end(); + + // The third canvas is a window inside a window, so we can verify + // that pen coordinates are calculated correctly. + auto canvas_widget_2 = new CanvasWindow(430, 10, 200, 200, "CV2"); + canvas_widget_2->end(); + + window->end(); + + // A fourth canvas is a top level window by itself. + auto cv_window = cvwin = new CanvasWindow(100, 380, 200, 200, "Canvas Window"); + + // All canvases subscribe to pen events. + Fl::Pen::subscribe(canvas_widget_0); + Fl::Pen::subscribe(canvas_widget_1); + Fl::Pen::subscribe(canvas_widget_2); + Fl::Pen::subscribe(cv_window); + + window->show(argc, argv); + canvas_widget_2->show(); + cv_window->show(); + + return Fl::run(); +} |
