Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • nyx/larvatagger.jl
1 result
Show changes
Commits on Source (7)
...@@ -7,3 +7,4 @@ ...@@ -7,3 +7,4 @@
*.outline *.outline
*.spine *.spine
*.mat *.mat
*.label
# This file is machine-generated - editing it directly is not advised # 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" manifest_format = "2.0"
project_hash = "fccf84810b56f9ba8947369bbaf414d318bc47f2" project_hash = "423cfe5e2b370a6df6f85f87fd1406fd07136176"
[[deps.AbstractFFTs]] [[deps.AbstractFFTs]]
deps = ["LinearAlgebra"] deps = ["LinearAlgebra"]
...@@ -985,11 +985,11 @@ version = "1.2.0" ...@@ -985,11 +985,11 @@ version = "1.2.0"
[[deps.NyxWidgets]] [[deps.NyxWidgets]]
deps = ["Bonito", "Colors", "Format", "LazyArtifacts", "Observables"] deps = ["Bonito", "Colors", "Format", "LazyArtifacts", "Observables"]
git-tree-sha1 = "936f80aa61413c47da00f96abbc0186078698bca" git-tree-sha1 = "e18ab14817871c54419e4cef12f9fc4dc589f6fe"
repo-rev = "main" repo-rev = "main"
repo-url = "https://gitlab.com/dbc-nyx/NyxWidgets.jl" repo-url = "https://gitlab.com/dbc-nyx/NyxWidgets.jl"
uuid = "c288fd06-43d3-4b04-8307-797133353e2e" uuid = "c288fd06-43d3-4b04-8307-797133353e2e"
version = "0.1.1" version = "0.2.0"
[[deps.Observables]] [[deps.Observables]]
git-tree-sha1 = "7438a59546cf62428fc9d1bc94729146d37a7225" git-tree-sha1 = "7438a59546cf62428fc9d1bc94729146d37a7225"
......
# This file is machine-generated - editing it directly is not advised # 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" manifest_format = "2.0"
project_hash = "0fb14e688a33d3d8ba0bbce1542d1ada113967c7" project_hash = "029a32b6e21d0f2c52e6dccd0a009e9681bdff18"
[[deps.AbstractFFTs]] [[deps.AbstractFFTs]]
deps = ["LinearAlgebra"] deps = ["LinearAlgebra"]
...@@ -971,11 +971,11 @@ version = "1.2.0" ...@@ -971,11 +971,11 @@ version = "1.2.0"
[[deps.NyxWidgets]] [[deps.NyxWidgets]]
deps = ["Bonito", "Colors", "Format", "LazyArtifacts", "Observables"] deps = ["Bonito", "Colors", "Format", "LazyArtifacts", "Observables"]
git-tree-sha1 = "936f80aa61413c47da00f96abbc0186078698bca" git-tree-sha1 = "e18ab14817871c54419e4cef12f9fc4dc589f6fe"
repo-rev = "main" repo-rev = "main"
repo-url = "https://gitlab.com/dbc-nyx/NyxWidgets.jl" repo-url = "https://gitlab.com/dbc-nyx/NyxWidgets.jl"
uuid = "c288fd06-43d3-4b04-8307-797133353e2e" uuid = "c288fd06-43d3-4b04-8307-797133353e2e"
version = "0.1.1" version = "0.2.0"
[[deps.Observables]] [[deps.Observables]]
git-tree-sha1 = "7438a59546cf62428fc9d1bc94729146d37a7225" git-tree-sha1 = "7438a59546cf62428fc9d1bc94729146d37a7225"
......
name = "LarvaTagger" name = "LarvaTagger"
uuid = "8b3b36f1-dfed-446e-8561-ea19fe966a4d" uuid = "8b3b36f1-dfed-446e-8561-ea19fe966a4d"
authors = ["François Laurent", "Institut Pasteur"] authors = ["François Laurent", "Institut Pasteur"]
version = "0.19.0" version = "0.19.1"
[deps] [deps]
Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8" Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8"
...@@ -27,7 +27,7 @@ WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008" ...@@ -27,7 +27,7 @@ WGLMakie = "276b4fcb-3e11-5398-bf8b-a0c2d153d008"
[compat] [compat]
Bonito = "< 4.0.0" Bonito = "< 4.0.0"
NyxWidgets = "0.1.1" NyxWidgets = ">= 0.2.0"
ObservationPolicies = "0.2.4" ObservationPolicies = "0.2.4"
PlanarLarvae = ">= 0.11.2" PlanarLarvae = ">= 0.11.2"
TidyObservables = "0.1.1" TidyObservables = "0.1.1"
......
FROM julia:1.10.6-bullseye AS base FROM julia:1.10.8-bullseye AS base
ARG PROJECT_DIR=/app ARG PROJECT_DIR=/app
ARG BRANCH=main ARG BRANCH=main
......
...@@ -52,7 +52,7 @@ import|merge|--version|-V) ...@@ -52,7 +52,7 @@ import|merge|--version|-V)
LarvaTagger LarvaTagger
Usage: 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 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> [--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] 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: ...@@ -71,6 +71,7 @@ Options:
--pixelsize=<μm> Camera pixel size, in micrometers. --pixelsize=<μm> Camera pixel size, in micrometers.
--backends=<path> Path to backend repository. --backends=<path> Path to backend repository.
--port=<number> Port number the server listens to. --port=<number> Port number the server listens to.
--server-url=<url> Server address, for remote access.
--viewer Disable editing capabilities. --viewer Disable editing capabilities.
--browser Automatically open a browser tab at the served location. --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. --view-factor=<real> Scaling factor for the larva views; default is 2 on macOS, 1 elsewhere.
...@@ -104,6 +105,8 @@ Commands: ...@@ -104,6 +105,8 @@ Commands:
The optional positional argument <file-path> can also be the data root directory. The optional positional argument <file-path> can also be the data root directory.
Backends defined in LarvaTagger project root directory are automatically found. Other Backends defined in LarvaTagger project root directory are automatically found. Other
backend locations can be specified with the --backends argument. 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. import Generate a label file and store metadata.
......
...@@ -9,7 +9,7 @@ export main ...@@ -9,7 +9,7 @@ export main
usage = """LarvaTagger.jl - launch the server-based GUI. usage = """LarvaTagger.jl - launch the server-based GUI.
Usage: 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 larvatagger-gui.jl -h | --help
Options: Options:
...@@ -17,6 +17,7 @@ Options: ...@@ -17,6 +17,7 @@ Options:
-q --quiet Do not show instructions. -q --quiet Do not show instructions.
--backends=<path> Path to backend repository. --backends=<path> Path to backend repository.
--port=<number> Port number the server listens to. --port=<number> Port number the server listens to.
--server-url=<url> Server address, for remote access.
--viewer Disable editing capabilities. --viewer Disable editing capabilities.
--browser Automatically open a browser tab at the served location. --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. --view-factor=<real> Scaling factor for the larva views; default is 2 on macOS, 1 elsewhere.
...@@ -25,6 +26,9 @@ Options: ...@@ -25,6 +26,9 @@ Options:
The optional positional argument <file-path> can also be the data root directory. The optional positional argument <file-path> can also be the data root directory.
Backends defined in LarvaTagger project root directory are automatically found. Other Backends defined in LarvaTagger project root directory are automatically found. Other
backend locations can be specified with the --backends argument. 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) function main(args=ARGS; exit_on_error=false)
...@@ -82,12 +86,30 @@ 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 = parsed_args["--port"]
port = isnothing(port) ? 9284 : parse(Int, 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"] if parsed_args["--browser"]
Bonito.HTTPServer.openurl("http://127.0.0.1:$(port)") Bonito.HTTPServer.openurl("http://127.0.0.1:$(port)")
end end
if verbose 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 end
if verbose if verbose
@info "Press Ctrl+D to stop the server (or Ctrl+C if in PowerShell)" @info "Press Ctrl+D to stop the server (or Ctrl+C if in PowerShell)"
......
...@@ -100,36 +100,6 @@ function createtag!(controller, tag::ObservableTag) ...@@ -100,36 +100,6 @@ function createtag!(controller, tag::ObservableTag)
return true return true
end 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) function activatetag!(controller, tag::ObservableTag)
tag.active[] && return false tag.active[] && return false
tagname = tag.name[] tagname = tag.name[]
...@@ -161,6 +131,8 @@ function exists(tagname::String, tagsource, lut) ...@@ -161,6 +131,8 @@ function exists(tagname::String, tagsource, lut)
return false return false
end end
exists(tagsource::ObservableTag, lut) = exists(tagsource.name[], lut)
function assignmentfailed!(controller, reason) function assignmentfailed!(controller, reason)
taggingevents(controller).assignmentfailed[] = reason taggingevents(controller).assignmentfailed[] = reason
end end
...@@ -224,6 +196,15 @@ getplayer(hub::ControllerHub) = haskey(hub, :player) ? hub[:player] : getplayer( ...@@ -224,6 +196,15 @@ getplayer(hub::ControllerHub) = haskey(hub, :player) ? hub[:player] : getplayer(
gettags(c) = gettags(gethub(c)[:larva]) gettags(c) = gettags(gethub(c)[:larva])
gettags(c::LarvaController) = c.tag_lut 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 # events
function hoverlarva!(controller::LarvaController, larva_id::LarvaID) function hoverlarva!(controller::LarvaController, larva_id::LarvaID)
...@@ -375,15 +356,22 @@ function setevents!(controller, tagvalidator::ValidatorBundle{ObservableTag}) ...@@ -375,15 +356,22 @@ function setevents!(controller, tagvalidator::ValidatorBundle{ObservableTag})
deactivatetag!(controller, tag) deactivatetag!(controller, tag)
end end
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) on(viewfailed(tagvalidator, :name)) do (_, failure)
tagevents(controller).renamefailed[] = (tag.name[] => failure) tagevents(controller).renamefailed[] = (tag.name[] => failure)
end end
on(tagvalidator, :name) do newname, curname previousname = Ref(tag.name[])
#renametag!(controller, tag) on(tag.name) do newname
transaction = curname => newname prevname = previousname[]
push!(transactions(controller), RenameTag(transaction)) @assert newname != prevname
tagevents(controller).renamed[] = transaction @info "Tag \"$(prevname)\" renamed \"$(newname)\""
return Validate transaction = prevname => newname
push!(transactions(controller), RenameTag(transaction))
previousname[] = newname
end end
onany(tag.active, tag.name, tag.color) do _, _, _ onany(tag.active, tag.name, tag.color) do _, _, _
notify(gettags(controller)) notify(gettags(controller))
......
...@@ -35,11 +35,18 @@ function larvaeditor(path=nothing; ...@@ -35,11 +35,18 @@ function larvaeditor(path=nothing;
backend_directory::AbstractString=projectdir, backend_directory::AbstractString=projectdir,
manualtag::Union{Nothing, String, Symbol}="edited", manualtag::Union{Nothing, String, Symbol}="edited",
title="LarvaTagger", title="LarvaTagger",
root_directory=nothing,
enable_uploads=false,
enable_downloads=false,
prepare_download=nothing,
enable_new_directories=false,
enable_delete=false,
kwargs...) kwargs...)
# to (re-)load a file, the app is reloaded with the filepath as sole information # to (re-)load a file, the app is reloaded with the filepath as sole information
# from previous session # 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 App(title=title) do session::Session
...@@ -47,6 +54,12 @@ function larvaeditor(path=nothing; ...@@ -47,6 +54,12 @@ function larvaeditor(path=nothing;
# used for method dispatch (here `Session` is taken to represent Bonito) # used for method dispatch (here `Session` is taken to represent Bonito)
controller[:frontend] = Session controller[:frontend] = Session
controller[:manualtag] = manualtag
if !isnothing(root_directory)
controller[:workingdirectory] = workingdir(controller, root_directory)
end
tryopenfile(controller, input) tryopenfile(controller, input)
editor = EditorView(larvaviewer(controller; editor = EditorView(larvaviewer(controller;
...@@ -54,9 +67,13 @@ function larvaeditor(path=nothing; ...@@ -54,9 +67,13 @@ function larvaeditor(path=nothing;
multipletags=allow_multiple_tags, multipletags=allow_multiple_tags,
kwargs...), kwargs...),
larvafilter(controller), larvafilter(controller),
tagfilter(controller; manualtag=manualtag), tagfilter(controller),
metadataeditor(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), backendmenu(controller, backend_directory),
loadanimation(controller), loadanimation(controller),
twooptiondialog(controller)) twooptiondialog(controller))
......
...@@ -7,7 +7,9 @@ struct WorkingDirectory ...@@ -7,7 +7,9 @@ struct WorkingDirectory
content::Observable{Vector{String}} content::Observable{Vector{String}}
end 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) function workingdir(controller, root::String, path::String=""; secure::Bool=true)
if secure if secure
...@@ -107,7 +109,7 @@ function tryopenfile(controller, path; reload::Bool=false) ...@@ -107,7 +109,7 @@ function tryopenfile(controller, path; reload::Bool=false)
@info "Cannot load file" file=path @info "Cannot load file" file=path
return return
elseif haskey(hub, :input) elseif haskey(hub, :input)
hub[:input][] = isabspath(path) ? relpath(path) : path hub[:input][] = path
end end
# tag_lut # tag_lut
fallback_color = theme[:LarvaPlot][:fallback_color] fallback_color = theme[:LarvaPlot][:fallback_color]
...@@ -185,8 +187,9 @@ function tryopenfile(controller, path; reload::Bool=false) ...@@ -185,8 +187,9 @@ function tryopenfile(controller, path; reload::Bool=false)
end end
secondarytags = records[:secondarytags] secondarytags = records[:secondarytags]
if !isnothing(secondarytags) if !isnothing(secondarytags)
manualtag = getmanualtag(hub)
for tag in secondarytags 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)) push!(tag_lut, ObservableTag(original; color=fallback_color, active=true))
end end
end end
...@@ -274,7 +277,7 @@ function savetofile(controller, file; datafile=nothing, merge=false) ...@@ -274,7 +277,7 @@ function savetofile(controller, file; datafile=nothing, merge=false)
@assert length(dataset) == 1 @assert length(dataset) == 1
run = first(values(dataset)) run = first(values(dataset))
lut = gettags(controller)[] lut = gettags(controller)[]
manualtag = gethub(controller)[:tag].manualtag manualtag = getmanualtag(controller)
has_explicit_editions = false has_explicit_editions = false
if !explicit_editions_needed(controller, manualtag) if !explicit_editions_needed(controller, manualtag)
manualtag = nothing manualtag = nothing
...@@ -356,6 +359,7 @@ function savetofile(controller, file; datafile=nothing, merge=false) ...@@ -356,6 +359,7 @@ function savetofile(controller, file; datafile=nothing, merge=false)
end end
Datasets.to_json_file(filepath, dataset) Datasets.to_json_file(filepath, dataset)
Taggers.check_permissions(filepath) Taggers.check_permissions(filepath)
notify(FileBrowsers.workingdir(gethub(controller)[:browser]))
end end
end end
......
...@@ -40,7 +40,7 @@ canvas { ...@@ -40,7 +40,7 @@ canvas {
.cp-tab .cp-tab-switch:checked ~ .control-panel { .cp-tab .cp-tab-switch:checked ~ .control-panel {
display: block; 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); background: var(--theme-main-color);
} }
.control-panel { .control-panel {
...@@ -63,6 +63,10 @@ canvas { ...@@ -63,6 +63,10 @@ canvas {
background-color: var(--nyx-icon-fill-color); background-color: var(--nyx-icon-fill-color);
} }
.nyx-filebrowser-entrycontrols {
margin-left: -2.25rem;
}
div.scrollable { div.scrollable {
position: relative; position: relative;
margin-right: 0; margin-right: 0;
......
...@@ -19,10 +19,22 @@ struct TagModel <: AbstractTag ...@@ -19,10 +19,22 @@ struct TagModel <: AbstractTag
color::OptionalColor color::OptionalColor
active::Union{Nothing, Bool} active::Union{Nothing, Bool}
secondary::Bool secondary::Bool
frozen::Bool
end end
TagModel(name::Name, secondary::Bool=false) = TagModel(name, nothing, nothing, secondary) function TagModel(name::Name, secondary::Bool=false; frozen::Bool=false)
TagModel(name::Name, color::OptionalColor, active::Union{Nothing, Bool}) = TagModel(name, color, active, 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 struct ObservableTag <: AbstractTag
name::AbstractObservable{String} name::AbstractObservable{String}
...@@ -54,6 +66,8 @@ end ...@@ -54,6 +66,8 @@ end
ObservableTag() = ObservableTag("", "#000000") ObservableTag() = ObservableTag("", "#000000")
isfrozen(t::ObservableTag) = isfrozen(t.original)
isnative(t::ObservableTag) = !isnothing(t.original) isnative(t::ObservableTag) = !isnothing(t.original)
const TagLUT = Vector{ObservableTag} const TagLUT = Vector{ObservableTag}
...@@ -204,8 +218,9 @@ function TidyObservables.newcontroller(tag::ObservableTag, taglut::AbstractObser ...@@ -204,8 +218,9 @@ function TidyObservables.newcontroller(tag::ObservableTag, taglut::AbstractObser
on(ctrl, :name) do name, current on(ctrl, :name) do name, current
if isempty(name) if isempty(name)
Invalidate(; revert=!isempty(current)) Invalidate(; revert=!isempty(current))
elseif isfrozen(tag)
Invalidate(; revert=name != current)
elseif exists(name, tag, taglut) elseif exists(name, tag, taglut)
@info (name, current, lookup(taglut[], name))
Invalidate() Invalidate()
else else
Validate(name) Validate(name)
......
...@@ -450,9 +450,9 @@ function setkeyboardevents!(scene::Scene, controller) ...@@ -450,9 +450,9 @@ function setkeyboardevents!(scene::Scene, controller)
on(events(scene).keyboardbutton) do event on(events(scene).keyboardbutton) do event
if event.action == Keyboard.press if event.action == Keyboard.press
if event.key in (Keyboard.left, Keyboard.down) if event.key in (Keyboard.left, Keyboard.down)
stepbackward!(player) Players.stepbackward(player)
elseif event.key in (Keyboard.right, Keyboard.up) elseif event.key in (Keyboard.right, Keyboard.up)
stepforward!(player) Players.stepforward(player)
end end
end end
end end
...@@ -562,7 +562,8 @@ function DecoratedLarvae(larvae::Vector{DecoratedLarva}) ...@@ -562,7 +562,8 @@ function DecoratedLarvae(larvae::Vector{DecoratedLarva})
@assert hovering_active[] @assert hovering_active[]
i = current_larva[] i = current_larva[]
if i == j 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 end
if 0 < i if 0 < i
i_decorated = larvae[i].decorated i_decorated = larvae[i].decorated
......
getsession(c) = gethub(c)[:session] getsession(c) = gethub(c)[:session]
setsession!(c, session) = (gethub(c)[:session] = session) setsession!(c, session) = (gethub(c)[:session] = session)
using Makie: label_info
const r = Bonito.jsrender const r = Bonito.jsrender
...@@ -431,6 +432,7 @@ mutable struct TagController ...@@ -431,6 +432,7 @@ mutable struct TagController
hub::ControllerHub hub::ControllerHub
view view
fallback_color::String fallback_color::String
# deprecation: manualtag is now stored in `hub` as well with key :manualtag
manualtag manualtag
end end
...@@ -451,21 +453,28 @@ function TagController(controller::ControllerHub; ...@@ -451,21 +453,28 @@ function TagController(controller::ControllerHub;
manualtag) manualtag)
end end
getmanualtag(c) = gethub(c)[:tag].manualtag #getmanualtag(c::TagController) = c.manualtag
newtagname(::TagController) = NoTag newtagname(::TagController) = NoTag
isfrozen(c::TagController, tag) = isfrozen(lookup(gettags(c)[], tag, true))
struct IndividualTagView struct IndividualTagView
controller::TagController controller::TagController
tag::ObservableTag # view tag::ObservableTag # view
frozen::Bool
dom_id dom_id
end end
function IndividualTagView(controller::TagController, tag::ObservableTag) function IndividualTagView(controller::TagController, tag::ObservableTag, frozen::Bool)
IndividualTagView(controller, tag, dom_id()) IndividualTagView(controller, tag, frozen, dom_id())
end
function tagview(controller::TagController, tag::ObservableTag)
IndividualTagView(controller, tag, isfrozen(tag))
end end
tagview(controller, tag) = IndividualTagView(controller, tag) isfrozen(tag::IndividualTagView) = tag.frozen
function lowerdom(ti::IndividualTagView) function lowerdom(ti::IndividualTagView)
tag = ti.tag tag = ti.tag
...@@ -497,8 +506,9 @@ function Bonito.jsrender(session::Session, ti::IndividualTagView) ...@@ -497,8 +506,9 @@ function Bonito.jsrender(session::Session, ti::IndividualTagView)
tag = ti.tag tag = ti.tag
selector = dom_selector(ti) * " .tag-name" selector = dom_selector(ti) * " .tag-name"
events = tagevents(ti.controller) events = tagevents(ti.controller)
on(events.renamefailed) do (name, failure) on(session, events.renamefailed) do (name, failure)
name == tag.name[] || return # 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)\"" @info "Reverting to \"$(name)\" after failure: \"$(failure)\""
evaljs(session, js"document.querySelector($selector).value = $name;") evaljs(session, js"document.querySelector($selector).value = $name;")
end end
...@@ -542,8 +552,10 @@ function tagfilter(controller; manualtag=nothing) ...@@ -542,8 +552,10 @@ function tagfilter(controller; manualtag=nothing)
end end
function lowerdom(tf::TagFilter) function lowerdom(tf::TagFilter)
manualtag = getmanualtag(tf.controller)
tags = newview!(gethub(tf.controller)[:taghooks]) tags = newview!(gethub(tf.controller)[:taghooks])
for tag in reverse(tags) for tag in reverse(tags)
tag.name[] == manualtag && continue
push!(tf.views, tagview(tf.controller, tag)) push!(tf.views, tagview(tf.controller, tag))
end end
return DOM.div(scrollable(tf.views...), return DOM.div(scrollable(tf.views...),
...@@ -609,49 +621,47 @@ function TagSelector(controller::TagController, ...@@ -609,49 +621,47 @@ function TagSelector(controller::TagController,
multiple::Union{Nothing, Bool}=nothing, multiple::Union{Nothing, Bool}=nothing,
) )
tag_lut = gettags(controller) tag_lut = gettags(controller)
getnames(tags) = [tag.name[] for tag in tags if tag.active[]] manualtag = getmanualtag(controller)
tagnames = Observable(getnames(tag_lut[])) tagnames = map(tag_lut) do tags
on(tag_lut) do tags [tag.name[] for tag in tags if tag.active[] && tag.name[] != manualtag]
tagnames[] = getnames(tags)
end end
registered_observables = Observable{Bool}[] 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 multitag = filterevents(controller).allow_multiple_tags
if !isnothing(multiple) if !isnothing(multiple)
multitag[] = multiple multitag[] = multiple
end end
fromjs = nothing
if selectable if selectable
fromjs = Observable("") fromjs = Observable("")
on(fromjs) do actuatedtag on(fromjs) do actuatedtag
for (tag, selected) in selectedtags[] for (tag, selected) in selectedtags[]
if tag == actuatedtag if tag == actuatedtag
toggletag!(controller, tag, selected, selectedtags, selectable) if isfrozen(controller, tag)
flag_active_larva_as_edited(controller) @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 break
end end
end 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 end
activelarva = getactivelarva(controller) activelarva = getactivelarva(controller)
onany(gettimestep(controller), onany(gettimestep(controller),
...@@ -685,12 +695,22 @@ function TagSelector(controller::TagController, ...@@ -685,12 +695,22 @@ function TagSelector(controller::TagController,
#notify(refresh_view) # applies only to the tag selector #notify(refresh_view) # applies only to the tag selector
notify(gettimestep(controller)) # full refresh notify(gettimestep(controller)) # full refresh
end end
return ctrl return TagSelector(controller,
tagnames,
selectedtags,
selectable,
isnothing(multiple),
fromjs,
dom_id())
end end
function refresh_selected_tags(controller, id, timestep, taglut, selectedtags, selectable) function refresh_selected_tags(controller, id, timestep, taglut, selectedtags, selectable)
larva = getlarva(controller, id) larva = getlarva(controller, id)
tags = getusertags(larva, timestep; lut=taglut) 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] tagnames = [tag.name[] for tag in tags]
jscode = String[] jscode = String[]
for (name, selected) in Observables.to_value(selectedtags) for (name, selected) in Observables.to_value(selectedtags)
...@@ -741,32 +761,31 @@ function refresh_selected_tag(controller, ...@@ -741,32 +761,31 @@ function refresh_selected_tag(controller,
end end
function toggletag!(controller, tagname, selected, selectedtags, selectable) function toggletag!(controller, tagname, selected, selectedtags, selectable)
selected[] = !selected[]
multitag = filterevents(controller).allow_multiple_tags[] multitag = filterevents(controller).allow_multiple_tags[]
multitag || selected[] || @debug "Deselecting a tag in a single tag setting"
#
larva_id = getactivelarva(controller)[] larva_id = getactivelarva(controller)[]
larva = getlarva(controller, larva_id) larva = getlarva(controller, larva_id)
timestep = gettimestep(controller)[] 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) tags = getusertags!(larva, timestep; lut=taglut)
tag = lookup(taglut, tagname) tag = lookup(taglut, tagname)
isnothing(tag) && throw(KeyError(tagname)) isnothing(tag) && throw(KeyError(tagname))
# # toggle
if selected[] selected′= !selected[]
transaction = if selected′
if multitag || isempty(tags) if multitag || isempty(tags)
transaction = AddTag(larva_id, timestep, tagname)
settag!(tags, tag) settag!(tags, tag)
refresh_selected_tags(controller, tags, selectedtags, selectable)
AddTag(larva_id, timestep, tagname)
else else
transaction = OverrideTags(larva_id, timestep, tagname, collect(tags))
settag!(tags, tag; single=true) settag!(tags, tag; single=true)
#
refresh_selected_tag(controller, tag, selectedtags, selectable) refresh_selected_tag(controller, tag, selectedtags, selectable)
OverrideTags(larva_id, timestep, tagname, collect(tags))
end end
else else
@assert !isempty(tags) @assert !isempty(tags)
transaction = RemoveTag(larva_id, timestep, tagname)
deletetag!(tags, tag) deletetag!(tags, tag)
RemoveTag(larva_id, timestep, tagname)
end end
push!(transactions(controller), transaction) push!(transactions(controller), transaction)
notify(larva.usertags) notify(larva.usertags)
...@@ -808,11 +827,7 @@ function Bonito.jsrender(session::Session, ts::TagSelector) ...@@ -808,11 +827,7 @@ function Bonito.jsrender(session::Session, ts::TagSelector)
if ts.selectable if ts.selectable
evaljs(session, js"LarvaTagger.setTagSelector($(ts.fromjs))") evaljs(session, js"LarvaTagger.setTagSelector($(ts.fromjs))")
end end
selectedtags = ts.selected on(session, ts.selected) do selected
prevtags = Ref(empty(selectedtags[]))
on(session, selectedtags) do selected
selected == prevtags[] && return
prevtags[] = selected
jsdom = r(session, lowertags(selected, ts.selectable)) jsdom = r(session, lowertags(selected, ts.selectable))
htmldom = replace(string(jsdom), "'" => "\\'") htmldom = replace(string(jsdom), "'" => "\\'")
evaljs(session, js""" evaljs(session, js"""
...@@ -831,7 +846,8 @@ function tagselector(controller; ...@@ -831,7 +846,8 @@ function tagselector(controller;
multipletags::Union{Nothing, Bool}=nothing, multipletags::Union{Nothing, Bool}=nothing,
) )
c = gethub(controller) 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 end
mutable struct TrackViewer mutable struct TrackViewer
...@@ -902,10 +918,17 @@ struct FileMenu ...@@ -902,10 +918,17 @@ struct FileMenu
browser::FileBrowser browser::FileBrowser
end 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) wd = getworkingdir(controller)
dir = joinpath(wd.root, wd.path[]) 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 on(FileBrowsers.selectedfile(browser)) do file
tryopenfile(controller, file; reload=true) tryopenfile(controller, file; reload=true)
end end
...@@ -920,9 +943,10 @@ function filemenu(controller; kwargs...) ...@@ -920,9 +943,10 @@ function filemenu(controller; kwargs...)
on(FilePickers.uploadedfile(browser)) do fileinfo on(FilePickers.uploadedfile(browser)) do fileinfo
filepath = joinpath(workingdir[], fileinfo["name"]) filepath = joinpath(workingdir[], fileinfo["name"])
saveinputfile(filepath, fileinfo["content"]) saveinputfile(filepath, fileinfo["content"])
notify(workingdir)
end end
end end
return FileMenu(gethub(controller), browser) return FileMenu(controller, browser)
end end
function Bonito.jsrender(session::Session, fm::FileMenu) function Bonito.jsrender(session::Session, fm::FileMenu)
......