widget filters & manual internal events - cnjinhao/nana GitHub Wiki
Concept Premise (the why):
Nana's widgets come with alot of generic functionality out-of-the-box, and in most cases this is going to be good enough for the typical-application. However, consider if one were to desire to change the behavior of a widget or to build on top of one (eg, make their own customized widgets). A developer can do this in Nana via custom event-bindings for a given widget... Yet, what happens if one of their custom bindings happens to overlap with the internal (pre-defined) events of that widget?
With few exceptions (such as key_char), bound events do not provide a means to block a widget's internal events. Event filtering provides a workaround for this type of problem, and tries to give more freedom to alter pre-defined widget behavior. By using filters and explicit internal-event invocation a developer can not only act on events, but also act on them before OR after a widget's internal processing, control when they occur, or completely prevent them from occurring.
listbox key-rebind example:
-In the following example we will "add" WASD navigation key-bindings to a listbox, and also later filter the arrow keys in addition to swapping mouse-click selection for double-click selection.
#include <nana/gui.hpp>
#include <nana/gui/widgets/listbox.hpp>
int main()
{
nana::form fm;
fm.caption(L"emit_internal_event example");
nana::place place(fm);
place.div("<a>");
auto& field = place.field("a");
nana::listbox list(fm);
list.append_header(u8"Test");
for (int i = 0; i < 50; i++)
{
std::stringstream fmt;
fmt << u8"listbox " << i;
list.at(0).append(fmt.str());
}
field << list;
list.events().key_press.connect([](const nana::arg_keyboard& arg) {
nana::arg_keyboard clone = arg;
switch (arg.key)
{
case nana::keyboard::os_arrow_left:
case 'W':
case 'A':
clone.key = nana::keyboard::os_arrow_up;
nana::API::emit_internal_event(nana::event_code::key_press, arg.window_handle, clone);
break;
case nana::keyboard::os_arrow_right:
case 'S':
case 'D':
clone.key = nana::keyboard::os_arrow_down;
nana::API::emit_internal_event(nana::event_code::key_press, arg.window_handle, clone);
break;
default:break;
}
});
place.collocate();
fm.modality();
return 0;
}
As you might notice, there are now many keys for this widget that can move the selection forward and backwards. nana::keyboard::os_arrow_left
, W
, A
, and nana::keyboard::os_arrow_up
(default) respectively move backwards a row.
case nana::keyboard::os_arrow_left:
case 'W':
case 'A':
clone.key = nana::keyboard::os_arrow_up;
nana::API::emit_internal_event(nana::event_code::key_press, arg.window_handle, clone);
break;
There are of course other key bindings that have an effect on the widget, such as pageup & pagedown here too. Let's say that hypothetically we want to leave all these inplace, however we wish to disable the functionality of the arrow keys (in favor of WASD). [perhaps we have some other plan for the arrow keys in our project, such as "swapping active widgets"]
list.filter_event(nana::event_code::key_press, true /*disable internal processing for this event*/);
"filter_event" provides a way to shut off or disable the automatic invocation of internal-events for a widget. This has all kinds of purposes, as that means one can choose to execute before the internal-event rather than after, or to block it.
list.filter_event(nana::event_code::key_press, true /*disable internal processing for this event*/);
list.events().key_press.connect([](const nana::arg_keyboard& arg) {
bool bAllowed = true;
// we can do things before the internal event, such as flipping bAllowed to false.
if (bAllowed)
nana::API::emit_internal_event(nana::event_code::key_press, arg.window_handle, arg);
// or ... we can do things after it, like most normal event bindings.
});
For the sake of clarity the above would have no-effect on the listbox widget.
-However, you might notice this has some nice implications far as what we were trying to do, so let's add this concept to our listbox example.
list.filter_event(nana::event_code::key_press, true /*disable internal processing for this event*/);
list.events().key_press.connect([](const nana::arg_keyboard& arg) {
nana::arg_keyboard clone = arg;
switch (arg.key)
{
case 'W':
case 'A':
clone.key = nana::keyboard::os_arrow_up;
nana::API::emit_internal_event(nana::event_code::key_press, arg.window_handle, clone);
break;
case 'S':
case 'D':
clone.key = nana::keyboard::os_arrow_down;
nana::API::emit_internal_event(nana::event_code::key_press, arg.window_handle, clone);
break;
case nana::keyboard::os_arrow_up:
case nana::keyboard::os_arrow_down:
break; // block these keys
default: // otherwise, just pass on the key_press to the internal event
nana::API::emit_internal_event(nana::event_code::key_press, arg.window_handle, arg);
break;
}
});
Now, the arrow keys have no effect on our listbox widget. Yet you'll also notice that we still have the ability to move about the control using all the others, such as pageup, pagedown, and our rebound WASD key-cluster.
For the sake of "playing", let's also rebind mouse selection over to double-click.
list.filter_event({nana::event_code::key_press,
nana::event_code::mouse_down, // mouse click selection happens on mouse-down normally
nana::event_code::dbl_click // also shut off double-click
}, true /*disable internal processing for these events*/);
list.events().dbl_click.connect([](const nana::arg_mouse& arg) {
nana::API::emit_internal_event(nana::event_code::mouse_down, arg.window_handle, arg);
});
... and now, in order to select a row in the listbox we would have to double-click it. The same applies to column sort selection, as double-click now acts internally as mouse clicks had previously.
While the changes we've made to this listbox probably don't seem that exciting, hopefully this will act as a stepping stone in building more complex widget-interactions. Consider the implications of blocking widget events with forwarding events to their parent. (eg, parent form)
if (is_hotkey)
nana::API::emit_event(nana::event_code::key_press, nana::API::get_parent_window(arg.window_handle), arg);
else
nana::API::emit_internal_event(nana::event_code::key_press, arg.window_handle, arg);
-This opens all sorts of possibilities for implementing hotkey-like behaviors, not just on keyboard keystrokes, yet also for mouse events too.
The jist:
Filtering events is a very powerful tool in Nana's arsenal, one that permits an alternative to creating derived classes, and that helps avoid rebuilding the library. Using it, you have the granularity and control to keep the portions of a widget's events that you want, while blocking or replacing those that you don't. This provides more means to re-purpose or further customize existing base-widgets in Nana without creating new ones.