diff options
author | StapleButter <thetotalworm@gmail.com> | 2017-09-09 02:30:51 +0200 |
---|---|---|
committer | StapleButter <thetotalworm@gmail.com> | 2017-09-09 02:30:51 +0200 |
commit | 70e4841d311d68689724768157cc9cbfbde7a9fc (patch) | |
tree | ba9499f77d1258530a7e60aa6e1732c41d98161c /src/libui_sdl/libui/unix | |
parent | 81747d6c34eb159481a6ca3f283d065fa3568617 (diff) |
another UI attempt, I guess.
sorry.
Diffstat (limited to 'src/libui_sdl/libui/unix')
42 files changed, 5401 insertions, 0 deletions
diff --git a/src/libui_sdl/libui/unix/CMakeLists.txt b/src/libui_sdl/libui/unix/CMakeLists.txt new file mode 100644 index 0000000..9300bcb --- /dev/null +++ b/src/libui_sdl/libui/unix/CMakeLists.txt @@ -0,0 +1,85 @@ +# 3 june 2016 + +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED gtk+-3.0) + +list(APPEND _LIBUI_SOURCES + unix/alloc.c + unix/area.c + unix/box.c + unix/button.c + unix/cellrendererbutton.c + unix/checkbox.c + unix/child.c + unix/colorbutton.c + unix/combobox.c + unix/control.c + unix/datetimepicker.c + unix/debug.c + unix/draw.c + unix/drawmatrix.c + unix/drawpath.c + unix/drawtext.c + unix/editablecombo.c + unix/entry.c + unix/fontbutton.c + unix/form.c + unix/future.c + unix/graphemes.c + unix/grid.c + unix/group.c + unix/image.c + unix/label.c + unix/main.c + unix/menu.c + unix/multilineentry.c + unix/progressbar.c + unix/radiobuttons.c + unix/separator.c + unix/slider.c + unix/spinbox.c + unix/stddialogs.c + unix/tab.c + unix/text.c + unix/util.c + unix/window.c +) +set(_LIBUI_SOURCES ${_LIBUI_SOURCES} PARENT_SCOPE) + +list(APPEND _LIBUI_INCLUDEDIRS + unix +) +set(_LIBUI_INCLUDEDIRS _LIBUI_INCLUDEDIRS PARENT_SCOPE) + +set(_LIBUINAME libui PARENT_SCOPE) +if(NOT BUILD_SHARED_LIBS) + set(_LIBUINAME libui-temporary PARENT_SCOPE) +endif() +macro(_handle_static) + set_target_properties(${_LIBUINAME} PROPERTIES + ARCHIVE_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}") + set(_aname $<TARGET_FILE:${_LIBUINAME}>) + set(_oname libui-combined.o) + add_custom_command( + OUTPUT ${_oname} + COMMAND + ld -r --whole-archive ${_aname} -o ${_oname} + COMMAND + objcopy --localize-hidden ${_oname} + COMMENT "Removing hidden symbols") + add_library(libui STATIC ${_oname}) + # otherwise cmake won't know which linker to use + set_target_properties(libui PROPERTIES + LINKER_LANGUAGE C) + set(_aname) + set(_oname) +endmacro() + +# TODO the other variables don't work? +set(_LIBUI_CFLAGS + ${GTK_CFLAGS} +PARENT_SCOPE) + +set(_LIBUI_LIBS + ${GTK_LDFLAGS} m ${CMAKE_DL_LIBS} +PARENT_SCOPE) diff --git a/src/libui_sdl/libui/unix/alloc.c b/src/libui_sdl/libui/unix/alloc.c new file mode 100644 index 0000000..2561efa --- /dev/null +++ b/src/libui_sdl/libui/unix/alloc.c @@ -0,0 +1,84 @@ +// 7 april 2015 +#include <string.h> +#include "uipriv_unix.h" + +static GPtrArray *allocations; + +#define UINT8(p) ((uint8_t *) (p)) +#define PVOID(p) ((void *) (p)) +#define EXTRA (sizeof (size_t) + sizeof (const char **)) +#define DATA(p) PVOID(UINT8(p) + EXTRA) +#define BASE(p) PVOID(UINT8(p) - EXTRA) +#define SIZE(p) ((size_t *) (p)) +#define CCHAR(p) ((const char **) (p)) +#define TYPE(p) CCHAR(UINT8(p) + sizeof (size_t)) + +void initAlloc(void) +{ + allocations = g_ptr_array_new(); +} + +static void uninitComplain(gpointer ptr, gpointer data) +{ + char **str = (char **) data; + char *str2; + + if (*str == NULL) + *str = g_strdup_printf(""); + str2 = g_strdup_printf("%s%p %s\n", *str, ptr, *TYPE(ptr)); + g_free(*str); + *str = str2; +} + +void uninitAlloc(void) +{ + char *str = NULL; + + if (allocations->len == 0) { + g_ptr_array_free(allocations, TRUE); + return; + } + g_ptr_array_foreach(allocations, uninitComplain, &str); + userbug("Some data was leaked; either you left a uiControl lying around or there's a bug in libui itself. Leaked data:\n%s", str); + g_free(str); +} + +void *uiAlloc(size_t size, const char *type) +{ + void *out; + + out = g_malloc0(EXTRA + size); + *SIZE(out) = size; + *TYPE(out) = type; + g_ptr_array_add(allocations, out); + return DATA(out); +} + +void *uiRealloc(void *p, size_t new, const char *type) +{ + void *out; + size_t *s; + + if (p == NULL) + return uiAlloc(new, type); + p = BASE(p); + out = g_realloc(p, EXTRA + new); + s = SIZE(out); + if (new <= *s) + memset(((uint8_t *) DATA(out)) + *s, 0, new - *s); + *s = new; + if (g_ptr_array_remove(allocations, p) == FALSE) + implbug("%p not found in allocations array in uiRealloc()", p); + g_ptr_array_add(allocations, out); + return DATA(out); +} + +void uiFree(void *p) +{ + if (p == NULL) + implbug("attempt to uiFree(NULL)"); + p = BASE(p); + g_free(p); + if (g_ptr_array_remove(allocations, p) == FALSE) + implbug("%p not found in allocations array in uiFree()", p); +} 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; +} diff --git a/src/libui_sdl/libui/unix/box.c b/src/libui_sdl/libui/unix/box.c new file mode 100644 index 0000000..23fb7f7 --- /dev/null +++ b/src/libui_sdl/libui/unix/box.c @@ -0,0 +1,159 @@ +// 7 april 2015 +#include "uipriv_unix.h" + +struct boxChild { + uiControl *c; + int stretchy; + gboolean oldhexpand; + GtkAlign oldhalign; + gboolean oldvexpand; + GtkAlign oldvalign; +}; + +struct uiBox { + uiUnixControl c; + GtkWidget *widget; + GtkContainer *container; + GtkBox *box; + GArray *controls; + int vertical; + int padded; + GtkSizeGroup *stretchygroup; // ensures all stretchy controls have the same size +}; + +uiUnixControlAllDefaultsExceptDestroy(uiBox) + +#define ctrl(b, i) &g_array_index(b->controls, struct boxChild, i) + +static void uiBoxDestroy(uiControl *c) +{ + uiBox *b = uiBox(c); + struct boxChild *bc; + guint i; + + // kill the size group + g_object_unref(b->stretchygroup); + // free all controls + for (i = 0; i < b->controls->len; i++) { + bc = ctrl(b, i); + uiControlSetParent(bc->c, NULL); + // and make sure the widget itself stays alive + uiUnixControlSetContainer(uiUnixControl(bc->c), b->container, TRUE); + uiControlDestroy(bc->c); + } + g_array_free(b->controls, TRUE); + // and then ourselves + g_object_unref(b->widget); + uiFreeControl(uiControl(b)); +} + +void uiBoxAppend(uiBox *b, uiControl *c, int stretchy) +{ + struct boxChild bc; + GtkWidget *widget; + + bc.c = c; + bc.stretchy = stretchy; + widget = GTK_WIDGET(uiControlHandle(bc.c)); + bc.oldhexpand = gtk_widget_get_hexpand(widget); + bc.oldhalign = gtk_widget_get_halign(widget); + bc.oldvexpand = gtk_widget_get_vexpand(widget); + bc.oldvalign = gtk_widget_get_valign(widget); + + if (bc.stretchy) { + if (b->vertical) { + gtk_widget_set_vexpand(widget, TRUE); + gtk_widget_set_valign(widget, GTK_ALIGN_FILL); + } else { + gtk_widget_set_hexpand(widget, TRUE); + gtk_widget_set_halign(widget, GTK_ALIGN_FILL); + } + gtk_size_group_add_widget(b->stretchygroup, widget); + } else + if (b->vertical) + gtk_widget_set_vexpand(widget, FALSE); + else + gtk_widget_set_hexpand(widget, FALSE); + // and make them fill the opposite direction + if (b->vertical) { + gtk_widget_set_hexpand(widget, TRUE); + gtk_widget_set_halign(widget, GTK_ALIGN_FILL); + } else { + gtk_widget_set_vexpand(widget, TRUE); + gtk_widget_set_valign(widget, GTK_ALIGN_FILL); + } + + uiControlSetParent(bc.c, uiControl(b)); + uiUnixControlSetContainer(uiUnixControl(bc.c), b->container, FALSE); + g_array_append_val(b->controls, bc); +} + +void uiBoxDelete(uiBox *b, int index) +{ + struct boxChild *bc; + GtkWidget *widget; + + bc = ctrl(b, index); + widget = GTK_WIDGET(uiControlHandle(bc->c)); + + uiControlSetParent(bc->c, NULL); + uiUnixControlSetContainer(uiUnixControl(bc->c), b->container, TRUE); + + if (bc->stretchy) + gtk_size_group_remove_widget(b->stretchygroup, widget); + gtk_widget_set_hexpand(widget, bc->oldhexpand); + gtk_widget_set_halign(widget, bc->oldhalign); + gtk_widget_set_vexpand(widget, bc->oldvexpand); + gtk_widget_set_valign(widget, bc->oldvalign); + + g_array_remove_index(b->controls, index); +} + +int uiBoxPadded(uiBox *b) +{ + return b->padded; +} + +void uiBoxSetPadded(uiBox *b, int padded) +{ + b->padded = padded; + if (b->padded) + if (b->vertical) + gtk_box_set_spacing(b->box, gtkYPadding); + else + gtk_box_set_spacing(b->box, gtkXPadding); + else + gtk_box_set_spacing(b->box, 0); +} + +static uiBox *finishNewBox(GtkOrientation orientation) +{ + uiBox *b; + + uiUnixNewControl(uiBox, b); + + b->widget = gtk_box_new(orientation, 0); + b->container = GTK_CONTAINER(b->widget); + b->box = GTK_BOX(b->widget); + + b->vertical = orientation == GTK_ORIENTATION_VERTICAL; + + if (b->vertical) + b->stretchygroup = gtk_size_group_new(GTK_SIZE_GROUP_VERTICAL); + else + b->stretchygroup = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL); + + b->controls = g_array_new(FALSE, TRUE, sizeof (struct boxChild)); + + return b; +} + +uiBox *uiNewHorizontalBox(void) +{ + return finishNewBox(GTK_ORIENTATION_HORIZONTAL); +} + +uiBox *uiNewVerticalBox(void) +{ + return finishNewBox(GTK_ORIENTATION_VERTICAL); +} diff --git a/src/libui_sdl/libui/unix/button.c b/src/libui_sdl/libui/unix/button.c new file mode 100644 index 0000000..00a87f4 --- /dev/null +++ b/src/libui_sdl/libui/unix/button.c @@ -0,0 +1,55 @@ +// 10 june 2015 +#include "uipriv_unix.h" + +struct uiButton { + uiUnixControl c; + GtkWidget *widget; + GtkButton *button; + void (*onClicked)(uiButton *, void *); + void *onClickedData; +}; + +uiUnixControlAllDefaults(uiButton) + +static void onClicked(GtkButton *button, gpointer data) +{ + uiButton *b = uiButton(data); + + (*(b->onClicked))(b, b->onClickedData); +} + +static void defaultOnClicked(uiButton *b, void *data) +{ + // do nothing +} + +char *uiButtonText(uiButton *b) +{ + return uiUnixStrdupText(gtk_button_get_label(b->button)); +} + +void uiButtonSetText(uiButton *b, const char *text) +{ + gtk_button_set_label(b->button, text); +} + +void uiButtonOnClicked(uiButton *b, void (*f)(uiButton *, void *), void *data) +{ + b->onClicked = f; + b->onClickedData = data; +} + +uiButton *uiNewButton(const char *text) +{ + uiButton *b; + + uiUnixNewControl(uiButton, b); + + b->widget = gtk_button_new_with_label(text); + b->button = GTK_BUTTON(b->widget); + + g_signal_connect(b->widget, "clicked", G_CALLBACK(onClicked), b); + uiButtonOnClicked(b, defaultOnClicked, NULL); + + return b; +} diff --git a/src/libui_sdl/libui/unix/cellrendererbutton.c b/src/libui_sdl/libui/unix/cellrendererbutton.c new file mode 100644 index 0000000..e3bbf48 --- /dev/null +++ b/src/libui_sdl/libui/unix/cellrendererbutton.c @@ -0,0 +1,299 @@ +// 28 june 2016 +#include "uipriv_unix.h" + +// TODOs +// - it's a rather tight fit +// - selected row text color is white +// - resizing a column with a button in it crashes the program +// - accessibility +// - right side too big? + +#define cellRendererButtonType (cellRendererButton_get_type()) +#define cellRendererButton(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), cellRendererButtonType, cellRendererButton)) +#define isCellRendererButton(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), cellRendererButtonType)) +#define cellRendererButtonClass(class) (G_TYPE_CHECK_CLASS_CAST((class), cellRendererButtonType, cellRendererButtonClass)) +#define isCellRendererButtonClass(class) (G_TYPE_CHECK_CLASS_TYPE((class), cellRendererButton)) +#define getCellRendererButtonClass(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), cellRendererButtonType, cellRendererButtonClass)) + +typedef struct cellRendererButton cellRendererButton; +typedef struct cellRendererButtonClass cellRendererButtonClass; + +struct cellRendererButton { + GtkCellRenderer parent_instance; + char *text; +}; + +struct cellRendererButtonClass { + GtkCellRendererClass parent_class; +}; + +G_DEFINE_TYPE(cellRendererButton, cellRendererButton, GTK_TYPE_CELL_RENDERER) + +static void cellRendererButton_init(cellRendererButton *c) +{ + g_object_set(c, "mode", GTK_CELL_RENDERER_MODE_ACTIVATABLE, NULL); + // the standard cell renderers all do this + gtk_cell_renderer_set_padding(GTK_CELL_RENDERER(c), 2, 2); +} + +static void cellRendererButton_dispose(GObject *obj) +{ + G_OBJECT_CLASS(cellRendererButton_parent_class)->dispose(obj); +} + +static void cellRendererButton_finalize(GObject *obj) +{ + cellRendererButton *c = cellRendererButton(obj); + + if (c->text != NULL) { + g_free(c->text); + c->text = NULL; + } + G_OBJECT_CLASS(cellRendererButton_parent_class)->finalize(obj); +} + +static GtkSizeRequestMode cellRendererButton_get_request_mode(GtkCellRenderer *r) +{ + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + +// this is basically what GtkCellRendererToggle did in 3.10 and does in 3.20, as well as what the Foreign Drawing gtk3-demo demo does +static GtkStyleContext *setButtonStyle(GtkWidget *widget) +{ + GtkStyleContext *base, *context; + GtkWidgetPath *path; + + base = gtk_widget_get_style_context(widget); + context = gtk_style_context_new(); + + path = gtk_widget_path_copy(gtk_style_context_get_path(base)); + gtk_widget_path_append_type(path, G_TYPE_NONE); + if (!FUTURE_gtk_widget_path_iter_set_object_name(path, -1, "button")) + // not on 3.20; try the type + gtk_widget_path_iter_set_object_type(path, -1, GTK_TYPE_BUTTON); + + gtk_style_context_set_path(context, path); + gtk_style_context_set_parent(context, base); + // the gtk3-demo example (which says we need to do this) uses gtk_widget_path_iter_get_state(path, -1) but that's not available until 3.14 + // TODO make a future for that too + gtk_style_context_set_state(context, gtk_style_context_get_state(base)); + gtk_widget_path_unref(path); + + // and if the above widget path screwery stil doesn't work, this will + gtk_style_context_add_class(context, GTK_STYLE_CLASS_BUTTON); + + return context; +} + +void unsetButtonStyle(GtkStyleContext *context) +{ + g_object_unref(context); +} + +// this is based on what GtkCellRendererText does +static void cellRendererButton_get_preferred_width(GtkCellRenderer *r, GtkWidget *widget, gint *minimum, gint *natural) +{ + cellRendererButton *c = cellRendererButton(r); + gint xpad; + PangoLayout *layout; + PangoRectangle rect; + gint out; + + gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, NULL); + + layout = gtk_widget_create_pango_layout(widget, c->text); + pango_layout_set_width(layout, -1); + pango_layout_get_extents(layout, NULL, &rect); + g_object_unref(layout); + + out = 2 * xpad + PANGO_PIXELS_CEIL(rect.width); + if (minimum != NULL) + *minimum = out; + if (natural != NULL) + *natural = out; +} + +// this is based on what GtkCellRendererText does +static void cellRendererButton_get_preferred_height_for_width(GtkCellRenderer *r, GtkWidget *widget, gint width, gint *minimum, gint *natural) +{ + cellRendererButton *c = cellRendererButton(r); + gint xpad, ypad; + PangoLayout *layout; + gint height; + gint out; + + gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, &ypad); + + layout = gtk_widget_create_pango_layout(widget, c->text); + pango_layout_set_width(layout, ((2 * xpad + width) * PANGO_SCALE)); + pango_layout_get_pixel_size(layout, NULL, &height); + g_object_unref(layout); + + out = 2 * ypad + height; + if (minimum != NULL) + *minimum = out; + if (natural != NULL) + *natural = out; +} + +// this is basically what GtkCellRendererText does +static void cellRendererButton_get_preferred_height(GtkCellRenderer *r, GtkWidget *widget, gint *minimum, gint *natural) +{ + gint width; + + gtk_cell_renderer_get_preferred_width(r, widget, &width, NULL); + gtk_cell_renderer_get_preferred_height_for_width(r, widget, width, minimum, natural); +} + +// this is based on what GtkCellRendererText does +static void cellRendererButton_get_aligned_area(GtkCellRenderer *r, GtkWidget *widget, GtkCellRendererState flags, const GdkRectangle *cell_area, GdkRectangle *aligned_area) +{ + cellRendererButton *c = cellRendererButton(r); + gint xpad, ypad; + PangoLayout *layout; + PangoRectangle rect; + gfloat xalign, yalign; + gint xoffset, yoffset; + + gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, &ypad); + + layout = gtk_widget_create_pango_layout(widget, c->text); + pango_layout_set_width(layout, -1); + pango_layout_get_pixel_extents(layout, NULL, &rect); + + xoffset = 0; + yoffset = 0; + if (cell_area != NULL) { + gtk_cell_renderer_get_alignment(GTK_CELL_RENDERER(c), &xalign, &yalign); + xoffset = cell_area->width - (2 * xpad + rect.width); + // use explicit casts just to be safe + if (gtk_widget_get_direction(widget) == GTK_TEXT_DIR_RTL) + xoffset = ((gdouble) xoffset) * (1.0 - xalign); + else + xoffset *= ((gdouble) xoffset) * xalign; + yoffset = yalign * (cell_area->height - (2 * ypad + rect.height)); + yoffset = MAX(yoffset, 0); + } + + aligned_area->x = cell_area->x + xoffset; + aligned_area->y = cell_area->y + yoffset; + aligned_area->width = 2 * xpad + rect.width; + aligned_area->height = 2 * ypad + rect.height; + + g_object_unref(layout); +} + +// this is based on both what GtkCellRendererText does and what GtkCellRendererToggle does +static void cellRendererButton_render(GtkCellRenderer *r, cairo_t *cr, GtkWidget *widget, const GdkRectangle *background_area, const GdkRectangle *cell_area, GtkCellRendererState flags) +{ + cellRendererButton *c = cellRendererButton(r); + gint xpad, ypad; + GdkRectangle alignedArea; + gint xoffset, yoffset; + GtkStyleContext *context; + PangoLayout *layout; + PangoRectangle rect; + + gtk_cell_renderer_get_padding(GTK_CELL_RENDERER(c), &xpad, &ypad); + gtk_cell_renderer_get_aligned_area(GTK_CELL_RENDERER(c), widget, flags, cell_area, &alignedArea); + xoffset = alignedArea.x - cell_area->x; + yoffset = alignedArea.y - cell_area->y; + + context = setButtonStyle(widget); + layout = gtk_widget_create_pango_layout(widget, c->text); + + gtk_render_background(context, cr, + background_area->x + xoffset + xpad, + background_area->y + yoffset + ypad, + background_area->width - 2 * xpad, + background_area->height - 2 * ypad); + gtk_render_frame(context, cr, + background_area->x + xoffset + xpad, + background_area->y + yoffset + ypad, + background_area->width - 2 * xpad, + background_area->height - 2 * ypad); + + pango_layout_set_width(layout, -1); + pango_layout_get_pixel_extents(layout, NULL, &rect); + xoffset -= rect.x; + gtk_render_layout(context, cr, + cell_area->x + xoffset + xpad, + cell_area->y + yoffset + ypad, + layout); + + g_object_unref(layout); + unsetButtonStyle(context); +} + +static guint clickedSignal; + +static gboolean cellRendererButton_activate(GtkCellRenderer *r, GdkEvent *e, GtkWidget *widget, const gchar *path, const GdkRectangle *background_area, const GdkRectangle *cell_area, GtkCellRendererState flags) +{ + g_signal_emit(r, clickedSignal, 0, path); + return TRUE; +} + +static GParamSpec *props[2] = { NULL, NULL }; + +static void cellRendererButton_set_property(GObject *object, guint prop, const GValue *value, GParamSpec *pspec) +{ + cellRendererButton *c = cellRendererButton(object); + + if (prop != 1) { + G_OBJECT_WARN_INVALID_PROPERTY_ID(c, prop, pspec); + return; + } + if (c->text != NULL) + g_free(c->text); + c->text = g_value_dup_string(value); + // GtkCellRendererText doesn't queue a redraw; we won't either +} + +static void cellRendererButton_get_property(GObject *object, guint prop, GValue *value, GParamSpec *pspec) +{ + cellRendererButton *c = cellRendererButton(object); + + if (prop != 1) { + G_OBJECT_WARN_INVALID_PROPERTY_ID(c, prop, pspec); + return; + } + g_value_set_string(value, c->text); +} + +static void cellRendererButton_class_init(cellRendererButtonClass *class) +{ + G_OBJECT_CLASS(class)->dispose = cellRendererButton_dispose; + G_OBJECT_CLASS(class)->finalize = cellRendererButton_finalize; + G_OBJECT_CLASS(class)->set_property = cellRendererButton_set_property; + G_OBJECT_CLASS(class)->get_property = cellRendererButton_get_property; + GTK_CELL_RENDERER_CLASS(class)->get_request_mode = cellRendererButton_get_request_mode; + GTK_CELL_RENDERER_CLASS(class)->get_preferred_width = cellRendererButton_get_preferred_width; + GTK_CELL_RENDERER_CLASS(class)->get_preferred_height_for_width = cellRendererButton_get_preferred_height_for_width; + GTK_CELL_RENDERER_CLASS(class)->get_preferred_height = cellRendererButton_get_preferred_height; + // don't provide a get_preferred_width_for_height() + GTK_CELL_RENDERER_CLASS(class)->get_aligned_area = cellRendererButton_get_aligned_area; + // don't provide a get_size() + GTK_CELL_RENDERER_CLASS(class)->render = cellRendererButton_render; + GTK_CELL_RENDERER_CLASS(class)->activate = cellRendererButton_activate; + // don't provide a start_editing() + + props[1] = g_param_spec_string("text", + "Text", + "Button text", + "", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties(G_OBJECT_CLASS(class), 2, props); + + clickedSignal = g_signal_new("clicked", + G_TYPE_FROM_CLASS(class), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_STRING); +} + +GtkCellRenderer *newCellRendererButton(void) +{ + return GTK_CELL_RENDERER(g_object_new(cellRendererButtonType, NULL)); +} diff --git a/src/libui_sdl/libui/unix/checkbox.c b/src/libui_sdl/libui/unix/checkbox.c new file mode 100644 index 0000000..47f8514 --- /dev/null +++ b/src/libui_sdl/libui/unix/checkbox.c @@ -0,0 +1,78 @@ +// 10 june 2015 +#include "uipriv_unix.h" + +struct uiCheckbox { + uiUnixControl c; + GtkWidget *widget; + GtkButton *button; + GtkToggleButton *toggleButton; + GtkCheckButton *checkButton; + void (*onToggled)(uiCheckbox *, void *); + void *onToggledData; + gulong onToggledSignal; +}; + +uiUnixControlAllDefaults(uiCheckbox) + +static void onToggled(GtkToggleButton *b, gpointer data) +{ + uiCheckbox *c = uiCheckbox(data); + + (*(c->onToggled))(c, c->onToggledData); +} + +static void defaultOnToggled(uiCheckbox *c, void *data) +{ + // do nothing +} + +char *uiCheckboxText(uiCheckbox *c) +{ + return uiUnixStrdupText(gtk_button_get_label(c->button)); +} + +void uiCheckboxSetText(uiCheckbox *c, const char *text) +{ + gtk_button_set_label(GTK_BUTTON(c->button), text); +} + +void uiCheckboxOnToggled(uiCheckbox *c, void (*f)(uiCheckbox *, void *), void *data) +{ + c->onToggled = f; + c->onToggledData = data; +} + +int uiCheckboxChecked(uiCheckbox *c) +{ + return gtk_toggle_button_get_active(c->toggleButton) != FALSE; +} + +void uiCheckboxSetChecked(uiCheckbox *c, int checked) +{ + gboolean active; + + active = FALSE; + if (checked) + active = TRUE; + // we need to inhibit sending of ::toggled because this WILL send a ::toggled otherwise + g_signal_handler_block(c->toggleButton, c->onToggledSignal); + gtk_toggle_button_set_active(c->toggleButton, active); + g_signal_handler_unblock(c->toggleButton, c->onToggledSignal); +} + +uiCheckbox *uiNewCheckbox(const char *text) +{ + uiCheckbox *c; + + uiUnixNewControl(uiCheckbox, c); + + c->widget = gtk_check_button_new_with_label(text); + c->button = GTK_BUTTON(c->widget); + c->toggleButton = GTK_TOGGLE_BUTTON(c->widget); + c->checkButton = GTK_CHECK_BUTTON(c->widget); + + c->onToggledSignal = g_signal_connect(c->widget, "toggled", G_CALLBACK(onToggled), c); + uiCheckboxOnToggled(c, defaultOnToggled, NULL); + + return c; +} diff --git a/src/libui_sdl/libui/unix/child.c b/src/libui_sdl/libui/unix/child.c new file mode 100644 index 0000000..b4a0967 --- /dev/null +++ b/src/libui_sdl/libui/unix/child.c @@ -0,0 +1,120 @@ +// 28 august 2015 +#include "uipriv_unix.h" + +// This file contains helpers for managing child controls. + +struct child { + uiControl *c; + GtkWidget *widget; + + gboolean oldhexpand; + GtkAlign oldhalign; + gboolean oldvexpand; + GtkAlign oldvalign; + + // Some children can be boxed; that is, they can have an optionally-margined box around them. + // uiGroup, uiTab, and uiWindow all do this. + GtkWidget *box; + + // If the child is not boxed, this is its parent. + // If the child is boxed, this is the box. + GtkContainer *parent; + + // This flag is for users of these functions. + // For uiBox, this is "spaced". + // For uiTab, this is "margined". (uiGroup and uiWindow have to maintain their margined state themselves, since the margined state is independent of whether there is a child for those two.) + int flag; +}; + +struct child *newChild(uiControl *child, uiControl *parent, GtkContainer *parentContainer) +{ + struct child *c; + + if (child == NULL) + return NULL; + + c = uiNew(struct child); + c->c = child; + c->widget = GTK_WIDGET(uiControlHandle(c->c)); + + c->oldhexpand = gtk_widget_get_hexpand(c->widget); + c->oldhalign = gtk_widget_get_halign(c->widget); + c->oldvexpand = gtk_widget_get_vexpand(c->widget); + c->oldvalign = gtk_widget_get_valign(c->widget); + + uiControlSetParent(c->c, parent); + uiUnixControlSetContainer(uiUnixControl(c->c), parentContainer, FALSE); + c->parent = parentContainer; + + return c; +} + +struct child *newChildWithBox(uiControl *child, uiControl *parent, GtkContainer *parentContainer, int margined) +{ + struct child *c; + GtkWidget *box; + + if (child == NULL) + return NULL; + box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + gtk_widget_show(box); + c = newChild(child, parent, GTK_CONTAINER(box)); + gtk_widget_set_hexpand(c->widget, TRUE); + gtk_widget_set_halign(c->widget, GTK_ALIGN_FILL); + gtk_widget_set_vexpand(c->widget, TRUE); + gtk_widget_set_valign(c->widget, GTK_ALIGN_FILL); + c->box = box; + gtk_container_add(parentContainer, c->box); + childSetMargined(c, margined); + return c; +} + +void childRemove(struct child *c) +{ + uiControlSetParent(c->c, NULL); + uiUnixControlSetContainer(uiUnixControl(c->c), c->parent, TRUE); + + gtk_widget_set_hexpand(c->widget, c->oldhexpand); + gtk_widget_set_halign(c->widget, c->oldhalign); + gtk_widget_set_vexpand(c->widget, c->oldvexpand); + gtk_widget_set_valign(c->widget, c->oldvalign); + + if (c->box != NULL) + gtk_widget_destroy(c->box); + + uiFree(c); +} + +void childDestroy(struct child *c) +{ + uiControl *child; + + child = c->c; + childRemove(c); + uiControlDestroy(child); +} + +GtkWidget *childWidget(struct child *c) +{ + return c->widget; +} + +int childFlag(struct child *c) +{ + return c->flag; +} + +void childSetFlag(struct child *c, int flag) +{ + c->flag = flag; +} + +GtkWidget *childBox(struct child *c) +{ + return c->box; +} + +void childSetMargined(struct child *c, int margined) +{ + setMargined(GTK_CONTAINER(c->box), margined); +} diff --git a/src/libui_sdl/libui/unix/colorbutton.c b/src/libui_sdl/libui/unix/colorbutton.c new file mode 100644 index 0000000..393b16f --- /dev/null +++ b/src/libui_sdl/libui/unix/colorbutton.c @@ -0,0 +1,80 @@ +// 15 may 2016 +#include "uipriv_unix.h" + +struct uiColorButton { + uiUnixControl c; + GtkWidget *widget; + GtkButton *button; + GtkColorButton *cb; + GtkColorChooser *cc; + void (*onChanged)(uiColorButton *, void *); + void *onChangedData; +}; + +uiUnixControlAllDefaults(uiColorButton) + +static void onColorSet(GtkColorButton *button, gpointer data) +{ + uiColorButton *b = uiColorButton(data); + + (*(b->onChanged))(b, b->onChangedData); +} + +static void defaultOnChanged(uiColorButton *b, void *data) +{ + // do nothing +} + +void uiColorButtonColor(uiColorButton *b, double *r, double *g, double *bl, double *a) +{ + GdkRGBA rgba; + + gtk_color_chooser_get_rgba(b->cc, &rgba); + *r = rgba.red; + *g = rgba.green; + *bl = rgba.blue; + *a = rgba.alpha; +} + +void uiColorButtonSetColor(uiColorButton *b, double r, double g, double bl, double a) +{ + GdkRGBA rgba; + + rgba.red = r; + rgba.green = g; + rgba.blue = bl; + rgba.alpha = a; + // no need to inhibit the signal; color-set is documented as only being sent when the user changes the color + gtk_color_chooser_set_rgba(b->cc, &rgba); +} + +void uiColorButtonOnChanged(uiColorButton *b, void (*f)(uiColorButton *, void *), void *data) +{ + b->onChanged = f; + b->onChangedData = data; +} + +uiColorButton *uiNewColorButton(void) +{ + uiColorButton *b; + GdkRGBA black; + + uiUnixNewControl(uiColorButton, b); + + // I'm not sure what the initial color is; set up a real one + black.red = 0.0; + black.green = 0.0; + black.blue = 0.0; + black.alpha = 1.0; + b->widget = gtk_color_button_new_with_rgba(&black); + b->button = GTK_BUTTON(b->widget); + b->cb = GTK_COLOR_BUTTON(b->widget); + b->cc = GTK_COLOR_CHOOSER(b->widget); + + gtk_color_chooser_set_use_alpha(b->cc, TRUE); + + g_signal_connect(b->widget, "color-set", G_CALLBACK(onColorSet), b); + uiColorButtonOnChanged(b, defaultOnChanged, NULL); + + return b; +} diff --git a/src/libui_sdl/libui/unix/combobox.c b/src/libui_sdl/libui/unix/combobox.c new file mode 100644 index 0000000..6fed804 --- /dev/null +++ b/src/libui_sdl/libui/unix/combobox.c @@ -0,0 +1,66 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +struct uiCombobox { + uiUnixControl c; + GtkWidget *widget; + GtkComboBox *combobox; + GtkComboBoxText *comboboxText; + void (*onSelected)(uiCombobox *, void *); + void *onSelectedData; + gulong onSelectedSignal; +}; + +uiUnixControlAllDefaults(uiCombobox) + +static void onChanged(GtkComboBox *cbox, gpointer data) +{ + uiCombobox *c = uiCombobox(data); + + (*(c->onSelected))(c, c->onSelectedData); +} + +static void defaultOnSelected(uiCombobox *c, void *data) +{ + // do nothing +} + +void uiComboboxAppend(uiCombobox *c, const char *text) +{ + gtk_combo_box_text_append(c->comboboxText, NULL, text); +} + +int uiComboboxSelected(uiCombobox *c) +{ + return gtk_combo_box_get_active(c->combobox); +} + +void uiComboboxSetSelected(uiCombobox *c, int n) +{ + // we need to inhibit sending of ::changed because this WILL send a ::changed otherwise + g_signal_handler_block(c->combobox, c->onSelectedSignal); + gtk_combo_box_set_active(c->combobox, n); + g_signal_handler_unblock(c->combobox, c->onSelectedSignal); +} + +void uiComboboxOnSelected(uiCombobox *c, void (*f)(uiCombobox *c, void *data), void *data) +{ + c->onSelected = f; + c->onSelectedData = data; +} + +uiCombobox *uiNewCombobox(void) +{ + uiCombobox *c; + + uiUnixNewControl(uiCombobox, c); + + c->widget = gtk_combo_box_text_new(); + c->combobox = GTK_COMBO_BOX(c->widget); + c->comboboxText = GTK_COMBO_BOX_TEXT(c->widget); + + c->onSelectedSignal = g_signal_connect(c->widget, "changed", G_CALLBACK(onChanged), c); + uiComboboxOnSelected(c, defaultOnSelected, NULL); + + return c; +} diff --git a/src/libui_sdl/libui/unix/control.c b/src/libui_sdl/libui/unix/control.c new file mode 100644 index 0000000..f6fdcea --- /dev/null +++ b/src/libui_sdl/libui/unix/control.c @@ -0,0 +1,14 @@ +// 16 august 2015 +#include "uipriv_unix.h" + +void uiUnixControlSetContainer(uiUnixControl *c, GtkContainer *container, gboolean remove) +{ + (*(c->SetContainer))(c, container, remove); +} + +#define uiUnixControlSignature 0x556E6978 + +uiUnixControl *uiUnixAllocControl(size_t n, uint32_t typesig, const char *typenamestr) +{ + return uiUnixControl(uiAllocControl(n, uiUnixControlSignature, typesig, typenamestr)); +} diff --git a/src/libui_sdl/libui/unix/datetimepicker.c b/src/libui_sdl/libui/unix/datetimepicker.c new file mode 100644 index 0000000..19689a2 --- /dev/null +++ b/src/libui_sdl/libui/unix/datetimepicker.c @@ -0,0 +1,599 @@ +// 4 september 2015 +#include "uipriv_unix.h" + +// LONGTERM imitate gnome-calendar's day/month/year entries above the calendar +// LONGTERM allow entering a 24-hour hour in the hour spinbutton and adjust accordingly + +#define dateTimePickerWidgetType (dateTimePickerWidget_get_type()) +#define dateTimePickerWidget(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), dateTimePickerWidgetType, dateTimePickerWidget)) +#define isDateTimePickerWidget(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), dateTimePickerWidgetType)) +#define dateTimePickerWidgetClass(class) (G_TYPE_CHECK_CLASS_CAST((class), dateTimePickerWidgetType, dateTimePickerWidgetClass)) +#define isDateTimePickerWidgetClass(class) (G_TYPE_CHECK_CLASS_TYPE((class), dateTimePickerWidget)) +#define getDateTimePickerWidgetClass(obj) (G_TYPE_INSTANCE_GET_CLASS((obj), dateTimePickerWidgetType, dateTimePickerWidgetClass)) + +typedef struct dateTimePickerWidget dateTimePickerWidget; +typedef struct dateTimePickerWidgetClass dateTimePickerWidgetClass; + +struct dateTimePickerWidget { + GtkToggleButton parent_instance; + + gulong toggledSignal; + + gboolean hasTime; + gboolean hasDate; + + GtkWidget *window; + GtkWidget *box; + GtkWidget *calendar; + GtkWidget *timebox; + GtkWidget *hours; + GtkWidget *minutes; + GtkWidget *seconds; + GtkWidget *ampm; + + gulong hoursBlock; + gulong minutesBlock; + gulong secondsBlock; + gulong ampmBlock; + + GdkDevice *keyboard; + GdkDevice *mouse; +}; + +struct dateTimePickerWidgetClass { + GtkToggleButtonClass parent_class; +}; + +G_DEFINE_TYPE(dateTimePickerWidget, dateTimePickerWidget, GTK_TYPE_TOGGLE_BUTTON) + +static int realSpinValue(GtkSpinButton *spinButton) +{ + GtkAdjustment *adj; + + adj = gtk_spin_button_get_adjustment(spinButton); + return (int) gtk_adjustment_get_value(adj); +} + +static void setRealSpinValue(GtkSpinButton *spinButton, int value, gulong block) +{ + GtkAdjustment *adj; + + g_signal_handler_block(spinButton, block); + adj = gtk_spin_button_get_adjustment(spinButton); + gtk_adjustment_set_value(adj, value); + g_signal_handler_unblock(spinButton, block); +} + +static GDateTime *selected(dateTimePickerWidget *d) +{ + // choose a day for which all times are likely to be valid for the default date in case we're only dealing with time + guint year = 1970, month = 1, day = 1; + guint hour = 0, minute = 0, second = 0; + + if (d->hasDate) { + gtk_calendar_get_date(GTK_CALENDAR(d->calendar), &year, &month, &day); + month++; // GtkCalendar/GDateTime differences + } + if (d->hasTime) { + hour = realSpinValue(GTK_SPIN_BUTTON(d->hours)); + if (realSpinValue(GTK_SPIN_BUTTON(d->ampm)) != 0) + hour += 12; + minute = realSpinValue(GTK_SPIN_BUTTON(d->minutes)); + second = realSpinValue(GTK_SPIN_BUTTON(d->seconds)); + } + return g_date_time_new_local(year, month, day, hour, minute, second); +} + +static void setLabel(dateTimePickerWidget *d) +{ + GDateTime *dt; + char *fmt; + char *msg; + gboolean free; + + dt = selected(d); + free = FALSE; + if (d->hasDate && d->hasTime) { + // don't use D_T_FMT; that's too verbose + fmt = g_strdup_printf("%s %s", nl_langinfo(D_FMT), nl_langinfo(T_FMT)); + free = TRUE; + } else if (d->hasDate) + fmt = nl_langinfo(D_FMT); + else + fmt = nl_langinfo(T_FMT); + msg = g_date_time_format(dt, fmt); + gtk_button_set_label(GTK_BUTTON(d), msg); + g_free(msg); + if (free) + g_free(fmt); + g_date_time_unref(dt); +} + +static void dateTimeChanged(dateTimePickerWidget *d) +{ + setLabel(d); + // TODO fire event here +} + +// we don't want ::toggled to be sent again +static void setActive(dateTimePickerWidget *d, gboolean active) +{ + g_signal_handler_block(d, d->toggledSignal); + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(d), active); + g_signal_handler_unblock(d, d->toggledSignal); +} + +// like startGrab() below, a lot of this is in the order that GtkComboBox does it +static void endGrab(dateTimePickerWidget *d) +{ + if (d->keyboard != NULL) + gdk_device_ungrab(d->keyboard, GDK_CURRENT_TIME); + gdk_device_ungrab(d->mouse, GDK_CURRENT_TIME); + gtk_device_grab_remove(d->window, d->mouse); + d->keyboard = NULL; + d->mouse = NULL; +} + +static void hidePopup(dateTimePickerWidget *d) +{ + endGrab(d); + gtk_widget_hide(d->window); + setActive(d, FALSE); +} + +// this consolidates a good chunk of what GtkComboBox does +static gboolean startGrab(dateTimePickerWidget *d) +{ + GdkDevice *dev; + guint32 time; + GdkWindow *window; + GdkDevice *keyboard, *mouse; + + dev = gtk_get_current_event_device(); + if (dev == NULL) { + // this is what GtkComboBox does + // since no device was set, just use the first available "master device" + GdkDisplay *disp; + GdkDeviceManager *dm; + GList *list; + + disp = gtk_widget_get_display(GTK_WIDGET(d)); + dm = gdk_display_get_device_manager(disp); + list = gdk_device_manager_list_devices(dm, GDK_DEVICE_TYPE_MASTER); + dev = (GdkDevice *) (list->data); + g_list_free(list); + } + + time = gtk_get_current_event_time(); + keyboard = dev; + mouse = gdk_device_get_associated_device(dev); + if (gdk_device_get_source(dev) != GDK_SOURCE_KEYBOARD) { + dev = mouse; + mouse = keyboard; + keyboard = dev; + } + + window = gtk_widget_get_window(d->window); + if (keyboard != NULL) + if (gdk_device_grab(keyboard, window, + GDK_OWNERSHIP_WINDOW, TRUE, + GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK, + NULL, time) != GDK_GRAB_SUCCESS) + return FALSE; + if (mouse != NULL) + if (gdk_device_grab(mouse, window, + GDK_OWNERSHIP_WINDOW, TRUE, + GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK, + NULL, time) != GDK_GRAB_SUCCESS) { + if (keyboard != NULL) + gdk_device_ungrab(keyboard, time); + return FALSE; + } + + gtk_device_grab_add(d->window, mouse, TRUE); + d->keyboard = keyboard; + d->mouse = mouse; + return TRUE; +} + +// based on gtk_combo_box_list_position() in the GTK+ source code +static void allocationToScreen(dateTimePickerWidget *d, gint *x, gint *y) +{ + GdkWindow *window; + GtkAllocation a; + GtkRequisition aWin; + GdkScreen *screen; + GdkRectangle workarea; + int otherY; + + gtk_widget_get_allocation(GTK_WIDGET(d), &a); + gtk_widget_get_preferred_size(d->window, &aWin, NULL); + *x = 0; + *y = 0; + if (!gtk_widget_get_has_window(GTK_WIDGET(d))) { + *x = a.x; + *y = a.y; + } + window = gtk_widget_get_window(GTK_WIDGET(d)); + gdk_window_get_root_coords(window, *x, *y, x, y); + if (gtk_widget_get_direction(GTK_WIDGET(d)) == GTK_TEXT_DIR_RTL) + *x += a.width - aWin.width; + + // now adjust to prevent the box from going offscreen + screen = gtk_widget_get_screen(GTK_WIDGET(d)); + gdk_screen_get_monitor_workarea(screen, + gdk_screen_get_monitor_at_window(screen, window), + &workarea); + if (*x < workarea.x) // too far to the left? + *x = workarea.x; + else if (*x + aWin.width > (workarea.x + workarea.width)) // too far to the right? + *x = (workarea.x + workarea.width) - aWin.width; + // this isn't the same algorithm used by GtkComboBox + // first, get our two choices; *y for down and otherY for up + otherY = *y - aWin.height; + *y += a.height; + // and use otherY if we're too low + if (*y + aWin.height >= workarea.y + workarea.height) + *y = otherY; +} + +static void showPopup(dateTimePickerWidget *d) +{ + GtkWidget *toplevel; + gint x, y; + + // GtkComboBox does it + toplevel = gtk_widget_get_toplevel(GTK_WIDGET(d)); + if (GTK_IS_WINDOW(toplevel)) + gtk_window_group_add_window(gtk_window_get_group(GTK_WINDOW(toplevel)), GTK_WINDOW(d->window)); + + allocationToScreen(d, &x, &y); + gtk_window_move(GTK_WINDOW(d->window), x, y); + + gtk_widget_show(d->window); + setActive(d, TRUE); + + if (!startGrab(d)) + hidePopup(d); +} + +static void onToggled(GtkToggleButton *b, gpointer data) +{ + dateTimePickerWidget *d = dateTimePickerWidget(b); + + if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(d))) + showPopup(d); + else + hidePopup(d); +} + +static gboolean grabBroken(GtkWidget *w, GdkEventGrabBroken *e, gpointer data) +{ + dateTimePickerWidget *d = dateTimePickerWidget(data); + + hidePopup(d); + return TRUE; // this is what GtkComboBox does +} + +static gboolean buttonReleased(GtkWidget *w, GdkEventButton *e, gpointer data) +{ + dateTimePickerWidget *d = dateTimePickerWidget(data); + int winx, winy; + GtkAllocation wina; + gboolean in; + + gtk_widget_get_allocation(d->window, &wina); + winx = 0; + winy = 0; + if (!gtk_widget_get_has_window(d->window)) { + winx = wina.x; + winy = wina.y; + } + gdk_window_get_root_coords(gtk_widget_get_window(d->window), winx, winy, &winx, &winy); + in = TRUE; + if (e->x_root < winx) + in = FALSE; + if (e->x_root >= (winx + wina.width)) + in = FALSE; + if (e->y_root < winy) + in = FALSE; + if (e->y_root >= (winy + wina.height)) + in = FALSE; + if (!in) + hidePopup(d); + return TRUE; // this is what GtkComboBox does +} + +static gint hoursSpinboxInput(GtkSpinButton *sb, gpointer ptr, gpointer data) +{ + double *out = (double *) ptr; + const gchar *text; + int value; + + text = gtk_entry_get_text(GTK_ENTRY(sb)); + value = (int) g_strtod(text, NULL); + if (value < 0 || value > 12) + return GTK_INPUT_ERROR; + if (value == 12) // 12 to the user is 0 internally + value = 0; + *out = (double) value; + return TRUE; +} + +static gboolean hoursSpinboxOutput(GtkSpinButton *sb, gpointer data) +{ + gchar *text; + int value; + + value = realSpinValue(sb); + if (value == 0) // 0 internally is 12 to the user + value = 12; + text = g_strdup_printf("%d", value); + gtk_entry_set_text(GTK_ENTRY(sb), text); + g_free(text); + return TRUE; +} + +static gboolean zeroPadSpinbox(GtkSpinButton *sb, gpointer data) +{ + gchar *text; + int value; + + value = realSpinValue(sb); + text = g_strdup_printf("%02d", value); + gtk_entry_set_text(GTK_ENTRY(sb), text); + g_free(text); + return TRUE; +} + +// this is really hacky but we can't use GtkCombobox here :( +static gint ampmSpinboxInput(GtkSpinButton *sb, gpointer ptr, gpointer data) +{ + double *out = (double *) ptr; + const gchar *text; + char firstAM, firstPM; + + text = gtk_entry_get_text(GTK_ENTRY(sb)); + // LONGTERM don't use ASCII here for case insensitivity + firstAM = g_ascii_tolower(nl_langinfo(AM_STR)[0]); + firstPM = g_ascii_tolower(nl_langinfo(PM_STR)[0]); + for (; *text != '\0'; text++) + if (g_ascii_tolower(*text) == firstAM) { + *out = 0; + return TRUE; + } else if (g_ascii_tolower(*text) == firstPM) { + *out = 1; + return TRUE; + } + return GTK_INPUT_ERROR; +} + +static gboolean ampmSpinboxOutput(GtkSpinButton *sb, gpointer data) +{ + int value; + + value = gtk_spin_button_get_value_as_int(sb); + if (value == 0) + gtk_entry_set_text(GTK_ENTRY(sb), nl_langinfo(AM_STR)); + else + gtk_entry_set_text(GTK_ENTRY(sb), nl_langinfo(PM_STR)); + return TRUE; +} + +static void spinboxChanged(GtkSpinButton *sb, gpointer data) +{ + dateTimePickerWidget *d = dateTimePickerWidget(data); + + dateTimeChanged(d); +} + +static GtkWidget *newSpinbox(dateTimePickerWidget *d, int min, int max, gint (*input)(GtkSpinButton *, gpointer, gpointer), gboolean (*output)(GtkSpinButton *, gpointer), gulong *block) +{ + GtkWidget *sb; + + sb = gtk_spin_button_new_with_range(min, max, 1); + gtk_spin_button_set_digits(GTK_SPIN_BUTTON(sb), 0); + gtk_spin_button_set_wrap(GTK_SPIN_BUTTON(sb), TRUE); + gtk_orientable_set_orientation(GTK_ORIENTABLE(sb), GTK_ORIENTATION_VERTICAL); + *block = g_signal_connect(sb, "value-changed", G_CALLBACK(spinboxChanged), d); + if (input != NULL) + g_signal_connect(sb, "input", G_CALLBACK(input), NULL); + if (output != NULL) + g_signal_connect(sb, "output", G_CALLBACK(output), NULL); + return sb; +} + +static void dateChanged(GtkCalendar *c, gpointer data) +{ + dateTimePickerWidget *d = dateTimePickerWidget(data); + + dateTimeChanged(d); +} + +static void setDateOnly(dateTimePickerWidget *d) +{ + d->hasTime = FALSE; + gtk_container_remove(GTK_CONTAINER(d->box), d->timebox); +} + +static void setTimeOnly(dateTimePickerWidget *d) +{ + d->hasDate = FALSE; + gtk_container_remove(GTK_CONTAINER(d->box), d->calendar); +} + +static void dateTimePickerWidget_init(dateTimePickerWidget *d) +{ + GDateTime *dt; + gint year, month, day; + gint hour; + gulong calendarBlock; + + d->window = gtk_window_new(GTK_WINDOW_POPUP); + gtk_window_set_resizable(GTK_WINDOW(d->window), FALSE); + gtk_window_set_attached_to(GTK_WINDOW(d->window), GTK_WIDGET(d)); + gtk_window_set_decorated(GTK_WINDOW(d->window), FALSE); + gtk_window_set_deletable(GTK_WINDOW(d->window), FALSE); + gtk_window_set_type_hint(GTK_WINDOW(d->window), GDK_WINDOW_TYPE_HINT_COMBO); + gtk_window_set_skip_taskbar_hint(GTK_WINDOW(d->window), TRUE); + gtk_window_set_skip_pager_hint(GTK_WINDOW(d->window), TRUE); + gtk_window_set_has_resize_grip(GTK_WINDOW(d->window), FALSE); + gtk_container_set_border_width(GTK_CONTAINER(d->window), 12); + // and make it stand out a bit + gtk_style_context_add_class(gtk_widget_get_style_context(d->window), "frame"); + + d->box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6); + gtk_container_add(GTK_CONTAINER(d->window), d->box); + + d->calendar = gtk_calendar_new(); + calendarBlock = g_signal_connect(d->calendar, "day-selected", G_CALLBACK(dateChanged), d); + gtk_container_add(GTK_CONTAINER(d->box), d->calendar); + + d->timebox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); + gtk_widget_set_valign(d->timebox, GTK_ALIGN_CENTER); + gtk_container_add(GTK_CONTAINER(d->box), d->timebox); + + d->hours = newSpinbox(d, 0, 11, hoursSpinboxInput, hoursSpinboxOutput, &(d->hoursBlock)); + gtk_container_add(GTK_CONTAINER(d->timebox), d->hours); + + gtk_container_add(GTK_CONTAINER(d->timebox), + gtk_label_new(":")); + + d->minutes = newSpinbox(d, 0, 59, NULL, zeroPadSpinbox, &(d->minutesBlock)); + gtk_container_add(GTK_CONTAINER(d->timebox), d->minutes); + + gtk_container_add(GTK_CONTAINER(d->timebox), + gtk_label_new(":")); + + d->seconds = newSpinbox(d, 0, 59, NULL, zeroPadSpinbox, &(d->secondsBlock)); + gtk_container_add(GTK_CONTAINER(d->timebox), d->seconds); + + // LONGTERM this should be the case, but that interferes with grabs + // switch to it when we can drop GTK+ 3.10 and use popovers +#if 0 + d->ampm = gtk_combo_box_text_new(); + gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(d->ampm), NULL, "AM"); + gtk_combo_box_text_append(GTK_COMBO_BOX_TEXT(d->ampm), NULL, "PM"); +#endif + d->ampm = newSpinbox(d, 0, 1, ampmSpinboxInput, ampmSpinboxOutput, &(d->ampmBlock)); + gtk_spin_button_set_numeric(GTK_SPIN_BUTTON(d->ampm), FALSE); + gtk_widget_set_valign(d->ampm, GTK_ALIGN_CENTER); + gtk_container_add(GTK_CONTAINER(d->timebox), d->ampm); + + gtk_widget_show_all(d->box); + + g_signal_connect(d->window, "grab-broken-event", G_CALLBACK(grabBroken), d); + g_signal_connect(d->window, "button-release-event", G_CALLBACK(buttonReleased), d); + + d->toggledSignal = g_signal_connect(d, "toggled", G_CALLBACK(onToggled), NULL); + d->keyboard = NULL; + d->mouse = NULL; + + d->hasTime = TRUE; + d->hasDate = TRUE; + + // set the current date/time + // notice how we block signals from firing + dt = g_date_time_new_now_local(); + g_date_time_get_ymd(dt, &year, &month, &day); + month--; // GDateTime/GtkCalendar differences + g_signal_handler_block(d->calendar, calendarBlock); + gtk_calendar_select_month(GTK_CALENDAR(d->calendar), month, year); + gtk_calendar_select_day(GTK_CALENDAR(d->calendar), day); + g_signal_handler_unblock(d->calendar, calendarBlock); + hour = g_date_time_get_hour(dt); + if (hour >= 12) { + hour -= 12; + setRealSpinValue(GTK_SPIN_BUTTON(d->ampm), 1, d->ampmBlock); + } + setRealSpinValue(GTK_SPIN_BUTTON(d->hours), hour, d->hoursBlock); + setRealSpinValue(GTK_SPIN_BUTTON(d->minutes), g_date_time_get_minute(dt), d->minutesBlock); + setRealSpinValue(GTK_SPIN_BUTTON(d->seconds), g_date_time_get_seconds(dt), d->secondsBlock); + g_date_time_unref(dt); +} + +static void dateTimePickerWidget_dispose(GObject *obj) +{ + dateTimePickerWidget *d = dateTimePickerWidget(obj); + + if (d->window != NULL) { + gtk_widget_destroy(d->window); + d->window = NULL; + } + G_OBJECT_CLASS(dateTimePickerWidget_parent_class)->dispose(obj); +} + +static void dateTimePickerWidget_finalize(GObject *obj) +{ + G_OBJECT_CLASS(dateTimePickerWidget_parent_class)->finalize(obj); +} + +static void dateTimePickerWidget_class_init(dateTimePickerWidgetClass *class) +{ + G_OBJECT_CLASS(class)->dispose = dateTimePickerWidget_dispose; + G_OBJECT_CLASS(class)->finalize = dateTimePickerWidget_finalize; +} + +static GtkWidget *newDTP(void) +{ + GtkWidget *w; + + w = GTK_WIDGET(g_object_new(dateTimePickerWidgetType, "label", "", NULL)); + setLabel(dateTimePickerWidget(w)); + return w; +} + +static GtkWidget *newDP(void) +{ + GtkWidget *w; + + w = GTK_WIDGET(g_object_new(dateTimePickerWidgetType, "label", "", NULL)); + setDateOnly(dateTimePickerWidget(w)); + setLabel(dateTimePickerWidget(w)); + return w; +} + +static GtkWidget *newTP(void) +{ + GtkWidget *w; + + w = GTK_WIDGET(g_object_new(dateTimePickerWidgetType, "label", "", NULL)); + setTimeOnly(dateTimePickerWidget(w)); + setLabel(dateTimePickerWidget(w)); + return w; +} + +struct uiDateTimePicker { + uiUnixControl c; + GtkWidget *widget; + dateTimePickerWidget *d; +}; + +uiUnixControlAllDefaults(uiDateTimePicker) + +uiDateTimePicker *finishNewDateTimePicker(GtkWidget *(*fn)(void)) +{ + uiDateTimePicker *d; + + uiUnixNewControl(uiDateTimePicker, d); + + d->widget = (*fn)(); + d->d = dateTimePickerWidget(d->widget); + + return d; +} + +uiDateTimePicker *uiNewDateTimePicker(void) +{ + return finishNewDateTimePicker(newDTP); +} + +uiDateTimePicker *uiNewDatePicker(void) +{ + return finishNewDateTimePicker(newDP); +} + +uiDateTimePicker *uiNewTimePicker(void) +{ + return finishNewDateTimePicker(newTP); +} diff --git a/src/libui_sdl/libui/unix/debug.c b/src/libui_sdl/libui/unix/debug.c new file mode 100644 index 0000000..c948db6 --- /dev/null +++ b/src/libui_sdl/libui/unix/debug.c @@ -0,0 +1,14 @@ +// 13 may 2016 +#include "uipriv_unix.h" + +// LONGTERM don't halt on release builds + +void realbug(const char *file, const char *line, const char *func, const char *prefix, const char *format, va_list ap) +{ + char *a, *b; + + a = g_strdup_printf("[libui] %s:%s:%s() %s", file, line, func, prefix); + b = g_strdup_vprintf(format, ap); + g_critical("%s%s", a, b); + G_BREAKPOINT(); +} diff --git a/src/libui_sdl/libui/unix/draw.c b/src/libui_sdl/libui/unix/draw.c new file mode 100644 index 0000000..2d7a636 --- /dev/null +++ b/src/libui_sdl/libui/unix/draw.c @@ -0,0 +1,141 @@ +// 6 september 2015 +#include "uipriv_unix.h" +#include "draw.h" + +uiDrawContext *newContext(cairo_t *cr) +{ + uiDrawContext *c; + + c = uiNew(uiDrawContext); + c->cr = cr; + return c; +} + +void freeContext(uiDrawContext *c) +{ + uiFree(c); +} + +static cairo_pattern_t *mkbrush(uiDrawBrush *b) +{ + cairo_pattern_t *pat; + size_t i; + + switch (b->Type) { + case uiDrawBrushTypeSolid: + pat = cairo_pattern_create_rgba(b->R, b->G, b->B, b->A); + break; + case uiDrawBrushTypeLinearGradient: + pat = cairo_pattern_create_linear(b->X0, b->Y0, b->X1, b->Y1); + break; + case uiDrawBrushTypeRadialGradient: + // make the start circle radius 0 to make it a point + pat = cairo_pattern_create_radial( + b->X0, b->Y0, 0, + b->X1, b->Y1, b->OuterRadius); + break; +// case uiDrawBrushTypeImage: + } + if (cairo_pattern_status(pat) != CAIRO_STATUS_SUCCESS) + implbug("error creating pattern in mkbrush(): %s", + cairo_status_to_string(cairo_pattern_status(pat))); + switch (b->Type) { + case uiDrawBrushTypeLinearGradient: + case uiDrawBrushTypeRadialGradient: + for (i = 0; i < b->NumStops; i++) + cairo_pattern_add_color_stop_rgba(pat, + b->Stops[i].Pos, + b->Stops[i].R, + b->Stops[i].G, + b->Stops[i].B, + b->Stops[i].A); + } + return pat; +} + +void uiDrawStroke(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b, uiDrawStrokeParams *p) +{ + cairo_pattern_t *pat; + + runPath(path, c->cr); + pat = mkbrush(b); + cairo_set_source(c->cr, pat); + switch (p->Cap) { + case uiDrawLineCapFlat: + cairo_set_line_cap(c->cr, CAIRO_LINE_CAP_BUTT); + break; + case uiDrawLineCapRound: + cairo_set_line_cap(c->cr, CAIRO_LINE_CAP_ROUND); + break; + case uiDrawLineCapSquare: + cairo_set_line_cap(c->cr, CAIRO_LINE_CAP_SQUARE); + break; + } + switch (p->Join) { + case uiDrawLineJoinMiter: + cairo_set_line_join(c->cr, CAIRO_LINE_JOIN_MITER); + cairo_set_miter_limit(c->cr, p->MiterLimit); + break; + case uiDrawLineJoinRound: + cairo_set_line_join(c->cr, CAIRO_LINE_JOIN_ROUND); + break; + case uiDrawLineJoinBevel: + cairo_set_line_join(c->cr, CAIRO_LINE_JOIN_BEVEL); + break; + } + cairo_set_line_width(c->cr, p->Thickness); + cairo_set_dash(c->cr, p->Dashes, p->NumDashes, p->DashPhase); + cairo_stroke(c->cr); + cairo_pattern_destroy(pat); +} + +void uiDrawFill(uiDrawContext *c, uiDrawPath *path, uiDrawBrush *b) +{ + cairo_pattern_t *pat; + + runPath(path, c->cr); + pat = mkbrush(b); + cairo_set_source(c->cr, pat); + switch (pathFillMode(path)) { + case uiDrawFillModeWinding: + cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_WINDING); + break; + case uiDrawFillModeAlternate: + cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_EVEN_ODD); + break; + } + cairo_fill(c->cr); + cairo_pattern_destroy(pat); +} + +void uiDrawTransform(uiDrawContext *c, uiDrawMatrix *m) +{ + cairo_matrix_t cm; + + m2c(m, &cm); + cairo_transform(c->cr, &cm); +} + +void uiDrawClip(uiDrawContext *c, uiDrawPath *path) +{ + runPath(path, c->cr); + switch (pathFillMode(path)) { + case uiDrawFillModeWinding: + cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_WINDING); + break; + case uiDrawFillModeAlternate: + cairo_set_fill_rule(c->cr, CAIRO_FILL_RULE_EVEN_ODD); + break; + } + cairo_clip(c->cr); +} + +void uiDrawSave(uiDrawContext *c) +{ + cairo_save(c->cr); +} + +void uiDrawRestore(uiDrawContext *c) +{ + cairo_restore(c->cr); +} diff --git a/src/libui_sdl/libui/unix/draw.h b/src/libui_sdl/libui/unix/draw.h new file mode 100644 index 0000000..dbd82ff --- /dev/null +++ b/src/libui_sdl/libui/unix/draw.h @@ -0,0 +1,13 @@ +// 5 may 2016 + +// draw.c +struct uiDrawContext { + cairo_t *cr; +}; + +// drawpath.c +extern void runPath(uiDrawPath *p, cairo_t *cr); +extern uiDrawFillMode pathFillMode(uiDrawPath *path); + +// drawmatrix.c +extern void m2c(uiDrawMatrix *m, cairo_matrix_t *c); diff --git a/src/libui_sdl/libui/unix/drawmatrix.c b/src/libui_sdl/libui/unix/drawmatrix.c new file mode 100644 index 0000000..ac7ac57 --- /dev/null +++ b/src/libui_sdl/libui/unix/drawmatrix.c @@ -0,0 +1,109 @@ +// 6 september 2015 +#include "uipriv_unix.h" +#include "draw.h" + +void m2c(uiDrawMatrix *m, cairo_matrix_t *c) +{ + c->xx = m->M11; + c->yx = m->M12; + c->xy = m->M21; + c->yy = m->M22; + c->x0 = m->M31; + c->y0 = m->M32; +} + +static void c2m(cairo_matrix_t *c, uiDrawMatrix *m) +{ + m->M11 = c->xx; + m->M12 = c->yx; + m->M21 = c->xy; + m->M22 = c->yy; + m->M31 = c->x0; + m->M32 = c->y0; +} + +void uiDrawMatrixTranslate(uiDrawMatrix *m, double x, double y) +{ + cairo_matrix_t c; + + m2c(m, &c); + cairo_matrix_translate(&c, x, y); + c2m(&c, m); +} + +void uiDrawMatrixScale(uiDrawMatrix *m, double xCenter, double yCenter, double x, double y) +{ + cairo_matrix_t c; + double xt, yt; + + m2c(m, &c); + xt = x; + yt = y; + scaleCenter(xCenter, yCenter, &xt, &yt); + cairo_matrix_translate(&c, xt, yt); + cairo_matrix_scale(&c, x, y); + cairo_matrix_translate(&c, -xt, -yt); + c2m(&c, m); +} + +void uiDrawMatrixRotate(uiDrawMatrix *m, double x, double y, double amount) +{ + cairo_matrix_t c; + + m2c(m, &c); + cairo_matrix_translate(&c, x, y); + cairo_matrix_rotate(&c, amount); + cairo_matrix_translate(&c, -x, -y); + c2m(&c, m); +} + +void uiDrawMatrixSkew(uiDrawMatrix *m, double x, double y, double xamount, double yamount) +{ + fallbackSkew(m, x, y, xamount, yamount); +} + +void uiDrawMatrixMultiply(uiDrawMatrix *dest, uiDrawMatrix *src) +{ + cairo_matrix_t c; + cairo_matrix_t d; + + m2c(dest, &c); + m2c(src, &d); + cairo_matrix_multiply(&c, &c, &d); + c2m(&c, dest); +} + +int uiDrawMatrixInvertible(uiDrawMatrix *m) +{ + cairo_matrix_t c; + + m2c(m, &c); + return cairo_matrix_invert(&c) == CAIRO_STATUS_SUCCESS; +} + +int uiDrawMatrixInvert(uiDrawMatrix *m) +{ + cairo_matrix_t c; + + m2c(m, &c); + if (cairo_matrix_invert(&c) != CAIRO_STATUS_SUCCESS) + return 0; + c2m(&c, m); + return 1; +} + +void uiDrawMatrixTransformPoint(uiDrawMatrix *m, double *x, double *y) +{ + cairo_matrix_t c; + + m2c(m, &c); + cairo_matrix_transform_point(&c, x, y); +} + +void uiDrawMatrixTransformSize(uiDrawMatrix *m, double *x, double *y) +{ + cairo_matrix_t c; + + m2c(m, &c); + cairo_matrix_transform_distance(&c, x, y); +} diff --git a/src/libui_sdl/libui/unix/drawpath.c b/src/libui_sdl/libui/unix/drawpath.c new file mode 100644 index 0000000..a0165fb --- /dev/null +++ b/src/libui_sdl/libui/unix/drawpath.c @@ -0,0 +1,199 @@ +// 6 september 2015 +#include "uipriv_unix.h" +#include "draw.h" + +struct uiDrawPath { + GArray *pieces; + uiDrawFillMode fillMode; + gboolean ended; +}; + +struct piece { + int type; + double d[8]; + int b; +}; + +enum { + newFigure, + newFigureArc, + lineTo, + arcTo, + bezierTo, + closeFigure, + addRect, +}; + +uiDrawPath *uiDrawNewPath(uiDrawFillMode mode) +{ + uiDrawPath *p; + + p = uiNew(uiDrawPath); + p->pieces = g_array_new(FALSE, TRUE, sizeof (struct piece)); + p->fillMode = mode; + return p; +} + +void uiDrawFreePath(uiDrawPath *p) +{ + g_array_free(p->pieces, TRUE); + uiFree(p); +} + +static void add(uiDrawPath *p, struct piece *piece) +{ + if (p->ended) + userbug("You cannot modify a uiDrawPath that has been ended. (path: %p)", p); + g_array_append_vals(p->pieces, piece, 1); +} + +void uiDrawPathNewFigure(uiDrawPath *p, double x, double y) +{ + struct piece piece; + + piece.type = newFigure; + piece.d[0] = x; + piece.d[1] = y; + add(p, &piece); +} + +void uiDrawPathNewFigureWithArc(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative) +{ + struct piece piece; + + if (sweep > 2 * uiPi) + sweep = 2 * uiPi; + piece.type = newFigureArc; + piece.d[0] = xCenter; + piece.d[1] = yCenter; + piece.d[2] = radius; + piece.d[3] = startAngle; + piece.d[4] = sweep; + piece.b = negative; + add(p, &piece); +} + +void uiDrawPathLineTo(uiDrawPath *p, double x, double y) +{ + struct piece piece; + + piece.type = lineTo; + piece.d[0] = x; + piece.d[1] = y; + add(p, &piece); +} + +void uiDrawPathArcTo(uiDrawPath *p, double xCenter, double yCenter, double radius, double startAngle, double sweep, int negative) +{ + struct piece piece; + + if (sweep > 2 * uiPi) + sweep = 2 * uiPi; + piece.type = arcTo; + piece.d[0] = xCenter; + piece.d[1] = yCenter; + piece.d[2] = radius; + piece.d[3] = startAngle; + piece.d[4] = sweep; + piece.b = negative; + add(p, &piece); +} + +void uiDrawPathBezierTo(uiDrawPath *p, double c1x, double c1y, double c2x, double c2y, double endX, double endY) +{ + struct piece piece; + + piece.type = bezierTo; + piece.d[0] = c1x; + piece.d[1] = c1y; + piece.d[2] = c2x; + piece.d[3] = c2y; + piece.d[4] = endX; + piece.d[5] = endY; + add(p, &piece); +} + +void uiDrawPathCloseFigure(uiDrawPath *p) +{ + struct piece piece; + + piece.type = closeFigure; + add(p, &piece); +} + +void uiDrawPathAddRectangle(uiDrawPath *p, double x, double y, double width, double height) +{ + struct piece piece; + + piece.type = addRect; + piece.d[0] = x; + piece.d[1] = y; + piece.d[2] = width; + piece.d[3] = height; + add(p, &piece); +} + +void uiDrawPathEnd(uiDrawPath *p) +{ + p->ended = TRUE; +} + +void runPath(uiDrawPath *p, cairo_t *cr) +{ + guint i; + struct piece *piece; + void (*arc)(cairo_t *, double, double, double, double, double); + + if (!p->ended) + userbug("You cannot draw with a uiDrawPath that has not been ended. (path: %p)", p); + cairo_new_path(cr); + for (i = 0; i < p->pieces->len; i++) { + piece = &g_array_index(p->pieces, struct piece, i); + switch (piece->type) { + case newFigure: + cairo_move_to(cr, piece->d[0], piece->d[1]); + break; + case newFigureArc: + cairo_new_sub_path(cr); + // fall through + case arcTo: + arc = cairo_arc; + if (piece->b) + arc = cairo_arc_negative; + (*arc)(cr, + piece->d[0], + piece->d[1], + piece->d[2], + piece->d[3], + piece->d[3] + piece->d[4]); + break; + case lineTo: + cairo_line_to(cr, piece->d[0], piece->d[1]); + break; + case bezierTo: + cairo_curve_to(cr, + piece->d[0], + piece->d[1], + piece->d[2], + piece->d[3], + piece->d[4], + piece->d[5]); + break; + case closeFigure: + cairo_close_path(cr); + break; + case addRect: + cairo_rectangle(cr, + piece->d[0], + piece->d[1], + piece->d[2], + piece->d[3]); + break; + } + } +} + +uiDrawFillMode pathFillMode(uiDrawPath *path) +{ + return path->fillMode; +} diff --git a/src/libui_sdl/libui/unix/drawtext.c b/src/libui_sdl/libui/unix/drawtext.c new file mode 100644 index 0000000..7078e1a --- /dev/null +++ b/src/libui_sdl/libui/unix/drawtext.c @@ -0,0 +1,293 @@ +// 6 september 2015 +#include "uipriv_unix.h" +#include "draw.h" + +struct uiDrawFontFamilies { + PangoFontFamily **f; + int n; +}; + +uiDrawFontFamilies *uiDrawListFontFamilies(void) +{ + uiDrawFontFamilies *ff; + PangoFontMap *map; + + ff = uiNew(uiDrawFontFamilies); + map = pango_cairo_font_map_get_default(); + pango_font_map_list_families(map, &(ff->f), &(ff->n)); + // do not free map; it's a shared resource + return ff; +} + +int uiDrawFontFamiliesNumFamilies(uiDrawFontFamilies *ff) +{ + return ff->n; +} + +char *uiDrawFontFamiliesFamily(uiDrawFontFamilies *ff, int n) +{ + PangoFontFamily *f; + + f = ff->f[n]; + return uiUnixStrdupText(pango_font_family_get_name(f)); +} + +void uiDrawFreeFontFamilies(uiDrawFontFamilies *ff) +{ + g_free(ff->f); + uiFree(ff); +} + +struct uiDrawTextFont { + PangoFont *f; +}; + +uiDrawTextFont *mkTextFont(PangoFont *f, gboolean ref) +{ + uiDrawTextFont *font; + + font = uiNew(uiDrawTextFont); + font->f = f; + if (ref) + g_object_ref(font->f); + return font; +} + +static const PangoWeight pangoWeights[] = { + [uiDrawTextWeightThin] = PANGO_WEIGHT_THIN, + [uiDrawTextWeightUltraLight] = PANGO_WEIGHT_ULTRALIGHT, + [uiDrawTextWeightLight] = PANGO_WEIGHT_LIGHT, + [uiDrawTextWeightBook] = PANGO_WEIGHT_BOOK, + [uiDrawTextWeightNormal] = PANGO_WEIGHT_NORMAL, + [uiDrawTextWeightMedium] = PANGO_WEIGHT_MEDIUM, + [uiDrawTextWeightSemiBold] = PANGO_WEIGHT_SEMIBOLD, + [uiDrawTextWeightBold] = PANGO_WEIGHT_BOLD, + [uiDrawTextWeightUltraBold] = PANGO_WEIGHT_ULTRABOLD, + [uiDrawTextWeightHeavy] = PANGO_WEIGHT_HEAVY, + [uiDrawTextWeightUltraHeavy] = PANGO_WEIGHT_ULTRAHEAVY, +}; + +static const PangoStyle pangoItalics[] = { + [uiDrawTextItalicNormal] = PANGO_STYLE_NORMAL, + [uiDrawTextItalicOblique] = PANGO_STYLE_OBLIQUE, + [uiDrawTextItalicItalic] = PANGO_STYLE_ITALIC, +}; + +static const PangoStretch pangoStretches[] = { + [uiDrawTextStretchUltraCondensed] = PANGO_STRETCH_ULTRA_CONDENSED, + [uiDrawTextStretchExtraCondensed] = PANGO_STRETCH_EXTRA_CONDENSED, + [uiDrawTextStretchCondensed] = PANGO_STRETCH_CONDENSED, + [uiDrawTextStretchSemiCondensed] = PANGO_STRETCH_SEMI_CONDENSED, + [uiDrawTextStretchNormal] = PANGO_STRETCH_NORMAL, + [uiDrawTextStretchSemiExpanded] = PANGO_STRETCH_SEMI_EXPANDED, + [uiDrawTextStretchExpanded] = PANGO_STRETCH_EXPANDED, + [uiDrawTextStretchExtraExpanded] = PANGO_STRETCH_EXTRA_EXPANDED, + [uiDrawTextStretchUltraExpanded] = PANGO_STRETCH_ULTRA_EXPANDED, +}; + +// we need a context for a few things +// the documentation suggests creating cairo_t-specific, GdkScreen-specific, or even GtkWidget-specific contexts, but we can't really do that because we want our uiDrawTextFonts and uiDrawTextLayouts to be context-independent +// we could use pango_font_map_create_context(pango_cairo_font_map_get_default()) but that will ignore GDK-specific settings +// so let's use gdk_pango_context_get() instead; even though it's for the default screen only, it's good enough for us +#define mkGenericPangoCairoContext() (gdk_pango_context_get()) + +PangoFont *pangoDescToPangoFont(PangoFontDescription *pdesc) +{ + PangoFont *f; + PangoContext *context; + + // in this case, the context is necessary for the metrics to be correct + context = mkGenericPangoCairoContext(); + f = pango_font_map_load_font(pango_cairo_font_map_get_default(), context, pdesc); + if (f == NULL) { + // LONGTERM + g_error("[libui] no match in pangoDescToPangoFont(); report to andlabs"); + } + g_object_unref(context); + return f; +} + +uiDrawTextFont *uiDrawLoadClosestFont(const uiDrawTextFontDescriptor *desc) +{ + PangoFont *f; + PangoFontDescription *pdesc; + + pdesc = pango_font_description_new(); + pango_font_description_set_family(pdesc, + desc->Family); + pango_font_description_set_size(pdesc, + (gint) (desc->Size * PANGO_SCALE)); + pango_font_description_set_weight(pdesc, + pangoWeights[desc->Weight]); + pango_font_description_set_style(pdesc, + pangoItalics[desc->Italic]); + pango_font_description_set_stretch(pdesc, + pangoStretches[desc->Stretch]); + f = pangoDescToPangoFont(pdesc); + pango_font_description_free(pdesc); + return mkTextFont(f, FALSE); // we hold the initial reference; no need to ref +} + +void uiDrawFreeTextFont(uiDrawTextFont *font) +{ + g_object_unref(font->f); + uiFree(font); +} + +uintptr_t uiDrawTextFontHandle(uiDrawTextFont *font) +{ + return (uintptr_t) (font->f); +} + +void uiDrawTextFontDescribe(uiDrawTextFont *font, uiDrawTextFontDescriptor *desc) +{ + PangoFontDescription *pdesc; + + // this creates a copy; we free it later + pdesc = pango_font_describe(font->f); + + // TODO + + pango_font_description_free(pdesc); +} + +// See https://developer.gnome.org/pango/1.30/pango-Cairo-Rendering.html#pango-Cairo-Rendering.description +// Note that we convert to double before dividing to make sure the floating-point stuff is right +#define pangoToCairo(pango) (((double) (pango)) / PANGO_SCALE) +#define cairoToPango(cairo) ((gint) ((cairo) * PANGO_SCALE)) + +void uiDrawTextFontGetMetrics(uiDrawTextFont *font, uiDrawTextFontMetrics *metrics) +{ + PangoFontMetrics *pm; + + pm = pango_font_get_metrics(font->f, NULL); + metrics->Ascent = pangoToCairo(pango_font_metrics_get_ascent(pm)); + metrics->Descent = pangoToCairo(pango_font_metrics_get_descent(pm)); + // Pango doesn't seem to expose this :( Use 0 and hope for the best. + metrics->Leading = 0; + metrics->UnderlinePos = pangoToCairo(pango_font_metrics_get_underline_position(pm)); + metrics->UnderlineThickness = pangoToCairo(pango_font_metrics_get_underline_thickness(pm)); + pango_font_metrics_unref(pm); +} + +// note: PangoCairoLayouts are tied to a given cairo_t, so we can't store one in this device-independent structure +struct uiDrawTextLayout { + char *s; + ptrdiff_t *graphemes; + PangoFont *defaultFont; + double width; + PangoAttrList *attrs; +}; + +uiDrawTextLayout *uiDrawNewTextLayout(const char *text, uiDrawTextFont *defaultFont, double width) +{ + uiDrawTextLayout *layout; + PangoContext *context; + + layout = uiNew(uiDrawTextLayout); + layout->s = g_strdup(text); + context = mkGenericPangoCairoContext(); + layout->graphemes = graphemes(layout->s, context); + g_object_unref(context); + layout->defaultFont = defaultFont->f; + g_object_ref(layout->defaultFont); // retain a copy + uiDrawTextLayoutSetWidth(layout, width); + layout->attrs = pango_attr_list_new(); + return layout; +} + +void uiDrawFreeTextLayout(uiDrawTextLayout *layout) +{ + pango_attr_list_unref(layout->attrs); + g_object_unref(layout->defaultFont); + uiFree(layout->graphemes); + g_free(layout->s); + uiFree(layout); +} + +void uiDrawTextLayoutSetWidth(uiDrawTextLayout *layout, double width) +{ + layout->width = width; +} + +static void prepareLayout(uiDrawTextLayout *layout, PangoLayout *pl) +{ + PangoFontDescription *desc; + int width; + + pango_layout_set_text(pl, layout->s, -1); + + // again, this makes a copy + desc = pango_font_describe(layout->defaultFont); + // this is safe; the description is copied + pango_layout_set_font_description(pl, desc); + pango_font_description_free(desc); + + width = cairoToPango(layout->width); + if (layout->width < 0) + width = -1; + pango_layout_set_width(pl, width); + + pango_layout_set_attributes(pl, layout->attrs); +} + +void uiDrawTextLayoutExtents(uiDrawTextLayout *layout, double *width, double *height) +{ + PangoContext *context; + PangoLayout *pl; + PangoRectangle logical; + + // in this case, the context is necessary to create the layout + // the layout takes a ref on the context so we can unref it afterward + context = mkGenericPangoCairoContext(); + pl = pango_layout_new(context); + g_object_unref(context); + prepareLayout(layout, pl); + + pango_layout_get_extents(pl, NULL, &logical); + + g_object_unref(pl); + + *width = pangoToCairo(logical.width); + *height = pangoToCairo(logical.height); +} + +void uiDrawText(uiDrawContext *c, double x, double y, uiDrawTextLayout *layout) +{ + PangoLayout *pl; + + pl = pango_cairo_create_layout(c->cr); + prepareLayout(layout, pl); + + cairo_move_to(c->cr, x, y); + pango_cairo_show_layout(c->cr, pl); + + g_object_unref(pl); +} + +static void addAttr(uiDrawTextLayout *layout, PangoAttribute *attr, int startChar, int endChar) +{ + attr->start_index = layout->graphemes[startChar]; + attr->end_index = layout->graphemes[endChar]; + pango_attr_list_insert(layout->attrs, attr); + // pango_attr_list_insert() takes attr; we don't free it +} + +void uiDrawTextLayoutSetColor(uiDrawTextLayout *layout, int startChar, int endChar, double r, double g, double b, double a) +{ + PangoAttribute *attr; + guint16 rr, gg, bb, aa; + + rr = (guint16) (r * 65535); + gg = (guint16) (g * 65535); + bb = (guint16) (b * 65535); + aa = (guint16) (a * 65535); + + attr = pango_attr_foreground_new(rr, gg, bb); + addAttr(layout, attr, startChar, endChar); + + // TODO what if aa == 0? + attr = FUTURE_pango_attr_foreground_alpha_new(aa); + if (attr != NULL) + addAttr(layout, attr, startChar, endChar); +} diff --git a/src/libui_sdl/libui/unix/editablecombo.c b/src/libui_sdl/libui/unix/editablecombo.c new file mode 100644 index 0000000..7ee3829 --- /dev/null +++ b/src/libui_sdl/libui/unix/editablecombo.c @@ -0,0 +1,79 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +struct uiEditableCombobox { + uiUnixControl c; + GtkWidget *widget; + GtkBin *bin; + GtkComboBox *combobox; + GtkComboBoxText *comboboxText; + void (*onChanged)(uiEditableCombobox *, void *); + void *onChangedData; + gulong onChangedSignal; +}; + +uiUnixControlAllDefaults(uiEditableCombobox) + +static void onChanged(GtkComboBox *cbox, gpointer data) +{ + uiEditableCombobox *c = uiEditableCombobox(data); + + (*(c->onChanged))(c, c->onChangedData); +} + +static void defaultOnChanged(uiEditableCombobox *c, void *data) +{ + // do nothing +} + +void uiEditableComboboxAppend(uiEditableCombobox *c, const char *text) +{ + gtk_combo_box_text_append(c->comboboxText, NULL, text); +} + +char *uiEditableComboboxText(uiEditableCombobox *c) +{ + char *s; + char *out; + + s = gtk_combo_box_text_get_active_text(c->comboboxText); + // s will always be non-NULL in the case of a combobox with an entry (according to the source code) + out = uiUnixStrdupText(s); + g_free(s); + return out; +} + +void uiEditableComboboxSetText(uiEditableCombobox *c, const char *text) +{ + GtkEntry *e; + + // we need to inhibit sending of ::changed because this WILL send a ::changed otherwise + g_signal_handler_block(c->combobox, c->onChangedSignal); + // since there isn't a gtk_combo_box_text_set_active_text()... + e = GTK_ENTRY(gtk_bin_get_child(c->bin)); + gtk_entry_set_text(e, text); + g_signal_handler_unblock(c->combobox, c->onChangedSignal); +} + +void uiEditableComboboxOnChanged(uiEditableCombobox *c, void (*f)(uiEditableCombobox *c, void *data), void *data) +{ + c->onChanged = f; + c->onChangedData = data; +} + +uiEditableCombobox *uiNewEditableCombobox(void) +{ + uiEditableCombobox *c; + + uiUnixNewControl(uiEditableCombobox, c); + + c->widget = gtk_combo_box_text_new_with_entry(); + c->bin = GTK_BIN(c->widget); + c->combobox = GTK_COMBO_BOX(c->widget); + c->comboboxText = GTK_COMBO_BOX_TEXT(c->widget); + + c->onChangedSignal = g_signal_connect(c->widget, "changed", G_CALLBACK(onChanged), c); + uiEditableComboboxOnChanged(c, defaultOnChanged, NULL); + + return c; +} diff --git a/src/libui_sdl/libui/unix/entry.c b/src/libui_sdl/libui/unix/entry.c new file mode 100644 index 0000000..4a9a1d0 --- /dev/null +++ b/src/libui_sdl/libui/unix/entry.c @@ -0,0 +1,97 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +struct uiEntry { + uiUnixControl c; + GtkWidget *widget; + GtkEntry *entry; + GtkEditable *editable; + void (*onChanged)(uiEntry *, void *); + void *onChangedData; + gulong onChangedSignal; +}; + +uiUnixControlAllDefaults(uiEntry) + +static void onChanged(GtkEditable *editable, gpointer data) +{ + uiEntry *e = uiEntry(data); + + (*(e->onChanged))(e, e->onChangedData); +} + +static void defaultOnChanged(uiEntry *e, void *data) +{ + // do nothing +} + +char *uiEntryText(uiEntry *e) +{ + return uiUnixStrdupText(gtk_entry_get_text(e->entry)); +} + +void uiEntrySetText(uiEntry *e, const char *text) +{ + // we need to inhibit sending of ::changed because this WILL send a ::changed otherwise + g_signal_handler_block(e->editable, e->onChangedSignal); + gtk_entry_set_text(e->entry, text); + g_signal_handler_unblock(e->editable, e->onChangedSignal); + // don't queue the control for resize; entry sizes are independent of their contents +} + +void uiEntryOnChanged(uiEntry *e, void (*f)(uiEntry *, void *), void *data) +{ + e->onChanged = f; + e->onChangedData = data; +} + +int uiEntryReadOnly(uiEntry *e) +{ + return gtk_editable_get_editable(e->editable) == FALSE; +} + +void uiEntrySetReadOnly(uiEntry *e, int readonly) +{ + gboolean editable; + + editable = TRUE; + if (readonly) + editable = FALSE; + gtk_editable_set_editable(e->editable, editable); +} + +static uiEntry *finishNewEntry(GtkWidget *w, const gchar *signal) +{ + uiEntry *e; + + uiUnixNewControl(uiEntry, e); + + e->widget = w; + e->entry = GTK_ENTRY(e->widget); + e->editable = GTK_EDITABLE(e->widget); + + e->onChangedSignal = g_signal_connect(e->widget, signal, G_CALLBACK(onChanged), e); + uiEntryOnChanged(e, defaultOnChanged, NULL); + + return e; +} + +uiEntry *uiNewEntry(void) +{ + return finishNewEntry(gtk_entry_new(), "changed"); +} + +uiEntry *uiNewPasswordEntry(void) +{ + GtkWidget *e; + + e = gtk_entry_new(); + gtk_entry_set_visibility(GTK_ENTRY(e), FALSE); + return finishNewEntry(e, "changed"); +} + +// TODO make it use a separate function to be type-safe +uiEntry *uiNewSearchEntry(void) +{ + return finishNewEntry(gtk_search_entry_new(), "search-changed"); +} diff --git a/src/libui_sdl/libui/unix/fontbutton.c b/src/libui_sdl/libui/unix/fontbutton.c new file mode 100644 index 0000000..f8047e0 --- /dev/null +++ b/src/libui_sdl/libui/unix/fontbutton.c @@ -0,0 +1,70 @@ +// 14 april 2016 +#include "uipriv_unix.h" + +struct uiFontButton { + uiUnixControl c; + GtkWidget *widget; + GtkButton *button; + GtkFontButton *fb; + GtkFontChooser *fc; + void (*onChanged)(uiFontButton *, void *); + void *onChangedData; +}; + +uiUnixControlAllDefaults(uiFontButton) + +// TODO NOTE no need to inhibit the signal; font-set is documented as only being sent when the user changes the font +static void onFontSet(GtkFontButton *button, gpointer data) +{ + uiFontButton *b = uiFontButton(data); + + (*(b->onChanged))(b, b->onChangedData); +} + +static void defaultOnChanged(uiFontButton *b, void *data) +{ + // do nothing +} + +uiDrawTextFont *uiFontButtonFont(uiFontButton *b) +{ + PangoFont *f; + PangoFontDescription *desc; + + desc = gtk_font_chooser_get_font_desc(b->fc); + f = pangoDescToPangoFont(desc); + // desc is transfer-full and thus is a copy + pango_font_description_free(desc); + return mkTextFont(f, FALSE); // we hold the initial reference; no need to ref +} + +void uiFontButtonOnChanged(uiFontButton *b, void (*f)(uiFontButton *, void *), void *data) +{ + b->onChanged = f; + b->onChangedData = data; +} + +uiFontButton *uiNewFontButton(void) +{ + uiFontButton *b; + + uiUnixNewControl(uiFontButton, b); + + b->widget = gtk_font_button_new(); + b->button = GTK_BUTTON(b->widget); + b->fb = GTK_FONT_BUTTON(b->widget); + b->fc = GTK_FONT_CHOOSER(b->widget); + + // match behavior on other platforms + gtk_font_button_set_show_style(b->fb, TRUE); + gtk_font_button_set_show_size(b->fb, TRUE); + gtk_font_button_set_use_font(b->fb, FALSE); + gtk_font_button_set_use_size(b->fb, FALSE); + // other customizations + gtk_font_chooser_set_show_preview_entry(b->fc, TRUE); + + g_signal_connect(b->widget, "font-set", G_CALLBACK(onFontSet), b); + uiFontButtonOnChanged(b, defaultOnChanged, NULL); + + return b; +} diff --git a/src/libui_sdl/libui/unix/form.c b/src/libui_sdl/libui/unix/form.c new file mode 100644 index 0000000..54422b3 --- /dev/null +++ b/src/libui_sdl/libui/unix/form.c @@ -0,0 +1,159 @@ +// 8 june 2016 +#include "uipriv_unix.h" + +struct formChild { + uiControl *c; + int stretchy; + GtkWidget *label; + gboolean oldhexpand; + GtkAlign oldhalign; + gboolean oldvexpand; + GtkAlign oldvalign; + GBinding *labelBinding; +}; + +struct uiForm { + uiUnixControl c; + GtkWidget *widget; + GtkContainer *container; + GtkGrid *grid; + GArray *children; + int padded; + GtkSizeGroup *stretchygroup; // ensures all stretchy controls have the same size +}; + +uiUnixControlAllDefaultsExceptDestroy(uiForm) + +#define ctrl(f, i) &g_array_index(f->children, struct formChild, i) + +static void uiFormDestroy(uiControl *c) +{ + uiForm *f = uiForm(c); + struct formChild *fc; + guint i; + + // kill the size group + g_object_unref(f->stretchygroup); + // free all controls + for (i = 0; i < f->children->len; i++) { + fc = ctrl(f, i); + uiControlSetParent(fc->c, NULL); + uiUnixControlSetContainer(uiUnixControl(fc->c), f->container, TRUE); + uiControlDestroy(fc->c); + gtk_widget_destroy(fc->label); + } + g_array_free(f->children, TRUE); + // and then ourselves + g_object_unref(f->widget); + uiFreeControl(uiControl(f)); +} + +void uiFormAppend(uiForm *f, const char *label, uiControl *c, int stretchy) +{ + struct formChild fc; + GtkWidget *widget; + guint row; + + fc.c = c; + widget = GTK_WIDGET(uiControlHandle(fc.c)); + fc.stretchy = stretchy; + fc.oldhexpand = gtk_widget_get_hexpand(widget); + fc.oldhalign = gtk_widget_get_halign(widget); + fc.oldvexpand = gtk_widget_get_vexpand(widget); + fc.oldvalign = gtk_widget_get_valign(widget); + + if (stretchy) { + gtk_widget_set_vexpand(widget, TRUE); + gtk_widget_set_valign(widget, GTK_ALIGN_FILL); + gtk_size_group_add_widget(f->stretchygroup, widget); + } else + gtk_widget_set_vexpand(widget, FALSE); + // and make them fill horizontally + gtk_widget_set_hexpand(widget, TRUE); + gtk_widget_set_halign(widget, GTK_ALIGN_FILL); + + fc.label = gtk_label_new(label); + gtk_widget_set_hexpand(fc.label, FALSE); + gtk_widget_set_halign(fc.label, GTK_ALIGN_END); + gtk_widget_set_vexpand(fc.label, FALSE); + if (GTK_IS_SCROLLED_WINDOW(widget)) + gtk_widget_set_valign(fc.label, GTK_ALIGN_START); + else + gtk_widget_set_valign(fc.label, GTK_ALIGN_CENTER); + gtk_style_context_add_class(gtk_widget_get_style_context(fc.label), "dim-label"); + row = f->children->len; + gtk_grid_attach(f->grid, fc.label, + 0, row, + 1, 1); + // and make them share visibility so if the control is hidden, so is its label + fc.labelBinding = g_object_bind_property(GTK_WIDGET(uiControlHandle(fc.c)), "visible", + fc.label, "visible", + G_BINDING_SYNC_CREATE); + + uiControlSetParent(fc.c, uiControl(f)); + uiUnixControlSetContainer(uiUnixControl(fc.c), f->container, FALSE); + g_array_append_val(f->children, fc); + + // move the widget to the correct place + gtk_container_child_set(f->container, widget, + "left-attach", 1, + "top-attach", row, + NULL); +} + +void uiFormDelete(uiForm *f, int index) +{ + struct formChild *fc; + GtkWidget *widget; + + fc = ctrl(f, index); + widget = GTK_WIDGET(uiControlHandle(fc->c)); + + gtk_widget_destroy(fc->label); + + uiControlSetParent(fc->c, NULL); + uiUnixControlSetContainer(uiUnixControl(fc->c), f->container, TRUE); + + if (fc->stretchy) + gtk_size_group_remove_widget(f->stretchygroup, widget); + gtk_widget_set_hexpand(widget, fc->oldhexpand); + gtk_widget_set_halign(widget, fc->oldhalign); + gtk_widget_set_vexpand(widget, fc->oldvexpand); + gtk_widget_set_valign(widget, fc->oldvalign); + + g_array_remove_index(f->children, index); +} + +int uiFormPadded(uiForm *f) +{ + return f->padded; +} + +void uiFormSetPadded(uiForm *f, int padded) +{ + f->padded = padded; + if (f->padded) { + gtk_grid_set_row_spacing(f->grid, gtkYPadding); + gtk_grid_set_column_spacing(f->grid, gtkXPadding); + } else { + gtk_grid_set_row_spacing(f->grid, 0); + gtk_grid_set_column_spacing(f->grid, 0); + } +} + +uiForm *uiNewForm(void) +{ + uiForm *f; + + uiUnixNewControl(uiForm, f); + + f->widget = gtk_grid_new(); + f->container = GTK_CONTAINER(f->widget); + f->grid = GTK_GRID(f->widget); + + f->stretchygroup = gtk_size_group_new(GTK_SIZE_GROUP_VERTICAL); + + f->children = g_array_new(FALSE, TRUE, sizeof (struct formChild)); + + return f; +} diff --git a/src/libui_sdl/libui/unix/future.c b/src/libui_sdl/libui/unix/future.c new file mode 100644 index 0000000..1f9f532 --- /dev/null +++ b/src/libui_sdl/libui/unix/future.c @@ -0,0 +1,42 @@ +// 29 june 2016 +#include "uipriv_unix.h" + +// functions FROM THE FUTURE! +// in some cases, because being held back by LTS releases sucks :/ +// in others, because parts of GTK+ being unstable until recently also sucks :/ + +// added in pango 1.38; we need 1.36 +static PangoAttribute *(*newFGAlphaAttr)(guint16 alpha) = NULL; + +// added in GTK+ 3.20; we need 3.10 +static void (*gwpIterSetObjectName)(GtkWidgetPath *path, gint pos, const char *name) = NULL; + +// note that we treat any error as "the symbols aren't there" (and don't care if dlclose() failed) +void loadFutures(void) +{ + void *handle; + + // dlsym() walks the dependency chain, so opening the current process should be sufficient + handle = dlopen(NULL, RTLD_LAZY); + if (handle == NULL) + return; +#define GET(var, fn) *((void **) (&var)) = dlsym(handle, #fn) + GET(newFGAlphaAttr, pango_attr_foreground_alpha_new); + GET(gwpIterSetObjectName, gtk_widget_path_iter_set_object_name); + dlclose(handle); +} + +PangoAttribute *FUTURE_pango_attr_foreground_alpha_new(guint16 alpha) +{ + if (newFGAlphaAttr == NULL) + return NULL; + return (*newFGAlphaAttr)(alpha); +} + +gboolean FUTURE_gtk_widget_path_iter_set_object_name(GtkWidgetPath *path, gint pos, const char *name) +{ + if (gwpIterSetObjectName == NULL) + return FALSE; + (*gwpIterSetObjectName)(path, pos, name); + return TRUE; +} diff --git a/src/libui_sdl/libui/unix/graphemes.c b/src/libui_sdl/libui/unix/graphemes.c new file mode 100644 index 0000000..a2c47b7 --- /dev/null +++ b/src/libui_sdl/libui/unix/graphemes.c @@ -0,0 +1,31 @@ +// 25 may 2016 +#include "uipriv_unix.h" + +ptrdiff_t *graphemes(const char *text, PangoContext *context) +{ + size_t len, lenchars; + PangoLogAttr *logattrs; + ptrdiff_t *out; + ptrdiff_t *op; + size_t i; + + len = strlen(text); + lenchars = g_utf8_strlen(text, -1); + logattrs = (PangoLogAttr *) uiAlloc((lenchars + 1) * sizeof (PangoLogAttr), "PangoLogAttr[]"); + pango_get_log_attrs(text, len, + -1, NULL, + logattrs, lenchars + 1); + + // should be more than enough + out = (ptrdiff_t *) uiAlloc((lenchars + 2) * sizeof (ptrdiff_t), "ptrdiff_t[]"); + op = out; + for (i = 0; i < lenchars; i++) + if (logattrs[i].is_cursor_position != 0) + // TODO optimize this + *op++ = g_utf8_offset_to_pointer(text, i) - text; + // and do the last one + *op++ = len; + + uiFree(logattrs); + return out; +} diff --git a/src/libui_sdl/libui/unix/grid.c b/src/libui_sdl/libui/unix/grid.c new file mode 100644 index 0000000..6d9813b --- /dev/null +++ b/src/libui_sdl/libui/unix/grid.c @@ -0,0 +1,141 @@ +// 9 june 2016 +#include "uipriv_unix.h" + +struct gridChild { + uiControl *c; + GtkWidget *label; + gboolean oldhexpand; + GtkAlign oldhalign; + gboolean oldvexpand; + GtkAlign oldvalign; +}; + +struct uiGrid { + uiUnixControl c; + GtkWidget *widget; + GtkContainer *container; + GtkGrid *grid; + GArray *children; + int padded; +}; + +uiUnixControlAllDefaultsExceptDestroy(uiGrid) + +#define ctrl(g, i) &g_array_index(g->children, struct gridChild, i) + +static void uiGridDestroy(uiControl *c) +{ + uiGrid *g = uiGrid(c); + struct gridChild *gc; + guint i; + + // free all controls + for (i = 0; i < g->children->len; i++) { + gc = ctrl(g, i); + uiControlSetParent(gc->c, NULL); + uiUnixControlSetContainer(uiUnixControl(gc->c), g->container, TRUE); + uiControlDestroy(gc->c); + } + g_array_free(g->children, TRUE); + // and then ourselves + g_object_unref(g->widget); + uiFreeControl(uiControl(g)); +} + +#define TODO_MASSIVE_HACK(c) \ + if (!uiUnixControl(c)->addedBefore) { \ + g_object_ref_sink(GTK_WIDGET(uiControlHandle(uiControl(c)))); \ + gtk_widget_show(GTK_WIDGET(uiControlHandle(uiControl(c)))); \ + uiUnixControl(c)->addedBefore = TRUE; \ + } + +static const GtkAlign gtkAligns[] = { + [uiAlignFill] = GTK_ALIGN_FILL, + [uiAlignStart] = GTK_ALIGN_START, + [uiAlignCenter] = GTK_ALIGN_CENTER, + [uiAlignEnd] = GTK_ALIGN_END, +}; + +static const GtkPositionType gtkPositions[] = { + [uiAtLeading] = GTK_POS_LEFT, + [uiAtTop] = GTK_POS_TOP, + [uiAtTrailing] = GTK_POS_RIGHT, + [uiAtBottom] = GTK_POS_BOTTOM, +}; + +static GtkWidget *prepare(struct gridChild *gc, uiControl *c, int hexpand, uiAlign halign, int vexpand, uiAlign valign) +{ + GtkWidget *widget; + + gc->c = c; + widget = GTK_WIDGET(uiControlHandle(gc->c)); + gc->oldhexpand = gtk_widget_get_hexpand(widget); + gc->oldhalign = gtk_widget_get_halign(widget); + gc->oldvexpand = gtk_widget_get_vexpand(widget); + gc->oldvalign = gtk_widget_get_valign(widget); + gtk_widget_set_hexpand(widget, hexpand != 0); + gtk_widget_set_halign(widget, gtkAligns[halign]); + gtk_widget_set_vexpand(widget, vexpand != 0); + gtk_widget_set_valign(widget, gtkAligns[valign]); + return widget; +} + +void uiGridAppend(uiGrid *g, uiControl *c, int left, int top, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign) +{ + struct gridChild gc; + GtkWidget *widget; + + widget = prepare(&gc, c, hexpand, halign, vexpand, valign); + uiControlSetParent(gc.c, uiControl(g)); + TODO_MASSIVE_HACK(uiUnixControl(gc.c)); + gtk_grid_attach(g->grid, widget, + left, top, + xspan, yspan); + g_array_append_val(g->children, gc); +} + +void uiGridInsertAt(uiGrid *g, uiControl *c, uiControl *existing, uiAt at, int xspan, int yspan, int hexpand, uiAlign halign, int vexpand, uiAlign valign) +{ + struct gridChild gc; + GtkWidget *widget; + + widget = prepare(&gc, c, hexpand, halign, vexpand, valign); + uiControlSetParent(gc.c, uiControl(g)); + TODO_MASSIVE_HACK(uiUnixControl(gc.c)); + gtk_grid_attach_next_to(g->grid, widget, + GTK_WIDGET(uiControlHandle(existing)), gtkPositions[at], + xspan, yspan); + g_array_append_val(g->children, gc); +} + +int uiGridPadded(uiGrid *g) +{ + return g->padded; +} + +void uiGridSetPadded(uiGrid *g, int padded) +{ + g->padded = padded; + if (g->padded) { + gtk_grid_set_row_spacing(g->grid, gtkYPadding); + gtk_grid_set_column_spacing(g->grid, gtkXPadding); + } else { + gtk_grid_set_row_spacing(g->grid, 0); + gtk_grid_set_column_spacing(g->grid, 0); + } +} + +uiGrid *uiNewGrid(void) +{ + uiGrid *g; + + uiUnixNewControl(uiGrid, g); + + g->widget = gtk_grid_new(); + g->container = GTK_CONTAINER(g->widget); + g->grid = GTK_GRID(g->widget); + + g->children = g_array_new(FALSE, TRUE, sizeof (struct gridChild)); + + return g; +} diff --git a/src/libui_sdl/libui/unix/group.c b/src/libui_sdl/libui/unix/group.c new file mode 100644 index 0000000..6238a1b --- /dev/null +++ b/src/libui_sdl/libui/unix/group.c @@ -0,0 +1,89 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +struct uiGroup { + uiUnixControl c; + GtkWidget *widget; + GtkContainer *container; + GtkBin *bin; + GtkFrame *frame; + + // unfortunately, even though a GtkFrame is a GtkBin, calling gtk_container_set_border_width() on it /includes/ the GtkFrame's label; we don't want tht + struct child *child; + + int margined; +}; + +uiUnixControlAllDefaultsExceptDestroy(uiGroup) + +static void uiGroupDestroy(uiControl *c) +{ + uiGroup *g = uiGroup(c); + + if (g->child != NULL) + childDestroy(g->child); + g_object_unref(g->widget); + uiFreeControl(uiControl(g)); +} + +char *uiGroupTitle(uiGroup *g) +{ + return uiUnixStrdupText(gtk_frame_get_label(g->frame)); +} + +void uiGroupSetTitle(uiGroup *g, const char *text) +{ + gtk_frame_set_label(g->frame, text); +} + +void uiGroupSetChild(uiGroup *g, uiControl *child) +{ + if (g->child != NULL) + childRemove(g->child); + g->child = newChildWithBox(child, uiControl(g), g->container, g->margined); +} + +int uiGroupMargined(uiGroup *g) +{ + return g->margined; +} + +void uiGroupSetMargined(uiGroup *g, int margined) +{ + g->margined = margined; + if (g->child != NULL) + childSetMargined(g->child, g->margined); +} + +uiGroup *uiNewGroup(const char *text) +{ + uiGroup *g; + gfloat yalign; + GtkLabel *label; + PangoAttribute *bold; + PangoAttrList *boldlist; + + uiUnixNewControl(uiGroup, g); + + g->widget = gtk_frame_new(text); + g->container = GTK_CONTAINER(g->widget); + g->bin = GTK_BIN(g->widget); + g->frame = GTK_FRAME(g->widget); + + // with GTK+, groupboxes by default have frames and slightly x-offset regular text + // they should have no frame and fully left-justified, bold text + // preserve default y-alignment + gtk_frame_get_label_align(g->frame, NULL, &yalign); + gtk_frame_set_label_align(g->frame, 0, yalign); + gtk_frame_set_shadow_type(g->frame, GTK_SHADOW_NONE); + label = GTK_LABEL(gtk_frame_get_label_widget(g->frame)); + // this is the boldness level used by GtkPrintUnixDialog + // (it technically uses "bold" but see pango's pango-enum-types.c for the name conversion; GType is weird) + bold = pango_attr_weight_new(PANGO_WEIGHT_BOLD); + boldlist = pango_attr_list_new(); + pango_attr_list_insert(boldlist, bold); + gtk_label_set_attributes(label, boldlist); + pango_attr_list_unref(boldlist); // thanks baedert in irc.gimp.net/#gtk+ + + return g; +} diff --git a/src/libui_sdl/libui/unix/image.c b/src/libui_sdl/libui/unix/image.c new file mode 100644 index 0000000..a79e550 --- /dev/null +++ b/src/libui_sdl/libui/unix/image.c @@ -0,0 +1,120 @@ +// 27 june 2016 +#include "uipriv_unix.h" + +struct uiImage { + double width; + double height; + GPtrArray *images; +}; + +static void freeImageRep(gpointer item) +{ + cairo_surface_t *cs = (cairo_surface_t *) item; + unsigned char *buf; + + buf = cairo_image_surface_get_data(cs); + cairo_surface_destroy(cs); + uiFree(buf); +} + +uiImage *uiNewImage(double width, double height) +{ + uiImage *i; + + i = uiNew(uiImage); + i->width = width; + i->height = height; + i->images = g_ptr_array_new_with_free_func(freeImageRep); + return i; +} + +void uiFreeImage(uiImage *i) +{ + g_ptr_array_free(i->images, TRUE); + uiFree(i); +} + +void uiImageAppend(uiImage *i, void *pixels, int pixelWidth, int pixelHeight, int pixelStride) +{ + cairo_surface_t *cs; + unsigned char *buf, *p; + uint8_t *src = (uint8_t *) pixels; + int cstride; + int y; + + cstride = cairo_format_stride_for_width(CAIRO_FORMAT_ARGB32, pixelWidth); + buf = (unsigned char *) uiAlloc((cstride * pixelHeight * 4) * sizeof (unsigned char), "unsigned char[]"); + p = buf; + for (y = 0; y < pixelStride * pixelHeight; y += pixelStride) { + memmove(p, src + y, cstride); + p += cstride; + } + cs = cairo_image_surface_create_for_data(buf, CAIRO_FORMAT_ARGB32, + pixelWidth, pixelHeight, + cstride); + if (cairo_surface_status(cs) != CAIRO_STATUS_SUCCESS) + /* TODO */; + cairo_surface_flush(cs); + g_ptr_array_add(i->images, cs); +} + +struct matcher { + cairo_surface_t *best; + int distX; + int distY; + int targetX; + int targetY; + gboolean foundLarger; +}; + +// TODO is this the right algorithm? +static void match(gpointer surface, gpointer data) +{ + cairo_surface_t *cs = (cairo_surface_t *) surface; + struct matcher *m = (struct matcher *) data; + int x, y; + int x2, y2; + + x = cairo_image_surface_get_width(cs); + y = cairo_image_surface_get_height(cs); + if (m->best == NULL) + goto writeMatch; + + if (x < m->targetX && y < m->targetY) + if (m->foundLarger) + // always prefer larger ones + return; + if (x >= m->targetX && y >= m->targetY && !m->foundLarger) + // we set foundLarger below + goto writeMatch; + + x2 = abs(m->targetX - x); + y2 = abs(m->targetY - y); + if (x2 < m->distX && y2 < m->distY) + goto writeMatch; + + // TODO weight one dimension? threshhold? + return; + +writeMatch: + // must set this here too; otherwise the first image will never have ths set + if (x >= m->targetX && y >= m->targetY && !m->foundLarger) + m->foundLarger = TRUE; + m->best = cs; + m->distX = abs(m->targetX - x); + m->distY = abs(m->targetY - y); +} + +cairo_surface_t *imageAppropriateSurface(uiImage *i, GtkWidget *w) +{ + struct matcher m; + + m.best = NULL; + m.distX = G_MAXINT; + m.distY = G_MAXINT; + m.targetX = i->width * gtk_widget_get_scale_factor(w); + m.targetY = i->height * gtk_widget_get_scale_factor(w); + m.foundLarger = FALSE; + g_ptr_array_foreach(i->images, match, &m); + return m.best; +} diff --git a/src/libui_sdl/libui/unix/label.c b/src/libui_sdl/libui/unix/label.c new file mode 100644 index 0000000..b39fc7c --- /dev/null +++ b/src/libui_sdl/libui/unix/label.c @@ -0,0 +1,36 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +struct uiLabel { + uiUnixControl c; + GtkWidget *widget; + GtkMisc *misc; + GtkLabel *label; +}; + +uiUnixControlAllDefaults(uiLabel) + +char *uiLabelText(uiLabel *l) +{ + return uiUnixStrdupText(gtk_label_get_text(l->label)); +} + +void uiLabelSetText(uiLabel *l, const char *text) +{ + gtk_label_set_text(l->label, text); +} + +uiLabel *uiNewLabel(const char *text) +{ + uiLabel *l; + + uiUnixNewControl(uiLabel, l); + + l->widget = gtk_label_new(text); + l->misc = GTK_MISC(l->widget); + l->label = GTK_LABEL(l->widget); + + gtk_misc_set_alignment(l->misc, 0, 0); + + return l; +} diff --git a/src/libui_sdl/libui/unix/main.c b/src/libui_sdl/libui/unix/main.c new file mode 100644 index 0000000..2998bf3 --- /dev/null +++ b/src/libui_sdl/libui/unix/main.c @@ -0,0 +1,108 @@ +// 6 april 2015 +#include "uipriv_unix.h" + +uiInitOptions options; + +const char *uiInit(uiInitOptions *o) +{ + GError *err = NULL; + const char *msg; + + options = *o; + if (gtk_init_with_args(NULL, NULL, NULL, NULL, NULL, &err) == FALSE) { + msg = g_strdup(err->message); + g_error_free(err); + return msg; + } + initAlloc(); + loadFutures(); + return NULL; +} + +void uiUninit(void) +{ + uninitMenus(); + uninitAlloc(); +} + +void uiFreeInitError(const char *err) +{ + g_free((gpointer) err); +} + +static gboolean (*iteration)(gboolean) = NULL; + +void uiMain(void) +{ + iteration = gtk_main_iteration_do; + gtk_main(); +} + +static gboolean stepsQuit = FALSE; + +// the only difference is we ignore the return value from gtk_main_iteration_do(), since it will always be TRUE if gtk_main() was never called +// gtk_main_iteration_do() will still run the main loop regardless +static gboolean stepsIteration(gboolean block) +{ + gtk_main_iteration_do(block); + return stepsQuit; +} + +void uiMainSteps(void) +{ + iteration = stepsIteration; +} + +int uiMainStep(int wait) +{ + gboolean block; + + block = FALSE; + if (wait) + block = TRUE; + return (*iteration)(block) == FALSE; +} + +// gtk_main_quit() may run immediately, or it may wait for other pending events; "it depends" (thanks mclasen in irc.gimp.net/#gtk+) +// PostQuitMessage() on Windows always waits, so we must do so too +// we'll do it by using an idle callback +static gboolean quit(gpointer data) +{ + if (iteration == stepsIteration) + stepsQuit = TRUE; + // TODO run a gtk_main() here just to do the cleanup steps of syncing the clipboard and other stuff gtk_main() does before it returns + else + gtk_main_quit(); + return FALSE; +} + +void uiQuit(void) +{ + gdk_threads_add_idle(quit, NULL); +} + +struct queued { + void (*f)(void *); + void *data; +}; + +static gboolean doqueued(gpointer data) +{ + struct queued *q = (struct queued *) data; + + (*(q->f))(q->data); + g_free(q); + return FALSE; +} + +void uiQueueMain(void (*f)(void *data), void *data) +{ + struct queued *q; + + // we have to use g_new0()/g_free() because uiAlloc() is only safe to call on the main thread + // for some reason it didn't affect me, but it did affect krakjoe + q = g_new0(struct queued, 1); + q->f = f; + q->data = data; + gdk_threads_add_idle(doqueued, q); +} diff --git a/src/libui_sdl/libui/unix/menu.c b/src/libui_sdl/libui/unix/menu.c new file mode 100644 index 0000000..5ccb4a5 --- /dev/null +++ b/src/libui_sdl/libui/unix/menu.c @@ -0,0 +1,366 @@ +// 23 april 2015 +#include "uipriv_unix.h" + +static GArray *menus = NULL; +static gboolean menusFinalized = FALSE; +static gboolean hasQuit = FALSE; +static gboolean hasPreferences = FALSE; +static gboolean hasAbout = FALSE; + +struct uiMenu { + char *name; + GArray *items; // []*uiMenuItem +}; + +struct uiMenuItem { + char *name; + int type; + void (*onClicked)(uiMenuItem *, uiWindow *, void *); + void *onClickedData; + GType gtype; // template for new instances; kept in sync with everything else + gboolean disabled; + gboolean checked; + GHashTable *windows; // map[GtkMenuItem]*menuItemWindow +}; + +struct menuItemWindow { + uiWindow *w; + gulong signal; +}; + +enum { + typeRegular, + typeCheckbox, + typeQuit, + typePreferences, + typeAbout, + typeSeparator, +}; + +// we do NOT want programmatic updates to raise an ::activated signal +static void singleSetChecked(GtkCheckMenuItem *menuitem, gboolean checked, gulong signal) +{ + g_signal_handler_block(menuitem, signal); + gtk_check_menu_item_set_active(menuitem, checked); + g_signal_handler_unblock(menuitem, signal); +} + +static void setChecked(uiMenuItem *item, gboolean checked) +{ + GHashTableIter iter; + gpointer widget; + gpointer ww; + struct menuItemWindow *w; + + item->checked = checked; + g_hash_table_iter_init(&iter, item->windows); + while (g_hash_table_iter_next(&iter, &widget, &ww)) { + w = (struct menuItemWindow *) ww; + singleSetChecked(GTK_CHECK_MENU_ITEM(widget), item->checked, w->signal); + } +} + +static void onClicked(GtkMenuItem *menuitem, gpointer data) +{ + uiMenuItem *item = uiMenuItem(data); + struct menuItemWindow *w; + + // we need to manually update the checked states of all menu items if one changes + // notice that this is getting the checked state of the menu item that this signal is sent from + if (item->type == typeCheckbox) + setChecked(item, gtk_check_menu_item_get_active(GTK_CHECK_MENU_ITEM(menuitem))); + + w = (struct menuItemWindow *) g_hash_table_lookup(item->windows, menuitem); + (*(item->onClicked))(item, w->w, item->onClickedData); +} + +static void defaultOnClicked(uiMenuItem *item, uiWindow *w, void *data) +{ + // do nothing +} + +static void onQuitClicked(uiMenuItem *item, uiWindow *w, void *data) +{ + if (shouldQuit()) + uiQuit(); +} + +static void menuItemEnableDisable(uiMenuItem *item, gboolean enabled) +{ + GHashTableIter iter; + gpointer widget; + + item->disabled = !enabled; + g_hash_table_iter_init(&iter, item->windows); + while (g_hash_table_iter_next(&iter, &widget, NULL)) + gtk_widget_set_sensitive(GTK_WIDGET(widget), enabled); +} + +void uiMenuItemEnable(uiMenuItem *item) +{ + menuItemEnableDisable(item, TRUE); +} + +void uiMenuItemDisable(uiMenuItem *item) +{ + menuItemEnableDisable(item, FALSE); +} + +void uiMenuItemOnClicked(uiMenuItem *item, void (*f)(uiMenuItem *, uiWindow *, void *), void *data) +{ + if (item->type == typeQuit) + userbug("You cannot call uiMenuItemOnClicked() on a Quit item; use uiOnShouldQuit() instead."); + item->onClicked = f; + item->onClickedData = data; +} + +int uiMenuItemChecked(uiMenuItem *item) +{ + return item->checked != FALSE; +} + +void uiMenuItemSetChecked(uiMenuItem *item, int checked) +{ + gboolean c; + + // use explicit values + c = FALSE; + if (checked) + c = TRUE; + setChecked(item, c); +} + +static uiMenuItem *newItem(uiMenu *m, int type, const char *name) +{ + uiMenuItem *item; + + if (menusFinalized) + userbug("You cannot create a new menu item after menus have been finalized."); + + item = uiNew(uiMenuItem); + + g_array_append_val(m->items, item); + + item->type = type; + switch (item->type) { + case typeQuit: + item->name = g_strdup("Quit"); + break; + case typePreferences: + item->name = g_strdup("Preferences..."); + break; + case typeAbout: + item->name = g_strdup("About"); + break; + case typeSeparator: + break; + default: + item->name = g_strdup(name); + break; + } + + if (item->type == typeQuit) { + // can't call uiMenuItemOnClicked() here + item->onClicked = onQuitClicked; + item->onClickedData = NULL; + } else + uiMenuItemOnClicked(item, defaultOnClicked, NULL); + + switch (item->type) { + case typeCheckbox: + item->gtype = GTK_TYPE_CHECK_MENU_ITEM; + break; + case typeSeparator: + item->gtype = GTK_TYPE_SEPARATOR_MENU_ITEM; + break; + default: + item->gtype = GTK_TYPE_MENU_ITEM; + break; + } + + item->windows = g_hash_table_new(g_direct_hash, g_direct_equal); + + return item; +} + +uiMenuItem *uiMenuAppendItem(uiMenu *m, const char *name) +{ + return newItem(m, typeRegular, name); +} + +uiMenuItem *uiMenuAppendCheckItem(uiMenu *m, const char *name) +{ + return newItem(m, typeCheckbox, name); +} + +uiMenuItem *uiMenuAppendQuitItem(uiMenu *m) +{ + if (hasQuit) + userbug("You cannot have multiple Quit menu items in the same program."); + hasQuit = TRUE; + newItem(m, typeSeparator, NULL); + return newItem(m, typeQuit, NULL); +} + +uiMenuItem *uiMenuAppendPreferencesItem(uiMenu *m) +{ + if (hasPreferences) + userbug("You cannot have multiple Preferences menu items in the same program."); + hasPreferences = TRUE; + newItem(m, typeSeparator, NULL); + return newItem(m, typePreferences, NULL); +} + +uiMenuItem *uiMenuAppendAboutItem(uiMenu *m) +{ + if (hasAbout) + userbug("You cannot have multiple About menu items in the same program."); + hasAbout = TRUE; + newItem(m, typeSeparator, NULL); + return newItem(m, typeAbout, NULL); +} + +void uiMenuAppendSeparator(uiMenu *m) +{ + newItem(m, typeSeparator, NULL); +} + +uiMenu *uiNewMenu(const char *name) +{ + uiMenu *m; + + if (menusFinalized) + userbug("You cannot create a new menu after menus have been finalized."); + if (menus == NULL) + menus = g_array_new(FALSE, TRUE, sizeof (uiMenu *)); + + m = uiNew(uiMenu); + + g_array_append_val(menus, m); + + m->name = g_strdup(name); + m->items = g_array_new(FALSE, TRUE, sizeof (uiMenuItem *)); + + return m; +} + +static void appendMenuItem(GtkMenuShell *submenu, uiMenuItem *item, uiWindow *w) +{ + GtkWidget *menuitem; + gulong signal; + struct menuItemWindow *ww; + + menuitem = g_object_new(item->gtype, NULL); + if (item->name != NULL) + gtk_menu_item_set_label(GTK_MENU_ITEM(menuitem), item->name); + if (item->type != typeSeparator) { + signal = g_signal_connect(menuitem, "activate", G_CALLBACK(onClicked), item); + gtk_widget_set_sensitive(menuitem, !item->disabled); + if (item->type == typeCheckbox) + singleSetChecked(GTK_CHECK_MENU_ITEM(menuitem), item->checked, signal); + } + gtk_menu_shell_append(submenu, menuitem); + ww = uiNew(struct menuItemWindow); + ww->w = w; + ww->signal = signal; + g_hash_table_insert(item->windows, menuitem, ww); +} + +GtkWidget *makeMenubar(uiWindow *w) +{ + GtkWidget *menubar; + guint i, j; + uiMenu *m; + GtkWidget *menuitem; + GtkWidget *submenu; + + menusFinalized = TRUE; + + menubar = gtk_menu_bar_new(); + + if (menus != NULL) + for (i = 0; i < menus->len; i++) { + m = g_array_index(menus, uiMenu *, i); + menuitem = gtk_menu_item_new_with_label(m->name); + submenu = gtk_menu_new(); + gtk_menu_item_set_submenu(GTK_MENU_ITEM(menuitem), submenu); + for (j = 0; j < m->items->len; j++) + appendMenuItem(GTK_MENU_SHELL(submenu), g_array_index(m->items, uiMenuItem *, j), w); + gtk_menu_shell_append(GTK_MENU_SHELL(menubar), menuitem); + } + + gtk_widget_set_hexpand(menubar, TRUE); + gtk_widget_set_halign(menubar, GTK_ALIGN_FILL); + return menubar; +} + +struct freeMenuItemData { + GArray *items; + guint i; +}; + +static void freeMenuItem(GtkWidget *widget, gpointer data) +{ + struct freeMenuItemData *fmi = (struct freeMenuItemData *) data; + uiMenuItem *item; + struct menuItemWindow *w; + + item = g_array_index(fmi->items, uiMenuItem *, fmi->i); + w = (struct menuItemWindow *) g_hash_table_lookup(item->windows, widget); + if (g_hash_table_remove(item->windows, widget) == FALSE) + implbug("GtkMenuItem %p not in menu item's item/window map", widget); + uiFree(w); + fmi->i++; +} + +static void freeMenu(GtkWidget *widget, gpointer data) +{ + guint *i = (guint *) data; + uiMenu *m; + GtkMenuItem *item; + GtkWidget *submenu; + struct freeMenuItemData fmi; + + m = g_array_index(menus, uiMenu *, *i); + item = GTK_MENU_ITEM(widget); + submenu = gtk_menu_item_get_submenu(item); + fmi.items = m->items; + fmi.i = 0; + gtk_container_foreach(GTK_CONTAINER(submenu), freeMenuItem, &fmi); + (*i)++; +} + +void freeMenubar(GtkWidget *mb) +{ + guint i; + + i = 0; + gtk_container_foreach(GTK_CONTAINER(mb), freeMenu, &i); + // no need to worry about destroying any widgets; destruction of the window they're in will do it for us +} + +void uninitMenus(void) +{ + uiMenu *m; + uiMenuItem *item; + guint i, j; + + if (menus == NULL) + return; + for (i = 0; i < menus->len; i++) { + m = g_array_index(menus, uiMenu *, i); + g_free(m->name); + for (j = 0; j < m->items->len; j++) { + item = g_array_index(m->items, uiMenuItem *, j); + if (g_hash_table_size(item->windows) != 0) + // TODO is this really a userbug()? + implbug("menu item %p (%s) still has uiWindows attached; did you forget to destroy some windows?", item, item->name); + g_free(item->name); + g_hash_table_destroy(item->windows); + uiFree(item); + } + g_array_free(m->items, TRUE); + uiFree(m); + } + g_array_free(menus, TRUE); +} diff --git a/src/libui_sdl/libui/unix/multilineentry.c b/src/libui_sdl/libui/unix/multilineentry.c new file mode 100644 index 0000000..09ffd46 --- /dev/null +++ b/src/libui_sdl/libui/unix/multilineentry.c @@ -0,0 +1,124 @@ +// 6 december 2015 +#include "uipriv_unix.h" + +struct uiMultilineEntry { + uiUnixControl c; + GtkWidget *widget; + GtkContainer *scontainer; + GtkScrolledWindow *sw; + GtkWidget *textviewWidget; + GtkTextView *textview; + GtkTextBuffer *textbuf; + void (*onChanged)(uiMultilineEntry *, void *); + void *onChangedData; + gulong onChangedSignal; +}; + +uiUnixControlAllDefaults(uiMultilineEntry) + +static void onChanged(GtkTextBuffer *textbuf, gpointer data) +{ + uiMultilineEntry *e = uiMultilineEntry(data); + + (*(e->onChanged))(e, e->onChangedData); +} + +static void defaultOnChanged(uiMultilineEntry *e, void *data) +{ + // do nothing +} + +char *uiMultilineEntryText(uiMultilineEntry *e) +{ + GtkTextIter start, end; + char *tret, *out; + + gtk_text_buffer_get_start_iter(e->textbuf, &start); + gtk_text_buffer_get_end_iter(e->textbuf, &end); + tret = gtk_text_buffer_get_text(e->textbuf, &start, &end, TRUE); + // theoretically we could just return tret because uiUnixStrdupText() is just g_strdup(), but if that ever changes we can't, so let's do it this way to be safe + out = uiUnixStrdupText(tret); + g_free(tret); + return out; +} + +void uiMultilineEntrySetText(uiMultilineEntry *e, const char *text) +{ + // we need to inhibit sending of ::changed because this WILL send a ::changed otherwise + g_signal_handler_block(e->textbuf, e->onChangedSignal); + gtk_text_buffer_set_text(e->textbuf, text, -1); + g_signal_handler_unblock(e->textbuf, e->onChangedSignal); +} + +// TODO scroll to end? +void uiMultilineEntryAppend(uiMultilineEntry *e, const char *text) +{ + GtkTextIter end; + + gtk_text_buffer_get_end_iter(e->textbuf, &end); + // we need to inhibit sending of ::changed because this WILL send a ::changed otherwise + g_signal_handler_block(e->textbuf, e->onChangedSignal); + gtk_text_buffer_insert(e->textbuf, &end, text, -1); + g_signal_handler_unblock(e->textbuf, e->onChangedSignal); +} + +void uiMultilineEntryOnChanged(uiMultilineEntry *e, void (*f)(uiMultilineEntry *e, void *data), void *data) +{ + e->onChanged = f; + e->onChangedData = data; +} + +int uiMultilineEntryReadOnly(uiMultilineEntry *e) +{ + return gtk_text_view_get_editable(e->textview) == FALSE; +} + +void uiMultilineEntrySetReadOnly(uiMultilineEntry *e, int readonly) +{ + gboolean editable; + + editable = TRUE; + if (readonly) + editable = FALSE; + gtk_text_view_set_editable(e->textview, editable); +} + +static uiMultilineEntry *finishMultilineEntry(GtkPolicyType hpolicy, GtkWrapMode wrapMode) +{ + uiMultilineEntry *e; + + uiUnixNewControl(uiMultilineEntry, e); + + e->widget = gtk_scrolled_window_new(NULL, NULL); + e->scontainer = GTK_CONTAINER(e->widget); + e->sw = GTK_SCROLLED_WINDOW(e->widget); + gtk_scrolled_window_set_policy(e->sw, + hpolicy, + GTK_POLICY_AUTOMATIC); + gtk_scrolled_window_set_shadow_type(e->sw, GTK_SHADOW_IN); + + e->textviewWidget = gtk_text_view_new(); + e->textview = GTK_TEXT_VIEW(e->textviewWidget); + gtk_text_view_set_wrap_mode(e->textview, wrapMode); + + gtk_container_add(e->scontainer, e->textviewWidget); + // and make the text view visible; only the scrolled window's visibility is controlled by libui + gtk_widget_show(e->textviewWidget); + + e->textbuf = gtk_text_view_get_buffer(e->textview); + + e->onChangedSignal = g_signal_connect(e->textbuf, "changed", G_CALLBACK(onChanged), e); + uiMultilineEntryOnChanged(e, defaultOnChanged, NULL); + + return e; +} + +uiMultilineEntry *uiNewMultilineEntry(void) +{ + return finishMultilineEntry(GTK_POLICY_NEVER, GTK_WRAP_WORD); +} + +uiMultilineEntry *uiNewNonWrappingMultilineEntry(void) +{ + return finishMultilineEntry(GTK_POLICY_AUTOMATIC, GTK_WRAP_NONE); +} diff --git a/src/libui_sdl/libui/unix/progressbar.c b/src/libui_sdl/libui/unix/progressbar.c new file mode 100644 index 0000000..9b543b0 --- /dev/null +++ b/src/libui_sdl/libui/unix/progressbar.c @@ -0,0 +1,71 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +struct uiProgressBar { + uiUnixControl c; + GtkWidget *widget; + GtkProgressBar *pbar; + gboolean indeterminate; + guint pulser; +}; + +uiUnixControlAllDefaultsExceptDestroy(uiProgressBar) + +static void uiProgressBarDestroy(uiControl *c) +{ + uiProgressBar *p = uiProgressBar(c); + + // be sure to stop the timeout now + if (p->indeterminate) + g_source_remove(p->pulser); + g_object_unref(p->widget); + uiFreeControl(uiControl(p)); +} + +int uiProgressBarValue(uiProgressBar *p) +{ + if (p->indeterminate) + return -1; + return (int) (gtk_progress_bar_get_fraction(p->pbar) * 100); +} + +static gboolean pulse(void* data) +{ + uiProgressBar *p = uiProgressBar(data); + + gtk_progress_bar_pulse(p->pbar); + return TRUE; +} + +void uiProgressBarSetValue(uiProgressBar *p, int value) +{ + if (value == -1) { + if (!p->indeterminate) { + p->indeterminate = TRUE; + // TODO verify the timeout + p->pulser = g_timeout_add(100, pulse, p); + } + return; + } + if (p->indeterminate) { + p->indeterminate = FALSE; + g_source_remove(p->pulser); + } + + if (value < 0 || value > 100) + userbug("Value %d is out of range for a uiProgressBar.", value); + + gtk_progress_bar_set_fraction(p->pbar, ((gdouble) value) / 100); +} + +uiProgressBar *uiNewProgressBar(void) +{ + uiProgressBar *p; + + uiUnixNewControl(uiProgressBar, p); + + p->widget = gtk_progress_bar_new(); + p->pbar = GTK_PROGRESS_BAR(p->widget); + + return p; +} diff --git a/src/libui_sdl/libui/unix/radiobuttons.c b/src/libui_sdl/libui/unix/radiobuttons.c new file mode 100644 index 0000000..da41107 --- /dev/null +++ b/src/libui_sdl/libui/unix/radiobuttons.c @@ -0,0 +1,121 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +// on GTK+ a uiRadioButtons is a GtkBox with each of the GtkRadioButtons as children + +struct uiRadioButtons { + uiUnixControl c; + GtkWidget *widget; + GtkContainer *container; + GtkBox *box; + GPtrArray *buttons; + void (*onSelected)(uiRadioButtons *, void *); + void *onSelectedData; + gboolean changing; +}; + +uiUnixControlAllDefaultsExceptDestroy(uiRadioButtons) + +static void defaultOnSelected(uiRadioButtons *r, void *data) +{ + // do nothing +} + +static void onToggled(GtkToggleButton *tb, gpointer data) +{ + uiRadioButtons *r = uiRadioButtons(data); + + // only care if a button is selected + if (!gtk_toggle_button_get_active(tb)) + return; + // ignore programmatic changes + if (r->changing) + return; + (*(r->onSelected))(r, r->onSelectedData); +} + +static void uiRadioButtonsDestroy(uiControl *c) +{ + uiRadioButtons *r = uiRadioButtons(c); + GtkWidget *b; + + while (r->buttons->len != 0) { + b = GTK_WIDGET(g_ptr_array_remove_index(r->buttons, 0)); + gtk_widget_destroy(b); + } + g_ptr_array_free(r->buttons, TRUE); + // and free ourselves + g_object_unref(r->widget); + uiFreeControl(uiControl(r)); +} + +void uiRadioButtonsAppend(uiRadioButtons *r, const char *text) +{ + GtkWidget *rb; + GtkRadioButton *previous; + + previous = NULL; + if (r->buttons->len > 0) + previous = GTK_RADIO_BUTTON(g_ptr_array_index(r->buttons, 0)); + rb = gtk_radio_button_new_with_label_from_widget(previous, text); + g_signal_connect(rb, "toggled", G_CALLBACK(onToggled), r); + gtk_container_add(r->container, rb); + g_ptr_array_add(r->buttons, rb); + gtk_widget_show(rb); +} + +int uiRadioButtonsSelected(uiRadioButtons *r) +{ + GtkToggleButton *tb; + guint i; + + for (i = 0; i < r->buttons->len; i++) { + tb = GTK_TOGGLE_BUTTON(g_ptr_array_index(r->buttons, i)); + if (gtk_toggle_button_get_active(tb)) + return i; + } + return -1; +} + +void uiRadioButtonsSetSelected(uiRadioButtons *r, int n) +{ + GtkToggleButton *tb; + gboolean active; + + active = TRUE; + // TODO this doesn't work + if (n == -1) { + n = uiRadioButtonsSelected(r); + if (n == -1) // no selection; keep it that way + return; + active = FALSE; + } + tb = GTK_TOGGLE_BUTTON(g_ptr_array_index(r->buttons, n)); + // this is easier than remembering all the signals + r->changing = TRUE; + gtk_toggle_button_set_active(tb, active); + r->changing = FALSE; +} + +void uiRadioButtonsOnSelected(uiRadioButtons *r, void (*f)(uiRadioButtons *, void *), void *data) +{ + r->onSelected = f; + r->onSelectedData = data; +} + +uiRadioButtons *uiNewRadioButtons(void) +{ + uiRadioButtons *r; + + uiUnixNewControl(uiRadioButtons, r); + + r->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + r->container = GTK_CONTAINER(r->widget); + r->box = GTK_BOX(r->widget); + + r->buttons = g_ptr_array_new(); + + uiRadioButtonsOnSelected(r, defaultOnSelected, NULL); + + return r; +} diff --git a/src/libui_sdl/libui/unix/separator.c b/src/libui_sdl/libui/unix/separator.c new file mode 100644 index 0000000..02c75da --- /dev/null +++ b/src/libui_sdl/libui/unix/separator.c @@ -0,0 +1,34 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +struct uiSeparator { + uiUnixControl c; + GtkWidget *widget; + GtkSeparator *separator; +}; + +uiUnixControlAllDefaults(uiSeparator) + +uiSeparator *uiNewHorizontalSeparator(void) +{ + uiSeparator *s; + + uiUnixNewControl(uiSeparator, s); + + s->widget = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL); + s->separator = GTK_SEPARATOR(s->widget); + + return s; +} + +uiSeparator *uiNewVerticalSeparator(void) +{ + uiSeparator *s; + + uiUnixNewControl(uiSeparator, s); + + s->widget = gtk_separator_new(GTK_ORIENTATION_VERTICAL); + s->separator = GTK_SEPARATOR(s->widget); + + return s; +} diff --git a/src/libui_sdl/libui/unix/slider.c b/src/libui_sdl/libui/unix/slider.c new file mode 100644 index 0000000..7f0cc24 --- /dev/null +++ b/src/libui_sdl/libui/unix/slider.c @@ -0,0 +1,71 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +struct uiSlider { + uiUnixControl c; + GtkWidget *widget; + GtkRange *range; + GtkScale *scale; + void (*onChanged)(uiSlider *, void *); + void *onChangedData; + gulong onChangedSignal; +}; + +uiUnixControlAllDefaults(uiSlider) + +static void onChanged(GtkRange *range, gpointer data) +{ + uiSlider *s = uiSlider(data); + + (*(s->onChanged))(s, s->onChangedData); +} + +static void defaultOnChanged(uiSlider *s, void *data) +{ + // do nothing +} + +int uiSliderValue(uiSlider *s) +{ + return gtk_range_get_value(s->range); +} + +void uiSliderSetValue(uiSlider *s, int value) +{ + // we need to inhibit sending of ::value-changed because this WILL send a ::value-changed otherwise + g_signal_handler_block(s->range, s->onChangedSignal); + gtk_range_set_value(s->range, value); + g_signal_handler_unblock(s->range, s->onChangedSignal); +} + +void uiSliderOnChanged(uiSlider *s, void (*f)(uiSlider *, void *), void *data) +{ + s->onChanged = f; + s->onChangedData = data; +} + +uiSlider *uiNewSlider(int min, int max) +{ + uiSlider *s; + int temp; + + if (min >= max) { + temp = min; + min = max; + max = temp; + } + + uiUnixNewControl(uiSlider, s); + + s->widget = gtk_scale_new_with_range(GTK_ORIENTATION_HORIZONTAL, min, max, 1); + s->range = GTK_RANGE(s->widget); + s->scale = GTK_SCALE(s->widget); + + // ensure integers, just to be safe + gtk_scale_set_digits(s->scale, 0); + + s->onChangedSignal = g_signal_connect(s->scale, "value-changed", G_CALLBACK(onChanged), s); + uiSliderOnChanged(s, defaultOnChanged, NULL); + + return s; +} diff --git a/src/libui_sdl/libui/unix/spinbox.c b/src/libui_sdl/libui/unix/spinbox.c new file mode 100644 index 0000000..90a5d3c --- /dev/null +++ b/src/libui_sdl/libui/unix/spinbox.c @@ -0,0 +1,72 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +struct uiSpinbox { + uiUnixControl c; + GtkWidget *widget; + GtkEntry *entry; + GtkSpinButton *spinButton; + void (*onChanged)(uiSpinbox *, void *); + void *onChangedData; + gulong onChangedSignal; +}; + +uiUnixControlAllDefaults(uiSpinbox) + +static void onChanged(GtkSpinButton *sb, gpointer data) +{ + uiSpinbox *s = uiSpinbox(data); + + (*(s->onChanged))(s, s->onChangedData); +} + +static void defaultOnChanged(uiSpinbox *s, void *data) +{ + // do nothing +} + +int uiSpinboxValue(uiSpinbox *s) +{ + return gtk_spin_button_get_value(s->spinButton); +} + +void uiSpinboxSetValue(uiSpinbox *s, int value) +{ + // we need to inhibit sending of ::value-changed because this WILL send a ::value-changed otherwise + g_signal_handler_block(s->spinButton, s->onChangedSignal); + // this clamps for us + gtk_spin_button_set_value(s->spinButton, (gdouble) value); + g_signal_handler_unblock(s->spinButton, s->onChangedSignal); +} + +void uiSpinboxOnChanged(uiSpinbox *s, void (*f)(uiSpinbox *, void *), void *data) +{ + s->onChanged = f; + s->onChangedData = data; +} + +uiSpinbox *uiNewSpinbox(int min, int max) +{ + uiSpinbox *s; + int temp; + + if (min >= max) { + temp = min; + min = max; + max = temp; + } + + uiUnixNewControl(uiSpinbox, s); + + s->widget = gtk_spin_button_new_with_range(min, max, 1); + s->entry = GTK_ENTRY(s->widget); + s->spinButton = GTK_SPIN_BUTTON(s->widget); + + // ensure integers, just to be safe + gtk_spin_button_set_digits(s->spinButton, 0); + + s->onChangedSignal = g_signal_connect(s->spinButton, "value-changed", G_CALLBACK(onChanged), s); + uiSpinboxOnChanged(s, defaultOnChanged, NULL); + + return s; +} diff --git a/src/libui_sdl/libui/unix/stddialogs.c b/src/libui_sdl/libui/unix/stddialogs.c new file mode 100644 index 0000000..93302f7 --- /dev/null +++ b/src/libui_sdl/libui/unix/stddialogs.c @@ -0,0 +1,66 @@ +// 26 june 2015 +#include "uipriv_unix.h" + +// LONGTERM figure out why, and describe, that this is the desired behavior +// LONGTERM also point out that font and color buttons also work like this + +#define windowWindow(w) (GTK_WINDOW(uiControlHandle(uiControl(w)))) + +static char *filedialog(GtkWindow *parent, GtkFileChooserAction mode, const gchar *confirm) +{ + GtkWidget *fcd; + GtkFileChooser *fc; + gint response; + char *filename; + + fcd = gtk_file_chooser_dialog_new(NULL, parent, mode, + "_Cancel", GTK_RESPONSE_CANCEL, + confirm, GTK_RESPONSE_ACCEPT, + NULL); + fc = GTK_FILE_CHOOSER(fcd); + gtk_file_chooser_set_local_only(fc, FALSE); + gtk_file_chooser_set_select_multiple(fc, FALSE); + gtk_file_chooser_set_show_hidden(fc, TRUE); + gtk_file_chooser_set_do_overwrite_confirmation(fc, TRUE); + gtk_file_chooser_set_create_folders(fc, TRUE); + response = gtk_dialog_run(GTK_DIALOG(fcd)); + if (response != GTK_RESPONSE_ACCEPT) { + gtk_widget_destroy(fcd); + return NULL; + } + filename = uiUnixStrdupText(gtk_file_chooser_get_filename(fc)); + gtk_widget_destroy(fcd); + return filename; +} + +char *uiOpenFile(uiWindow *parent) +{ + return filedialog(windowWindow(parent), GTK_FILE_CHOOSER_ACTION_OPEN, "_Open"); +} + +char *uiSaveFile(uiWindow *parent) +{ + return filedialog(windowWindow(parent), GTK_FILE_CHOOSER_ACTION_SAVE, "_Save"); +} + +static void msgbox(GtkWindow *parent, const char *title, const char *description, GtkMessageType type, GtkButtonsType buttons) +{ + GtkWidget *md; + + md = gtk_message_dialog_new(parent, GTK_DIALOG_MODAL, + type, buttons, + "%s", title); + gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(md), "%s", description); + gtk_dialog_run(GTK_DIALOG(md)); + gtk_widget_destroy(md); +} + +void uiMsgBox(uiWindow *parent, const char *title, const char *description) +{ + msgbox(windowWindow(parent), title, description, GTK_MESSAGE_OTHER, GTK_BUTTONS_OK); +} + +void uiMsgBoxError(uiWindow *parent, const char *title, const char *description) +{ + msgbox(windowWindow(parent), title, description, GTK_MESSAGE_ERROR, GTK_BUTTONS_OK); +} diff --git a/src/libui_sdl/libui/unix/tab.c b/src/libui_sdl/libui/unix/tab.c new file mode 100644 index 0000000..552e0e3 --- /dev/null +++ b/src/libui_sdl/libui/unix/tab.c @@ -0,0 +1,97 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +struct uiTab { + uiUnixControl c; + + GtkWidget *widget; + GtkContainer *container; + GtkNotebook *notebook; + + GArray *pages; // []*struct child +}; + +uiUnixControlAllDefaultsExceptDestroy(uiTab) + +static void uiTabDestroy(uiControl *c) +{ + uiTab *t = uiTab(c); + guint i; + struct child *page; + + for (i = 0; i < t->pages->len; i++) { + page = g_array_index(t->pages, struct child *, i); + childDestroy(page); + } + g_array_free(t->pages, TRUE); + // and free ourselves + g_object_unref(t->widget); + uiFreeControl(uiControl(t)); +} + +void uiTabAppend(uiTab *t, const char *name, uiControl *child) +{ + uiTabInsertAt(t, name, t->pages->len, child); +} + +void uiTabInsertAt(uiTab *t, const char *name, int n, uiControl *child) +{ + struct child *page; + + // this will create a tab, because of gtk_container_add() + page = newChildWithBox(child, uiControl(t), t->container, 0); + + gtk_notebook_set_tab_label_text(t->notebook, childBox(page), name); + gtk_notebook_reorder_child(t->notebook, childBox(page), n); + + g_array_insert_val(t->pages, n, page); +} + +void uiTabDelete(uiTab *t, int n) +{ + struct child *page; + + page = g_array_index(t->pages, struct child *, n); + // this will remove the tab, because gtk_widget_destroy() calls gtk_container_remove() + childRemove(page); + g_array_remove_index(t->pages, n); +} + +int uiTabNumPages(uiTab *t) +{ + return t->pages->len; +} + +int uiTabMargined(uiTab *t, int n) +{ + struct child *page; + + page = g_array_index(t->pages, struct child *, n); + return childFlag(page); +} + +void uiTabSetMargined(uiTab *t, int n, int margined) +{ + struct child *page; + + page = g_array_index(t->pages, struct child *, n); + childSetFlag(page, margined); + childSetMargined(page, childFlag(page)); +} + +uiTab *uiNewTab(void) +{ + uiTab *t; + + uiUnixNewControl(uiTab, t); + + t->widget = gtk_notebook_new(); + t->container = GTK_CONTAINER(t->widget); + t->notebook = GTK_NOTEBOOK(t->widget); + + gtk_notebook_set_scrollable(t->notebook, TRUE); + + t->pages = g_array_new(FALSE, TRUE, sizeof (struct child *)); + + return t; +} diff --git a/src/libui_sdl/libui/unix/text.c b/src/libui_sdl/libui/unix/text.c new file mode 100644 index 0000000..ad92738 --- /dev/null +++ b/src/libui_sdl/libui/unix/text.c @@ -0,0 +1,12 @@ +// 9 april 2015 +#include "uipriv_unix.h" + +char *uiUnixStrdupText(const char *t) +{ + return g_strdup(t); +} + +void uiFreeText(char *t) +{ + g_free(t); +} diff --git a/src/libui_sdl/libui/unix/uipriv_unix.h b/src/libui_sdl/libui/unix/uipriv_unix.h new file mode 100644 index 0000000..33ff1e3 --- /dev/null +++ b/src/libui_sdl/libui/unix/uipriv_unix.h @@ -0,0 +1,65 @@ +// 22 april 2015 +#define GLIB_VERSION_MIN_REQUIRED GLIB_VERSION_2_40 +#define GLIB_VERSION_MAX_ALLOWED GLIB_VERSION_2_40 +#define GDK_VERSION_MIN_REQUIRED GDK_VERSION_3_10 +#define GDK_VERSION_MAX_ALLOWED GDK_VERSION_3_10 +#include <gtk/gtk.h> +#include <math.h> +#include <dlfcn.h> // see drawtext.c +#include <langinfo.h> +#include <string.h> +#include <stdlib.h> +#include "../ui.h" +#include "../ui_unix.h" +#include "../common/uipriv.h" + +#define gtkXMargin 12 +#define gtkYMargin 12 +#define gtkXPadding 12 +#define gtkYPadding 6 + +// menu.c +extern GtkWidget *makeMenubar(uiWindow *); +extern void freeMenubar(GtkWidget *); +extern void uninitMenus(void); + +// alloc.c +extern void initAlloc(void); +extern void uninitAlloc(void); + +// util.c +extern void setMargined(GtkContainer *, int); + +// child.c +extern struct child *newChild(uiControl *child, uiControl *parent, GtkContainer *parentContainer); +extern struct child *newChildWithBox(uiControl *child, uiControl *parent, GtkContainer *parentContainer, int margined); +extern void childRemove(struct child *c); +extern void childDestroy(struct child *c); +extern GtkWidget *childWidget(struct child *c); +extern int childFlag(struct child *c); +extern void childSetFlag(struct child *c, int flag); +extern GtkWidget *childBox(struct child *c); +extern void childSetMargined(struct child *c, int margined); + +// draw.c +extern uiDrawContext *newContext(cairo_t *); +extern void freeContext(uiDrawContext *); + +// drawtext.c +extern uiDrawTextFont *mkTextFont(PangoFont *f, gboolean add); +extern PangoFont *pangoDescToPangoFont(PangoFontDescription *pdesc); + +// graphemes.c +extern ptrdiff_t *graphemes(const char *text, PangoContext *context); + +// image.c +/*TODO remove this*/typedef struct uiImage uiImage; +extern cairo_surface_t *imageAppropriateSurface(uiImage *i, GtkWidget *w); + +// cellrendererbutton.c +extern GtkCellRenderer *newCellRendererButton(void); + +// future.c +extern void loadFutures(void); +extern PangoAttribute *FUTURE_pango_attr_foreground_alpha_new(guint16 alpha); +extern gboolean FUTURE_gtk_widget_path_iter_set_object_name(GtkWidgetPath *path, gint pos, const char *name); diff --git a/src/libui_sdl/libui/unix/util.c b/src/libui_sdl/libui/unix/util.c new file mode 100644 index 0000000..7f4f43f --- /dev/null +++ b/src/libui_sdl/libui/unix/util.c @@ -0,0 +1,10 @@ +// 18 april 2015 +#include "uipriv_unix.h" + +void setMargined(GtkContainer *c, int margined) +{ + if (margined) + gtk_container_set_border_width(c, gtkXMargin); + else + gtk_container_set_border_width(c, 0); +} diff --git a/src/libui_sdl/libui/unix/window.c b/src/libui_sdl/libui/unix/window.c new file mode 100644 index 0000000..ea9ba37 --- /dev/null +++ b/src/libui_sdl/libui/unix/window.c @@ -0,0 +1,279 @@ +// 11 june 2015 +#include "uipriv_unix.h" + +struct uiWindow { + uiUnixControl c; + + GtkWidget *widget; + GtkContainer *container; + GtkWindow *window; + + GtkWidget *vboxWidget; + GtkContainer *vboxContainer; + GtkBox *vbox; + + GtkWidget *childHolderWidget; + GtkContainer *childHolderContainer; + + GtkWidget *menubar; + + uiControl *child; + int margined; + + int (*onClosing)(uiWindow *, void *); + void *onClosingData; + void (*onContentSizeChanged)(uiWindow *, void *); + void *onContentSizeChangedData; + gboolean fullscreen; +}; + +static gboolean onClosing(GtkWidget *win, GdkEvent *e, gpointer data) +{ + uiWindow *w = uiWindow(data); + + // manually destroy the window ourselves; don't let the delete-event handler do it + if ((*(w->onClosing))(w, w->onClosingData)) + uiControlDestroy(uiControl(w)); + // don't continue to the default delete-event handler; we destroyed the window by now + return TRUE; +} + +static void onSizeAllocate(GtkWidget *widget, GdkRectangle *allocation, gpointer data) +{ + uiWindow *w = uiWindow(data); + + // TODO deal with spurious size-allocates + (*(w->onContentSizeChanged))(w, w->onContentSizeChangedData); +} + +static int defaultOnClosing(uiWindow *w, void *data) +{ + return 0; +} + +static void defaultOnPositionContentSizeChanged(uiWindow *w, void *data) +{ + // do nothing +} + +static void uiWindowDestroy(uiControl *c) +{ + uiWindow *w = uiWindow(c); + + // first hide ourselves + gtk_widget_hide(w->widget); + // now destroy the child + if (w->child != NULL) { + uiControlSetParent(w->child, NULL); + uiUnixControlSetContainer(uiUnixControl(w->child), w->childHolderContainer, TRUE); + uiControlDestroy(w->child); + } + // now destroy the menus, if any + if (w->menubar != NULL) + freeMenubar(w->menubar); + gtk_widget_destroy(w->childHolderWidget); + gtk_widget_destroy(w->vboxWidget); + // and finally free ourselves + // use gtk_widget_destroy() instead of g_object_unref() because GTK+ has internal references (see #165) + gtk_widget_destroy(w->widget); + uiFreeControl(uiControl(w)); +} + +uiUnixControlDefaultHandle(uiWindow) + +uiControl *uiWindowParent(uiControl *c) +{ + return NULL; +} + +void uiWindowSetParent(uiControl *c, uiControl *parent) +{ + uiUserBugCannotSetParentOnToplevel("uiWindow"); +} + +static int uiWindowToplevel(uiControl *c) +{ + return 1; +} + +uiUnixControlDefaultVisible(uiWindow) + +static void uiWindowShow(uiControl *c) +{ + uiWindow *w = uiWindow(c); + + // don't use gtk_widget_show_all() as that will show all children, regardless of user settings + // don't use gtk_widget_show(); that doesn't bring to front or give keyboard focus + // (gtk_window_present() does call gtk_widget_show() though) + gtk_window_present(w->window); +} + +uiUnixControlDefaultHide(uiWindow) +uiUnixControlDefaultEnabled(uiWindow) +uiUnixControlDefaultEnable(uiWindow) +uiUnixControlDefaultDisable(uiWindow) +// TODO? +uiUnixControlDefaultSetContainer(uiWindow) + +char *uiWindowTitle(uiWindow *w) +{ + return uiUnixStrdupText(gtk_window_get_title(w->window)); +} + +void uiWindowSetTitle(uiWindow *w, const char *title) +{ + gtk_window_set_title(w->window, title); +} + +void uiWindowContentSize(uiWindow *w, int *width, int *height) +{ + GtkAllocation allocation; + + gtk_widget_get_allocation(w->childHolderWidget, &allocation); + *width = allocation.width; + *height = allocation.height; +} + +void uiWindowSetContentSize(uiWindow *w, int width, int height) +{ + GtkAllocation childAlloc; + gint winWidth, winHeight; + + // we need to resize the child holder widget to the given size + // we can't resize that without running the event loop + // but we can do gtk_window_set_size() + // so how do we deal with the differences in sizes? + // simple arithmetic, of course! + + // from what I can tell, the return from gtk_widget_get_allocation(w->window) and gtk_window_get_size(w->window) will be the same + // this is not affected by Wayland and not affected by GTK+ builtin CSD + // so we can safely juse use them to get the real window size! + // since we're using gtk_window_resize(), use the latter + gtk_window_get_size(w->window, &winWidth, &winHeight); + + // now get the child holder widget's current allocation + gtk_widget_get_allocation(w->childHolderWidget, &childAlloc); + // and punch that out of the window size + winWidth -= childAlloc.width; + winHeight -= childAlloc.height; + + // now we just need to add the new size back in + winWidth += width; + winHeight += height; + // and set it + // this will not move the window in my tests, so we're good + gtk_window_resize(w->window, winWidth, winHeight); +} + +int uiWindowFullscreen(uiWindow *w) +{ + return w->fullscreen; +} + +// TODO use window-state-event to track +// TODO does this send an extra size changed? +// TODO what behavior do we want? +void uiWindowSetFullscreen(uiWindow *w, int fullscreen) +{ + w->fullscreen = fullscreen; + if (w->fullscreen) + gtk_window_fullscreen(w->window); + else + gtk_window_unfullscreen(w->window); +} + +void uiWindowOnContentSizeChanged(uiWindow *w, void (*f)(uiWindow *, void *), void *data) +{ + w->onContentSizeChanged = f; + w->onContentSizeChangedData = data; +} + +void uiWindowOnClosing(uiWindow *w, int (*f)(uiWindow *, void *), void *data) +{ + w->onClosing = f; + w->onClosingData = data; +} + +int uiWindowBorderless(uiWindow *w) +{ + return gtk_window_get_decorated(w->window) == FALSE; +} + +void uiWindowSetBorderless(uiWindow *w, int borderless) +{ + gtk_window_set_decorated(w->window, borderless == 0); +} + +// TODO save and restore expands and aligns +void uiWindowSetChild(uiWindow *w, uiControl *child) +{ + if (w->child != NULL) { + uiControlSetParent(w->child, NULL); + uiUnixControlSetContainer(uiUnixControl(w->child), w->childHolderContainer, TRUE); + } + w->child = child; + if (w->child != NULL) { + uiControlSetParent(w->child, uiControl(w)); + uiUnixControlSetContainer(uiUnixControl(w->child), w->childHolderContainer, FALSE); + } +} + +int uiWindowMargined(uiWindow *w) +{ + return w->margined; +} + +void uiWindowSetMargined(uiWindow *w, int margined) +{ + w->margined = margined; + setMargined(w->childHolderContainer, w->margined); +} + +uiWindow *uiNewWindow(const char *title, int width, int height, int hasMenubar) +{ + uiWindow *w; + + uiUnixNewControl(uiWindow, w); + + w->widget = gtk_window_new(GTK_WINDOW_TOPLEVEL); + w->container = GTK_CONTAINER(w->widget); + w->window = GTK_WINDOW(w->widget); + + gtk_window_set_title(w->window, title); + gtk_window_resize(w->window, width, height); + + w->vboxWidget = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); + w->vboxContainer = GTK_CONTAINER(w->vboxWidget); + w->vbox = GTK_BOX(w->vboxWidget); + + // set the vbox as the GtkWindow child + gtk_container_add(w->container, w->vboxWidget); + + if (hasMenubar) { + w->menubar = makeMenubar(uiWindow(w)); + gtk_container_add(w->vboxContainer, w->menubar); + } + + w->childHolderWidget = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0); + w->childHolderContainer = GTK_CONTAINER(w->childHolderWidget); + gtk_widget_set_hexpand(w->childHolderWidget, TRUE); + gtk_widget_set_halign(w->childHolderWidget, GTK_ALIGN_FILL); + gtk_widget_set_vexpand(w->childHolderWidget, TRUE); + gtk_widget_set_valign(w->childHolderWidget, GTK_ALIGN_FILL); + gtk_container_add(w->vboxContainer, w->childHolderWidget); + + // show everything in the vbox, but not the GtkWindow itself + gtk_widget_show_all(w->vboxWidget); + + // and connect our events + g_signal_connect(w->widget, "delete-event", G_CALLBACK(onClosing), w); + g_signal_connect(w->childHolderWidget, "size-allocate", G_CALLBACK(onSizeAllocate), w); + uiWindowOnClosing(w, defaultOnClosing, NULL); + uiWindowOnContentSizeChanged(w, defaultOnPositionContentSizeChanged, NULL); + + // normally it's SetParent() that does this, but we can't call SetParent() on a uiWindow + // TODO we really need to clean this up, especially since see uiWindowDestroy() above + g_object_ref(w->widget); + + return w; +} |