Makie.jl Textbox

I have a use case for an array of TextBox to use for data entry. I want to populate all Textboxes when the user clicks a “Load” button and then after the user makes some changes to some Textboxes and clicks another button “Save” I want to read all the new values and save them.

I want to process the new set of data at the same time so events based on changes to individual textboxes are not much use, therefore the “Save” button is the trigger. I’m getting confused with .display_string and .stored_string. Documentation refers to stored_string as internal use, but it looks like this is the place that the string is stored after a new value is entered. At this point my code looks like this …

function LoadtoGui(tbarray::Vector{Any},df::DataFrame)
       tbarray[1].displayed_string[] = df[1,:field1]
       tbarray[2].displayed_string[] = df[1,:field2]
end

function SavefromGui(tbarray::Vector{Any},db)
       newvalue1 = tbarray[1].stored_string[]
       newvalue2 = tbarray[2].stored_string[] 
      # now save the data in db
end

This seems to work fine, but if the user loads record1 changes the value and presses enter and then calls LoadtoGui with record2, the previously entered value in .stored_string persists. As a result some textboxes have modified record1 values and some modified record2 values in .stored_string.

I’m not at all sure I’m using a Textbox as designed, and I can’t find any documentation show a similar use case. Can anyone point me to some docs or examples that might help.

Thanks
Steve

end

That’s not quite correct, displayed_string is the one for internal use. You can read it to know what’s currently being displayed, but you shouldn’t set it to anything. stored_string is the correct one to read for knowing if the submitted content has changed (it doesn’t change immediately when letters are added or removed, only on submit).

When you set tbarray[1].displayed_string[] = df[1,:field1] you’re just manually changing the observable that makes the textbox show that text, but that doesn’t mean that it then stores the new setting correctly. Underneath is still the old value. For setting a value programmatically, there’s the set! function although it doesn’t seem to be documented (maybe open an issue for that?) https://github.com/MakieOrg/Makie.jl/blob/master/src/makielayout/blocks/textbox.jl#L319-L331

1 Like

Thanks Jules,

Set! sounds like what I need.
When I try

set!(tbarray[2],"a new value")

I get ERROR: UndefVarError: set! not defined
sorry, I don’t know what I’m doing wrong.

Steve

I get ERROR: UndefVarError: set! not defined

That function is not exported by Makie. One way to call it is
Makie.set!(tbarray[2],"a new value")
to tell julia where to find the function.

Thanks for the help getting this far.
The setting and reading of the textbox value now works fine, sometimes.

Now I have a very strange error I’m not sure how to address. I have reproduced the error below in a simple demonstration.
To reproduce…
1.If I run the lines down to the comment
2. edit either textbox and press enter or not press enter
3. run the last two lines
4. I consistently get an error

I’m using vscode, Windows, julia v1.9.1, GLMakie v0.8.6

using GLMakie
fig = Figure(resolution=(500, 500));display(fig)
guiTextboxGrid = fig[1,1]= GridLayout(default_colgap=0,rowgap=1,padding=(0.0,0.0,0.0,0.0))
global textboxes = Vector{Any}(undef,12)
textboxes[1] =  Textbox(guiTextboxGrid[1,1], reset_on_defocus=true,placeholder = "f1", height=40,width=200,fontsize=22)
textboxes[2] =  Textbox(guiTextboxGrid[2,1], reset_on_defocus=true,placeholder = "f2", height=40,width=200,fontsize=22)
Makie.set!(textboxes[1],"a string1")
Makie.set!(textboxes[2],"another string1")
#set focus on either textbox and add a char 
Makie.set!(textboxes[1],"a string2")
Makie.set!(textboxes[2],"another string2")

I have tried lots of variations like the reset_on_focus=true or not, press enter or not, etc, but 100% of the time I run this I get this error…

ERROR: MethodError: Cannot `convert` an object of type Nothing to an object of type Vector{Point{2, Float32}}   

Closest candidates are:
  convert(::Type{Array{T, N}}, ::StaticArraysCore.SizedArray{S, T, N, N, Array{T, N}}) where {S, T, N}
   @ StaticArrays C:\Users\steve\.julia\packages\StaticArrays\O6dgq\src\SizedArray.jl:88
  convert(::Type{Array{T, N}}, ::StaticArraysCore.SizedArray{S, T, N, M, TData} where {M, TData<:AbstractArray{T, M}}) where {T, S, N}
   @ StaticArrays C:\Users\steve\.julia\packages\StaticArrays\O6dgq\src\SizedArray.jl:82
  convert(::Type{T}, ::AbstractArray) where T<:Array    
   @ Base array.jl:613
  ...

with this stack trace…

