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 0ea314adccb30bce9fbffe13585002e033a80e95..ad40fb617e22a23c656a06f82595d154bbd3fd28 100644 --- a/src/editor.jl +++ b/src/editor.jl @@ -54,6 +54,8 @@ 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 @@ -65,7 +67,7 @@ function larvaeditor(path=nothing; multipletags=allow_multiple_tags, kwargs...), larvafilter(controller), - tagfilter(controller; manualtag=manualtag), + tagfilter(controller), metadataeditor(controller), filemenu(controller; upload_button=enable_uploads, download_button=enable_downloads, diff --git a/src/files.jl b/src/files.jl index aa2e3d112c32dcf8e126bd8732b27c28daf82b38..100d81d8ff00940cd436e091bf046ce385a688b3 100644 --- a/src/files.jl +++ b/src/files.jl @@ -187,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 @@ -276,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 @@ -358,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/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 19cfa30f67bba9408fca99ac94d096b66792aa8f..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 diff --git a/src/wgl.jl b/src/wgl.jl index 7a4972fbb2cd6960910ad9dd655ed52de7963783..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 @@ -912,6 +928,7 @@ function filemenu(controller; upload_button=false, download_button=false, 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 @@ -929,7 +946,7 @@ function filemenu(controller; upload_button=false, download_button=false, notify(workingdir) end end - return FileMenu(gethub(controller), browser) + return FileMenu(controller, browser) end function Bonito.jsrender(session::Session, fm::FileMenu)