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.
This commit is contained in:
Matthias Melcher
2025-11-17 21:10:01 +01:00
committed by GitHub
parent d623ad08a9
commit fa65cd6321
13 changed files with 1514 additions and 43 deletions

View File

@@ -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

View File

@@ -76,8 +76,19 @@ 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; }
};

View File

@@ -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

465
FL/core/pen_events.H Normal file
View File

@@ -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

View File

@@ -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.

View File

@@ -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
)

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -27,5 +27,6 @@
void Fl::grab(Fl_Window *win)
{
Fl::Pen::release();
screen_driver()->grab(win);
}

File diff suppressed because it is too large Load Diff

View File

@@ -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; }

View File

@@ -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)

314
test/penpal.cxx Normal file
View File

@@ -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();
}