Stacktrace:
  [1] setproperty!(x::Observable{Vector{Point{2, Float32}}}, f::Symbol, v::Nothing)
    @ Base .\Base.jl:38
  [2] setindex!(observable::Observable, val::Any)
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:85
  [3] (::Observables.MapCallback)(value::Any)
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:431
  [4] #invokelatest#2
    @ .\essentials.jl:816 [inlined]
  [5] invokelatest
    @ .\essentials.jl:813 [inlined]
  [6] notify
    @ C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:169 [inlined]
  [7] setindex!(observable::Observable, val::Any)       
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:86
--- the last 5 lines are repeated 2 more times ---      
 [18] (::Makie.var"#175#177"{Attributes, Observable{Tuple{Vector{Point{3, Float32}}}}, DataType})(kwargs::Tuple{}, args::Point{3, Float32})
    @ Makie C:\Users\steve\.julia\packages\Makie\iECbF\src\interfaces.jl:342
 [19] invokelatest(::Any, ::Any, ::Vararg{Any}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Base .\essentials.jl:816
 [20] invokelatest(::Any, ::Any, ::Vararg{Any})
    @ Base .\essentials.jl:813
 [21] (::Observables.OnAny)(value::Any)
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:415
 [22] #invokelatest#2
    @ .\essentials.jl:816 [inlined]
 [23] invokelatest
    @ .\essentials.jl:813 [inlined]
 [24] notify
    @ C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:169 [inlined]
 [25] setindex!(observable::Observable, val::Any)       
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:86
 [26] (::Makie.var"#1573#1575"{Label, Base.RefValue{GeometryBasics.HyperRectangle{2, Float32}}, Observable{Float32}, Observable{Point{3, Float32}}})(bbox::GeometryBasics.HyperRectangle{2, Float32}, padding::NTuple{4, Int64})    @ Makie C:\Users\steve\.julia\packages\Makie\iECbF\src\makielayout\blocks\label.jl:45
 [27] invokelatest(::Any, ::Any, ::Vararg{Any}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Base .\essentials.jl:816
 [28] invokelatest(::Any, ::Any, ::Vararg{Any})
    @ Base .\essentials.jl:813
 [29] (::Observables.OnAny)(value::Any)
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:415
 [30] #invokelatest#2
    @ .\essentials.jl:816 [inlined]
 [31] invokelatest
    @ .\essentials.jl:813 [inlined]
 [32] notify
    @ C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:169 [inlined]
 [33] setindex!(observable::Observable, val::Any)       
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:86
 [34] update_computedbbox!(computedbbox::Observable{GeometryBasics.HyperRectangle{2, Float32}}, suggestedbbox::GeometryBasics.HyperRectangle{2, Float32}, alignment::Tuple{Float32, Float32}, reporteddimensions::GridLayoutBase.Dimensions, alignmode::Inside, protrusions::GridLayoutBase.RectSides{Float32}, sizeattrs::Observable{Tuple{Union{Nothing, Float32, Auto, Fixed, Relative}, Union{Nothing, Float32, Auto, Fixed, Relative}}}, autosizeobservable::Observable{Tuple{Union{Nothing, Float32}, Union{Nothing, Float32}}})
    @ GridLayoutBase C:\Users\steve\.julia\packages\GridLayoutBase\lYdxT\src\layoutobservables.jl:356
 [35] (::GridLayoutBase.var"#107#109"{Observable{GeometryBasics.HyperRectangle{2, Float32}}, Observable{Tuple{Float32, Float32}}, Observable{Tuple{Union{Nothing, Float32, Auto, Fixed, Relative}, Union{Nothing, Float32, Auto, Fixed, Relative}}}, Observable{Tuple{Union{Nothing, Float32}, Union{Nothing, Float32}}}, Observable{Any}, Observable{GridLayoutBase.RectSides{Float32}}, Base.RefValue{Union{Nothing, GridLayoutBase.GridContent{GridLayout}}}, Observable{GeometryBasics.HyperRectangle{2, Float32}}})(rdims::GridLayoutBase.Dimensions)
    @ GridLayoutBase C:\Users\steve\.julia\packages\GridLayoutBase\lYdxT\src\layoutobservables.jl:226
 [36] invokelatest(::Any, ::Any, ::Vararg{Any}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Base .\essentials.jl:816
 [37] invokelatest(::Any, ::Any, ::Vararg{Any})
    @ Base .\essentials.jl:813
 [38] (::Observables.OnAny)(value::Any)
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:415
 [39] #invokelatest#2
    @ .\essentials.jl:816 [inlined]
 [40] invokelatest
    @ .\essentials.jl:813 [inlined]
 [41] notify
    @ C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:169 [inlined]
 [42] setindex!(observable::Observable, val::Any)
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:86
 [43] (::Observables.MapCallback)(value::Any)
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:431
 [44] #invokelatest#2
    @ .\essentials.jl:816 [inlined]
 [45] invokelatest
    @ .\essentials.jl:813 [inlined]
 [46] notify
    @ C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:169 [inlined]
 [47] setindex!(observable::Observable, val::Any)       
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:86
 [48] (::Makie.var"#1572#1574"{Label, Base.RefValue{GeometryBasics.HyperRectangle{2, Float32}}, MakieCore.Text{Tuple{Vector{Point{3, Float32}}}}, LayoutObservables{GridLayout}})(#unused#::String, #unused#::Float32, #unused#::Symbol, #unused#::Float32, #unused#::Float32, padding::NTuple{4, Int64})
    @ Makie C:\Users\steve\.julia\packages\Makie\iECbF\src\makielayout\blocks\label.jl:26
 [49] invokelatest(::Any, ::Any, ::Vararg{Any}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
    @ Base .\essentials.jl:816
 [50] invokelatest(::Any, ::Any, ::Vararg{Any})
    @ Base .\essentials.jl:813
 [51] (::Observables.OnAny)(value::Any)
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:415
 [52] #invokelatest#2
    @ .\essentials.jl:816 [inlined]
 [53] invokelatest
    @ .\essentials.jl:813 [inlined]
 [54] notify
    @ C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:169 [inlined]
 [55] setindex!(observable::Observable, val::Any)
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:86
 [56] (::Observables.MapCallback)(value::Any)
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:431
 [57] #invokelatest#2
    @ .\essentials.jl:816 [inlined]
 [58] invokelatest
    @ .\essentials.jl:813 [inlined]
 [59] notify
    @ C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:169 [inlined]
 [60] setindex!(observable::Observable, val::Any)       
    @ Observables C:\Users\steve\.julia\packages\Observables\PHGQ8\src\Observables.jl:86
 [61] setproperty!
    @ C:\Users\steve\.julia\packages\Makie\iECbF\src\makielayout\blocks.jl:445 [inlined]
 [62] set!(tb::Textbox, string::String)
    @ Makie C:\Users\steve\.julia\packages\Makie\iECbF\src\makielayout\blocks\textbox.jl:328

Thanks for the working code and steps to reproduce, it helped a lot to understand the issue.

Indeed, same here (julia 1.9.2, GLMakie 0.8.6, in Pluto 0.19.27, on linux).
What seems to matter is the position of the cursor prior to the set!.
If the set! shrinks the string,
so that the cursor would be at an invalid position (outside the new string),
then the error occurs. Otherwise it works just fine.
(the “add a char” step shifted the position of the cursor,
and the next set! string was shorter in your example).

Actually it looks a lot like
Textbox errors depending on cursor position upon submission · Issue #2037 · MakieOrg/Makie.jl · GitHub,
which has been fixed, but might give a hint.

So that looks like a bug, and filing an issue seems relevant.
If you want to help narrow down even more,
maybe you might make the example even more minimal with a single textbox (no array) ?

Thanks for the guidance. You were correct, the issue is dependent on the position of the cursor and only reproduced when the cursor position is less than the number of new chars added from the end of the string.

using GLMakie
fig = Figure(resolution=(500, 500));display(fig)
atextbox =  Textbox(fig)
Makie.set!(atextbox,"a string1")
# set focus on a textbox and add n chars
# do or don't press enter (both reproduce the error)
# setting cursor position less than n chars from the end of the string
# is the only way to reproduce the issue
Makie.set!(atextbox,"a string2")

Reading the previous issue I have a feeling there is a workaround but I don’t have the skill to distill it from all the information. I will create a bug report.

Thanks for your help.
Steve

Thanks for creating the issue Textbox set! fails · Issue #3069 · MakieOrg/Makie.jl · GitHub.
Note to further improve readability:
it is possible to tell github that a block of code is julia and get syntax highlighting) by typing

 ```julia

instead of bare triple quotes.


Reading the previous issue I have a feeling there is a workaround

Indeed, there is a workaround (Makie gives us a lot of control, so it is almost always the case :slight_smile:): define a custom method, e.g.

function custom_set!(tbox::Makie.Textbox, new_text)
	old_text = tbox.displayed_string[]
	old_pos = tbox.cursorindex[]
	if length(new_text) == 0
		# for now setting new_text to "" errors
		# TODO: is resetting the correct thing to do ?
		Makie.reset!(tbox)
	elseif length(new_text) == length(old_text)
		# no length change, cursor is already at the correct position
		Makie.set!(tbox, new_text)
	elseif length(new_text) > length(old_text)
		if old_pos == length(old_text)
			# Let's keep the cursor at the end.
			# Need to add the new text first,
			# to make the new cursor position valid.
			Makie.set!(tbox, new_text)
			tbox.cursorindex[] = length(new_text)
		else
			# cursor was inside the text; keep cursor position
			Makie.set!(tbox, new_text)
		end
	else
		# new text is shorter
		# place the cursor at a future valid position first (not outside new_text)
		tbox.cursorindex[] = min(old_pos, length(new_text))
		Makie.set!(tbox, new_text)
	end
end

and use that instead of Makie.set!, like custom_set!(atextbox, "a string2").

That works perfectly as a workaround, thanks.

1 Like