Improved, yet compatible, widget callback system using macros (#729)

* adds FL/fl_callback.macros.H
* adds FL_FUNCTION_CALLBACK_n(widget, function, [type, data])
* adds FL_METHOD_CALLBACK_n(widget, class, instance, method, [type, data])
* adds FL_INLINE_CALLBACK_n(widget, [type, name, data], callback_body)
* adds `examples/callback`
* full documentation
This commit is contained in:
Matthias Melcher
2023-08-15 11:36:58 +02:00
committed by GitHub
parent e6440ca0a8
commit 10d9010ed9
9 changed files with 969 additions and 47 deletions

View File

@@ -121,6 +121,8 @@ Changes in FLTK 1.4.0 Released: ??? ?? 2022
- Added Fl_Surface_Device::push_current(new_surface) and
Fl_Surface_Device::pop_current() to set/unset the current surface
receiving graphics commands.
- New macros for easy function and method callbacks with multiple
type safe arguments (see FL_METHOD_CALLBACK_1 etc.) .
New Configuration Options (ABI Version)

View File

@@ -70,6 +70,24 @@ struct FL_EXPORT Fl_Label {
};
/** A class prototype that allows for additional data in callbacks.
Users can extend this class and pass it to widget callbacks. Widgets can
take ownership of the callback data, deleting the data when the widget
itself is deleted.
The destructor of this class is virtual, allowing for additional code to
deallocate resources when the user data is deleted.
\see FL_FUNCTION_CALLBACK, FL_METHOD_CALLBACK, FL_INLINE_CALLBACK
\see Fl_Widget::callback(Fl_Callback*, Fl_Callback_User_Data*, bool)
\see Fl_Widget::user_data(Fl_Callback_User_Data*, bool)
*/
class Fl_Callback_User_Data {
public:
virtual ~Fl_Callback_User_Data() { }
};
/** Fl_Widget is the base class for all widgets in FLTK.
@@ -152,7 +170,7 @@ protected:
CLIP_CHILDREN = 1<<11, ///< all drawing within this widget will be clipped (Fl_Group)
MENU_WINDOW = 1<<12, ///< a temporary popup window, dismissed by clicking outside (Fl_Window)
TOOLTIP_WINDOW = 1<<13, ///< a temporary popup, transparent to events, and dismissed easily (Fl_Window)
MODAL = 1<<14, ///< a window blocking input to all other winows (Fl_Window)
MODAL = 1<<14, ///< a window blocking input to all other windows (Fl_Window)
NO_OVERLAY = 1<<15, ///< window not using a hardware overlay plane (Fl_Menu_Window)
GROUP_RELATIVE = 1<<16, ///< Reserved, not implemented. DO NOT USE.
COPIED_TOOLTIP = 1<<17, ///< the widget tooltip is internally copied, its destruction is handled by the widget
@@ -161,6 +179,7 @@ protected:
NEEDS_KEYBOARD = 1<<20, ///< set on touch screen devices if a widget needs a keyboard when it gets the focus. Reserved, not yet used in 1.4.0. \see Fl_Widget::needs_keyboard()
IMAGE_BOUND = 1<<21, ///< binding the image to the widget will transfer ownership, so that the widget will delete the image when it is no longer needed
DEIMAGE_BOUND = 1<<22, ///< bind the inactive image to the widget, so the widget deletes the image when it is no longer needed
AUTO_DELETE_USER_DATA = 1<<23, ///< automatically call `delete` on the user_data pointer when destroying this widget; if set, user_data must point to a class derived from the class Fl_Callback_User_Data
// Note to devs: add new FLTK core flags above this line (up to 1<<28).
@@ -667,12 +686,27 @@ public:
*/
Fl_Callback_p callback() const {return callback_;}
/** Sets the current callback function for the widget.
/** Sets the current callback function and data for the widget.
Each widget has a single callback.
\param[in] cb new callback
\param[in] p user data
*/
void callback(Fl_Callback* cb, void* p) {callback_ = cb; user_data_ = p;}
void callback(Fl_Callback* cb, void* p) {
callback_ = cb;
user_data(p);
}
/** Sets the current callback function and managed user data for the widget.
Setting auto_free will transfer ownership of the callback user data to the
widget. Deleting the widget will then also delete the user data.
\param[in] cb new callback
\param[in] p user data
\param[in] auto_free if set, the widget will free user data when destroyed
*/
void callback(Fl_Callback* cb, Fl_Callback_User_Data* p, bool auto_free) {
callback_ = cb;
user_data(p, auto_free);
}
/** Sets the current callback function for the widget.
Each widget has a single callback.
@@ -695,7 +729,7 @@ public:
*/
void callback(Fl_Callback1* cb, long p = 0) {
callback_ = (Fl_Callback*)(fl_intptr_t)(cb);
user_data_ = (void*)(fl_intptr_t)p;
user_data((void*)(fl_intptr_t)p);
}
/** Gets the user data for this widget.
@@ -704,11 +738,11 @@ public:
*/
void* user_data() const {return user_data_;}
/** Sets the user data for this widget.
Sets the new user data (void *) argument that is passed to the callback function.
\param[in] v new user data
*/
void user_data(void* v) {user_data_ = v;}
/** \brief Sets the user data for this widget. */
void user_data(void* v);
/** \brief Sets the user data for this widget. */
void user_data(Fl_Callback_User_Data* v, bool auto_free);
/** Gets the current user data (long) argument that is passed to the callback function.
@@ -727,7 +761,7 @@ public:
\see argument()
*/
void argument(long v) {user_data_ = (void*)(fl_intptr_t)v;}
void argument(long v) {user_data((void*)(fl_intptr_t)v);}
/** Returns the conditions under which the callback is called.

607
FL/fl_callback_macros.H Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -550,6 +550,72 @@ int xyz_data;
button->callback(xyz_callback, &xyz_data);
\endcode
\note You cannot delete a widget inside a callback, as the
widget may still be accessed by FLTK after your callback
is completed. Instead, use the Fl::delete_widget()
method to mark your widget for deletion when it is safe
to do so.
Many programmers new to FLTK or C++ try to use a
non-static class method instead of a static class method
or function for their callback. Since callbacks are done
outside a C++ class, the `this` pointer is not
initialized for class methods.
To work around this problem, define a static method
in your class that accepts a pointer to the class, and
then have the static method call the class method(s) as
needed. The data pointer you provide to the
\p callback() method of the widget can be a
pointer to the instance of your class.
\code
class Foo {
void my_callback(Fl_Widget *w);
static void my_static_callback(Fl_Widget *w, void *f) { ((Foo *)f)->my_callback(w); }
...
}
...
w->callback(my_static_callback, (void *)this);
\endcode
In an effort to make callbacks easier, more flexible, and type safe, FLTK
provides three groups of macros that generate the code needed to call class
methods directly with up to five custom parameters.
- `FL_FUNCTION_CALLBACK_#(WIDGET, FUNCTION, ...)` creates code for callbacks to
functions and static class methods with up to five arguments. The `#` must
be replaced by the number of callback arguments.
- `FL_METHOD_CALLBACK_#(WIDGET, CLASS, SELF, METH, ...)` creates code for
callbacks to arbitrary public class methods
- `FL_INLINE_CALLBACK_#(WIDGET, ..., FUNCTION_BODY)` creates code for callback
functions that are very close to (almost in the same line) the widget
creation code, similar to lambda function in C++11. The last argument of
this macro is the callback code.
The syntax is a bit unconventional, but the resulting code is flexible and
needs no additional maintenance. It is also C++98 compatible. For example:
\code
#include <FL/fl_callback_macros.H>
...
Fl_String *str = new Fl_String("FLTK");
Fl_Button *btn = new Fl_Button(10, 10, 100, 100);
FL_METHOD_CALLBACK_2(btn, Fl_String, str, insert, int, 2, const char*, "...");
...
Fl_Button *inline_cb_btn_2 = new Fl_Button(390, 60, 180, 25, "2 args");
FL_INLINE_CALLBACK_2( inline_cb_btn_2,
const char *, text, "FLTK", int, number, 2,
{
fl_message("We received the message %s with %d!", text, number);
}
);
\endcode
\see Fl_Widget::callback(Fl_Callback*, void*), FL_FUNCTION_CALLBACK_3, FL_METHOD_CALLBACK_1, FL_INLINE_CALLBACK_2
\section common_when When and Reason
Normally callbacks are performed only when the value of the
widget changes. You can change this using the Fl_Widget::when()
method:
@@ -564,45 +630,13 @@ button->when(FL_WHEN_ENTER_KEY_ALWAYS);
button->when(FL_WHEN_CHANGED | FL_WHEN_NOT_CHANGED);
\endcode
<CENTER><TABLE WIDTH="80%" BORDER="1" CELLPADDING="5" CELLSPACING="0" BGCOLOR="#cccccc">
<TR>
<TD><B>Note:</B>
Within the callback, you can query why the callback was called using
Fl::callback_reason(). For example, setting
You cannot delete a widget inside a callback, as the
widget may still be accessed by FLTK after your callback
is completed. Instead, use the Fl::delete_widget()
method to mark your widget for deletion when it is safe
to do so.
\code myInput->when(FL_WHEN_RELEASE|FL_WHEN_CHANGED) \endcode
<B>Hint:</B>
Many programmers new to FLTK or C++ try to use a
non-static class method instead of a static class method
or function for their callback. Since callbacks are done
outside a C++ class, the <tt>this</tt> pointer is not
initialized for class methods.
To work around this problem, define a static method
in your class that accepts a pointer to the class, and
then have the static method call the class method(s) as
needed. The data pointer you provide to the
\p callback() method of the widget can be a
pointer to the instance of your class.
\code
class Foo {
void my_callback(Fl_Widget *w);
static void my_static_callback(Fl_Widget *w, void *f) { ((Foo *)f)->my_callback(w); }
...
}
...
w->callback(my_static_callback, (void *)this);
\endcode
</TD>
</TR>
</TABLE></CENTER>
for a text input field may return \ref FL_REASON_LOST_FOCUS or
\ref FL_REASON_CHANGED as a callback reason.
\section common_shortcuts Shortcuts

View File

@@ -32,6 +32,7 @@ file (MAKE_DIRECTORY ${EXECUTABLE_OUTPUT_PATH})
set (SIMPLE_SOURCES
chart-simple
callbacks
browser-simple
draggable-group
howto-add_fd-and-popen

View File

@@ -28,6 +28,7 @@ ALL = animgifimage$(EXEEXT) \
animgifimage-resize$(EXEEXT) \
browser-simple$(EXEEXT) \
cairo-draw-x$(EXEEXT) \
callbacks$(EXEEXT) \
chart-simple$(EXEEXT) \
draggable-group$(EXEEXT) \
howto-add_fd-and-popen$(EXEEXT) \

152
examples/callbacks.cxx Normal file
View File

@@ -0,0 +1,152 @@
//
// Callback macros example program for the Fast Light Tool Kit (FLTK).
//
// Copyright 2023 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 <stdio.h>
#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_String.H>
#include <FL/fl_ask.H>
#include <FL/fl_callback_macros.H>
Fl_Window *window = NULL;
/*
Here is a list of callback functions that can take custom parameters and are
not limited to FLTK's built-in `void*` or `long` user_data.
*/
void hello_0_args_cb() {
fl_message("Hello with 0 arguments");
}
void hello_2_args_cb(Fl_String &text, int number) {
fl_message("Hello with 2 arguments,\n\"%s\" and '%d'", text.c_str(), number);
}
void hello_4_args_cb(int a1, int a2, int a3, int a4) {
fl_message("Hello with 4 arguments:\n%d %d %d %d", a1, a2, a3, a4);
}
/*
We create our own little class here that uses method callbacks.
*/
class MyButton : public Fl_Button {
// id will be set in the constructor
int id_;
public:
// create a simple push button
MyButton(int x, int y, int w, int h, const char *label, int id)
: Fl_Button(x, y, w, h, label), id_(id)
{ }
// public non-static callback method
void hello(int a, int b, int c) {
// it's not a static method, so we have full access to all members, i.e. 'id_'
fl_message("MyButton has the id %d\nand was called with the custom parameters\n%d, %d, and %d.", id_, a, b, c);
}
};
/*
Whenever the code created by the macro is called, custom parameters are
duplicated. This ensures that each widget created dynamically with the same
function has its own separate set of user data at runtime. Consequently,
multiple widgets can be created dynamically, and each widget will have its
own unique set of parameters.
*/
void make_button(Fl_Window *win, int set) {
int y_lut[] = { 60, 90 };
const char *label_lut[] = { "id 2 (5, 6, 7)", "id 3 (6, 7, 8)" };
MyButton *btn = new MyButton(200, y_lut[set], 180, 25, label_lut[set], set+2);
FL_METHOD_CALLBACK_3(btn, MyButton, btn, hello, int, set+5, int, set+6, int, set+7);
}
int main(int argc, char ** argv) {
window = new Fl_Window(580, 120);
/* -- testing function callbacks with multiple arguments
These buttons demo the use of the CALLBACK macro to call standard C style
functions with up to five custom parameters.
*/
new Fl_Box(10, 5, 180, 25, "Function Callbacks:");
Fl_Button *func_cb_btn_0 = new Fl_Button(10, 30, 180, 25, "0 args");
FL_FUNCTION_CALLBACK_0(func_cb_btn_0, hello_0_args_cb);
Fl_Button *func_cb_btn_2 = new Fl_Button(10, 60, 180, 25, "2 args");
FL_FUNCTION_CALLBACK_2(func_cb_btn_2, hello_2_args_cb, Fl_String, "FLTK", int, 2);
Fl_Button *func_cb_btn_4 = new Fl_Button(10, 90, 180, 25, "4 args");
FL_FUNCTION_CALLBACK_4(func_cb_btn_4, hello_4_args_cb, int, 1, int, 2, int, 3, int, 4);
/* -- testing non-static method callbacks with multiple arguments
The following buttons call non-static class methods with custom parameters.
Check the class above to see how this is implemented.
*/
new Fl_Box(200, 5, 180, 25, "Method Callbacks:");
MyButton *meth_cb_btn_0 = new MyButton(200, 30, 180, 25, "id 1 (1, 2, 3)", 1);
// 1: the macro needs a pointer to the button first
// 2: we can call a method in any class, but here we call ourselves
// 3: call a method in our own class, so we need to set 'meth_cb_btn_0' again
// Note: we could just as well call a method in a different class.
// 4: this is the method that we want to call; it must be "public"
// 5: add zero to five parameter triplets, note the comma placement
FL_METHOD_CALLBACK_3(meth_cb_btn_0, MyButton, meth_cb_btn_0, hello, int, 1, int, 2, int, 3);
// call the same FL_METHOD_CALLBACK macro multiple times to ensure we get
// individual parameter sets
make_button(window, 0);
make_button(window, 1);
/* -- testing inline callback functions
Adding a simple Lambda style functionality to FLTK without actually using
lambdas and staying C++99 compatible.
*/
new Fl_Box(390, 5, 180, 25, "Inline Callbacks:");
Fl_Button *inline_cb_btn_0 = new Fl_Button(390, 30, 180, 25, "0 args");
FL_INLINE_CALLBACK_0(inline_cb_btn_0,
{ fl_message("Inline callback with 0 args."); }
);
Fl_Button *inline_cb_btn_2 = new Fl_Button(390, 60, 180, 25, "2 args");
FL_INLINE_CALLBACK_2(inline_cb_btn_2,
const char *, text, "FLTK", int, number, 2,
{ fl_message("We received the message %s with %d!", text, number); }
);
Fl_Button *inline_cb_btn_4 = new Fl_Button(390, 90, 180, 25, "4 args");
FL_INLINE_CALLBACK_4(inline_cb_btn_4,
int, x, window->x(),
int, y, window->y(),
int, w, window->w(),
int, h, window->h(),
{ fl_message("The main window was at\nx:%d, y:%d, w:%d, h:%d\n"
"when the callback was created\n"
"and is now at x:%d, y:%d", x, y, w, h,
window->x(), window->y());
}
);
window->end();
window->show(argc,argv);
return Fl::run();
}

View File

@@ -178,6 +178,8 @@ Fl_Widget::~Fl_Widget() {
fl_throw_focus(this);
// remove stale entries from default callback queue (Fl::readqueue())
if (callback_ == default_callback) cleanup_readqueue(this);
if ( (flags_ & AUTO_DELETE_USER_DATA) && user_data_)
delete (Fl_Callback_User_Data*)user_data_;
}
/**
@@ -399,3 +401,28 @@ void Fl_Widget::do_callback(Fl_Widget *widget, void *arg, Fl_Callback_Reason rea
if (callback_ != default_callback)
clear_changed();
}
/*
\brief Sets the user data for this widget.
Sets the new user data (void *) argument that is passed to the callback function.
\param[in] v new user data
*/
void Fl_Widget::user_data(void* v) {
if ((flags_ & AUTO_DELETE_USER_DATA) && user_data_)
delete (Fl_Callback_User_Data*)user_data_;
clear_flag(AUTO_DELETE_USER_DATA);
user_data_ = v;
}
/*
\brief Sets the user data for this widget.
Sets the new user data (void *) argument that is passed to the callback function.
\param[in] v new user data
\param[in] auto_free if set, the widget will free user data when destroyed; defaults to false
*/
void Fl_Widget::user_data(Fl_Callback_User_Data* v, bool auto_free) {
user_data((void*)v);
if (auto_free)
set_flag(AUTO_DELETE_USER_DATA);
}

View File

@@ -17,8 +17,10 @@
#include "unittests.h"
#include <FL/Fl_Group.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Simple_Terminal.H>
#include <FL/Fl_String.H>
#include <FL/fl_callback_macros.H>
/* Test Fl_String constructor and assignment. */
TEST(Fl_String, Assignment) {
@@ -216,6 +218,68 @@ TEST(Fl_Preferences, Strings) {
return true;
}
bool cb1a_ok = false, cb1b_ok = false, cb1c_ok = false;
int cb1_alloc = 0;
class MyString : public Fl_String {
public:
MyString() : Fl_String() { cb1_alloc++; }
MyString(const MyString &str) : Fl_String(str) { cb1_alloc++; }
MyString(const char *t) : Fl_String(t) { cb1_alloc++; }
~MyString() { cb1_alloc--; }
};
void cb1(MyString a, int b) {
cb1a_ok = true;
if (strcmp(a.c_str(),"FLTK")==0) cb1b_ok = true;
if (b==4) cb1c_ok = true;
}
/* Test callback macros. */
TEST(Fl_Callback_Macros, FL_FUNCTION_CALLBACK) {
Fl_Group::current(NULL);
Fl_Button *btn = new Fl_Button(10, 10, 100, 100);
FL_FUNCTION_CALLBACK_2(btn, cb1, MyString, "FLTK", int, 4);
do { class Fl_Callback_User_Data_240 : public Fl_Callback_User_Data {
public: MyString a_; int b_;
static void cb(Fl_Widget *w, void *user_data) {
Fl_Callback_User_Data_240 *cbdata = (Fl_Callback_User_Data_240*)user_data; (void)cbdata; cb1(cbdata->a_, cbdata->b_); }; Fl_Callback_User_Data_240(MyString a, int b) : a_(a), b_(b) { } }; btn->callback(Fl_Callback_User_Data_240::cb, new Fl_Callback_User_Data_240("FLTK", 4), true); } while(0);
btn->do_callback();
delete btn;
EXPECT_TRUE(cb1a_ok); // callback called
EXPECT_TRUE(cb1b_ok); // string stored correctly
EXPECT_TRUE(cb1c_ok); // integer stored correctly
EXPECT_TRUE(cb1_alloc==0); // string destroyed correctly (allocated as often as deallocated)
return true;
}
TEST(Fl_Callback_Macros, FL_METHOD_CALLBACK) {
Fl_Group::current(NULL);
Fl_String *str = new Fl_String("FLTK");
Fl_Button *btn = new Fl_Button(10, 10, 100, 100);
FL_METHOD_CALLBACK_2(btn, Fl_String, str, insert, int, 2, const char*, "XX");
btn->do_callback();
EXPECT_STREQ(str->c_str(), "FLXXTK");
delete btn;
delete str;
return true;
}
int cb3a = 0, cb3b = 0;
TEST(Fl_Callback_Macros, FL_INLINE_CALLBACK) {
Fl_Group::current(NULL);
Fl_Button *btn = new Fl_Button(10, 10, 100, 100);
FL_INLINE_CALLBACK_2(btn,
int, a, 42, int, b, 16,
{ cb3a = a; cb3b = b; }
);
btn->do_callback();
EXPECT_EQ(cb3a, 42);
EXPECT_EQ(cb3b, 16);
delete btn;
return true;
}
//
//------- test aspects of the FLTK core library ----------
//