summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt10
-rw-r--r--src/Fl_Menu.cxx3
-rw-r--r--src/Fl_cocoa.mm58
-rw-r--r--src/Fl_grab.cxx1
-rw-r--r--src/drivers/Cocoa/Fl_Cocoa_Pen_Events.mm530
-rw-r--r--src/drivers/Stubs/Fl_Stubs_Pen_Events.cxx73
6 files changed, 670 insertions, 5 deletions
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; }