diff --git a/.gitignore b/.gitignore index fabc59673b602310da3e23d20703e29f6bb3976d..1f13b77fe16a7fd6953beb5d4e13fca2a358fb3e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ *.outline *.spine *.mat +*.label diff --git a/Manifest-v1.11.toml b/Manifest-v1.11.toml index f2d4c37b4ed7ab406a7b3b4bf344e8d1dbbfd2ed..53ceb33cf5341554f9098597cda4465d227dcd00 100644 --- a/Manifest-v1.11.toml +++ b/Manifest-v1.11.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.11.1" +julia_version = "1.11.3" manifest_format = "2.0" -project_hash = "fccf84810b56f9ba8947369bbaf414d318bc47f2" +project_hash = "423cfe5e2b370a6df6f85f87fd1406fd07136176" [[deps.AbstractFFTs]] deps = ["LinearAlgebra"] @@ -985,11 +985,11 @@ version = "1.2.0" [[deps.NyxWidgets]] deps = ["Bonito", "Colors", "Format", "LazyArtifacts", "Observables"] -git-tree-sha1 = "936f80aa61413c47da00f96abbc0186078698bca" +git-tree-sha1 = "e18ab14817871c54419e4cef12f9fc4dc589f6fe" repo-rev = "main" repo-url = "https://gitlab.com/dbc-nyx/NyxWidgets.jl" uuid = "c288fd06-43d3-4b04-8307-797133353e2e" -version = "0.1.1" +version = "0.2.0" [[deps.Observables]] git-tree-sha1 = "7438a59546cf62428fc9d1bc94729146d37a7225" diff --git a/Manifest.toml b/Manifest.toml index d02ed0ce6003ec651f18217bf255430a7306468b..cf255d7a031755ab63cb7eef5552258efa33f301 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,8 +1,8 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.10.6" +julia_version = "1.10.8" manifest_format = "2.0" -project_hash = "0fb14e688a33d3d8ba0bbce1542d1ada113967c7" +project_hash = "029a32b6e21d0f2c52e6dccd0a009e9681bdff18" [[deps.AbstractFFTs]] deps = ["LinearAlgebra"] @@ -971,11 +971,11 @@ version = "1.2.0" [[deps.NyxWidgets]] deps = ["Bonito", "Colors", "Format", "LazyArtifacts", "Observables"] -git-tree-sha1 = "936f80aa61413c47da00f96abbc0186078698bca" +git-tree-sha1 = "e18ab14817871c54419e4cef12f9fc4dc589f6fe" repo-rev = "main" repo-url = "https://gitlab.com/dbc-nyx/NyxWidgets.jl" uuid = "c288fd06-43d3-4b04-8307-797133353e2e" -version = "0.1.1" +version = "0.2.0" [[deps.Observables]] git-tree-sha1 = "7438a59546cf62428fc9d1bc94729146d37a7225" diff --git a/Project.toml b/Project.toml index 64da699db05a973147106bdfa26c67f49c02551a..60848ef53384d300254e240b3260af26052804df 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "LarvaTagger" uuid = "8b3b36f1-dfed-446e-8561-ea19fe966a4d" authors = ["François Laurent", "Institut Pasteur"] -version = "0.19.0" +version = "0.19.1" [deps] Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8" @@ -27,7 +27,7 @@ WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" [compat] Bonito = "< 4.0.0" -NyxWidgets = "0.1.1" +NyxWidgets = ">= 0.2.0" ObservationPolicies = "0.2.4" PlanarLarvae = ">= 0.11.2" TidyObservables = "0.1.1" diff --git a/recipes/Dockerfile b/recipes/Dockerfile index 4489a17ed460154b4c004ea8f3fc2a33409d1a45..11d0e1c7eed52e319119861f568fd68bbf6351e1 100644 --- a/recipes/Dockerfile +++ b/recipes/Dockerfile @@ -1,4 +1,4 @@ -FROM julia:1.10.6-bullseye AS base +FROM julia:1.10.8-bullseye AS base ARG PROJECT_DIR=/app ARG BRANCH=main diff --git a/scripts/larvatagger b/scripts/larvatagger index 5a0a65aed7cc2f537d6e5701784cba00852c3d00..c6007ad05c627fae96f6706abe230f56d1bcdaf0 100755 --- a/scripts/larvatagger +++ b/scripts/larvatagger @@ -52,7 +52,7 @@ import|merge|--version|-V) LarvaTagger Usage: - larvatagger open [<file-path>] [--backends=<path>] [--port=<number>] [--quiet] [--viewer] [--browser] [--view-factor=<real>] [--manual-label=<label>] + larvatagger open [<file-path>] [--backends=<path>] [--port=<number>] [--server-url=<url>] [--quiet] [--viewer] [--browser] [--view-factor=<real>] [--manual-label=<label>] larvatagger import <input-path> [<output-file>] [--id=<id>] [--framerate=<fps>] [--pixelsize=<μm>] [--overrides=<comma-separated-list>] [--default-label=<label>] [--manual-label=<label>] [--decode] [--copy-labels] larvatagger train <backend-path> <data-path> <model-instance> [--pretrained-model=<instance>] [--labels=<comma-separated-list>] [--sample-size=<N>] [--balancing-strategy=<strategy>] [--class-weights=<csv>] [--manual-label=<label>] [--layers=<N>] [--iterations=<N>] [--seed=<seed>] [--debug] larvatagger train <backend-path> <data-path> <model-instance> --fine-tune=<instance> [--balancing-strategy=<strategy>] [--manual-label=<label>] [--iterations=<N>] [--seed=<seed>] [--debug] @@ -71,6 +71,7 @@ Options: --pixelsize=<μm> Camera pixel size, in micrometers. --backends=<path> Path to backend repository. --port=<number> Port number the server listens to. + --server-url=<url> Server address, for remote access. --viewer Disable editing capabilities. --browser Automatically open a browser tab at the served location. --view-factor=<real> Scaling factor for the larva views; default is 2 on macOS, 1 elsewhere. @@ -104,6 +105,8 @@ Commands: The optional positional argument <file-path> can also be the data root directory. Backends defined in LarvaTagger project root directory are automatically found. Other backend locations can be specified with the --backends argument. + By default, if --server-url is specified, the port is appended to the ip address or + domain name. To prevent this behavior, include the desired port number in the url. import Generate a label file and store metadata. diff --git a/src/cli_open.jl b/src/cli_open.jl index 56ab8b563ed874cd35e429b3623872a417399aea..a1d46b678e4e4faf1b2790a1202297a6b354ca0e 100644 --- a/src/cli_open.jl +++ b/src/cli_open.jl @@ -9,7 +9,7 @@ export main usage = """LarvaTagger.jl - launch the server-based GUI. Usage: - larvatagger-gui.jl [<file-path>] [--backends=<path>] [--port=<number>] [--quiet] [--viewer] [--browser] [--view-factor=<real>] [--manual-label=<label>] + larvatagger-gui.jl [<file-path>] [--backends=<path>] [--port=<number>] [--server-url=<url>] [--quiet] [--viewer] [--browser] [--view-factor=<real>] [--manual-label=<label>] larvatagger-gui.jl -h | --help Options: @@ -17,6 +17,7 @@ Options: -q --quiet Do not show instructions. --backends=<path> Path to backend repository. --port=<number> Port number the server listens to. + --server-url=<url> Server address, for remote access. --viewer Disable editing capabilities. --browser Automatically open a browser tab at the served location. --view-factor=<real> Scaling factor for the larva views; default is 2 on macOS, 1 elsewhere. @@ -25,6 +26,9 @@ Options: The optional positional argument <file-path> can also be the data root directory. Backends defined in LarvaTagger project root directory are automatically found. Other backend locations can be specified with the --backends argument. + +By default, if --server-url is specified, the port is appended to the ip address or domain +name. To prevent this behavior, include the desired port number in the url. """ function main(args=ARGS; exit_on_error=false) @@ -82,12 +86,30 @@ function main(args=ARGS; exit_on_error=false) # port = parsed_args["--port"] port = isnothing(port) ? 9284 : parse(Int, port) - server = Server(app, "0.0.0.0", port) + proxy_url = parsed_args["--server-url"] + if isnothing(proxy_url) + proxy_url = "" + elseif !startswith(proxy_url, "http") + proxy_url = "http://$(proxy_url)" + end + server = Server(app, "0.0.0.0", port; proxy_url=proxy_url) + port = server.port + if !isempty(proxy_url) + protocol, remainder = split(proxy_url, "://") + if !(':' in remainder) + server.proxy_url = proxy_url = if '/' in remainder + server_name, path = split(remainder, '/'; limit=2) + "$(protocol)://$(server_name):$(port)/$(path)" + else + "$(protocol)://$(remainder):$(port)" + end + end + end if parsed_args["--browser"] Bonito.HTTPServer.openurl("http://127.0.0.1:$(port)") end if verbose - @info "The server is ready at http://127.0.0.1:$(port)" + @info "The server is ready at $(Bonito.HTTPServer.online_url(server, ""))" end if verbose @info "Press Ctrl+D to stop the server (or Ctrl+C if in PowerShell)" diff --git a/src/controllers.jl b/src/controllers.jl index 0482579159b7448a9deb1759c8bec5c9ae96a2ea..ee7fae7f3158be1114ed09e3e38b01f494871fbc 100644 --- a/src/controllers.jl +++ b/src/controllers.jl @@ -100,36 +100,6 @@ function createtag!(controller, tag::ObservableTag) return true end -function renametag!(controller, tag::ObservableTag, newtag::Union{Nothing, UserTag}=nothing) - tagname = tag.name[] - newtag = @something newtag tagname - if isempty(newtag) - @warn "empty tag name" - return false - elseif newtag != tagname - if exists(tag, controller) - renamefailed!(controller, tag, newtag) - return false - else - @info "Tag \"$(tagname)\" renamed \"$(newtag)\"" - transaction = tagname => newtag - push!(transactions(controller), RenameTag(transaction)) - tagevents(controller).renamed[] = transaction - end - end - return true -end - -function renamefailed!(controller, - current::ObservableTag, - failure::UserTag, - ) - tagname = current.name[] - @info "Renaming tag \"$(tagname)\" failed" - tagevents(controller).renamefailed[] = tagname => failure - notify(gettags(controller)) -end - function activatetag!(controller, tag::ObservableTag) tag.active[] && return false tagname = tag.name[] @@ -161,6 +131,8 @@ function exists(tagname::String, tagsource, lut) return false end +exists(tagsource::ObservableTag, lut) = exists(tagsource.name[], lut) + function assignmentfailed!(controller, reason) taggingevents(controller).assignmentfailed[] = reason end @@ -224,6 +196,15 @@ getplayer(hub::ControllerHub) = haskey(hub, :player) ? hub[:player] : getplayer( gettags(c) = gettags(gethub(c)[:larva]) gettags(c::LarvaController) = c.tag_lut +getmanualtag(c) = getmanualtag(gethub(c)) +function getmanualtag(c::ControllerHub) + tag = get(c, :manualtag, nothing) + if tag isa Symbol + tag = string(tag) + end + return tag +end + # events function hoverlarva!(controller::LarvaController, larva_id::LarvaID) @@ -375,15 +356,22 @@ function setevents!(controller, tagvalidator::ValidatorBundle{ObservableTag}) deactivatetag!(controller, tag) end end + on(modelfailed(tagvalidator, :name)) do (_, failure) + @error "Tag name update failure handled on the model side" tag.name[] failure + # this fails to trigger evaljs + tagevents(controller).renamefailed[] = (tag.name[] => failure) + end on(viewfailed(tagvalidator, :name)) do (_, failure) tagevents(controller).renamefailed[] = (tag.name[] => failure) end - on(tagvalidator, :name) do newname, curname - #renametag!(controller, tag) - transaction = curname => newname - push!(transactions(controller), RenameTag(transaction)) - tagevents(controller).renamed[] = transaction - return Validate + previousname = Ref(tag.name[]) + on(tag.name) do newname + prevname = previousname[] + @assert newname != prevname + @info "Tag \"$(prevname)\" renamed \"$(newname)\"" + transaction = prevname => newname + push!(transactions(controller), RenameTag(transaction)) + previousname[] = newname end onany(tag.active, tag.name, tag.color) do _, _, _ notify(gettags(controller)) diff --git a/src/editor.jl b/src/editor.jl index abf8b403d3f060476ffa83f071b7b1e165e7cbf9..ad40fb617e22a23c656a06f82595d154bbd3fd28 100644 --- a/src/editor.jl +++ b/src/editor.jl @@ -35,11 +35,18 @@ function larvaeditor(path=nothing; backend_directory::AbstractString=projectdir, manualtag::Union{Nothing, String, Symbol}="edited", title="LarvaTagger", + root_directory=nothing, + enable_uploads=false, + enable_downloads=false, + prepare_download=nothing, + enable_new_directories=false, + enable_delete=false, kwargs...) # to (re-)load a file, the app is reloaded with the filepath as sole information # from previous session - input = Ref{Union{Nothing, String}}(path) + T = Ref{Union{Nothing, String}} + input = path isa T ? path : T(path) App(title=title) do session::Session @@ -47,6 +54,12 @@ function larvaeditor(path=nothing; # used for method dispatch (here `Session` is taken to represent Bonito) controller[:frontend] = Session + controller[:manualtag] = manualtag + + if !isnothing(root_directory) + controller[:workingdirectory] = workingdir(controller, root_directory) + end + tryopenfile(controller, input) editor = EditorView(larvaviewer(controller; @@ -54,9 +67,13 @@ function larvaeditor(path=nothing; multipletags=allow_multiple_tags, kwargs...), larvafilter(controller), - tagfilter(controller; manualtag=manualtag), + tagfilter(controller), metadataeditor(controller), - filemenu(controller), + filemenu(controller; upload_button=enable_uploads, + download_button=enable_downloads, + prepare_download=prepare_download, + create_directory_button=enable_new_directories, + delete_button=enable_delete), backendmenu(controller, backend_directory), loadanimation(controller), twooptiondialog(controller)) diff --git a/src/files.jl b/src/files.jl index 397c78160d2df38634a5202d36675cc61b279c54..100d81d8ff00940cd436e091bf046ce385a688b3 100644 --- a/src/files.jl +++ b/src/files.jl @@ -7,7 +7,9 @@ struct WorkingDirectory content::Observable{Vector{String}} end -WorkingDirectory(controller, root::String, path::String="") = WorkingDirectory(controller, root, Validator(path), Observable(String[])) +function WorkingDirectory(controller, root::String, path::String="") + WorkingDirectory(controller, root, Validator(path), Observable(String[])) +end function workingdir(controller, root::String, path::String=""; secure::Bool=true) if secure @@ -107,7 +109,7 @@ function tryopenfile(controller, path; reload::Bool=false) @info "Cannot load file" file=path return elseif haskey(hub, :input) - hub[:input][] = isabspath(path) ? relpath(path) : path + hub[:input][] = path end # tag_lut fallback_color = theme[:LarvaPlot][:fallback_color] @@ -185,8 +187,9 @@ function tryopenfile(controller, path; reload::Bool=false) end secondarytags = records[:secondarytags] if !isnothing(secondarytags) + manualtag = getmanualtag(hub) for tag in secondarytags - original = TagModel(tag, true) + original = TagModel(tag, true; frozen=tag == manualtag) push!(tag_lut, ObservableTag(original; color=fallback_color, active=true)) end end @@ -274,7 +277,7 @@ function savetofile(controller, file; datafile=nothing, merge=false) @assert length(dataset) == 1 run = first(values(dataset)) lut = gettags(controller)[] - manualtag = gethub(controller)[:tag].manualtag + manualtag = getmanualtag(controller) has_explicit_editions = false if !explicit_editions_needed(controller, manualtag) manualtag = nothing @@ -356,6 +359,7 @@ function savetofile(controller, file; datafile=nothing, merge=false) end Datasets.to_json_file(filepath, dataset) Taggers.check_permissions(filepath) + notify(FileBrowsers.workingdir(gethub(controller)[:browser])) end end diff --git a/src/larvatagger.css b/src/larvatagger.css index 0ead1806f7f3b3194136418789a3d16ca34ad755..013b8a4acf974db08bbde37249d883491a754cab 100644 --- a/src/larvatagger.css +++ b/src/larvatagger.css @@ -40,7 +40,7 @@ canvas { .cp-tab .cp-tab-switch:checked ~ .control-panel { display: block; } -.cp-tab .cp-tab-switch:checked ~ .control-panel div { +.cp-tab .cp-tab-switch:checked ~ .control-panel > div { background: var(--theme-main-color); } .control-panel { @@ -63,6 +63,10 @@ canvas { background-color: var(--nyx-icon-fill-color); } +.nyx-filebrowser-entrycontrols { + margin-left: -2.25rem; +} + div.scrollable { position: relative; margin-right: 0; diff --git a/src/models.jl b/src/models.jl index 9910a28cadded6c41161031c487c93d567b6fc9e..6000cc4d287ffed3e46364258149316ce38c9d28 100644 --- a/src/models.jl +++ b/src/models.jl @@ -19,10 +19,22 @@ struct TagModel <: AbstractTag color::OptionalColor active::Union{Nothing, Bool} secondary::Bool + frozen::Bool end -TagModel(name::Name, secondary::Bool=false) = TagModel(name, nothing, nothing, secondary) -TagModel(name::Name, color::OptionalColor, active::Union{Nothing, Bool}) = TagModel(name, color, active, false) +function TagModel(name::Name, secondary::Bool=false; frozen::Bool=false) + TagModel(name, nothing, nothing, secondary, frozen) +end +function TagModel(name::Name, + color::OptionalColor, + active::Union{Nothing, Bool}, + secondary::Bool=false, + ) + TagModel(name, color, active, secondary, false) +end + +isfrozen(tag::TagModel) = tag.frozen +isfrozen(::Nothing) = false struct ObservableTag <: AbstractTag name::AbstractObservable{String} @@ -54,6 +66,8 @@ end ObservableTag() = ObservableTag("", "#000000") +isfrozen(t::ObservableTag) = isfrozen(t.original) + isnative(t::ObservableTag) = !isnothing(t.original) const TagLUT = Vector{ObservableTag} @@ -204,8 +218,9 @@ function TidyObservables.newcontroller(tag::ObservableTag, taglut::AbstractObser on(ctrl, :name) do name, current if isempty(name) Invalidate(; revert=!isempty(current)) + elseif isfrozen(tag) + Invalidate(; revert=name != current) elseif exists(name, tag, taglut) - @info (name, current, lookup(taglut[], name)) Invalidate() else Validate(name) diff --git a/src/plots.jl b/src/plots.jl index 4a76a07e37abd5b8b02ec19cc83924fb95b49504..2ab059ba16498000d7d2f2a6bdc3a2122754302e 100644 --- a/src/plots.jl +++ b/src/plots.jl @@ -450,9 +450,9 @@ function setkeyboardevents!(scene::Scene, controller) on(events(scene).keyboardbutton) do event if event.action == Keyboard.press if event.key in (Keyboard.left, Keyboard.down) - stepbackward!(player) + Players.stepbackward(player) elseif event.key in (Keyboard.right, Keyboard.up) - stepforward!(player) + Players.stepforward(player) end end end @@ -562,7 +562,8 @@ function DecoratedLarvae(larvae::Vector{DecoratedLarva}) @assert hovering_active[] i = current_larva[] if i == j - @warn "moving too fast? - please file an issue at https://gitlab.pasteur.fr/nyx/larvatagger.jl/-/issues" + @warn "Moving too fast?" + return end if 0 < i i_decorated = larvae[i].decorated diff --git a/src/wgl.jl b/src/wgl.jl index 9bdb2a73ed716cd420fa39d7bcd5b54b57b76f2d..fb4640d76c361210a95344674d35ec4f27d736bf 100644 --- a/src/wgl.jl +++ b/src/wgl.jl @@ -1,5 +1,6 @@ getsession(c) = gethub(c)[:session] setsession!(c, session) = (gethub(c)[:session] = session) +using Makie: label_info const r = Bonito.jsrender @@ -431,6 +432,7 @@ mutable struct TagController hub::ControllerHub view fallback_color::String + # deprecation: manualtag is now stored in `hub` as well with key :manualtag manualtag end @@ -451,21 +453,28 @@ function TagController(controller::ControllerHub; manualtag) end -getmanualtag(c) = gethub(c)[:tag].manualtag +#getmanualtag(c::TagController) = c.manualtag newtagname(::TagController) = NoTag +isfrozen(c::TagController, tag) = isfrozen(lookup(gettags(c)[], tag, true)) + struct IndividualTagView controller::TagController tag::ObservableTag # view + frozen::Bool dom_id end -function IndividualTagView(controller::TagController, tag::ObservableTag) - IndividualTagView(controller, tag, dom_id()) +function IndividualTagView(controller::TagController, tag::ObservableTag, frozen::Bool) + IndividualTagView(controller, tag, frozen, dom_id()) +end + +function tagview(controller::TagController, tag::ObservableTag) + IndividualTagView(controller, tag, isfrozen(tag)) end -tagview(controller, tag) = IndividualTagView(controller, tag) +isfrozen(tag::IndividualTagView) = tag.frozen function lowerdom(ti::IndividualTagView) tag = ti.tag @@ -497,8 +506,9 @@ function Bonito.jsrender(session::Session, ti::IndividualTagView) tag = ti.tag selector = dom_selector(ti) * " .tag-name" events = tagevents(ti.controller) - on(events.renamefailed) do (name, failure) - name == tag.name[] || return + on(session, events.renamefailed) do (name, failure) + # tag.name[] == failure || return # if event triggered by model failure + tag.name[] == name || return # if event triggered by view failure @info "Reverting to \"$(name)\" after failure: \"$(failure)\"" evaljs(session, js"document.querySelector($selector).value = $name;") end @@ -542,8 +552,10 @@ function tagfilter(controller; manualtag=nothing) end function lowerdom(tf::TagFilter) + manualtag = getmanualtag(tf.controller) tags = newview!(gethub(tf.controller)[:taghooks]) for tag in reverse(tags) + tag.name[] == manualtag && continue push!(tf.views, tagview(tf.controller, tag)) end return DOM.div(scrollable(tf.views...), @@ -609,49 +621,47 @@ function TagSelector(controller::TagController, multiple::Union{Nothing, Bool}=nothing, ) tag_lut = gettags(controller) - getnames(tags) = [tag.name[] for tag in tags if tag.active[]] - tagnames = Observable(getnames(tag_lut[])) - on(tag_lut) do tags - tagnames[] = getnames(tags) + manualtag = getmanualtag(controller) + tagnames = map(tag_lut) do tags + [tag.name[] for tag in tags if tag.active[] && tag.name[] != manualtag] end registered_observables = Observable{Bool}[] - selectedtags = Observable(Tuple{String, Observable{Bool}}[]) + selectedtags = map(tagnames; ignore_equal_values=true) do names + for i in length(registered_observables)+1:length(names) + newobs = Observable(false) + on(newobs) do b + @debug "Tag $(i) $(b ? "" : "un")activated" + end + push!(registered_observables, newobs) + end + collect(zip(names, registered_observables[1:length(names)])) + end + refresh_view = map(selectedtags) do _ + true + end multitag = filterevents(controller).allow_multiple_tags if !isnothing(multiple) multitag[] = multiple end + fromjs = nothing if selectable fromjs = Observable("") on(fromjs) do actuatedtag for (tag, selected) in selectedtags[] if tag == actuatedtag - toggletag!(controller, tag, selected, selectedtags, selectable) - flag_active_larva_as_edited(controller) + if isfrozen(controller, tag) + @warn "Actuated tag is frozen" actuatedtag + selected[] = !selected[] # this observable reflects the state of the + # UI; the option element is in an improper state, which will be + # undone on the next update of the selector + else + toggletag!(controller, tag, selected, selectedtags, selectable) + flag_active_larva_as_edited(controller) + end break end end end - else - fromjs = nothing - end - ctrl = TagSelector(controller, - tagnames, - selectedtags, - selectable, - isnothing(multiple), - fromjs, - dom_id()) - refresh_view = Observable(false) - on(tagnames) do names - for i in length(registered_observables)+1:length(names) - newobs = Observable(false) - on(newobs) do b - @debug "Tag $(i) $(b ? "" : "un")activated" - end - push!(registered_observables, newobs) - end - selectedtags[] = collect(zip(names, registered_observables[1:length(names)])) - notify(refresh_view) end activelarva = getactivelarva(controller) onany(gettimestep(controller), @@ -685,12 +695,22 @@ function TagSelector(controller::TagController, #notify(refresh_view) # applies only to the tag selector notify(gettimestep(controller)) # full refresh end - return ctrl + return TagSelector(controller, + tagnames, + selectedtags, + selectable, + isnothing(multiple), + fromjs, + dom_id()) end function refresh_selected_tags(controller, id, timestep, taglut, selectedtags, selectable) larva = getlarva(controller, id) tags = getusertags(larva, timestep; lut=taglut) + refresh_selected_tags(controller, tags, selectedtags, selectable) +end + +function refresh_selected_tags(controller, tags::UserTags, selectedtags, selectable) tagnames = [tag.name[] for tag in tags] jscode = String[] for (name, selected) in Observables.to_value(selectedtags) @@ -741,32 +761,31 @@ function refresh_selected_tag(controller, end function toggletag!(controller, tagname, selected, selectedtags, selectable) - selected[] = !selected[] multitag = filterevents(controller).allow_multiple_tags[] - multitag || selected[] || @debug "Deselecting a tag in a single tag setting" - # larva_id = getactivelarva(controller)[] larva = getlarva(controller, larva_id) timestep = gettimestep(controller)[] - taglut = gettags(controller)[] + taglut = gettags(controller)[] # should not be mutated here, therefore we use the model + # instead of the view; otherwise use the view gethub(controller)[:taghooks] tags = getusertags!(larva, timestep; lut=taglut) tag = lookup(taglut, tagname) isnothing(tag) && throw(KeyError(tagname)) - # - if selected[] + # toggle + selected′= !selected[] + transaction = if selected′ if multitag || isempty(tags) - transaction = AddTag(larva_id, timestep, tagname) settag!(tags, tag) + refresh_selected_tags(controller, tags, selectedtags, selectable) + AddTag(larva_id, timestep, tagname) else - transaction = OverrideTags(larva_id, timestep, tagname, collect(tags)) settag!(tags, tag; single=true) - # refresh_selected_tag(controller, tag, selectedtags, selectable) + OverrideTags(larva_id, timestep, tagname, collect(tags)) end else @assert !isempty(tags) - transaction = RemoveTag(larva_id, timestep, tagname) deletetag!(tags, tag) + RemoveTag(larva_id, timestep, tagname) end push!(transactions(controller), transaction) notify(larva.usertags) @@ -808,11 +827,7 @@ function Bonito.jsrender(session::Session, ts::TagSelector) if ts.selectable evaljs(session, js"LarvaTagger.setTagSelector($(ts.fromjs))") end - selectedtags = ts.selected - prevtags = Ref(empty(selectedtags[])) - on(session, selectedtags) do selected - selected == prevtags[] && return - prevtags[] = selected + on(session, ts.selected) do selected jsdom = r(session, lowertags(selected, ts.selectable)) htmldom = replace(string(jsdom), "'" => "\\'") evaljs(session, js""" @@ -831,7 +846,8 @@ function tagselector(controller; multipletags::Union{Nothing, Bool}=nothing, ) c = gethub(controller) - TagSelector(haskey(c, :tag) ? c[:tag] : TagController(c), editabletags, multipletags) + controller = haskey(c, :tag) ? c[:tag] : TagController(c) + TagSelector(controller, editabletags, multipletags) end mutable struct TrackViewer @@ -902,10 +918,17 @@ struct FileMenu browser::FileBrowser end -function filemenu(controller; kwargs...) +function filemenu(controller; upload_button=false, download_button=false, + prepare_download=nothing, create_directory_button=false, delete_button=false, + kwargs...) wd = getworkingdir(controller) dir = joinpath(wd.root, wd.path[]) - browser = FileBrowser(dir; root=wd.root, upload_button=false) + browser = FileBrowser(dir; root=wd.root, upload_button=upload_button, + download_button=download_button, + prepare_download=prepare_download, + create_directory_button=create_directory_button, + delete_button=delete_button) + gethub(controller)[:browser] = browser.model on(FileBrowsers.selectedfile(browser)) do file tryopenfile(controller, file; reload=true) end @@ -920,9 +943,10 @@ function filemenu(controller; kwargs...) on(FilePickers.uploadedfile(browser)) do fileinfo filepath = joinpath(workingdir[], fileinfo["name"]) saveinputfile(filepath, fileinfo["content"]) + notify(workingdir) end end - return FileMenu(gethub(controller), browser) + return FileMenu(controller, browser) end function Bonito.jsrender(session::Session, fm::FileMenu)