From 8979188f4cc811b8d6043e0e7e1d1e09c723ef2d Mon Sep 17 00:00:00 2001 From: Stefan Salewski Date: Sat, 14 Sep 2019 12:31:27 +0200 Subject: [PATCH] fixes and new examples --- README.adoc | 369 ++++++++++++++++++++----- examples/gtk3/connect_args.nim | 42 ++- examples/gtk3/css_colored_listview.nim | 88 ++++++ examples/gtk3/drawingarea.nim | 150 +++++----- examples/gtk3/makeAll | 5 +- examples/gtk3/simpledrawingarea.nim | 310 +++++++++++++++++++++ gintro.nimble | 2 +- gintro/cairo.nim | 55 ++-- gintro/gimpl.nim | 52 +++- 9 files changed, 888 insertions(+), 185 deletions(-) create mode 100644 examples/gtk3/css_colored_listview.nim create mode 100644 examples/gtk3/simpledrawingarea.nim diff --git a/README.adoc b/README.adoc index 43d0abd..80b131d 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,6 @@ = High level GTK3 (and GTK4) bindings for the Nim programming language (c) Stefan Salewski -//Version 0.5.3 +//Version 0.5.4 :experimental: :imagesdir: http://ssalewski.de/tmp :source-highlighter: pygments @@ -19,7 +19,7 @@ TIP: A more fancy copy of this document with dark source code background is avai NOTE: This work is partly based on earlier works of J. Mansour and has been supported by A. Rumpf and other _Nim_ and _GTK/Gnome_ developers. The `combinatorics` module was kindly provided by R. Behrends. -NOTE: Starting with current release 0.5.3 we do not generate field entries for objects and we do +NOTE: Starting with release 0.5.3 we do not generate field entries for objects and we do not generate class structs and private objects. Also we stopped exporting the low level functions like gtk_button_new(). For a real high-level binding we should not need these. If that is a serious limitation for you, then use release 0.5.2 for now and create an github issue for your use case, we @@ -1416,16 +1416,216 @@ proc main = main() ---- + +== A ListView example with CSS styling + +Recently C. Eric Cashon provided this example at https://discourse.gnome.org/t/gtk-treeview-cell-color-change/1750/3 + +I will show his original code here too, so we can compare it better with the Nim version. +We see that Nim code has currently some disadvantages still, for example we have no +varargs procs implemented, so setting of properties and attributes is done using GValues, +which is typesafe, but not really compact. That is not too bad, but we may consider +creating macros to support a more dense, but still typesafe way similar to C's varargs functions. + +[[cell_color1.c]] +[source,c] +.cell_color1.c +---- +// gcc -Wall cell_color1.c -o cell_color1 `pkg-config --cflags --libs gtk+-3.0` +// https://discourse.gnome.org/t/gtk-treeview-cell-color-change/1750/4 +// C. Eric Cashon + +#include + +enum +{ + ID, + PROGRAM, + COLOR1, + COLOR2, + COLUMNS +}; + +int main(int argc, char *argv[]) + { + gtk_init(&argc, &argv); + + GtkWidget *window=gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_title(GTK_WINDOW(window), "Select Cell"); + gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER); + gtk_window_set_default_size(GTK_WINDOW(window), 500, 500); + gtk_container_set_border_width(GTK_CONTAINER(window), 20); + g_signal_connect(window, "destroy", G_CALLBACK(gtk_main_quit), NULL); + + GtkTreeIter iter; + GtkListStore *store=gtk_list_store_new(COLUMNS, G_TYPE_UINT, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, ID, 0, PROGRAM, "Gedit", COLOR1, "DarkCyan", COLOR2, "cyan", -1); + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, ID, 1, PROGRAM, "Gimp", COLOR1, "LightSlateGray", COLOR2, "cyan", -1); + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, ID, 2, PROGRAM, "Inkscape", COLOR1, "DarkCyan", COLOR2, "cyan", -1); + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, ID, 3, PROGRAM, "Firefox", COLOR1, "LightSlateGray", COLOR2, "cyan", -1); + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, ID, 4, PROGRAM, "Calculator", COLOR1, "DarkCyan", COLOR2, "cyan", -1); + gtk_list_store_append(store, &iter); + gtk_list_store_set(store, &iter, ID, 5, PROGRAM, "Devhelp", COLOR1, "LightSlateGray", COLOR2, "cyan", -1); + + GtkWidget *tree=gtk_tree_view_new_with_model(GTK_TREE_MODEL(store)); + gtk_widget_set_hexpand(tree, TRUE); + gtk_widget_set_vexpand(tree, TRUE); + g_object_set(tree, "activate-on-single-click", TRUE, NULL); + + GtkTreeSelection *selection=gtk_tree_view_get_selection(GTK_TREE_VIEW(tree)); + gtk_tree_selection_set_mode(selection, GTK_SELECTION_SINGLE); + + GtkCellRenderer *renderer1=gtk_cell_renderer_text_new(); + g_object_set(renderer1, "editable", FALSE, NULL); + + GtkCellRenderer *renderer2=gtk_cell_renderer_text_new(); + g_object_set(renderer2, "editable", TRUE, NULL); + + //Bind the COLOR column to the "cell-background" property. + GtkTreeViewColumn *column1=gtk_tree_view_column_new_with_attributes("ID", renderer1, "text", ID, "cell-background", COLOR1, NULL); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column1); + GtkTreeViewColumn *column2 = gtk_tree_view_column_new_with_attributes("Program", renderer2, "text", PROGRAM, "cell-background", COLOR2, NULL); + gtk_tree_view_append_column(GTK_TREE_VIEW(tree), column2); + + GtkWidget *grid=gtk_grid_new(); + gtk_grid_attach(GTK_GRID(grid), tree, 0, 0, 1, 1); + + gtk_container_add(GTK_CONTAINER(window), grid); + + gchar *css_string=g_strdup("treeview{background-color: rgba(0,255,255,1.0); font-size:30pt} treeview:selected{background-color: rgba(255,255,0,1.0); color: rgba(0,0,255,1.0);}"); + GError *css_error=NULL; + GtkCssProvider *provider=gtk_css_provider_new(); + gtk_css_provider_load_from_data(provider, css_string, -1, &css_error); + gtk_style_context_add_provider_for_screen(gdk_screen_get_default(), GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + if(css_error!=NULL) + { + g_print("CSS loader error %s\n", css_error->message); + g_error_free(css_error); + } + g_object_unref(provider); + g_free(css_string); + + gtk_widget_show_all(window); + + gtk_main(); + return 0; + } +---- + +And this is the Nim version, created with c2nim and some manual tuning: + +[[css_colored_listview.nim]] +[source,nim] +.css_colored_listview.nim +---- +# nim c css_colored_listview.nim +import gintro/[gtk, gobject] +import gintro/gdk except Window # there is a problem with gdk.Window -- we have to investigate! +const # maybe we should use Nim's enum here? + Id = 0 + Program = 1 + Color1 = 2 + Color2 = 3 + Columns = 4 + +proc bye(w: Window) = + mainQuit() + echo "Bye..." + +proc toStringVal(s: string): Value = + let gtype = typeFromName("gchararray") + discard init(result, gtype) + setString(result, s) + +proc toUIntVal(i: int): Value = + let gtype = typeFromName("guint") + discard init(result, gtype) + setUint(result, i) + +proc toBoolVal(b: bool): Value = + let gtype = typeFromName("gboolean") + discard init(result, gtype) + setBoolean(result, b) + +# we use the old gtk style with init() as is used in the C original -- maybe better use modern app sytle +proc main() = + gtk.init() + let window = newWindow() + window.title = "Select Cell" + window.position = WindowPosition.center + window.defaultSize = (500, 500) + window.borderWidth = 20 + connect(window, "destroy", bye) + var iter: TreeIter + var h = [typeFromName("guint"), typeFromName("gchararray"), typeFromName("gchararray"), typeFromName("gchararray")] + var store = newListStore(Columns, cast[pointer]( unsafeaddr h)) # this cast is ugly, we should fix it in the bindings soon. + let progNames = ["Gedit", "Gimp", "Inkscape", "Firefox", "Calculator", "Devhelp"] + for i, n in progNames: + store.append(iter) # currently we have to use setValue() as there is no varargs proc as in C original + store.setValue(iter, Id, toUIntVal(i)) + store.setValue(iter, Program, toStringVal(n)) + store.setValue(iter, Color1, toStringVal(if (i and 1) != 0: "LightSlateGray" else: "DarkCyan")) + store.setValue(iter, Color2, toStringVal("cyan")) + var tree = newTreeViewWithModel(store) + tree.setHexpand + tree.setVexpand + setProperty(tree, "activate-on-single-click", toBoolVal(true)) + var selection = tree.getSelection() + selection.setMode(SelectionMode.single) + var renderer1 = newCellRendererText() + setProperty(renderer1, "editable", toBoolVal(false)) + var renderer2 = newCellRendererText() + setProperty(renderer2, "editable", toBoolVal(true)) + ## Bind the Color column to the "cell-background" property. + var column1 = newTreeViewColumn() + column1.setTitle("ID") + column1.packStart(renderer1, true) + column1.addAttribute(renderer1, "text", Id) + column1.addAttribute(renderer1, "cell-background", Color1) + discard tree.appendColumn(column1) + var column2 = newTreeViewColumn() + column1.setTitle("Program") + column1.packStart(renderer2, true) + column1.addAttribute(renderer2, "text", Program) + column1.addAttribute(renderer2, "cell-background", Color2) + discard tree.appendColumn(column2) + var grid = newGrid() # grid with only one occupied cell makes not much sense here -- but so we can add more widgets later + grid.attach(tree, 0, 0, 1, 1) + window.add(grid) + const cssString = # note: big font selected intentionally + """treeview{background-color: rgba(0,255,255,1.0); font-size:30pt} treeview:selected{background-color: rgba(255,255,0,1.0); + color: rgba(0,0,255,1.0);}""" + var provider = newCssProvider() + discard provider.loadFromData(cssString) + addProviderForScreen(getDefaultScreen(), provider, STYLE_PROVIDER_PRIORITY_APPLICATION) + window.showAll + gtk.main() + +main() +---- + +When you compile with `nim c -d:release -d:danger --passC:-flto css_colored_listview.nim` +you will get an executable size of 80k, which is big compared with the 20k of the C version, but +not too bad. + == A more advanced example for cairo drawing with zooming, panning, scrolling The following code is a plain Nim version of a drawing demo which I wrote some years ago in Ruby (http://ssalewski.de/PetEd-Demo.html.en). -With the latest fix to Nim GTK3 bindings it seems to work fine, but some rewriting for optimized performance -may be needed, I have to investigate that. Cairo surface is currently manually freed, because GC may have a too large delay. +Cairo surface is currently manually freed, because GC may have a too large delay. You can resize the window and zoom in with the mouse wheel. When zoomed in scroll bars appear. You can hold the middle mouse button pressed while moving the mouse for panning, and you can press left mouse button and move the mouse to first draw a selection rectangle and zoom into it when releasing the mouse button. +In the examples directory there is also a simplified version called `simpledrawingarea.nim` which does all +the drawings in the draw callback, without using a buffering surface. This is generally preferable for +plain applications. + [[drawingarea.nim]] [source,nim] .drawingarea.nim @@ -1445,6 +1645,8 @@ and move the mouse to first draw a selection rectangle and zoom into it when rel # And finally that custom surface and custom cairo context is an # important test for the language bindings. +# https://discourse.gnome.org/t/problem-with-gtkscrollbar-gtk-window-resize-and-gtk-adjustment-set-value/1081 + import gintro/[gtk, gdk, glib, gobject, gio, cairo] const @@ -1483,7 +1685,7 @@ Scrolling: Scrollbars Panning: Moving mouse while middle mouse button pressed """ -# drawing area and scroll bars in 2x2 table (PDA == Plain Drawing Area) +# drawing area and scroll bars in 2x2 grid (PDA == Plain Drawing Area) type PosAdj = ref object of Adjustment @@ -1504,6 +1706,8 @@ type selecting: bool userZoom: float surf: Surface + pattern: Pattern + cr: cairo.Context darea: DrawingArea hadjustment: PosAdj vadjustment: PosAdj @@ -1520,23 +1724,24 @@ type lastMousePosY: float zoomRectX1: float zoomRectY1: float + oldSizeX: int + oldSizeY: int drawWorld: proc (cr: Context) extents: proc (): tuple[x, y, w, h: float] proc drawingAreaDrawCb(darea: DrawingArea; cr: Context; this: PDA): bool = - if this.surf.isNil: return - cr.setSource(patternCreateForSurface(this.surf)) + if this.pattern.isNil: return + cr.setSource(this.pattern) cr.paint if this.selecting: cr.rectangle(this.lastButtonDownPosX, this.lastButtonDownPosY, - this.zoomRectX1 - this.lastButtonDownPosX, - this.zoomRectY1 - this.lastButtonDownPosY) + this.zoomRectX1 - this.lastButtonDownPosX, this.zoomRectY1 - this.lastButtonDownPosY) cr.setSource(0, 0, 1, 0.5) # SELECT_RECT_COL) # 0, 0, 1, 0.5 cr.fillPreserve cr.setSource(0, 0, 0) cr.setLineWidth(2) cr.stroke - return SignalEventStopPropagation + return gdk.EVENT_STOP # EVENT_PROPAGATE #return true # TRUE to stop other handlers from being invoked for the event. FALSE to propagate the event further. # clamp to correct values, 0 <= value <= (adj.upper - adj.pageSize), block calling onAdjustmentEvent() @@ -1553,27 +1758,47 @@ proc updateAdjustments(this: PDA; dx, dy: float) = updateVal(this.hadjustment, dx) updateVal(this.vadjustment, dy) -# maybe for optimization we should only allocate a new surface when its size has changed. proc paint(this: PDA) = - if this.surf != nil: - destroy(this.surf) # manually destroy surface -- GC would do it for us, but GC is slow... - this.surf = this.darea.window.createSimilarSurface(Content.color, - this.darea.allocatedWidth, this.darea.allocatedHeight) - let cr = newContext(this.surf) - cr.translate(this.hadjustment.upper * 0.5 - this.hadjustment.value, # our origin is the center + # echo "paint" + this.cr.save + this.cr.translate(this.hadjustment.upper * 0.5 - this.hadjustment.value, # our origin is the center this.vadjustment.upper * 0.5 - this.vadjustment.value) - cr.scale(this.fullScale * this.userZoom, this.fullScale * this.userZoom) - cr.translate(-this.dataX - this.dataWidth * 0.5, -this.dataY - this.dataHeight * 0.5) - this.drawWorld(cr) # call the user provided drawing function - destroy(cr) # we can also manually destroy the context here, but GC would do it for us + this.cr.scale(this.fullScale * this.userZoom, this.fullScale * this.userZoom) + this.cr.translate(-this.dataX - this.dataWidth * 0.5, -this.dataY - this.dataHeight * 0.5) + this.drawWorld(this.cr) # call the user provided drawing function + this.cr.restore proc dareaConfigureCallback(darea: DrawingArea; event: EventConfigure; this: PDA): bool = - this.updateAdjustments(0, 0) (this.dataX, this.dataY, this.dataWidth, this.dataHeight) = this.extents() # query user defined size this.fullScale = min(this.darea.allocatedWidth.float / this.dataWidth, this.darea.allocatedHeight.float / this.dataHeight) + if this.surf != nil: + destroy(this.surf) # manually destroy surface -- GC would do it for us, but GC is slow... + this.surf = this.darea.window.createSimilarSurface(Content.color, + this.darea.allocatedWidth, this.darea.allocatedHeight) + if this.pattern != nil: + patternDestroy(this.pattern) + if this.cr != nil: + destroy(this.cr) + this.pattern = patternCreateForSurface(this.surf) # pattern now owns the surface! + this.cr = newContext(this.surf) # this function references target! this.paint + return gdk.EVENT_STOP + +proc hscrollbarSizeAllocateCallback(s: Scrollbar; r: gdk.Rectangle; pda: PDA) = + pda.hadjustment.setUpper(r.width.float * pda.userZoom) + pda.hadjustment.setPageSize(r.width.float) + if pda.oldSizeX != 0: # this fix is not exact, as fullScale can ... + updateVal(pda.hadjustment, (r.width - pda.oldSizeX).float * 0.5) + pda.oldSizeX = r.width + +proc vscrollbarSizeAllocateCallback(s: Scrollbar; r: gdk.Rectangle; pda: PDA) = + pda.vadjustment.setUpper(r.height.float * pda.userZoom) + pda.vadjustment.setPageSize(r.height.float) + if pda.oldSizeY != 0: # ... change when window is rezized. But it's good enough! + updateVal(pda.vadjustment, (r.height - pda.oldSizeY).float * 0.5) + pda.oldSizeY = r.height proc updateAdjustmentsAndPaint(this: PDA; dx, dy: float) = this.updateAdjustments(dx, dy) @@ -1590,37 +1815,45 @@ proc getUserCoordinates(this: PDA; eventX, eventY: float): (float, float) = proc onMotion(darea: DrawingArea; event: EventMotion; this: PDA): bool = let state = getState(event) let (x, y) = event.getCoords - if (state.contains(button1)): # selecting + if state.contains(button1): # selecting this.selecting = true this.zoomRectX1 = x this.zoomRectY1 = y this.darea.queueDrawArea(0, 0, this.darea.allocatedWidth, this.darea.allocatedHeight) elif button2 in state: # panning this.updateAdjustmentsAndPaint(this.lastMousePosX - x, this.lastMousePosY - y) + else: + return gdk.EVENT_PROPAGATE this.lastMousePosX = x this.lastMousePosY = y + return gdk.EVENT_STOP #event.request # request more motion events ? # zooming with mouse wheel -- data near mouse pointer should not move if possible! # hadjustment.value + event.x is the position in our zoomed_in world, (userZoom / z0 - 1) # is the relative movement caused by zooming +# In other words, this is the delta-move d of a point at position P from zooming: +# d = newPos - P = P * scale - P = P * (z/z0) - P = P * (z/z0 - 1). We have to compensate for this d. proc scrollEvent(darea: DrawingArea; event: EventScroll; this: PDA): bool = let z0 = this.userZoom - var d = getScrollDirection(event) - var (x, y) = event.getCoords - if d == ScrollDirection.up: + case getScrollDirection(event) + of ScrollDirection.up: this.userZoom *= ZoomFactorMouseWheel - elif d == ScrollDirection.down: + of ScrollDirection.down: this.userZoom /= ZoomFactorMouseWheel - if (this.userZoom < 1): + if this.userZoom < 1: this.userZoom = 1 + else: + return gdk.EVENT_PROPAGATE if this.zoomNearMousepointer: + let (x, y) = event.getCoords this.updateAdjustmentsAndPaint((this.hadjustment.value + x) * (this.userZoom / z0 - 1), - (this.vadjustment.value + y) * (this.userZoom / z0 - 1)) + (this.vadjustment.value + y) * (this.userZoom / z0 - 1)) else: # zoom to center this.updateAdjustmentsAndPaint((this.hadjustment.value + this.darea.allocatedWidth.float * 0.5) * (this.userZoom / z0 - 1), (this.vadjustment.value + this.darea.allocatedHeight.float * 0.5) * (this.userZoom / z0 - 1)) + return gdk.EVENT_STOP proc buttonPressEvent(darea: DrawingArea; event: EventButton; this: PDA): bool = var (x, y) = event.getCoords @@ -1631,60 +1864,66 @@ proc buttonPressEvent(darea: DrawingArea; event: EventButton; this: PDA): bool = echo "buttonPressEvent", x, " ", y (x, y) = this.getUserCoordinates(x, y) echo "User coordinates: ", x, ' ', y, "\n" # to verify getUserCoordinates() + return gdk.EVENT_STOP # zoom into selected rectangle and center it +# math: we first center the selection rectangle, and then compensate for translation due to scale proc buttonReleaseEvent(darea: DrawingArea; event: EventButton; this: PDA): bool = - var (x, y) = event.getCoords - var b = getButton(event) + let (x, y) = event.getCoords + let b = getButton(event) if b == 1: this.selecting = false - let z1 = min(this.darea.allocatedWidth.float / ( - this.lastButtonDownPosX - x).abs, this.darea.allocatedHeight.float / ( - this.lastButtonDownPosY - y).abs) + let z1 = min(this.darea.allocatedWidth.float / (this.lastButtonDownPosX - x).abs, + this.darea.allocatedHeight.float / (this.lastButtonDownPosY - y).abs) if z1 < ZoomFactorSelectMax: # else selection rectangle will persist, we may output a message... this.userZoom *= z1 this.updateAdjustmentsAndPaint( - ((2 * this.hadjustment.value + x + this.lastButtonDownPosX) * z1 - - this.darea.allocatedWidth.float) * 0.5 - this.hadjustment.value, - ((2 * this.vadjustment.value + y + this.lastButtonDownPosY) * z1 - - this.darea.allocatedHeight.float) * 0.5 - this.vadjustment.value) - + ((x + this.lastButtonDownPosX) * z1 - this.darea.allocatedWidth.float) * 0.5 + this.hadjustment.value * (z1 - 1), + ((y + this.lastButtonDownPosY) * z1 - this.darea.allocatedHeight.float) * 0.5 + this.vadjustment.value * (z1 - 1)) + return gdk.EVENT_STOP + return gdk.EVENT_PROPAGATE + proc onAdjustmentEvent(this: PosAdj; pda: PDA) = pda.paint pda.darea.queueDrawArea(0, 0, pda.darea.allocatedWidth, pda.darea.allocatedHeight) proc newPDA: PDA = initGrid(result) + let da = newDrawingArea() + result.darea = da + da.setHExpand + da.setVExpand + da.connect("draw", drawingAreaDrawCb, result) + da.connect("configure-event", dareaConfigureCallback, result) + da.addEvents({EventFlag.buttonPress, EventFlag.buttonRelease, + EventFlag.scroll, button1Motion, button2Motion, pointerMotionHint}) + da.connect("motion-notify-event", onMotion, result) + da.connect("scroll_event", scrollEvent, result) + da.connect("button_press_event", buttonPressEvent, result) + da.connect("button_release_event", buttonReleaseEvent, result) result.zoomNearMousepointer = ZoomNearMousepointer # mouse wheel zooming result.userZoom = 1.0 - result.darea = newDrawingArea() - result.darea.setHExpand - result.darea.setVExpand - result.darea.connect("draw", drawingAreaDrawCb, result) - result.darea.connect("configure-event", dareaConfigureCallback, result) - result.darea.addEvents({EventFlag.buttonPress, EventFlag.buttonRelease, - EventFlag.scroll, button1Motion, button2Motion, pointerMotionHint}) - result.darea.connect("motion-notify-event", onMotion, result) - result.darea.connect("scroll_event", scrollEvent, result) - result.darea.connect("button_press_event", buttonPressEvent, result) - result.darea.connect("button_release_event", buttonReleaseEvent, result) result.hadjustment = newPosAdj() - result.hadjustment.handlerID = result.hadjustment.connect("value-changed", - onAdjustmentEvent, result) + result.hadjustment.handlerID = result.hadjustment.connect("value-changed", onAdjustmentEvent, result) result.vadjustment = newPosAdj() - result.vadjustment.handlerID = result.vadjustment.connect("value-changed", - onAdjustmentEvent, result) + result.vadjustment.handlerID = result.vadjustment.connect("value-changed", onAdjustmentEvent, result) result.hscrollbar = newScrollbar(Orientation.horizontal, result.hadjustment) result.vscrollbar = newScrollbar(Orientation.vertical, result.vadjustment) result.hscrollbar.setHExpand result.vscrollbar.setVExpand + result.hscrollbar.connect("size-allocate", hscrollbarSizeAllocateCallback, result) + result.vscrollbar.connect("size-allocate", vscrollbarSizeAllocateCallback, result) result.attach(result.darea, 0, 0, 1, 1) result.attach(result.vscrollbar, 1, 0, 1, 1) result.attach(result.hscrollbar, 0, 1, 1, 1) -proc appActivate(app: Application; initData: ref PDA_Data) = +proc appStartup(app: Application) = + echo "appStartup" + +proc appActivate(app: Application; initData: PDA_Data) = let window = newApplicationWindow(app) window.title = "Drawing example" + # window.defaultSize = initData.windowSize window.defaultSize = (initData.windowSize[0], initData.windowSize[1]) let pda = newPDA() pda.drawWorld = initData.draw @@ -1694,14 +1933,13 @@ proc appActivate(app: Application; initData: ref PDA_Data) = proc newDisplay*(initData: PDA_Data) = let app = newApplication("org.gtk.example") - var d = new (ref PDA_Data) - d[] = initData - connect(app, "activate", appActivate, d) + connect(app, "startup", appStartup) + connect(app, "activate", appActivate, initData) discard run(app) when isMainModule: - const # arbitrary locations for our data + const # arbitrary locations for our data DataX = 150.0 DataY = 250.0 DataWidth = 200.0 @@ -1721,21 +1959,16 @@ when isMainModule: cr.setSource(0, 0, 0) cr.setLineWidth(2) var i = 0.0 - while true: - if min(DataWidth - 2.0 * i, DataHeight - 2.0 * i) <= 0: - break + while min(DataWidth - 2 * i, DataHeight - 2 * i) > 0: cr.rectangle(DataX + i, DataY + i, DataWidth - 2 * i, DataHeight - 2 * i) - i += 10.0 + i += 10 cr.stroke proc test = - var data: PDA_Data - data.draw = drawWorld - data.extents = worldExtents - data.windowSize = (800, 600) + let data = PDA_Data(draw: drawWorld, extents: worldExtents, windowSize: (800, 600)) newDisplay(data) - test() + test() # 337 lines ---- We can use this module as a library easily and get this simple drawing tool with full zoom and scroll support: diff --git a/examples/gtk3/connect_args.nim b/examples/gtk3/connect_args.nim index b71d7eb..bc1350f 100644 --- a/examples/gtk3/connect_args.nim +++ b/examples/gtk3/connect_args.nim @@ -1,9 +1,9 @@ # nim c connect_args.nim -import gintro/[gtk, glib, gobject, gio] +import gintro/[gtk, gobject, gio] type O = object - i: int + i, j: int proc b1Callback(button: Button; str: string) = echo str @@ -12,7 +12,7 @@ proc b2Callback(button: Button; o: O) = echo "Value of field i in object o = ", o.i proc b3Callback(button: Button; r: ref O) = - echo "Value of field i in ref to object O = ", r.i + echo "Value of field i in ref to object o = ", r.i proc b4Callback(button: Button; w: ApplicationWindow) = if w.title == "Nim with GTK3": @@ -20,27 +20,23 @@ proc b4Callback(button: Button; w: ApplicationWindow) = else: w.title = "Nim with GTK3" -proc appActivate (app: Application) = - var o: O - var r: ref O - new r - o.i = 1234567 - r.i = 7654321 - let window = newApplicationWindow(app) - let box = newBox(Orientation.vertical, 0) +proc appActivate(app: Application) = + let + o = O(i: 1234567) + r = (ref O)(i: 7654321) + window = newApplicationWindow(app) + box = newBox(Orientation.vertical, 0) + b1 = newButton("Nim with GTK3") + b2 = newButton("Passing an object from stack") + b3 = newButton("Passing an object from heap") + b4 = newButton("Passing a Widget") window.title = "Parameters for callbacks" - let b1 = newButton("Nim with GTK3") - let b2 = newButton("Passing an object from stack") - let b3 = newButton("Passing an object from heap") - let b4 = newButton("Passing a Widget") - b1.connect("clicked", b1Callback, "is much fun.") - b2.connect("clicked", b2Callback, o) - b3.connect("clicked", b3Callback, r) - b4.connect("clicked", b4Callback, window) - box.add(b1) - box.add(b2) - box.add(b3) - box.add(b4) + b1.connect("clicked", b1Callback, "is much fun.") + b2.connect("clicked", b2Callback, o) + b3.connect("clicked", b3Callback, r) + b4.connect("clicked", b4Callback, window) + for b in [b1, b2, b3, b4]: + box.add(b) window.add(box) window.showAll diff --git a/examples/gtk3/css_colored_listview.nim b/examples/gtk3/css_colored_listview.nim new file mode 100644 index 0000000..6099e9f --- /dev/null +++ b/examples/gtk3/css_colored_listview.nim @@ -0,0 +1,88 @@ +## gcc -Wall cell_color1.c -o cell_color1 `pkg-config --cflags --libs gtk+-3.0` +## https://discourse.gnome.org/t/gtk-treeview-cell-color-change/1750/4 +## C. Eric Cashon + +# nim c css_colored_listview.nim +import gintro/[gtk, gobject] +import gintro/gdk except Window # there is a problem with gdk.Window -- we have to investigate! +const # maybe we should use Nim's enum here? + Id = 0 + Program = 1 + Color1 = 2 + Color2 = 3 + Columns = 4 + +proc bye(w: Window) = + mainQuit() + echo "Bye..." + +proc toStringVal(s: string): Value = + let gtype = typeFromName("gchararray") + discard init(result, gtype) + setString(result, s) + +proc toUIntVal(i: int): Value = + let gtype = typeFromName("guint") + discard init(result, gtype) + setUint(result, i) + +proc toBoolVal(b: bool): Value = + let gtype = typeFromName("gboolean") + discard init(result, gtype) + setBoolean(result, b) + +# we use the old gtk style with init() as is used in the C original -- maybe better use modern app sytle +proc main() = + gtk.init() + let window = newWindow() + window.title = "Select Cell" + window.position = WindowPosition.center + window.defaultSize = (500, 500) + window.borderWidth = 20 + connect(window, "destroy", bye) + var iter: TreeIter + var h = [typeFromName("guint"), typeFromName("gchararray"), typeFromName("gchararray"), typeFromName("gchararray")] + var store = newListStore(Columns, cast[pointer]( unsafeaddr h)) # this cast is ugly, we should fix it in the bindings soon. + let progNames = ["Gedit", "Gimp", "Inkscape", "Firefox", "Calculator", "Devhelp"] + for i, n in progNames: + store.append(iter) # currently we have to use setValue() as there is no varargs proc as in C original + store.setValue(iter, Id, toUIntVal(i)) + store.setValue(iter, Program, toStringVal(n)) + store.setValue(iter, Color1, toStringVal(if (i and 1) != 0: "LightSlateGray" else: "DarkCyan")) + store.setValue(iter, Color2, toStringVal("cyan")) + var tree = newTreeViewWithModel(store) + tree.setHexpand + tree.setVexpand + setProperty(tree, "activate-on-single-click", toBoolVal(true)) + var selection = tree.getSelection() + selection.setMode(SelectionMode.single) + var renderer1 = newCellRendererText() + setProperty(renderer1, "editable", toBoolVal(false)) + var renderer2 = newCellRendererText() + setProperty(renderer2, "editable", toBoolVal(true)) + ## Bind the Color column to the "cell-background" property. + var column1 = newTreeViewColumn() + column1.setTitle("ID") + column1.packStart(renderer1, true) + column1.addAttribute(renderer1, "text", Id) + column1.addAttribute(renderer1, "cell-background", Color1) + discard tree.appendColumn(column1) + var column2 = newTreeViewColumn() + column1.setTitle("Program") + column1.packStart(renderer2, true) + column1.addAttribute(renderer2, "text", Program) + column1.addAttribute(renderer2, "cell-background", Color2) + discard tree.appendColumn(column2) + var grid = newGrid() # grid with only one occupied cell makes not much sense here -- but so we can add more widgets later + grid.attach(tree, 0, 0, 1, 1) + window.add(grid) + const cssString = # note: big font selected intentionally + """treeview{background-color: rgba(0,255,255,1.0); font-size:30pt} treeview:selected{background-color: rgba(255,255,0,1.0); + color: rgba(0,0,255,1.0);}""" + var provider = newCssProvider() + discard provider.loadFromData(cssString) + addProviderForScreen(getDefaultScreen(), provider, STYLE_PROVIDER_PRIORITY_APPLICATION) + window.showAll + gtk.main() + +main() \ No newline at end of file diff --git a/examples/gtk3/drawingarea.nim b/examples/gtk3/drawingarea.nim index 9cdd39b..e52ec71 100644 --- a/examples/gtk3/drawingarea.nim +++ b/examples/gtk3/drawingarea.nim @@ -13,6 +13,8 @@ # And finally that custom surface and custom cairo context is an # important test for the language bindings. +# https://discourse.gnome.org/t/problem-with-gtkscrollbar-gtk-window-resize-and-gtk-adjustment-set-value/1081 + import gintro/[gtk, gdk, glib, gobject, gio, cairo] const @@ -51,7 +53,7 @@ Scrolling: Scrollbars Panning: Moving mouse while middle mouse button pressed """ -# drawing area and scroll bars in 2x2 table (PDA == Plain Drawing Area) +# drawing area and scroll bars in 2x2 grid (PDA == Plain Drawing Area) type PosAdj = ref object of Adjustment @@ -72,6 +74,8 @@ type selecting: bool userZoom: float surf: Surface + pattern: Pattern + cr: cairo.Context darea: DrawingArea hadjustment: PosAdj vadjustment: PosAdj @@ -88,17 +92,18 @@ type lastMousePosY: float zoomRectX1: float zoomRectY1: float + oldSizeX: int + oldSizeY: int drawWorld: proc (cr: Context) extents: proc (): tuple[x, y, w, h: float] proc drawingAreaDrawCb(darea: DrawingArea; cr: Context; this: PDA): bool = - if this.surf.isNil: return - cr.setSource(patternCreateForSurface(this.surf)) + if this.pattern.isNil: return + cr.setSource(this.pattern) cr.paint if this.selecting: cr.rectangle(this.lastButtonDownPosX, this.lastButtonDownPosY, - this.zoomRectX1 - this.lastButtonDownPosX, - this.zoomRectY1 - this.lastButtonDownPosY) + this.zoomRectX1 - this.lastButtonDownPosX, this.zoomRectY1 - this.lastButtonDownPosY) cr.setSource(0, 0, 1, 0.5) # SELECT_RECT_COL) # 0, 0, 1, 0.5 cr.fillPreserve cr.setSource(0, 0, 0) @@ -121,36 +126,47 @@ proc updateAdjustments(this: PDA; dx, dy: float) = updateVal(this.hadjustment, dx) updateVal(this.vadjustment, dy) -# maybe for optimization we should only allocate a new surface when its size has changed. proc paint(this: PDA) = - if this.surf != nil: - destroy(this.surf) # manually destroy surface -- GC would do it for us, but GC is slow... - this.surf = this.darea.window.createSimilarSurface(Content.color, - this.darea.allocatedWidth, this.darea.allocatedHeight) - let cr = newContext(this.surf) - cr.translate(this.hadjustment.upper * 0.5 - this.hadjustment.value, # our origin is the center + # echo "paint" + this.cr.save + this.cr.translate(this.hadjustment.upper * 0.5 - this.hadjustment.value, # our origin is the center this.vadjustment.upper * 0.5 - this.vadjustment.value) - cr.scale(this.fullScale * this.userZoom, this.fullScale * this.userZoom) - cr.translate(-this.dataX - this.dataWidth * 0.5, -this.dataY - this.dataHeight * 0.5) - this.drawWorld(cr) # call the user provided drawing function - destroy(cr) # we can also manually destroy the context here, but GC would do it for us + this.cr.scale(this.fullScale * this.userZoom, this.fullScale * this.userZoom) + this.cr.translate(-this.dataX - this.dataWidth * 0.5, -this.dataY - this.dataHeight * 0.5) + this.drawWorld(this.cr) # call the user provided drawing function + this.cr.restore proc dareaConfigureCallback(darea: DrawingArea; event: EventConfigure; this: PDA): bool = - #this.updateAdjustments(0, 0) (this.dataX, this.dataY, this.dataWidth, this.dataHeight) = this.extents() # query user defined size this.fullScale = min(this.darea.allocatedWidth.float / this.dataWidth, this.darea.allocatedHeight.float / this.dataHeight) + if this.surf != nil: + destroy(this.surf) # manually destroy surface -- GC would do it for us, but GC is slow... + this.surf = this.darea.window.createSimilarSurface(Content.color, + this.darea.allocatedWidth, this.darea.allocatedHeight) + if this.pattern != nil: + patternDestroy(this.pattern) + if this.cr != nil: + destroy(this.cr) + this.pattern = patternCreateForSurface(this.surf) # pattern now owns the surface! + this.cr = newContext(this.surf) # this function references target! this.paint + return gdk.EVENT_STOP proc hscrollbarSizeAllocateCallback(s: Scrollbar; r: gdk.Rectangle; pda: PDA) = pda.hadjustment.setUpper(r.width.float * pda.userZoom) pda.hadjustment.setPageSize(r.width.float) + if pda.oldSizeX != 0: # this fix is not exact, as fullScale can ... + updateVal(pda.hadjustment, (r.width - pda.oldSizeX).float * 0.5) + pda.oldSizeX = r.width proc vscrollbarSizeAllocateCallback(s: Scrollbar; r: gdk.Rectangle; pda: PDA) = pda.vadjustment.setUpper(r.height.float * pda.userZoom) pda.vadjustment.setPageSize(r.height.float) - + if pda.oldSizeY != 0: # ... change when window is rezized. But it's good enough! + updateVal(pda.vadjustment, (r.height - pda.oldSizeY).float * 0.5) + pda.oldSizeY = r.height proc updateAdjustmentsAndPaint(this: PDA; dx, dy: float) = this.updateAdjustments(dx, dy) @@ -167,37 +183,45 @@ proc getUserCoordinates(this: PDA; eventX, eventY: float): (float, float) = proc onMotion(darea: DrawingArea; event: EventMotion; this: PDA): bool = let state = getState(event) let (x, y) = event.getCoords - if (state.contains(button1)): # selecting + if state.contains(button1): # selecting this.selecting = true this.zoomRectX1 = x this.zoomRectY1 = y this.darea.queueDrawArea(0, 0, this.darea.allocatedWidth, this.darea.allocatedHeight) elif button2 in state: # panning this.updateAdjustmentsAndPaint(this.lastMousePosX - x, this.lastMousePosY - y) + else: + return gdk.EVENT_PROPAGATE this.lastMousePosX = x this.lastMousePosY = y + return gdk.EVENT_STOP #event.request # request more motion events ? # zooming with mouse wheel -- data near mouse pointer should not move if possible! # hadjustment.value + event.x is the position in our zoomed_in world, (userZoom / z0 - 1) # is the relative movement caused by zooming +# In other words, this is the delta-move d of a point at position P from zooming: +# d = newPos - P = P * scale - P = P * (z/z0) - P = P * (z/z0 - 1). We have to compensate for this d. proc scrollEvent(darea: DrawingArea; event: EventScroll; this: PDA): bool = let z0 = this.userZoom - var d = getScrollDirection(event) - var (x, y) = event.getCoords - if d == ScrollDirection.up: + case getScrollDirection(event) + of ScrollDirection.up: this.userZoom *= ZoomFactorMouseWheel - elif d == ScrollDirection.down: + of ScrollDirection.down: this.userZoom /= ZoomFactorMouseWheel - if (this.userZoom < 1): + if this.userZoom < 1: this.userZoom = 1 + else: + return gdk.EVENT_PROPAGATE if this.zoomNearMousepointer: + let (x, y) = event.getCoords this.updateAdjustmentsAndPaint((this.hadjustment.value + x) * (this.userZoom / z0 - 1), - (this.vadjustment.value + y) * (this.userZoom / z0 - 1)) + (this.vadjustment.value + y) * (this.userZoom / z0 - 1)) else: # zoom to center this.updateAdjustmentsAndPaint((this.hadjustment.value + this.darea.allocatedWidth.float * 0.5) * (this.userZoom / z0 - 1), (this.vadjustment.value + this.darea.allocatedHeight.float * 0.5) * (this.userZoom / z0 - 1)) + return gdk.EVENT_STOP proc buttonPressEvent(darea: DrawingArea; event: EventButton; this: PDA): bool = var (x, y) = event.getCoords @@ -208,49 +232,49 @@ proc buttonPressEvent(darea: DrawingArea; event: EventButton; this: PDA): bool = echo "buttonPressEvent", x, " ", y (x, y) = this.getUserCoordinates(x, y) echo "User coordinates: ", x, ' ', y, "\n" # to verify getUserCoordinates() + return gdk.EVENT_STOP # zoom into selected rectangle and center it +# math: we first center the selection rectangle, and then compensate for translation due to scale proc buttonReleaseEvent(darea: DrawingArea; event: EventButton; this: PDA): bool = - var (x, y) = event.getCoords - var b = getButton(event) + let (x, y) = event.getCoords + let b = getButton(event) if b == 1: this.selecting = false - let z1 = min(this.darea.allocatedWidth.float / ( - this.lastButtonDownPosX - x).abs, this.darea.allocatedHeight.float / ( - this.lastButtonDownPosY - y).abs) + let z1 = min(this.darea.allocatedWidth.float / (this.lastButtonDownPosX - x).abs, + this.darea.allocatedHeight.float / (this.lastButtonDownPosY - y).abs) if z1 < ZoomFactorSelectMax: # else selection rectangle will persist, we may output a message... this.userZoom *= z1 this.updateAdjustmentsAndPaint( - ((2 * this.hadjustment.value + x + this.lastButtonDownPosX) * z1 - - this.darea.allocatedWidth.float) * 0.5 - this.hadjustment.value, - ((2 * this.vadjustment.value + y + this.lastButtonDownPosY) * z1 - - this.darea.allocatedHeight.float) * 0.5 - this.vadjustment.value) - + ((x + this.lastButtonDownPosX) * z1 - this.darea.allocatedWidth.float) * 0.5 + this.hadjustment.value * (z1 - 1), + ((y + this.lastButtonDownPosY) * z1 - this.darea.allocatedHeight.float) * 0.5 + this.vadjustment.value * (z1 - 1)) + return gdk.EVENT_STOP + return gdk.EVENT_PROPAGATE + proc onAdjustmentEvent(this: PosAdj; pda: PDA) = pda.paint pda.darea.queueDrawArea(0, 0, pda.darea.allocatedWidth, pda.darea.allocatedHeight) proc newPDA: PDA = initGrid(result) + let da = newDrawingArea() + result.darea = da + da.setHExpand + da.setVExpand + da.connect("draw", drawingAreaDrawCb, result) + da.connect("configure-event", dareaConfigureCallback, result) + da.addEvents({EventFlag.buttonPress, EventFlag.buttonRelease, + EventFlag.scroll, button1Motion, button2Motion, pointerMotionHint}) + da.connect("motion-notify-event", onMotion, result) + da.connect("scroll_event", scrollEvent, result) + da.connect("button_press_event", buttonPressEvent, result) + da.connect("button_release_event", buttonReleaseEvent, result) result.zoomNearMousepointer = ZoomNearMousepointer # mouse wheel zooming result.userZoom = 1.0 - result.darea = newDrawingArea() - result.darea.setHExpand - result.darea.setVExpand - result.darea.connect("draw", drawingAreaDrawCb, result) - result.darea.connect("configure-event", dareaConfigureCallback, result) - result.darea.addEvents({EventFlag.buttonPress, EventFlag.buttonRelease, - EventFlag.scroll, button1Motion, button2Motion, pointerMotionHint}) - result.darea.connect("motion-notify-event", onMotion, result) - result.darea.connect("scroll_event", scrollEvent, result) - result.darea.connect("button_press_event", buttonPressEvent, result) - result.darea.connect("button_release_event", buttonReleaseEvent, result) result.hadjustment = newPosAdj() - result.hadjustment.handlerID = result.hadjustment.connect("value-changed", - onAdjustmentEvent, result) + result.hadjustment.handlerID = result.hadjustment.connect("value-changed", onAdjustmentEvent, result) result.vadjustment = newPosAdj() - result.vadjustment.handlerID = result.vadjustment.connect("value-changed", - onAdjustmentEvent, result) + result.vadjustment.handlerID = result.vadjustment.connect("value-changed", onAdjustmentEvent, result) result.hscrollbar = newScrollbar(Orientation.horizontal, result.hadjustment) result.vscrollbar = newScrollbar(Orientation.vertical, result.vadjustment) result.hscrollbar.setHExpand @@ -261,9 +285,13 @@ proc newPDA: PDA = result.attach(result.vscrollbar, 1, 0, 1, 1) result.attach(result.hscrollbar, 0, 1, 1, 1) -proc appActivate(app: Application; initData: ref PDA_Data) = +proc appStartup(app: Application) = + echo "appStartup" + +proc appActivate(app: Application; initData: PDA_Data) = let window = newApplicationWindow(app) window.title = "Drawing example" + # window.defaultSize = initData.windowSize window.defaultSize = (initData.windowSize[0], initData.windowSize[1]) let pda = newPDA() pda.drawWorld = initData.draw @@ -273,14 +301,13 @@ proc appActivate(app: Application; initData: ref PDA_Data) = proc newDisplay*(initData: PDA_Data) = let app = newApplication("org.gtk.example") - var d = new (ref PDA_Data) - d[] = initData - connect(app, "activate", appActivate, d) + connect(app, "startup", appStartup) + connect(app, "activate", appActivate, initData) discard run(app) when isMainModule: - const # arbitrary locations for our data + const # arbitrary locations for our data DataX = 150.0 DataY = 250.0 DataWidth = 200.0 @@ -300,18 +327,13 @@ when isMainModule: cr.setSource(0, 0, 0) cr.setLineWidth(2) var i = 0.0 - while true: - if min(DataWidth - 2.0 * i, DataHeight - 2.0 * i) <= 0: - break + while min(DataWidth - 2 * i, DataHeight - 2 * i) > 0: cr.rectangle(DataX + i, DataY + i, DataWidth - 2 * i, DataHeight - 2 * i) - i += 10.0 + i += 10 cr.stroke proc test = - var data: PDA_Data - data.draw = drawWorld - data.extents = worldExtents - data.windowSize = (800, 600) + let data = PDA_Data(draw: drawWorld, extents: worldExtents, windowSize: (800, 600)) newDisplay(data) - test() + test() # 337 lines diff --git a/examples/gtk3/makeAll b/examples/gtk3/makeAll index be7b682..8d5eade 100644 --- a/examples/gtk3/makeAll +++ b/examples/gtk3/makeAll @@ -12,4 +12,7 @@ nim c label.nim nim c listview.nim nim c menubar.nim nim c t0.nim - +nim c simpledrawingarea.nim +nim c css_colored_listview.nim +nim c gearsmenu.nim +nim c cairoImage.nim diff --git a/examples/gtk3/simpledrawingarea.nim b/examples/gtk3/simpledrawingarea.nim new file mode 100644 index 0000000..cf5cfd9 --- /dev/null +++ b/examples/gtk3/simpledrawingarea.nim @@ -0,0 +1,310 @@ +# Plain demo for zooming, panning, scrolling with GTK DrawingArea +# (c) S. Salewski, 21-DEC-2010 (initial Ruby version) +# Nim version April 2019 +# License MIT + +# This is a simplified example, where all the drawing is done in +# the draw callback function. No separate paint proc with own buffer surface. + +# https://discourse.gnome.org/t/problem-with-gtkscrollbar-gtk-window-resize-and-gtk-adjustment-set-value/1081 + +import gintro/[gtk, gdk, glib, gobject, gio, cairo] + +const + ZoomFactorMouseWheel = 1.1 + ZoomFactorSelectMax = 10 # ignore zooming in tiny selection + ZoomNearMousepointer = true # mouse wheel zooming -- to mouse-pointer or center + SelectRectCol = [0.0, 0, 1, 0.5] # blue with transparency + +discard """ +Zooming, scrolling, panning... + +|-------------------------| +|<-------- A ------------>| +| | +| |---------------| | +| | <---- a ----->| | +| | visible | | +| |---------------| | +| | +| | +|-------------------------| + +a is the visible, zoomed in area == darea.allocatedWidth +A is the total data range +A/a == userZoom >= 1 +For horizontal adjustment we use +hadjustment.setUpper(darea.allocatedWidth * userZoom) == A +hadjustment.setPageSize(darea.allocatedWidth) == a +So hadjustment.value == left side of visible area + +Initially, we set userZoom = 1, scale our data to fit into darea.allocatedWidth +and translate the origin of our data to (0, 0) + +Zooming: Mouse wheel or selecting a rectangle with left mouse button pressed +Scrolling: Scrollbars +Panning: Moving mouse while middle mouse button pressed +""" + +# drawing area and scroll bars in 2x2 grid (PDA == Plain Drawing Area) + +type + PosAdj = ref object of Adjustment + handlerID: uint64 + +proc newPosAdj: PosAdj = + initAdjustment(result, 0, 0, 1, 1, 10, 1) + +type + PDA_Data* = object + draw*: proc (cr: Context) + extents*: proc (): tuple[x, y, w, h: float] + windowSize*: tuple[w, h: int] + +type + PDA = ref object of Grid + zoomNearMousepointer: bool + selecting: bool + userZoom: float + darea: DrawingArea + hadjustment: PosAdj + vadjustment: PosAdj + hscrollbar: Scrollbar + vscrollbar: Scrollbar + fullScale: float + dataX: float + dataY: float + dataWidth: float + dataHeight: float + lastButtonDownPosX: float + lastButtonDownPosY: float + lastMousePosX: float + lastMousePosY: float + zoomRectX1: float + zoomRectY1: float + oldSizeX: int + oldSizeY: int + drawWorld: proc (cr: Context) + extents: proc (): tuple[x, y, w, h: float] + +proc drawingAreaDrawCb(darea: DrawingArea; cr: Context; this: PDA): bool = + cr.save + cr.translate(this.hadjustment.upper * 0.5 - this.hadjustment.value, # our origin is the center + this.vadjustment.upper * 0.5 - this.vadjustment.value) + cr.scale(this.fullScale * this.userZoom, this.fullScale * this.userZoom) + cr.translate(-this.dataX - this.dataWidth * 0.5, -this.dataY - this.dataHeight * 0.5) + this.drawWorld(cr) # call the user provided drawing function + cr.restore + if this.selecting: + cr.rectangle(this.lastButtonDownPosX, this.lastButtonDownPosY, + this.zoomRectX1 - this.lastButtonDownPosX, this.zoomRectY1 - this.lastButtonDownPosY) + cr.setSource(0, 0, 1, 0.5) # SELECT_RECT_COL) # 0, 0, 1, 0.5 + cr.fillPreserve + cr.setSource(0, 0, 0) + cr.setLineWidth(2) + cr.stroke + return gdk.EVENT_STOP # EVENT_PROPAGATE + #return true # TRUE to stop other handlers from being invoked for the event. FALSE to propagate the event further. + +# clamp to correct values, 0 <= value <= (adj.upper - adj.pageSize), block calling onAdjustmentEvent() +proc updateVal(adj: PosAdj; d: float) = + adj.signalHandlerBlock(adj.handlerID) + adj.setValue(max(0.0, min(adj.value + d, adj.upper - adj.pageSize))) + adj.signalHandlerUnblock(adj.handlerID) + +proc updateAdjustments(this: PDA; dx, dy: float) = + this.hadjustment.setUpper(this.darea.allocatedWidth.float * this.userZoom) + this.vadjustment.setUpper(this.darea.allocatedHeight.float * this.userZoom) + this.hadjustment.setPageSize(this.darea.allocatedWidth.float) + this.vadjustment.setPageSize(this.darea.allocatedHeight.float) + updateVal(this.hadjustment, dx) + updateVal(this.vadjustment, dy) + +proc dareaConfigureCallback(darea: DrawingArea; event: EventConfigure; this: PDA): bool = + (this.dataX, this.dataY, this.dataWidth, + this.dataHeight) = this.extents() # query user defined size + this.fullScale = min(this.darea.allocatedWidth.float / this.dataWidth, + this.darea.allocatedHeight.float / this.dataHeight) + return gdk.EVENT_STOP + +proc hscrollbarSizeAllocateCallback(s: Scrollbar; r: gdk.Rectangle; pda: PDA) = + pda.hadjustment.setUpper(r.width.float * pda.userZoom) + pda.hadjustment.setPageSize(r.width.float) + if pda.oldSizeX != 0: # this fix is not exact, as fullScale can ... + updateVal(pda.hadjustment, (r.width - pda.oldSizeX).float * 0.5) + pda.oldSizeX = r.width + +proc vscrollbarSizeAllocateCallback(s: Scrollbar; r: gdk.Rectangle; pda: PDA) = + pda.vadjustment.setUpper(r.height.float * pda.userZoom) + pda.vadjustment.setPageSize(r.height.float) + if pda.oldSizeY != 0: # ... change when window is rezized. But it's good enough! + updateVal(pda.vadjustment, (r.height - pda.oldSizeY).float * 0.5) + pda.oldSizeY = r.height + +proc updateAdjustmentsAndPaint(this: PDA; dx, dy: float) = + this.updateAdjustments(dx, dy) + this.darea.queueDrawArea(0, 0, this.darea.allocatedWidth, this.darea.allocatedHeight) + +# event coordinates to user space +proc getUserCoordinates(this: PDA; eventX, eventY: float): (float, float) = + ((eventX - this.hadjustment.upper * 0.5 + this.hadjustment.value) / ( + this.fullScale * this.userZoom) + this.dataX + this.dataWidth * 0.5, + (eventY - this.vadjustment.upper * 0.5 + this.vadjustment.value) / ( + this.fullScale * this.userZoom) + this.dataY + this.dataHeight * 0.5) + +proc onMotion(darea: DrawingArea; event: EventMotion; this: PDA): bool = + let state = getState(event) + let (x, y) = event.getCoords + if state.contains(button1): # selecting + this.selecting = true + this.zoomRectX1 = x + this.zoomRectY1 = y + this.darea.queueDrawArea(0, 0, this.darea.allocatedWidth, this.darea.allocatedHeight) + elif button2 in state: # panning + this.updateAdjustmentsAndPaint(this.lastMousePosX - x, this.lastMousePosY - y) + else: + return gdk.EVENT_PROPAGATE + this.lastMousePosX = x + this.lastMousePosY = y + return gdk.EVENT_STOP + #event.request # request more motion events ? + +# zooming with mouse wheel -- data near mouse pointer should not move if possible! +# hadjustment.value + event.x is the position in our zoomed_in world, (userZoom / z0 - 1) +# is the relative movement caused by zooming +# In other words, this is the delta-move d of a point at position P from zooming: +# d = newPos - P = P * scale - P = P * (z/z0) - P = P * (z/z0 - 1). We have to compensate for this d. +proc scrollEvent(darea: DrawingArea; event: EventScroll; this: PDA): bool = + let z0 = this.userZoom + case getScrollDirection(event) + of ScrollDirection.up: + this.userZoom *= ZoomFactorMouseWheel + of ScrollDirection.down: + this.userZoom /= ZoomFactorMouseWheel + if this.userZoom < 1: + this.userZoom = 1 + else: + return gdk.EVENT_PROPAGATE + if this.zoomNearMousepointer: + let (x, y) = event.getCoords + this.updateAdjustmentsAndPaint((this.hadjustment.value + x) * (this.userZoom / z0 - 1), + (this.vadjustment.value + y) * (this.userZoom / z0 - 1)) + else: # zoom to center + this.updateAdjustmentsAndPaint((this.hadjustment.value + + this.darea.allocatedWidth.float * 0.5) * (this.userZoom / z0 - 1), + (this.vadjustment.value + this.darea.allocatedHeight.float * 0.5) * (this.userZoom / z0 - 1)) + return gdk.EVENT_STOP + +proc buttonPressEvent(darea: DrawingArea; event: EventButton; this: PDA): bool = + var (x, y) = event.getCoords + this.lastMousePosX = x + this.lastMousePosY = y + this.lastButtonDownPosX = x + this.lastButtonDownPosY = y + echo "buttonPressEvent", x, " ", y + (x, y) = this.getUserCoordinates(x, y) + echo "User coordinates: ", x, ' ', y, "\n" # to verify getUserCoordinates() + return gdk.EVENT_STOP + +# zoom into selected rectangle and center it +# math: we first center the selection rectangle, and then compensate for translation due to scale +proc buttonReleaseEvent(darea: DrawingArea; event: EventButton; this: PDA): bool = + let (x, y) = event.getCoords + let b = getButton(event) + if b == 1: + this.selecting = false + let z1 = min(this.darea.allocatedWidth.float / (this.lastButtonDownPosX - x).abs, + this.darea.allocatedHeight.float / (this.lastButtonDownPosY - y).abs) + if z1 < ZoomFactorSelectMax: # else selection rectangle will persist, we may output a message... + this.userZoom *= z1 + this.updateAdjustmentsAndPaint( + ((x + this.lastButtonDownPosX) * z1 - this.darea.allocatedWidth.float) * 0.5 + this.hadjustment.value * (z1 - 1), + ((y + this.lastButtonDownPosY) * z1 - this.darea.allocatedHeight.float) * 0.5 + this.vadjustment.value * (z1 - 1)) + return gdk.EVENT_STOP + return gdk.EVENT_PROPAGATE + +proc onAdjustmentEvent(this: PosAdj; pda: PDA) = + pda.darea.queueDrawArea(0, 0, pda.darea.allocatedWidth, pda.darea.allocatedHeight) + +proc newPDA: PDA = + initGrid(result) + let da = newDrawingArea() + result.darea = da + da.setHExpand + da.setVExpand + da.connect("draw", drawingAreaDrawCb, result) + da.connect("configure-event", dareaConfigureCallback, result) + da.addEvents({EventFlag.buttonPress, EventFlag.buttonRelease, + EventFlag.scroll, button1Motion, button2Motion, pointerMotionHint}) + da.connect("motion-notify-event", onMotion, result) + da.connect("scroll_event", scrollEvent, result) + da.connect("button_press_event", buttonPressEvent, result) + da.connect("button_release_event", buttonReleaseEvent, result) + result.zoomNearMousepointer = ZoomNearMousepointer # mouse wheel zooming + result.userZoom = 1.0 + result.hadjustment = newPosAdj() + result.hadjustment.handlerID = result.hadjustment.connect("value-changed", onAdjustmentEvent, result) + result.vadjustment = newPosAdj() + result.vadjustment.handlerID = result.vadjustment.connect("value-changed", onAdjustmentEvent, result) + result.hscrollbar = newScrollbar(Orientation.horizontal, result.hadjustment) + result.vscrollbar = newScrollbar(Orientation.vertical, result.vadjustment) + result.hscrollbar.setHExpand + result.vscrollbar.setVExpand + result.hscrollbar.connect("size-allocate", hscrollbarSizeAllocateCallback, result) + result.vscrollbar.connect("size-allocate", vscrollbarSizeAllocateCallback, result) + result.attach(result.darea, 0, 0, 1, 1) + result.attach(result.vscrollbar, 1, 0, 1, 1) + result.attach(result.hscrollbar, 0, 1, 1, 1) + +proc appStartup(app: Application) = + echo "appStartup" + +proc appActivate(app: Application; initData: PDA_Data) = + let window = newApplicationWindow(app) + window.title = "Drawing example" + # window.defaultSize = initData.windowSize + window.defaultSize = (initData.windowSize[0], initData.windowSize[1]) + let pda = newPDA() + pda.drawWorld = initData.draw + pda.extents = initData.extents + window.add(pda) + showAll(window) + +proc newDisplay*(initData: PDA_Data) = + let app = newApplication("org.gtk.example") + connect(app, "startup", appStartup) + connect(app, "activate", appActivate, initData) + discard run(app) + +when isMainModule: + + const # arbitrary locations for our data + DataX = 150.0 + DataY = 250.0 + DataWidth = 200.0 + DataHeight = 120.0 + + # we need two user defined functions -- one gives the extent of the graphics, + # and the other does the cairo drawing using a cairo context. + + # bounding box of user data -- x, y, w, h -- top left corner, width, height + proc worldExtents(): (float, float, float, float) = + (DataX, DataY, DataWidth, DataHeight) # current extents of our user world + + # draw to cairo context + proc drawWorld(cr: cairo.Context) = + cr.setSource(1, 1, 1) + cr.paint + cr.setSource(0, 0, 0) + cr.setLineWidth(2) + var i = 0.0 + while min(DataWidth - 2 * i, DataHeight - 2 * i) > 0: + cr.rectangle(DataX + i, DataY + i, DataWidth - 2 * i, DataHeight - 2 * i) + i += 10 + cr.stroke + + proc test = + let data = PDA_Data(draw: drawWorld, extents: worldExtents, windowSize: (800, 600)) + newDisplay(data) + + test() # 310 lines diff --git a/gintro.nimble b/gintro.nimble index 6186a30..b5f8248 100644 --- a/gintro.nimble +++ b/gintro.nimble @@ -1,6 +1,6 @@ # Package -version = "0.5.3" +version = "0.5.4" author = "Stefan Salewski" description = "High level GObject-Introspection based GTK3/GTK4 bindings" license = "MIT" diff --git a/gintro/cairo.nim b/gintro/cairo.nim index 3285a26..1b666d7 100644 --- a/gintro/cairo.nim +++ b/gintro/cairo.nim @@ -1,7 +1,7 @@ # This is the high level cairo module for Nim -- based on the low level ngtk3 module, manually tuned. # (c) S. Salewski 2017, cairo 1.15.6 -# v0.4.18 -# 24-APR-2019 +# v0.5.04 +# 14-SEP-2019 # Cairo does not really support gobject introspection and gobject's toggle references. # So code for memory management is a bit different compared to other gtk modules. @@ -25,7 +25,8 @@ # result.impl = cairo_create(target.impl) # discard cairo_set_user_data(result.impl, NUDK, cast[pointer](result), gcuref) -# We have to call GC_ref() because we store no direct reference to the proxy object. +# We have to call GC_ref() because we store no direct reference to the proxy object, while +# cairo_get_target() allows us to get the surface back from cairo context. # For this example cairo_create() references the surface. # When ref count of surface is decreased, maybe because context is destroyed, then gcuref is called # which calls GC_unref() on proxy. When there is no other ref to proxy GC can free proxy and surface @@ -34,7 +35,8 @@ # But there is at least one design flaw: # When we create for example a surface and GC releases it, then gcunref() is called on it while # gc_ref() was newer called. When we consider that the cairo context is the only element which -# stores references to other objects, then we can simplify the code to +# stores references to other objects, then we can simplify the code to (Footnote: This is not really +# true, for example Pattern stores references to surface too -- but it should work similar.) # proc newImageSurface*(format: Format; width, height: int): Surface = # new(result, surfaceDestroy) @@ -55,7 +57,17 @@ # with # proc gcuref(o: pointer) {.cdecl.} = GC_unref(cast[RootRef](o)) +# cairo_image_surface_create() set refcount of the plain cairo surface to 1. +# cairo_create() increases refcount of surface (by 2), cairo destroy decreases +# surface refcount again. cairo_surface_set_user_data() ensures that +# Nim surface proxy element is unreffed when refcount of cairo surface +# drops to zero, so surfaceDestroy() frees the cairo memory. + +# Note: For some other procs like cairo_set_source_surface() we also need code like +# discard cairo_surface_set_user_data(target.impl, NUDK, cast[pointer](target), gcuref) + # Currently this is all not really tested. + # One problem may be procs like cairo_surface_create_similar() when called frequently, maybe # for each frame in animations. GC may be too slow in freeing the memory. # We should try to avoid such usage, or we may add a way to manually free memory. @@ -290,9 +302,6 @@ proc newContext*(target: Surface): Context = result.impl = cairo_create(target.impl) discard cairo_surface_set_user_data(target.impl, NUDK, cast[pointer](target), gcuref) - - - proc cairo_reference*(cr: ptr Context00): ptr Context00 {.importc, libcairo.} # proc reference*(cr: Context): Context = @@ -330,7 +339,11 @@ proc pushGroup*(cr: Context; content: Content) = proc cairo_pattern_destroy*(pattern: ptr Pattern00) {.importc, libcairo.} # proc patternDestroy*(pattern: Pattern) = - cairo_pattern_destroy(pattern.impl) + #cairo_pattern_destroy(pattern.impl) + + if pattern != nil and pattern.impl != nil: + cairo_pattern_destroy(pattern.impl) + pattern.impl = nil proc cairo_pattern_set_user_data*(pattern: ptr Pattern00; key: ptr UserDataKey; userData: pointer; destroy: DestroyFunc00): Status {.importc, libcairo.} @@ -405,11 +418,9 @@ proc getReferenceCount*(surface: Surface): int = proc cairo_set_source_surface*(cr: ptr Context00; surface: ptr Surface00; x, y: cdouble) {.importc, libcairo.} # proc setSourceSurface*(cr: Context; surface: Surface; x, y: float) = - #GC_ref(source) # we guess that surface is nor referenced, but a new intern pattern is created - let h = cairo_surface_get_reference_count(surface.impl) + GC_ref(surface) cairo_set_source_surface(cr.impl, surface.impl, x.cdouble, y.cdouble) - assert(cairo_surface_get_reference_count(surface.impl) == h) # is our guess OK? -# + discard cairo_surface_set_user_data(surface.impl, NUDK, cast[pointer](surface), gcuref) #const `sourceSurface=`* = setSourceSurface proc cairo_set_tolerance*(cr: ptr Context00; tolerance: cdouble) {.importc, libcairo.} @@ -1378,15 +1389,22 @@ proc cairo_pattern_get_user_data*(pattern: ptr Pattern00; key: ptr UserDataKey): proc getUserData*(pattern: Pattern; key: ptr UserDataKey): pointer = cairo_pattern_get_user_data(pattern.impl, key) +proc cairo_pattern_reference*(pattern: ptr Pattern00): ptr Pattern00 {.importc, libcairo.} +# +proc patternReference*(pattern: Pattern): Pattern = + discard cairo_pattern_reference(pattern.impl) + return pattern + +# Returns the current source pattern. This object is owned by cairo. +# To keep a reference to it, you must call cairo_pattern_reference(). proc cairo_get_source*(cr: ptr Context00): ptr Pattern00 {.importc, libcairo.} # proc getSource*(cr: Context): Pattern = let h = cairo_get_source(cr.impl) let d = cairo_pattern_get_user_data(h, NUDK) if d.isNil: - #assert false # may this happen? new(result, patternDestroy) - result.impl = h + result.impl = cairo_pattern_reference(h) else: result = cast[Pattern](d) assert(result.impl == h) @@ -2077,11 +2095,7 @@ proc patternCreateMesh*(): Pattern = result.impl = cairo_pattern_create_mesh() ### discard cairo_pattern_set_user_data(result.impl, NUDK, cast[pointer](result), gcuref) -proc cairo_pattern_reference*(pattern: ptr Pattern00): ptr Pattern00 {.importc, libcairo.} -# -proc patternReference*(pattern: Pattern): Pattern = - discard cairo_pattern_reference(pattern.impl) - return pattern + proc cairo_pattern_status*(pattern: ptr Pattern00): Status {.importc, libcairo.} # @@ -2821,4 +2835,5 @@ when CAIRO_HAS_TEE_SURFACE: discard cairo_tee_surface_index(surface.impl, index.cuint) return surface -# 2786 lines +# 2851 lines + diff --git a/gintro/gimpl.nim b/gintro/gimpl.nim index c867200..cff7f42 100644 --- a/gintro/gimpl.nim +++ b/gintro/gimpl.nim @@ -58,6 +58,8 @@ macro mconnect(widget: gobject.Object; signal: string; p: typed; arg: typed; ign inc(ProcID) let wt = getType(widget) let at = getTypeInst(arg) + #echo at.repr + #echo at.typeKind == ntyRef let wts = ($(wt[1].toStrLit)).replace(":ObjectType") let ats = $at.toStrLit let signalName = ($signal).replace("-", "_") # maybe we should just use plain proc names @@ -74,7 +76,10 @@ proc $1$2 {.cdecl.} = ahl = "(self: gobject.Object)" r1s.add(" $3(cast[$4](h)") if not ignoreArg.boolVal: - r1s.add(", cast[$5](xdata)") + if getTypeInst(arg).typeKind == ntyRef: + r1s.add(", cast[$5](xdata)") + else: + r1s.add(", cast[ptr $5](xdata)[]") r1s.add(")\n") else: # signals with multiple arguments and maybe using interface providers (sn, wid, num, ahl, all) = sci.split(RecSep) @@ -136,16 +141,19 @@ proc $1$2 {.cdecl.} = r1s.add(" if " & names[i] & "1" & ".isNil:\n") r1s.add(" new " & names[i] & "1" & "\n") r1s.add(" " & names[i] & "1.impl = " & names[i] & "\n") - if not ignoreArg.boolVal: - resu.add(", cast[$5](data)") + # resu.add(", cast[$5](data)") + if getTypeInst(arg).typeKind == ntyRef: + resu.add(", cast[$5](data)") + else: + resu.add(", cast[ptr $5](data)[]") resu.add(")") if resl == "gboolean": resu.add(".ord.gboolean") r1s.add(resu & "\n") all = all.replace(")", "; data: pointer)") r1s = r1s % [$procNameCdecl, all, $p, wts, ats] - #echo r1s + # echo r1s result = parseStmt(r1s) if not ignoreArg.boolVal: ahl = ahl.replace(")", "; arg: " & ats & ")") @@ -154,7 +162,7 @@ proc $1$2 {.cdecl.} = else: ahl = "(self: " & wts & ")" & ahl.split(")", 1)[1] - let r2s = + let r2sunused = if ignoreArg.boolVal: """ proc $1(self: $2; p: proc $3): culong {.discardable.} = @@ -164,7 +172,7 @@ $1($6, $7) else: """ proc $1(self: $2; p: proc $3; a: $4): culong {.discardable.} = - when a is RootRef: + when a is ref: GC_ref(a) sc$5(self, $6, cast[pointer](a)) else: @@ -172,10 +180,38 @@ proc $1(self: $2; p: proc $3; a: $4): culong {.discardable.} = new(ar) deepCopy(ar[], a) GC_ref(ar) - sc$5(self, $6, cast[pointer](ar[])) + # sc$5(self, $6, cast[pointer](ar[])) + sc$5(self, $6, cast[pointer](ar)) $1($7, $8, $9) """ % [$procName, wts, ahl, ats, signalName, $procNameCdecl, $(widget.toStrLit), $p, $(arg.toStrLit)] - #echo r2s + + # maybe we write this better this way: + let r2s = + if ignoreArg.boolVal: + """ +proc $1(self: $2; p: proc $3): culong {.discardable.} = + sc$4(self, $5, nil) +$1($6, $7) +""" % [$procName, wts, ahl, signalName, $procNameCdecl, $(widget.toStrLit), $p] + elif getTypeInst(arg).typeKind == ntyRef: + """ +proc $1(self: $2; p: proc $3; a: $4): culong {.discardable.} = + GC_ref(a) + sc$5(self, $6, cast[pointer](a)) +$1($7, $8, $9) +""" % [$procName, wts, ahl, ats, signalName, $procNameCdecl, $(widget.toStrLit), $p, $(arg.toStrLit)] + else: + """ +proc $1(self: $2; p: proc $3; a: $4): culong {.discardable.} = + var ar: ref $4 + new(ar) + deepCopy(ar[], a) + GC_ref(ar) + # sc$5(self, $6, cast[pointer](ar[])) + sc$5(self, $6, cast[pointer](ar)) +$1($7, $8, $9) +""" % [$procName, wts, ahl, ats, signalName, $procNameCdecl, $(widget.toStrLit), $p, $(arg.toStrLit)] + # echo r2s result.add(parseStmt(r2s)) template connect*(widget: gobject.Object; signal: string; p: typed; arg: typed): untyped =