diff --git a/FL/core/pen_events.H b/FL/core/pen_events.H index 6e873c4d1..9ed4ab4c8 100644 --- a/FL/core/pen_events.H +++ b/FL/core/pen_events.H @@ -44,12 +44,12 @@ namespace Pen { 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. - By returning 1 to pen events, the user can ask for more detailed events. + 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 form the event handler indicates that the event was not handled. - FLTK may convert the tablet event into a mouse event and resend it to - the relevant widget. + Returning 0 Fl::Pen::ENTER tells FLTK to supress further pen events until + Fl::Pen::LEAVE, and convert them into mouse events instead. @{ */ @@ -59,6 +59,8 @@ namespace Pen { 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 @@ -67,7 +69,7 @@ enum class Trait : uint32_t { /// 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 - DEVICE_ID = 0x0008, + PEN_ID = 0x0008, /// Pen may have an eraser tip ERASER = 0x0010, /// Pen returns a pressure value @@ -143,7 +145,7 @@ enum class State : uint32_t { 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 @@ -153,7 +155,7 @@ inline constexpr State operator|(State lhs, State rhs) { return static_cast(static_cast(lhs) | static_cast(rhs)); } -/* +/** \brief Bitwise AND operator for State enum. \param lhs Left-hand side state flags \param rhs Right-hand side state flags @@ -163,7 +165,7 @@ inline constexpr State operator&(State lhs, State rhs) { return static_cast(static_cast(lhs) & static_cast(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 @@ -182,37 +184,47 @@ inline State& operator|=(State& lhs, State rhs) { */ enum Event { /** - A pen was detected for the first time for this subscriber. + 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. - - Return 1 to receive ENTER and LEAVE events. */ 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. - Return 1 to receive MOVE events. */ ENTER, /** - \brief Pen left the widget area. - No event values are set. + 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. - Return 1 to receive DRAW events. + event_trigger() returns TIP_DOWN or ERASER_DOWN. */ TOUCH, @@ -222,10 +234,10 @@ enum Event { */ LIFT, - /** Pen moved without touching the surface. */ + /** Pen moved without touching the surface and no button is pressed. */ HOVER, - /** Pen moved while touching the surface. */ + /** Pen moved while touching the surface, or any button is pressed. */ DRAW, /** @@ -258,17 +270,38 @@ enum Event { */ 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] window must be a mapped (shown, visible) window + \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(Fl_Window *window); +FL_EXPORT extern Trait pen_traits(int pen_id = 0); /** - \brief Receive pen events when the pen is inside this widget, or all events. + \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 @@ -276,7 +309,7 @@ FL_EXPORT extern Trait pen_traits(Fl_Window *window); FL_EXPORT extern void subscribe(Fl_Widget* widget); /** - \brief Stop receiving pen events for this widget. + \brief Stop receiving Pen::ENTER for this widget. Deleting a widget will automatically unsubscribe it. \param widget Widget to unsubscribe from pen events */ @@ -291,7 +324,7 @@ FL_EXPORT extern void unsubscribe(Fl_Widget* widget); FL_EXPORT extern void grab(Fl_Widget* widget); /** - * \brief Release pen event handling after grab(). + \brief Release pen event handling after grab(). */ FL_EXPORT extern void release(); @@ -323,9 +356,9 @@ 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::DEVICE_ID + \see Trait::PEN_ID */ -FL_EXPORT extern int event_id(); +FL_EXPORT extern int event_pen_id(); /** \brief Returns the pressure between the tip or eraser and the surface. @@ -360,39 +393,38 @@ FL_EXPORT extern double 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 + \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 + \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) + \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. - * \return true if any bit is set + \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 + \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(); @@ -429,7 +461,6 @@ FL_EXPORT extern State event_trigger(); SDL3: https://github.com/libsdl-org/SDL/blob/main/include/SDL3/SDL_pen.h https://wiki.libsdl.org/SDL3/CategoryPen - */ diff --git a/src/drivers/Cocoa/Fl_Cocoa_Pen_Events.mm b/src/drivers/Cocoa/Fl_Cocoa_Pen_Events.mm index 3fa2d467c..99dfcc73f 100644 --- a/src/drivers/Cocoa/Fl_Cocoa_Pen_Events.mm +++ b/src/drivers/Cocoa/Fl_Cocoa_Pen_Events.mm @@ -91,6 +91,24 @@ static std::shared_ptr pushed_; static std::shared_ptr 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 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 }; @@ -101,7 +119,7 @@ struct EventData { double pressure { 1.0 }; double barrel_pressure { 0.0 }; double twist { 0.0 }; - int device_id { 0 }; + int pen_id { 0 }; Fl::Pen::State state { (Fl::Pen::State)0 }; Fl::Pen::State trigger { (Fl::Pen::State)0 }; }; @@ -125,14 +143,20 @@ struct EventData e; using namespace Fl::Pen; -// TODO: implement this +// Return a bit for everything that AppKit could return. Trait Fl::Pen::driver_traits() { - return Trait::DRIVER_AVAILABLE /* | and more! */ ; + return driver_traits_; } -// TODO: implement this -Trait Fl::Pen::pen_traits(Fl_Window *window) { - return Trait::DRIVER_AVAILABLE /* | and more as we discover them! */ ; +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) { @@ -157,7 +181,7 @@ double Fl::Pen::event_x_root() { return e.rx; } double Fl::Pen::event_y_root() { return e.ry; } -int Fl::Pen::event_id() { return e.device_id; } +int Fl::Pen::event_pen_id() { return e.pen_id; } double Fl::Pen::event_pressure() { return e.pressure; } @@ -257,6 +281,19 @@ static int pen_send(Fl_Widget *w, int event, State trigger, bool &copied) { 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, */ @@ -281,7 +318,6 @@ static State button_to_trigger(NSInteger button, bool down) Handle events coming from Cocoa. TODO: clickCount: store in Fl::event_clicks() capabilityMask is useless, because it is vendor defined - TODO: enteringProximity: TabletProximityType and Subtype */ bool fl_cocoa_tablet_handler(NSEvent *event, Fl_Window *eventWindow) { @@ -325,11 +361,7 @@ bool fl_cocoa_tablet_handler(NSEvent *event, Fl_Window *eventWindow) // TODO: verify actual values: root coordinates may be used for popup windows ev.rx = ev.x*s + eventWindow->x(); ev.ry = ev.y*s + eventWindow->y(); - if (is_proximity) { - // untested: use pointingDeviceID or pointingDeviceSerialNumber instead? - ev.device_id = (int)[event vendorID]; - device_type_ = [event pointingDeviceType]; - } else { + if (!is_proximity) { // Get the pressure data. ev.pressure = [event pressure]; ev.barrel_pressure = [event tangentialPressure]; @@ -339,6 +371,7 @@ bool fl_cocoa_tablet_handler(NSEvent *event, Fl_Window *eventWindow) 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) @@ -357,12 +390,52 @@ bool fl_cocoa_tablet_handler(NSEvent *event, Fl_Window *eventWindow) 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(); @@ -381,7 +454,8 @@ bool fl_cocoa_tablet_handler(NSEvent *event, Fl_Window *eventWindow) } below_pen_ = nullptr; if (bpen_now) { - if (pen_send(bpen_now, Fl::Pen::ENTER, State::NONE, event_data_copied)) { + 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); } @@ -393,7 +467,7 @@ bool fl_cocoa_tablet_handler(NSEvent *event, Fl_Window *eventWindow) return 0; } } else { - // TODO: handle non-position events (proximity) + // Anything to do here? } if (!receiver) diff --git a/src/drivers/Stubs/Fl_Stubs_Pen_Events.cxx b/src/drivers/Stubs/Fl_Stubs_Pen_Events.cxx index d72910818..0259f386c 100644 --- a/src/drivers/Stubs/Fl_Stubs_Pen_Events.cxx +++ b/src/drivers/Stubs/Fl_Stubs_Pen_Events.cxx @@ -36,9 +36,9 @@ namespace Pen { using namespace Fl::Pen; -Trait Fl::Pen::driver_traits() { return static_cast(0); } +Trait Fl::Pen::driver_traits() { return Trait::NONE; } -Trait Fl::Pen::pen_traits(Fl_Window *window) { return static_cast(0); } +Trait Fl::Pen::pen_traits(int pen_id) { return Trait::NONE; } void Fl::Pen::subscribe(Fl_Widget* widget) { } @@ -56,7 +56,7 @@ double Fl::Pen::event_x_root() { return 0.0; } double Fl::Pen::event_y_root() { return 0.0; } -int Fl::Pen::event_id() { return 0; } +int Fl::Pen::event_pen_id() { return 0; } double Fl::Pen::event_pressure() { return 1.0; } diff --git a/test/penpal.cxx b/test/penpal.cxx index fe4289e11..55135df1b 100644 --- a/test/penpal.cxx +++ b/test/penpal.cxx @@ -33,6 +33,11 @@ #include #include +// +// 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 }; @@ -54,31 +59,45 @@ public: 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: + 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: /* fall through */ + case Fl::Pen::TOUCH: + // Pen tip or eraser just touched the surface. + /* 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: return 1; - case Fl::Pen::LEAVE: overlay_ = NONE; 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: @@ -91,7 +110,8 @@ int CanvasInterface::cv_handle(int event) ov_y_ = Fl::event_y(); widget_->redraw(); return 1; - case FL_PUSH: /* fall through */ + case FL_PUSH: + /* fall through */ case FL_DRAG: overlay_ = DRAW; ov_x_ = Fl::event_x(); @@ -99,13 +119,21 @@ int CanvasInterface::cv_handle(int event) cv_paint(); widget_->redraw(); return 1; - case FL_RELEASE: return 1; - case FL_LEAVE: overlay_ = NONE; widget_->redraw(); return 1; + case FL_RELEASE: + return 1; + case FL_LEAVE: + overlay_ = NONE; + widget_->redraw(); + return 1; } return 0; } -void CanvasInterface::cv_draw() { +// +// 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()); @@ -138,11 +166,14 @@ void CanvasInterface::cv_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); - printf("%d\n", Fl::Pen::event_id()); + // printf("%d\n", Fl::Pen::event_pen_id()); break; } } +// +// Paint a circle with mouse events. +// void CanvasInterface::cv_paint() { if (!offscreen_) return; @@ -152,6 +183,10 @@ void CanvasInterface::cv_paint() { 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; @@ -164,6 +199,9 @@ void CanvasInterface::cv_pen_paint() { } +// +// 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) @@ -176,6 +214,10 @@ public: 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) @@ -188,23 +230,34 @@ public: void draw() override { return cv_draw(); } }; - +// +// Main app entry point +// int main(int argc, char **argv) { + // Create our main app window auto window = new Fl_Window(100, 100, 640, 220); + // One testing canvas is just a regular child widget of the window auto canvas_widget_0 = new CanvasWidget( 10, 10, 200, 200, "CV1"); + + // 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 = new CanvasWidget(220, 10, 200, 200, "CV2"); 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, "CV3"); canvas_widget_2->end(); window->end(); + // A fourth canvas is a top level window by itself. auto cv_window = new CanvasWindow(100, 380, 200, 200, "CV 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);