// +build !windows,!darwin,!plan9 // 14 march 2014 package ui import ( "fmt" "image" "unsafe" ) // #include "gtk_unix.h" // extern gboolean our_area_draw_callback(GtkWidget *, cairo_t *, gpointer); // extern gboolean our_area_button_press_event_callback(GtkWidget *, GdkEvent *, gpointer); // extern gboolean our_area_button_release_event_callback(GtkWidget *, GdkEvent *, gpointer); // extern gboolean our_area_motion_notify_event_callback(GtkWidget *, GdkEvent *, gpointer); // extern gboolean our_area_enterleave_notify_event_callback(GtkWidget *, GdkEvent *, gpointer); // extern gboolean our_area_key_press_event_callback(GtkWidget *, GdkEvent *, gpointer); // extern gboolean our_area_key_release_event_callback(GtkWidget *, GdkEvent *, gpointer); // extern gboolean our_area_focus_callback(GtkWidget *, GtkDirectionType, gpointer); // /* because cgo doesn't like ... */ // static inline void gtkGetDoubleClickSettings(GtkSettings *settings, gint *maxTime, gint *maxDistance) // { // g_object_get(settings, // "gtk-double-click-time", maxTime, // "gtk-double-click-distance", maxDistance, // NULL); // } import "C" type area struct { *areabase _widget *C.GtkWidget drawingarea *C.GtkDrawingArea scroller *scroller clickCounter *clickCounter } func newArea(ab *areabase) Area { widget := C.gtk_drawing_area_new() // the Area's size will be set later // we need to explicitly subscribe to mouse events with GtkDrawingArea C.gtk_widget_add_events(widget, C.GDK_BUTTON_PRESS_MASK|C.GDK_BUTTON_RELEASE_MASK|C.GDK_POINTER_MOTION_MASK|C.GDK_BUTTON_MOTION_MASK|C.GDK_ENTER_NOTIFY_MASK|C.GDK_LEAVE_NOTIFY_MASK) // and we need to allow focusing on a GtkDrawingArea to enable keyboard events C.gtk_widget_set_can_focus(widget, C.TRUE) a := &area{ areabase: ab, _widget: widget, drawingarea: (*C.GtkDrawingArea)(unsafe.Pointer(widget)), scroller: newScroller(widget, false, false), // not natively scrollable, no border clickCounter: new(clickCounter), } for _, c := range areaCallbacks { g_signal_connect( C.gpointer(unsafe.Pointer(a.drawingarea)), c.name, c.callback, C.gpointer(unsafe.Pointer(a))) } a.SetSize(a.width, a.height) return a } func (a *area) SetSize(width, height int) { a.width = width a.height = height C.gtk_widget_set_size_request(a._widget, C.gint(a.width), C.gint(a.height)) } func (a *area) RepaintAll() { C.gtk_widget_queue_draw(a._widget) } var areaCallbacks = []struct { name string callback C.GCallback }{ { "draw", area_draw_callback }, { "button-press-event", area_button_press_event_callback }, { "button-release-event", area_button_release_event_callback }, { "motion-notify-event", area_motion_notify_event_callback }, { "enter-notify-event", area_enterleave_notify_event_callback }, { "leave-notify-event", area_enterleave_notify_event_callback }, { "key-press-event", area_key_press_event_callback }, { "key-release-event", area_key_release_event_callback }, { "focus", area_focus_callback }, } //export our_area_draw_callback func our_area_draw_callback(widget *C.GtkWidget, cr *C.cairo_t, data C.gpointer) C.gboolean { var x0, y0, x1, y1 C.double var maxwid, maxht C.gint a := (*area)(unsafe.Pointer(data)) // thanks to desrt in irc.gimp.net/#gtk+ // these are in user coordinates, which match what coordinates we want by default, even out of a draw event handler (thanks johncc3, mclasen, and Company in irc.gimp.net/#gtk+) C.cairo_clip_extents(cr, &x0, &y0, &x1, &y1) // we do not need to clear the cliprect; GtkDrawingArea did it for us beforehand cliprect := image.Rect(int(x0), int(y0), int(x1), int(y1)) // the cliprect can actually fall outside the size of the Area; clip it by intersecting the two rectangles C.gtk_widget_get_size_request(widget, &maxwid, &maxht) cliprect = image.Rect(0, 0, int(maxwid), int(maxht)).Intersect(cliprect) if cliprect.Empty() { // no intersection; nothing to paint return C.FALSE // signals handled without stopping the event chain (thanks to desrt again) } i := a.handler.Paint(cliprect) surface := C.cairo_image_surface_create( C.CAIRO_FORMAT_ARGB32, // alpha-premultiplied; native byte order C.int(i.Rect.Dx()), C.int(i.Rect.Dy())) if status := C.cairo_surface_status(surface); status != C.CAIRO_STATUS_SUCCESS { panic(fmt.Errorf("cairo_create_image_surface() failed: %s\n", C.GoString(C.cairo_status_to_string(status)))) } // the flush and mark_dirty calls are required; see the cairo docs and https://git.gnome.org/browse/gtk+/tree/gdk/gdkcairo.c#n232 (thanks desrt in irc.gimp.net/#gtk+) C.cairo_surface_flush(surface) toARGB(i, uintptr(unsafe.Pointer(C.cairo_image_surface_get_data(surface))), int(C.cairo_image_surface_get_stride(surface))) C.cairo_surface_mark_dirty(surface) C.cairo_set_source_surface(cr, surface, 0, 0) // origin of the surface // that just set the brush that cairo uses: we have to actually draw now // (via https://developer.gnome.org/gtkmm-tutorial/stable/sec-draw-images.html.en) C.cairo_rectangle(cr, x0, y0, x1, y1) // breaking the nrom here since we have the coordinates as a C double already C.cairo_fill(cr) C.cairo_surface_destroy(surface) // free surface return C.FALSE // signals handled without stopping the event chain (thanks to desrt again) } var area_draw_callback = C.GCallback(C.our_area_draw_callback) func translateModifiers(state C.guint, window *C.GdkWindow) C.guint { // 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+) C.gdk_keymap_add_virtual_modifiers( C.gdk_keymap_get_for_display(C.gdk_window_get_display(window)), (*C.GdkModifierType)(unsafe.Pointer(&state))) return state } func makeModifiers(state C.guint) (m Modifiers) { if (state & C.GDK_CONTROL_MASK) != 0 { m |= Ctrl } if (state & C.GDK_META_MASK) != 0 { // TODO get equivalent for Alt m |= Alt } if (state & C.GDK_SHIFT_MASK) != 0 { m |= Shift } if (state & C.GDK_SUPER_MASK) != 0 { m |= Super } return m } // shared code for finishing up and sending a mouse event func finishMouseEvent(widget *C.GtkWidget, data C.gpointer, me MouseEvent, mb uint, x C.gdouble, y C.gdouble, state C.guint, gdkwindow *C.GdkWindow) { var areawidth, areaheight C.gint // on GTK+, mouse buttons 4-7 are for scrolling; if we got here, that's a mistake if mb >= 4 && mb <= 7 { return } a := (*area)(unsafe.Pointer(data)) state = translateModifiers(state, gdkwindow) me.Modifiers = makeModifiers(state) // the mb != # checks exclude the Up/Down button from Held if mb != 1 && (state&C.GDK_BUTTON1_MASK) != 0 { me.Held = append(me.Held, 1) } if mb != 2 && (state&C.GDK_BUTTON2_MASK) != 0 { me.Held = append(me.Held, 2) } if mb != 3 && (state&C.GDK_BUTTON3_MASK) != 0 { me.Held = append(me.Held, 3) } // don't check GDK_BUTTON4_MASK or GDK_BUTTON5_MASK because those are for the scrolling buttons mentioned above; there doesn't seem to be a way to detect higher buttons... (TODO) me.Pos = image.Pt(int(x), int(y)) C.gtk_widget_get_size_request(widget, &areawidth, &areaheight) if !me.Pos.In(image.Rect(0, 0, int(areawidth), int(areaheight))) { // outside the actual Area; no event return } // and finally, if the button ID >= 8, continue counting from 4, as above and as in the MouseEvent spec if me.Down >= 8 { me.Down -= 4 } if me.Up >= 8 { me.Up -= 4 } a.handler.Mouse(me) } // convenience name to make our intent clear const continueEventChain C.gboolean = C.FALSE const stopEventChain C.gboolean = C.TRUE // checking for a mouse click that makes the program/window active is meaningless on GTK+: it's a property of the window manager/X11, and it's the WM that decides if the program should become active or not // however, one thing is certain: the click event will ALWAYS be sent (to the window that the X11 decides to send it to) // I assume the same is true for Wayland // thanks Chipzz in irc.gimp.net/#gtk+ //export our_area_button_press_event_callback func our_area_button_press_event_callback(widget *C.GtkWidget, event *C.GdkEvent, data C.gpointer) C.gboolean { // clicking doesn't automatically transfer keyboard focus; we must do so manually (thanks tristan in irc.gimp.net/#gtk+) C.gtk_widget_grab_focus(widget) e := (*C.GdkEventButton)(unsafe.Pointer(event)) me := MouseEvent{ // GDK button ID == our button ID with some exceptions taken care of by finishMouseEvent() Down: uint(e.button), } var maxTime C.gint var maxDistance C.gint if e._type != C.GDK_BUTTON_PRESS { // ignore GDK's generated double-clicks and beyond; we handled those ourselves below return continueEventChain } a := (*area)(unsafe.Pointer(data)) // e.time is unsigned and in milliseconds // maxTime is also milliseconds; despite being gint, it is only allowed to be positive // maxDistance is also only allowed to be positive settings := C.gtk_widget_get_settings(widget) C.gtkGetDoubleClickSettings(settings, &maxTime, &maxDistance) me.Count = a.clickCounter.click(me.Down, int(e.x), int(e.y), uintptr(e.time), uintptr(maxTime), int(maxDistance), int(maxDistance)) finishMouseEvent(widget, data, me, me.Down, e.x, e.y, e.state, e.window) return continueEventChain } var area_button_press_event_callback = C.GCallback(C.our_area_button_press_event_callback) //export our_area_button_release_event_callback func our_area_button_release_event_callback(widget *C.GtkWidget, event *C.GdkEvent, data C.gpointer) C.gboolean { e := (*C.GdkEventButton)(unsafe.Pointer(event)) me := MouseEvent{ // GDK button ID == our button ID with some exceptions taken care of by finishMouseEvent() Up: uint(e.button), } finishMouseEvent(widget, data, me, me.Up, e.x, e.y, e.state, e.window) return continueEventChain } var area_button_release_event_callback = C.GCallback(C.our_area_button_release_event_callback) //export our_area_motion_notify_event_callback func our_area_motion_notify_event_callback(widget *C.GtkWidget, event *C.GdkEvent, data C.gpointer) C.gboolean { e := (*C.GdkEventMotion)(unsafe.Pointer(event)) me := MouseEvent{} finishMouseEvent(widget, data, me, 0, e.x, e.y, e.state, e.window) return continueEventChain } var area_motion_notify_event_callback = C.GCallback(C.our_area_motion_notify_event_callback) // 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 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)) // differentiating between enter-notify-event and leave-notify-event is unimportant //export our_area_enterleave_notify_event_callback func our_area_enterleave_notify_event_callback(widget *C.GtkWidget, event *C.GdkEvent, data C.gpointer) C.gboolean { a := (*area)(unsafe.Pointer(data)) a.clickCounter.reset() return continueEventChain } var area_enterleave_notify_event_callback = C.GCallback(C.our_area_enterleave_notify_event_callback) // shared code for doing a key event func doKeyEvent(widget *C.GtkWidget, event *C.GdkEvent, data C.gpointer, up bool) { var ke KeyEvent e := (*C.GdkEventKey)(unsafe.Pointer(event)) a := (*area)(unsafe.Pointer(data)) keyval := e.keyval // get modifiers now in case a modifier was pressed state := translateModifiers(e.state, e.window) ke.Modifiers = makeModifiers(state) if extkey, ok := extkeys[keyval]; ok { ke.ExtKey = extkey } else if mod, ok := modonlykeys[keyval]; ok { ke.Modifier = mod // don't include the modifier in ke.Modifiers ke.Modifiers &^= mod } else if xke, ok := fromScancode(uintptr(e.hardware_keycode) - 8); ok { // see events_notdarwin.go for details of the above map lookup // one of these will be nonzero ke.Key = xke.Key ke.ExtKey = xke.ExtKey } else { // no match return } ke.Up = up a.handler.Key(ke) } //export our_area_key_press_event_callback func our_area_key_press_event_callback(widget *C.GtkWidget, event *C.GdkEvent, data C.gpointer) C.gboolean { doKeyEvent(widget, event, data, false) return continueEventChain } var area_key_press_event_callback = C.GCallback(C.our_area_key_press_event_callback) //export our_area_key_release_event_callback func our_area_key_release_event_callback(widget *C.GtkWidget, event *C.GdkEvent, data C.gpointer) C.gboolean { doKeyEvent(widget, event, data, true) return continueEventChain } var area_key_release_event_callback = C.GCallback(C.our_area_key_release_event_callback) var extkeys = map[C.guint]ExtKey{ C.GDK_KEY_Escape: Escape, C.GDK_KEY_Insert: Insert, C.GDK_KEY_Delete: Delete, C.GDK_KEY_Home: Home, C.GDK_KEY_End: End, C.GDK_KEY_Page_Up: PageUp, C.GDK_KEY_Page_Down: PageDown, C.GDK_KEY_Up: Up, C.GDK_KEY_Down: Down, C.GDK_KEY_Left: Left, C.GDK_KEY_Right: Right, C.GDK_KEY_F1: F1, C.GDK_KEY_F2: F2, C.GDK_KEY_F3: F3, C.GDK_KEY_F4: F4, C.GDK_KEY_F5: F5, C.GDK_KEY_F6: F6, C.GDK_KEY_F7: F7, C.GDK_KEY_F8: F8, C.GDK_KEY_F9: F9, C.GDK_KEY_F10: F10, C.GDK_KEY_F11: F11, C.GDK_KEY_F12: F12, // numpad numeric keys and . are handled in events_notdarwin.go C.GDK_KEY_KP_Enter: NEnter, C.GDK_KEY_KP_Add: NAdd, C.GDK_KEY_KP_Subtract: NSubtract, C.GDK_KEY_KP_Multiply: NMultiply, C.GDK_KEY_KP_Divide: NDivide, } // sanity check func init() { included := make([]bool, _nextkeys) for _, v := range extkeys { included[v] = true } for i := 1; i < int(_nextkeys); i++ { if i >= int(N0) && i <= int(N9) { // skip numpad numbers and . continue } if i == int(NDot) { continue } if !included[i] { panic(fmt.Errorf("error: not all ExtKeys defined on Unix (missing %d)", i)) } } } var modonlykeys = map[C.guint]Modifiers{ C.GDK_KEY_Control_L: Ctrl, C.GDK_KEY_Control_R: Ctrl, C.GDK_KEY_Alt_L: Alt, C.GDK_KEY_Alt_R: Alt, C.GDK_KEY_Meta_L: Alt, C.GDK_KEY_Meta_R: Alt, C.GDK_KEY_Shift_L: Shift, C.GDK_KEY_Shift_R: Shift, C.GDK_KEY_Super_L: Super, C.GDK_KEY_Super_R: Super, } //export our_area_focus_callback func our_area_focus_callback(widget *C.GtkWidget, direction C.GtkDirectionType, data C.gpointer) C.gboolean { // TODO figure out how this callback affects keyboard scrolling if C.gtk_widget_is_focus(widget) == C.FALSE { // this event indicates entering focus; always allow it return continueEventChain } return stopEventChain } var area_focus_callback = C.GCallback(C.our_area_focus_callback) func (a *area) widget() *C.GtkWidget { return a._widget } func (a *area) setParent(p *controlParent) { a.scroller.setParent(p) } func (a *area) allocate(x int, y int, width int, height int, d *sizing) []*allocation { return baseallocate(a, x, y, width, height, d) } func (a *area) preferredSize(d *sizing) (width, height int) { // the preferred size of an Area is its size return a.width, a.height } func (a *area) commitResize(c *allocation, d *sizing) { a.scroller.commitResize(c, d) } func (a *area) getAuxResizeInfo(d *sizing) { // a Label to the left of an Area should be vertically aligned to the top d.shouldVAlignTop = true }