From 7a8b582a4a8ca48127d733f0c8de31d05d67de29 Mon Sep 17 00:00:00 2001 From: jwikmark <jacob@wikmark.se> Date: Mon, 26 Feb 2018 17:19:29 +0100 Subject: [PATCH] Added files --- src/LabGUI.jl | 34 +++++++++- src/construct.jl | 52 +++++++++++++++ src/gridmaker.jl | 50 ++++++++++++++ src/gui.jl | 165 ++++++++++++++++++++++++++++++++++++++++++++++ src/svg_helper.jl | 66 +++++++++++++++++++ 5 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 src/construct.jl create mode 100644 src/gridmaker.jl create mode 100644 src/gui.jl create mode 100644 src/svg_helper.jl diff --git a/src/LabGUI.jl b/src/LabGUI.jl index f9ab804..ffb52ed 100644 --- a/src/LabGUI.jl +++ b/src/LabGUI.jl @@ -1,5 +1,35 @@ module LabGUI +using WebIO, InteractNext +include("gridmaker.jl") +include("gui.jl") +include("construct.jl") +include("svg_helper.jl") -# package code goes here -end # module +#We redefine WebIO.newid to prevent nodes from getting infinitely high values, +# and Base.show is redefined to prevent memory leaks when constructing SVGs +const max_nodes = 100000 +let count=1 + global newid + node_space=Dict() + function WebIO.newid(prefix) + if count == max_nodes + count = 0 + end + string(prefix, "-", count+=1) + end +end + +function Base.show(io::IO, m::MIME"text/html", x::Node) + if haskey(x.props, :id) + id = "$(x.props[:id])-parent" + else + id = WebIO.newid("node") + end + x in keys(WebIO.showcbs) && (x = WebIO.showcbs[x](id)) + write(io, """<div id=$id></div> + <script>WebIO.mount('$id', '#$id',""") + WebIO.jsexpr(io, x) + write(io, ")</script>") +end +end diff --git a/src/construct.jl b/src/construct.jl new file mode 100644 index 0000000..2e0315f --- /dev/null +++ b/src/construct.jl @@ -0,0 +1,52 @@ +export @construct + +function make_widget(binding) + if binding.head != :(=) + error("@construct syntax error.") + end + sym, expr = binding.args + Expr(:(=), esc(sym), + Expr(:call, widget, esc(expr), string(sym))) +end + +function display_widgets(widgetvars) + map(v -> Expr(:call, esc(:display), esc(v)), widgetvars) +end + +function map_block(block, symbols) + lambda = Expr(:(->), Expr(:tuple, symbols...), + block) + :(map($lambda, $(map(s->:(obs($s)), symbols)...))) +end + +function symbols(bindings) + map(x->x.args[1], bindings) +end + +macro construct(expr) + if expr.head != :for + error("@construct syntax is @construct for ", + " [<variable>=<domain>,]... <expression> end") + end + block = expr.args[2] + if expr.args[1].head == :block + bindings = expr.args[1].args + else + bindings = [expr.args[1]] + end + syms = symbols(bindings) + + widgets = map(make_widget, bindings) + dict = Dict(zip(syms, 1:length(syms))) + quote + (Widget_Container([$(widgets...)], $(dict)), WebIO.render($(esc(map_block(block, syms))))) + end +end + +widget(x::Range, label="") = slider(x; label=label) +widget(x::Observable, label="") = x +widget(x::WebIO.Node{<:Any}, label="") = x +widget(x::AbstractVector, label="") = togglebuttons(x, label=label) +widget(x::Associative, label="") = togglebuttons(x, label=label) +widget(x::Bool, label="") = checkbox(x, label=label) +widget(x::AbstractString, label="") = textbox(x, label=label, typ=AbstractString) diff --git a/src/gridmaker.jl b/src/gridmaker.jl new file mode 100644 index 0000000..7a6d24f --- /dev/null +++ b/src/gridmaker.jl @@ -0,0 +1,50 @@ +export make_row, make_grid, setindex_ + +function make_row(columns::Integer) + #Returns tuple, needs to be Node(:div, tuple..) before use + style = Dict(:display => "inline-table", + :verticalAlign => "top", + :width => "$(100/columns)%") + #:transform => "scale($(1/columns))") + (Node(:div, style=style, "Lorem Ipsum") for i in 1:columns) +end + +function make_grid(rows::Integer, columns::Integer, width = 800, height = 1000) + style_row = Dict(:height => "$(100/rows)%", :overflow => "auto") + style_grid = Dict(:width => width, :height => height) + Node(:div, style=style_grid, (Node(:div, style=style_row, make_row(columns)...) + for i in 1:rows)...) +end + +#specify this later, should be some sort of typedef of WebIO.Node{WebIO.DOM} + + +# We give the content of the container if given two arguments +# If given one argument, we give the child +# This means a[r,c] = a[r][c][1] and not a[r][c] +# We to do this to keep the style (width and formatting of the columns) + +function Base.getindex(grid::WebIO.Node{WebIO.DOM}, ind::Integer) + grid.children[ind] +end + +function Base.getindex(grid::WebIO.Node{WebIO.DOM}, row::Integer, column::Integer) + grid[row][column][1] +end + + + +function setindex_(grid::WebIO.Node{WebIO.DOM}, + new_value, + ind::Integer) + #Make this look nicer + setchild(grid, ind, setchild(grid[ind], 1, Node(:div, new_value))) +end + +function setindex_(grid::WebIO.Node{WebIO.DOM}, + new_value, + row::Integer, + column::Integer) + #Make this look nicer + setchild(grid, row, setindex_(grid[row], new_value, column)) +end diff --git a/src/gui.jl b/src/gui.jl new file mode 100644 index 0000000..123d9d3 --- /dev/null +++ b/src/gui.jl @@ -0,0 +1,165 @@ +export Widget_Container, GUI, set!, add!, animate + +mutable struct Widget_Container + #Widgets with symbols + widgets::Array{WebIO.Node{WebIO.DOM},1} + vals::Dict +end + +function Widget_Container() + Widget_Container([], Dict()) +end + +function getindex_(widget::Widget_Container, s::Symbol) + #returns the index of the widget associated with symbol + widget.vals[s] +end + +function Base.getindex(widget::Widget_Container, n::Integer) + widget.widgets[n] +end + +function Base.getindex(widget::Widget_Container, s::Symbol) + #returns the widget associated with symbol + widget.widgets[getindex_(widget, s)] +end + +mutable struct GUI + widgets::Widget_Container + dom::WebIO.Node + data::Array{Array} + values::Array + #values contains the values of the widget observables, but isn't automatically updated +end + +function GUI(widgets::Widget_Container, dom::WebIO.Node) + GUI(widgets, dom, [[]], []) +end + +function GUI(n::Integer) + GUI(Widget_Container(), Node(:div), [Float64[] for i in 1:n], []) +end + +function GUI() + GUI(10) +end + +function set!(gui::GUI, widgets::Widget_Container) + gui.widgets = widgets + gui.values = [obs(widget).val for widget in gui.widgets.widgets] +end + +function set!(gui::GUI, dom::WebIO.Node) + gui.dom = dom +end + +function set!(gui::GUI, data::Array) + gui.data = data +end + +function Base.push!(gui::GUI, value, index::Integer) + Base.push!(gui.data[index], value) +end + +function Base.push!(gui::GUI, values::Tuple, indices::Tuple) + foreach(zip(values, indices)) do t + Base.push!(gui.data[t[2]], t[1]) + end +end + +function Base.getindex(gui::GUI, s::Symbol) + obs(gui.widgets[s])[] +end + +function Base.setindex!(gui::GUI, newval, s::Symbol) + obs(gui.widgets[s])[] = newval +end + +function add!(gui::GUI, widgets::Widget_Container) + newdict = copy(widgets.vals) + for key in keys(newdict) + newdict[key] += length(gui.widgets.widgets) + end + gui.widgets.widgets = vcat(gui.widgets.widgets, widgets.widgets) + gui.widgets.vals = merge(gui.widgets.vals, newdict) + gui.values = vcat(gui.values, [obs(w)[] for w in widgets.widgets]) +end + + +function heartbeat(f, t, n=0) + if n!=0 + @async for i in 1:n + t0 = time() + f() + tdif = time()-t0 + sleep(max(0, t-tdif)) + end + else + @async while true + t0 = time() + f() + tdif = time()-t0 + sleep(max(0,t-tdif)) + end + + end +end + +function has_changed(gui::GUI, s::Symbol) + if gui[s] == gui.values[getindex_(gui.widgets, s)] + return false + else + gui.values[getindex_(gui.widgets, s)] = gui[s] + return true + end +end + +function has_changed(gui::GUI, s::Array{Symbol, 1}) + !all(.!(has_changed.(gui, s))) +end + +# Usage: animate(gui, t) will periodically update all widgets, that is, call the +# listeners of all observables associated with widgets in the gui +# animate(gui, s::symbol(s)) will periodically update the widget(s) associated with s +# animate(gui, s::symbol(s), w::symbol(s)) will periodically update the widget(s) associated +# with s only if the widget(s) associated with w has/have changed + +# Note: Widgets will stop working after a while if their associated graphic isn't +# in the responder. This seems to be due to WebIO. + +function animate(gui::GUI, t=0.5) + ks = [k for k in keys(gui.widgets.vals)] + f = updater_function(gui, ks) + heartbeat(f, t) +end + +function animate(gui::GUI, s::Union{Symbol, Array}, t=0.5) + f = updater_function(gui, s) + heartbeat(f, t) +end + +function animate(gui::GUI, s::Union{Symbol, Array}, w::Union{Symbol, Array}, t=0.1) + f = updater_function(gui, s, w) + heartbeat(f, t) +end + +function update(gui::GUI, s::Symbol, watchsymbol=:0) + # s is the symbol associated with widget we wish to update + # if given a watchsymbol, it will only update the widget associated with s + # if the value of the widget associated with watchsymbol has changed + w = gui.widgets[s] + if watchsymbol==:0||has_changed(gui, watchsymbol) + obs(w).listeners[2](obs(w)[]) + end +end +function update(gui::GUI, a::Array, watchsymbol=:0) + W = [gui.widgets[s] for s in a] + if watchsymbol==:0||has_changed(gui, watchsymbol) + [obs(w).listeners[2](obs(w)[]) for w in W] + end +end + +function updater_function(gui::GUI, s::Union{Symbol, Array}, w=:0) + ()->update(gui::GUI, s, w) +end + diff --git a/src/svg_helper.jl b/src/svg_helper.jl new file mode 100644 index 0000000..60522e0 --- /dev/null +++ b/src/svg_helper.jl @@ -0,0 +1,66 @@ +#Provides some routines for easier svg construction, along with some +#(hopefully!) sane defaults + +export svg_rect, svg_circle, svg_line, svg_polyline, svg_polygon, default_height, default_width, default_posy, default_posx, s_black, s_red, s_green, s_blue, stdstyle, stdattr, svgsvg_symb +global const default_height = 900 +global const default_width = 600 +global const default_posy = 0 +global const default_posx = 0 + +global const s_black = (0,0,0) +global const s_red = (255,0,0) +global const s_green = (0,255,0) +global const s_blue = (0,0,255) + +global const stdattr = Dict("y" => "$default_posy", + "x" => "$default_posx", + "height" => "$default_height", + "width" => "$default_width") + +global const stdstyle = Dict(:fill => "rgb$s_black") + +global const svgsvg_symb = instanceof(dom"svg:svg"()) + +#################### + +function svg_rect(width, height, x=0, y=0, style = stdstyle) + attr = Dict("x" => "$x", + "y" => "$y", + "width" => "$width", + "height" => "$height") + Node(instanceof(dom"svg:rect"()), style = style, attributes = attr) +end + +function svg_circle(x, y, r, style=stdstyle) + attr = Dict("cy" => "$y", + "cx" => "$x", + "r" => "$r") + Node(instanceof(dom"svg:circle"()), style = style, attributes = attr) +end + +function svg_line(x1, y1, x2, y2, style=stdstyle) + attr = Dict("x1" => "$x1", + "y1" => "$y1", + "x2" => "$x2", + "y2" => "$y2") + Node(instanceof(dom"svg:line"()), style = style, attributes = attr) +end + +function svg_poly(ptsvector, style) + #there must be a better way to do this + pts = join((join((ptsvector[i][1], ptsvector[i][2]), ",") for i in 1:length(ptsvector)), " ") + attr = Dict("points" => pts) + Node(instanceof(dom"svg:polyline"()), style = style, attributes = attr) +end + +function svg_polyline(ptsvector, thickness, color) + style = Dict(:strokeWidth => "$thickness", + :fill => "none", + :stroke => "rgb$color") + svg_poly(ptsvector, style) +end + +function svg_polygon(ptsvector, color) + style = Dict( :fill => "rgb$color") + svg_poly(ptsvector, style) +end -- GitLab