diff options
Diffstat (limited to 'src/libui_sdl/libui/unix/area.c')
-rw-r--r-- | src/libui_sdl/libui/unix/area.c | 633 |
1 files changed, 633 insertions, 0 deletions
diff --git a/src/libui_sdl/libui/unix/area.c b/src/libui_sdl/libui/unix/area.c new file mode 100644 index 0000000..c46447c --- /dev/null +++ b/src/libui_sdl/libui/unix/area.c @@ -0,0 +1,633 @@ +// 4 september 2015 +#include "uipriv_unix.h" + +// notes: +// - G_DECLARE_DERIVABLE/FINAL_INTERFACE() requires glib 2.44 and that's starting with debian stretch (testing) (GTK+ 3.18) and ubuntu 15.04 (GTK+ 3.14) - debian jessie has 2.42 (GTK+ 3.14) +#define areaWidgetType (areaWidget_get_type()) +#define areaWidget(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), areaWidgetType, areaWidget)) +#define isAreaWidget(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), areaWidgetType)) +#define areaWidgetClass(class) (G_TYPE_CHECK_CLASS_CAST((class), areaWidgetType, areaWidgetClass)) +#define isAreaWidgetClass(class) (G_TYPE_CHECK_CLASS_TYPE((class), areaWidget)) +#define getAreaWidgetClass(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), areaWidgetType, areaWidgetClass)) + +typedef struct areaWidget areaWidget; +typedef struct areaWidgetClass areaWidgetClass; + +struct areaWidget { + GtkDrawingArea parent_instance; + uiArea *a; + // construct-only parameters aare not set until after the init() function has returned + // we need this particular object available during init(), so put it here instead of in uiArea + // keep a pointer in uiArea for convenience, though + clickCounter cc; +}; + +struct areaWidgetClass { + GtkDrawingAreaClass parent_class; +}; + +struct uiArea { + uiUnixControl c; + GtkWidget *widget; // either swidget or areaWidget depending on whether it is scrolling + + GtkWidget *swidget; + GtkContainer *scontainer; + GtkScrolledWindow *sw; + + GtkWidget *areaWidget; + GtkDrawingArea *drawingArea; + areaWidget *area; + + uiAreaHandler *ah; + + gboolean scrolling; + int scrollWidth; + int scrollHeight; + + // note that this is a pointer; see above + clickCounter *cc; + + // for user window drags + GdkEventButton *dragevent; +}; + +G_DEFINE_TYPE(areaWidget, areaWidget, GTK_TYPE_DRAWING_AREA) + +static void areaWidget_init(areaWidget *aw) +{ + // for events + gtk_widget_add_events(GTK_WIDGET(aw), + GDK_POINTER_MOTION_MASK | + GDK_BUTTON_MOTION_MASK | + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON_RELEASE_MASK | + GDK_KEY_PRESS_MASK | + GDK_KEY_RELEASE_MASK | + GDK_ENTER_NOTIFY_MASK | + GDK_LEAVE_NOTIFY_MASK); + + gtk_widget_set_can_focus(GTK_WIDGET(aw), TRUE); + + clickCounterReset(&(aw->cc)); +} + +static void areaWidget_dispose(GObject *obj) +{ + G_OBJECT_CLASS(areaWidget_parent_class)->dispose(obj); +} + +static void areaWidget_finalize(GObject *obj) +{ + G_OBJECT_CLASS(areaWidget_parent_class)->finalize(obj); +} + +static void areaWidget_size_allocate(GtkWidget *w, GtkAllocation *allocation) +{ + areaWidget *aw = areaWidget(w); + uiArea *a = aw->a; + + // GtkDrawingArea has a size_allocate() implementation; we need to call it + // this will call gtk_widget_set_allocation() for us + GTK_WIDGET_CLASS(areaWidget_parent_class)->size_allocate(w, allocation); + + if (!a->scrolling) + // we must redraw everything on resize because Windows requires it + gtk_widget_queue_resize(w); +} + +static void loadAreaSize(uiArea *a, double *width, double *height) +{ + GtkAllocation allocation; + + *width = 0; + *height = 0; + // don't provide size information for scrolling areas + if (!a->scrolling) { + gtk_widget_get_allocation(a->areaWidget, &allocation); + // these are already in drawing space coordinates + // for drawing, the size of drawing space has the same value as the widget allocation + // thanks to tristan in irc.gimp.net/#gtk+ + *width = allocation.width; + *height = allocation.height; + } +} + +static gboolean areaWidget_draw(GtkWidget *w, cairo_t *cr) +{ + areaWidget *aw = areaWidget(w); + uiArea *a = aw->a; + uiAreaDrawParams dp; + double clipX0, clipY0, clipX1, clipY1; + + dp.Context = newContext(cr); + + loadAreaSize(a, &(dp.AreaWidth), &(dp.AreaHeight)); + + cairo_clip_extents(cr, &clipX0, &clipY0, &clipX1, &clipY1); + dp.ClipX = clipX0; + dp.ClipY = clipY0; + dp.ClipWidth = clipX1 - clipX0; + dp.ClipHeight = clipY1 - clipY0; + + // no need to save or restore the graphics state to reset transformations; GTK+ does that for us + (*(a->ah->Draw))(a->ah, a, &dp); + + freeContext(dp.Context); + return FALSE; +} + +// to do this properly for scrolling areas, we need to +// - return the same value for min and nat +// - call gtk_widget_queue_resize() when the size changes +// thanks to Company in irc.gimp.net/#gtk+ +static void areaWidget_get_preferred_height(GtkWidget *w, gint *min, gint *nat) +{ + areaWidget *aw = areaWidget(w); + uiArea *a = aw->a; + + // always chain up just in case + GTK_WIDGET_CLASS(areaWidget_parent_class)->get_preferred_height(w, min, nat); + if (a->scrolling) { + *min = a->scrollHeight; + *nat = a->scrollHeight; + } +} + +static void areaWidget_get_preferred_width(GtkWidget *w, gint *min, gint *nat) +{ + areaWidget *aw = areaWidget(w); + uiArea *a = aw->a; + + // always chain up just in case + GTK_WIDGET_CLASS(areaWidget_parent_class)->get_preferred_width(w, min, nat); + if (a->scrolling) { + *min = a->scrollWidth; + *nat = a->scrollWidth; + } +} + +static guint translateModifiers(guint state, GdkWindow *window) +{ + GdkModifierType statetype; + + // GDK doesn't initialize the modifier flags fully; we have to explicitly tell it to (thanks to Daniel_S and daniels (two different people) in irc.gimp.net/#gtk+) + statetype = state; + gdk_keymap_add_virtual_modifiers( + gdk_keymap_get_for_display(gdk_window_get_display(window)), + &statetype); + return statetype; +} + +static uiModifiers toModifiers(guint state) +{ + uiModifiers m; + + m = 0; + if ((state & GDK_CONTROL_MASK) != 0) + m |= uiModifierCtrl; + if ((state & GDK_META_MASK) != 0) + m |= uiModifierAlt; + if ((state & GDK_MOD1_MASK) != 0) // GTK+ itself requires this to be Alt (just read through gtkaccelgroup.c) + m |= uiModifierAlt; + if ((state & GDK_SHIFT_MASK) != 0) + m |= uiModifierShift; + if ((state & GDK_SUPER_MASK) != 0) + m |= uiModifierSuper; + return m; +} + +// capture on drag is done automatically on GTK+ +static void finishMouseEvent(uiArea *a, uiAreaMouseEvent *me, guint mb, gdouble x, gdouble y, guint state, GdkWindow *window) +{ + // on GTK+, mouse buttons 4-7 are for scrolling; if we got here, that's a mistake + if (mb >= 4 && mb <= 7) + return; + // if the button ID >= 8, continue counting from 4, as in the MouseEvent spec + if (me->Down >= 8) + me->Down -= 4; + if (me->Up >= 8) + me->Up -= 4; + + state = translateModifiers(state, window); + me->Modifiers = toModifiers(state); + + // the mb != # checks exclude the Up/Down button from Held + me->Held1To64 = 0; + if (mb != 1 && (state & GDK_BUTTON1_MASK) != 0) + me->Held1To64 |= 1 << 0; + if (mb != 2 && (state & GDK_BUTTON2_MASK) != 0) + me->Held1To64 |= 1 << 1; + if (mb != 3 && (state & GDK_BUTTON3_MASK) != 0) + me->Held1To64 |= 1 << 2; + // don't check GDK_BUTTON4_MASK or GDK_BUTTON5_MASK because those are for the scrolling buttons mentioned above + // GDK expressly does not support any more buttons in the GdkModifierType; see https://git.gnome.org/browse/gtk+/tree/gdk/x11/gdkdevice-xi2.c#n763 (thanks mclasen in irc.gimp.net/#gtk+) + + // these are already in drawing space coordinates + // the size of drawing space has the same value as the widget allocation + // thanks to tristan in irc.gimp.net/#gtk+ + me->X = x; + me->Y = y; + + loadAreaSize(a, &(me->AreaWidth), &(me->AreaHeight)); + + (*(a->ah->MouseEvent))(a->ah, a, me); +} + +static gboolean areaWidget_button_press_event(GtkWidget *w, GdkEventButton *e) +{ + areaWidget *aw = areaWidget(w); + uiArea *a = aw->a; + gint maxTime, maxDistance; + GtkSettings *settings; + uiAreaMouseEvent me; + + // clicking doesn't automatically transfer keyboard focus; we must do so manually (thanks tristan in irc.gimp.net/#gtk+) + gtk_widget_grab_focus(w); + + // we handle multiple clicks ourselves here, in the same way as we do on Windows + if (e->type != GDK_BUTTON_PRESS) + // ignore GDK's generated double-clicks and beyond + return GDK_EVENT_PROPAGATE; + settings = gtk_widget_get_settings(w); + g_object_get(settings, + "gtk-double-click-time", &maxTime, + "gtk-double-click-distance", &maxDistance, + NULL); + // don't unref settings; it's transfer-none (thanks gregier in irc.gimp.net/#gtk+) + // e->time is guint32 + // e->x and e->y are floating-point; just make them 32-bit integers + // maxTime and maxDistance... are gint, which *should* fit, hopefully... + me.Count = clickCounterClick(a->cc, me.Down, + e->x, e->y, + e->time, maxTime, + maxDistance, maxDistance); + + me.Down = e->button; + me.Up = 0; + + // and set things up for window drags + a->dragevent = e; + finishMouseEvent(a, &me, e->button, e->x, e->y, e->state, e->window); + a->dragevent = NULL; + return GDK_EVENT_PROPAGATE; +} + +static gboolean areaWidget_button_release_event(GtkWidget *w, GdkEventButton *e) +{ + areaWidget *aw = areaWidget(w); + uiArea *a = aw->a; + uiAreaMouseEvent me; + + me.Down = 0; + me.Up = e->button; + me.Count = 0; + finishMouseEvent(a, &me, e->button, e->x, e->y, e->state, e->window); + return GDK_EVENT_PROPAGATE; +} + +static gboolean areaWidget_motion_notify_event(GtkWidget *w, GdkEventMotion *e) +{ + areaWidget *aw = areaWidget(w); + uiArea *a = aw->a; + uiAreaMouseEvent me; + + me.Down = 0; + me.Up = 0; + me.Count = 0; + finishMouseEvent(a, &me, 0, e->x, e->y, e->state, e->window); + return GDK_EVENT_PROPAGATE; +} + +// we want switching away from the control to reset the double-click counter, like with WM_ACTIVATE on Windows +// according to tristan in irc.gimp.net/#gtk+, doing this on both enter-notify-event and leave-notify-event is correct (and it seems to be true in my own tests; plus the events DO get sent when switching programs with the keyboard (just pointing that out)) +static gboolean onCrossing(areaWidget *aw, int left) +{ + uiArea *a = aw->a; + + (*(a->ah->MouseCrossed))(a->ah, a, left); + clickCounterReset(a->cc); + return GDK_EVENT_PROPAGATE; +} + +static gboolean areaWidget_enter_notify_event(GtkWidget *w, GdkEventCrossing *e) +{ + return onCrossing(areaWidget(w), 0); +} + +static gboolean areaWidget_leave_notify_event(GtkWidget *w, GdkEventCrossing *e) +{ + return onCrossing(areaWidget(w), 1); +} + +// note: there is no equivalent to WM_CAPTURECHANGED on GTK+; there literally is no way to break a grab like that (at least not on X11 and Wayland) +// even if I invoke the task switcher and switch processes, the mouse grab will still be held until I let go of all buttons +// therefore, no DragBroken() + +// we use GDK_KEY_Print as a sentinel because libui will never support the print screen key; that key belongs to the user + +static const struct { + guint keyval; + uiExtKey extkey; +} extKeys[] = { + { GDK_KEY_Escape, uiExtKeyEscape }, + { GDK_KEY_Insert, uiExtKeyInsert }, + { GDK_KEY_Delete, uiExtKeyDelete }, + { GDK_KEY_Home, uiExtKeyHome }, + { GDK_KEY_End, uiExtKeyEnd }, + { GDK_KEY_Page_Up, uiExtKeyPageUp }, + { GDK_KEY_Page_Down, uiExtKeyPageDown }, + { GDK_KEY_Up, uiExtKeyUp }, + { GDK_KEY_Down, uiExtKeyDown }, + { GDK_KEY_Left, uiExtKeyLeft }, + { GDK_KEY_Right, uiExtKeyRight }, + { GDK_KEY_F1, uiExtKeyF1 }, + { GDK_KEY_F2, uiExtKeyF2 }, + { GDK_KEY_F3, uiExtKeyF3 }, + { GDK_KEY_F4, uiExtKeyF4 }, + { GDK_KEY_F5, uiExtKeyF5 }, + { GDK_KEY_F6, uiExtKeyF6 }, + { GDK_KEY_F7, uiExtKeyF7 }, + { GDK_KEY_F8, uiExtKeyF8 }, + { GDK_KEY_F9, uiExtKeyF9 }, + { GDK_KEY_F10, uiExtKeyF10 }, + { GDK_KEY_F11, uiExtKeyF11 }, + { GDK_KEY_F12, uiExtKeyF12 }, + // numpad numeric keys and . are handled in events.c + { GDK_KEY_KP_Enter, uiExtKeyNEnter }, + { GDK_KEY_KP_Add, uiExtKeyNAdd }, + { GDK_KEY_KP_Subtract, uiExtKeyNSubtract }, + { GDK_KEY_KP_Multiply, uiExtKeyNMultiply }, + { GDK_KEY_KP_Divide, uiExtKeyNDivide }, + { GDK_KEY_Print, 0 }, +}; + +static const struct { + guint keyval; + uiModifiers mod; +} modKeys[] = { + { GDK_KEY_Control_L, uiModifierCtrl }, + { GDK_KEY_Control_R, uiModifierCtrl }, + { GDK_KEY_Alt_L, uiModifierAlt }, + { GDK_KEY_Alt_R, uiModifierAlt }, + { GDK_KEY_Meta_L, uiModifierAlt }, + { GDK_KEY_Meta_R, uiModifierAlt }, + { GDK_KEY_Shift_L, uiModifierShift }, + { GDK_KEY_Shift_R, uiModifierShift }, + { GDK_KEY_Super_L, uiModifierSuper }, + { GDK_KEY_Super_R, uiModifierSuper }, + { GDK_KEY_Print, 0 }, +}; + +static int areaKeyEvent(uiArea *a, int up, GdkEventKey *e) +{ + uiAreaKeyEvent ke; + guint state; + int i; + + ke.Key = 0; + ke.ExtKey = 0; + ke.Modifier = 0; + + state = translateModifiers(e->state, e->window); + ke.Modifiers = toModifiers(state); + + ke.Up = up; + + for (i = 0; extKeys[i].keyval != GDK_KEY_Print; i++) + if (extKeys[i].keyval == e->keyval) { + ke.ExtKey = extKeys[i].extkey; + goto keyFound; + } + + for (i = 0; modKeys[i].keyval != GDK_KEY_Print; i++) + if (modKeys[i].keyval == e->keyval) { + ke.Modifier = modKeys[i].mod; + // don't include the modifier in ke.Modifiers + ke.Modifiers &= ~ke.Modifier; + goto keyFound; + } + + if (fromScancode(e->hardware_keycode - 8, &ke)) + goto keyFound; + + // no supported key found; treat as unhandled + return 0; + +keyFound: + return (*(a->ah->KeyEvent))(a->ah, a, &ke); +} + +static gboolean areaWidget_key_press_event(GtkWidget *w, GdkEventKey *e) +{ + areaWidget *aw = areaWidget(w); + uiArea *a = aw->a; + + if (areaKeyEvent(a, 0, e)) + return GDK_EVENT_STOP; + return GDK_EVENT_PROPAGATE; +} + +static gboolean areaWidget_key_release_event(GtkWidget *w, GdkEventKey *e) +{ + areaWidget *aw = areaWidget(w); + uiArea *a = aw->a; + + if (areaKeyEvent(a, 1, e)) + return GDK_EVENT_STOP; + return GDK_EVENT_PROPAGATE; +} + +enum { + pArea = 1, + nProps, +}; + +static GParamSpec *pspecArea; + +static void areaWidget_set_property(GObject *obj, guint prop, const GValue *value, GParamSpec *pspec) +{ + areaWidget *aw = areaWidget(obj); + + switch (prop) { + case pArea: + aw->a = (uiArea *) g_value_get_pointer(value); + aw->a->cc = &(aw->cc); + return; + } + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec); +} + +static void areaWidget_get_property(GObject *obj, guint prop, GValue *value, GParamSpec *pspec) +{ + G_OBJECT_WARN_INVALID_PROPERTY_ID(obj, prop, pspec); +} + +static void areaWidget_class_init(areaWidgetClass *class) +{ + G_OBJECT_CLASS(class)->dispose = areaWidget_dispose; + G_OBJECT_CLASS(class)->finalize = areaWidget_finalize; + G_OBJECT_CLASS(class)->set_property = areaWidget_set_property; + G_OBJECT_CLASS(class)->get_property = areaWidget_get_property; + + GTK_WIDGET_CLASS(class)->size_allocate = areaWidget_size_allocate; + GTK_WIDGET_CLASS(class)->draw = areaWidget_draw; + GTK_WIDGET_CLASS(class)->get_preferred_height = areaWidget_get_preferred_height; + GTK_WIDGET_CLASS(class)->get_preferred_width = areaWidget_get_preferred_width; + GTK_WIDGET_CLASS(class)->button_press_event = areaWidget_button_press_event; + GTK_WIDGET_CLASS(class)->button_release_event = areaWidget_button_release_event; + GTK_WIDGET_CLASS(class)->motion_notify_event = areaWidget_motion_notify_event; + GTK_WIDGET_CLASS(class)->enter_notify_event = areaWidget_enter_notify_event; + GTK_WIDGET_CLASS(class)->leave_notify_event = areaWidget_leave_notify_event; + GTK_WIDGET_CLASS(class)->key_press_event = areaWidget_key_press_event; + GTK_WIDGET_CLASS(class)->key_release_event = areaWidget_key_release_event; + + pspecArea = g_param_spec_pointer("libui-area", + "libui-area", + "uiArea.", + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + g_object_class_install_property(G_OBJECT_CLASS(class), pArea, pspecArea); +} + +// control implementation + +uiUnixControlAllDefaults(uiArea) + +void uiAreaSetSize(uiArea *a, int width, int height) +{ + if (!a->scrolling) + userbug("You cannot call uiAreaSetSize() on a non-scrolling uiArea. (area: %p)", a); + a->scrollWidth = width; + a->scrollHeight = height; + gtk_widget_queue_resize(a->areaWidget); +} + +void uiAreaQueueRedrawAll(uiArea *a) +{ + gtk_widget_queue_draw(a->areaWidget); +} + +void uiAreaScrollTo(uiArea *a, double x, double y, double width, double height) +{ + // TODO + // TODO adjust adjustments and find source for that +} + +void uiAreaBeginUserWindowMove(uiArea *a) +{ + GtkWidget *toplevel; + + if (a->dragevent == NULL) + userbug("cannot call uiAreaBeginUserWindowMove() outside of a Mouse() with Down != 0"); + // TODO don't we have a libui function for this? did I scrap it? + // TODO widget or areaWidget? + toplevel = gtk_widget_get_toplevel(a->widget); + if (toplevel == NULL) { + // TODO + return; + } + // the docs say to do this + if (!gtk_widget_is_toplevel(toplevel)) { + // TODO + return; + } + if (!GTK_IS_WINDOW(toplevel)) { + // TODO + return; + } + gtk_window_begin_move_drag(GTK_WINDOW(toplevel), + a->dragevent->button, + a->dragevent->x_root, // TODO are these correct? + a->dragevent->y_root, + a->dragevent->time); +} + +static const GdkWindowEdge edges[] = { + [uiWindowResizeEdgeLeft] = GDK_WINDOW_EDGE_WEST, + [uiWindowResizeEdgeTop] = GDK_WINDOW_EDGE_NORTH, + [uiWindowResizeEdgeRight] = GDK_WINDOW_EDGE_EAST, + [uiWindowResizeEdgeBottom] = GDK_WINDOW_EDGE_SOUTH, + [uiWindowResizeEdgeTopLeft] = GDK_WINDOW_EDGE_NORTH_WEST, + [uiWindowResizeEdgeTopRight] = GDK_WINDOW_EDGE_NORTH_EAST, + [uiWindowResizeEdgeBottomLeft] = GDK_WINDOW_EDGE_SOUTH_WEST, + [uiWindowResizeEdgeBottomRight] = GDK_WINDOW_EDGE_SOUTH_EAST, +}; + +void uiAreaBeginUserWindowResize(uiArea *a, uiWindowResizeEdge edge) +{ + GtkWidget *toplevel; + + if (a->dragevent == NULL) + userbug("cannot call uiAreaBeginUserWindowResize() outside of a Mouse() with Down != 0"); + // TODO don't we have a libui function for this? did I scrap it? + // TODO widget or areaWidget? + toplevel = gtk_widget_get_toplevel(a->widget); + if (toplevel == NULL) { + // TODO + return; + } + // the docs say to do this + if (!gtk_widget_is_toplevel(toplevel)) { + // TODO + return; + } + if (!GTK_IS_WINDOW(toplevel)) { + // TODO + return; + } + gtk_window_begin_resize_drag(GTK_WINDOW(toplevel), + edges[edge], + a->dragevent->button, + a->dragevent->x_root, // TODO are these correct? + a->dragevent->y_root, + a->dragevent->time); +} + +uiArea *uiNewArea(uiAreaHandler *ah) +{ + uiArea *a; + + uiUnixNewControl(uiArea, a); + + a->ah = ah; + a->scrolling = FALSE; + + a->areaWidget = GTK_WIDGET(g_object_new(areaWidgetType, + "libui-area", a, + NULL)); + a->drawingArea = GTK_DRAWING_AREA(a->areaWidget); + a->area = areaWidget(a->areaWidget); + + a->widget = a->areaWidget; + + return a; +} + +uiArea *uiNewScrollingArea(uiAreaHandler *ah, int width, int height) +{ + uiArea *a; + + uiUnixNewControl(uiArea, a); + + a->ah = ah; + a->scrolling = TRUE; + a->scrollWidth = width; + a->scrollHeight = height; + + a->swidget = gtk_scrolled_window_new(NULL, NULL); + a->scontainer = GTK_CONTAINER(a->swidget); + a->sw = GTK_SCROLLED_WINDOW(a->swidget); + + a->areaWidget = GTK_WIDGET(g_object_new(areaWidgetType, + "libui-area", a, + NULL)); + a->drawingArea = GTK_DRAWING_AREA(a->areaWidget); + a->area = areaWidget(a->areaWidget); + + a->widget = a->swidget; + + gtk_container_add(a->scontainer, a->areaWidget); + // and make the area visible; only the scrolled window's visibility is controlled by libui + gtk_widget_show(a->areaWidget); + + return a; +} |