Rather then comment on the patch immediately, I thought I'd go back
and try to write up the problems we are trying to solve here in some
detail so I'd have them clear in my own mind.
- Owen
Issues to solve
===============
There are two basic problems that come up repeatedly with handling of
mouse events in GTK+:
1. With the modern method of using input-only windows to catch
events, we've lost the correspondance between the widget
hierarchy and the window hierarchy. Consider a
GtkDrawingArea in a notebook tab.
We used to have:
toplevel->window contains
notebook->window contains
drawing_area->window
Now we have:
toplevel->window contains
notebook->window
drawing_area->window
And drawing_area->window just coincidentally" sits on top of
notebook->window. With this change, a lot of properties of
X events no longer work right. Two examples:
- When the mouse moves from notebook->window to
drawing_area->window, we don't get a LEAVE detail=INFERIOR,
but rather a LEAVE detail=NONLINEAR, so it looks like
the mouse has left the notebook.
- If drawing_area->window doesn't select for button press
events, then the event is propagated at the X level not
to notebook->window but to toplevel->window. The notebook
doens't receive the events.
2. The need to have windows to catch mouse events in itself
is a problem. It's a frequent GTK+ FAQ, GTK+ widgets like
GtkLabel have complex logic for sometimes adding an event
widget, and an artificial restriction is introduced for
for drag sources and tooltips.
In addition, there are some other things about mouse event handling
which are less than ideal:
3. You can't reliably do prelighting on nested widgets. Even
if you have a solution for 1, there is still a problem with
a button in a notebook tab. When you get the detail=inferior
LeaveEvent, you don't know whether the the pointer left
the notebook to go to a drawing area (doesn't select for
button events, notebook still gets the event), or to a
button (selects for button events, notebook doesn't get
the event)
4. Shaped input areas don't work very well. We can return
TRUE or FALSE for button-press-event, based on where the
mouse was pressed, and the parent only gets the events where
we returned TRUE. But most widgets ignore button press
events with an unexpected window field. Prelighting on
the parent also doesn't work right, but its broken by
3 already.
(You can solve shaped input areas by using
gdk_window_shape_combine_mask() on an InputOnly window)
Event classification
====================
So, what events are we dealing with here? Basically the mouse events.
BUTTON_PRESS, 2BUTTON_PRESS, 3BUTTON_PRESS, BUTTON_RELEASE,
ENTER_NOTIFY, LEAVE_NOTIFY, MOTION_NOTIFY, PROXIMITY_IN, PROXIMITY_OUT
SCROLL
Other events we have are key events, which are delivered to toplevels
and propagated along the widget heirarchy already:
KEY_PRESS, KEY_RELEASE
A set of events that are related to windows (mostly toplevels) where
hierarchical propagation doesn't make sense:
CLIENT_EVENT, CONFIGURE, DELETE, DESTROY, EXPOSE, FOCUS_CHANGE,
MAP, PROPERTY_NOTIFY, UNMAP, VISIBILITY_NOTIFY, WINDOW_STATE
And events that are either obsolete or not directly relevant to
applications:
DRAG_ENTER, DRAG_LEAVE, DRAG_MOTION, DRAG_STATUS, DROP_START,
DROP_FINISHED, NO_EXPOSE, SETTING, SELECTION_CLEAR, SELECTION_REQUEST
SELECTION_NOTIFY, OWNER_CHANGE
So 12 events types, corresponding to 8 signals
(button-press-event, button-release-event, enter-notify-event,
leave-notify-event, motion-notify-event, proximity-in-event,
proximity-out-event, scroll-event.)
There is some subdivision of the mouse signals that makes sense:
- BUTTON_PRESS, 2BUTTON_PRESS, 3BUTTON_PRESS, BUTTON_RELEASE, SCROLL
These indicate explicit actions by the user, and must only be acted
on by only one recipient. If the event gets acted on twice, we may
get
stuck grabs or similar.
The general expected propation behavior is that if not handled
on a widget, they should get passed to the parent.
- MOTION_NOTIFY, PROXIMITY_IN, PROXIMITY_OUT
These indicate state changes rather than action. A signal handler
for one of these that returns TRUE is likely a bug.
The general expected propation behavior is that if they should
be sent to a widget, then to passed to the parent.
- ENTER_NOTIFY, LEAVE_NOTIFY
Instead of being generated once, these are generated on the
full set of affected widgets. So, propation doesn't make sense.
(However, X doesn't send motion events when it sends enter/leave
events, so they need to get propagated to get full motion
information)
Difficulties
============
In coming up with a fix for these problems, we have a number of
difficulties:
- If we want to have some other way of delivering events with
respect to widgets rather than windows, then we either have to:
- Reinvent the GdkEvent wheel. (GdkEventButton, GdkEventMotion,
GdkEventCrossing, GdkEventProximity, GdkEventScroll)
- Reinterpret the GdkEvent fields in some creative way.
We could set event->window to the widget->window always
and use coordinates relative to that. But what to do
about subwindow in GdkEventCrossing? The subwidget isn't
identified by a window. (Of course, I've never found a
use for this field, so NULL probably would be fine.)
- For compatibility, we still have to keep delivering events
the existing way.
- Implicit pointer grabs should be generated at the widget
level. (If you press the mouse in a widget, that widget
should be guaranteed to get the release.).
- Explicit point grabs should be honored. (If an app
calls gdk_pointer_grab() on a window, the event shouldn't
be delivered to other windows.
- We only have 7 available padding slots in GtkWidget for adding
signals with virtual functions. While having virtual function
slots for each signal is not crucial, it's generally something
we'd like to do. (To avoid having widgets connecting to
themselves.)
- Leaving that aside, we have 61 (yes, 61) signals on GtkWidget
currently. I'm reluctant to add 8 more.
- As much as possible, we need to avoid confusing programmers
using the GTK+ API with multiple ways to do the same thing.
Feel challenged yet?
Søren's proposal
================
So, how does Søren's proposal fit into what we've discussed above?
It handles is restricted to three events (enter/leave/motion) and
adds a signal for each:
void ::global-enter (widget, x, y);
void ::global-leave (widget);
void ::global-motion (widget, x, y);
So, very simple. Note that these events do not pay attention to
the widget hierarchy:
- motion events are delivered to *all* widgets that lie under the
pointer
(to *both* the notebook and the drawing area)
- There is no idea of an "inferior" leave/enter in the X sense:
no event is generated on the parent when the pointer moves from
parent to child.
(But see issue 3. for why "inferior" leaves aren't very interesting)
Other ideas
===========
What are some other ways we could do the API? (brainstorming, in order
of increasing radicalism)
- We could bundle everything into a single signal, along the
lines of ::event.
g_signal_connect (gtk_widget_get_eventer (widget), "mouse-event",
on_mouse_event, NULL);
- We could use an aggregate object for GtkWidget to provide
a new namespace for signals:
g_signal_connect (gtk_widget_get_eventer (widget), "enter",
on_enter, NULL);
- We could use the existing event signals, but deliver additional
events to widgets that wouldn't normally get events.
In the notebook tab case, when the user clicks on the drawing
area and X delivers the event to toplevel->window, we note that
it is over the area of the notebook tab, and propagate it
starting from the notebook rather than starting from the
toplevel.
Somewhat similarly, for enter/leave events, we generate
synthetic enter/leave events for a widget when a widget-crossing
doesn't correspond to a window-crossing.
To make the notebook-tab example work properly, we need to do
one other thing: we need to munge the detail on crossing
events when the window hierarchy doesn't correspond to the
widget hierarchy.
This should fix issue 1. and 2.
Two potential problems here:
- We're changing what events get delivered fairly radically,
and apps could get confused. Even if all the changes
"make sense", then
- If we allow no-window widgets to receive events, we
will get confusing inferior LeaveNotify events.
- Modification on the above, add a signal to GtkWidget to allow
a widget to decide whether it contains the pointer or not.
(Anybody returns TRUE, the widget contains the pointer)
This:
- Allows shaped input areas
- Fixes the inferior LeaveNotify problem noted in the last
problem, if we require a connection to ::hit-test for a
no-window widget to receive events
- And by fixing the inferior LeaveNotify it becomes reasonable
to enable nested-prelighting.
Simplest is to say that ::hit-test only applies when there
isn't a separate GdKWindow for the widget.
- We could make GDK allow (via emulation) an InputOnly window
to contain an InputOutput window. Then we get rid of the
window vs. widget hierarchy mismatch widget by widget.
With this label's widget->window ends up pointing to an
InputOnly window, so we also have to say that drawing to an
InputOnly window is redirected to the enearest InputOutput
ancestor
Appendix: a reference
=====================
While looking at a new way to handle mouse events in GTK+, it's
worth comparing with the DOM2 event model:
http://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/
There are various differences from GTK+, but the major one I'll
point out is:
Before the propagate-up stage (bubbling in DOM terminology)
there is a propagate-down stage where parent elements can
receive and optionally block events.
Attachment:
signature.asc
Description: This is a digitally signed message part