Skip to content
This repository has been archived by the owner on Jul 13, 2021. It is now read-only.

Commit

Permalink
Merge pull request #562 from JuliaPlots/jk/mouseobservables-and-lmenu
Browse files Browse the repository at this point in the history
  • Loading branch information
jkrumbiegel authored Nov 29, 2020
2 parents 604738f + fa13a52 commit 6023fd5
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 69 deletions.
6 changes: 3 additions & 3 deletions src/makielayout/lobjects/laxis.jl
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ function LAxis(parent::Scene; bbox = nothing, kwargs...)
# layout
layoutobservables.suggestedbbox[] = layoutobservables.suggestedbbox[]

mouseevents = addmouseevents!(scene)
mouseeventhandle = addmouseevents!(scene)
scrollevents = Node(ScrollEvent(0, 0))
keysevents = Node(KeysEvent(Set()))

Expand All @@ -312,7 +312,7 @@ function LAxis(parent::Scene; bbox = nothing, kwargs...)

la = LAxis(parent, scene, xaxislinks, yaxislinks, limits,
layoutobservables, attrs, block_limit_linking, decorations,
mouseevents, scrollevents, keysevents, interactions)
mouseeventhandle, scrollevents, keysevents, interactions)


function process_event(event)
Expand All @@ -321,7 +321,7 @@ function LAxis(parent::Scene; bbox = nothing, kwargs...)
end
end

on(process_event, mouseevents)
on(process_event, mouseeventhandle.obs)
on(process_event, scrollevents)
on(process_event, keysevents)

Expand Down
181 changes: 126 additions & 55 deletions src/makielayout/lobjects/lmenu.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function default_attributes(::Type{LMenu}, scene)
"The alignment of the menu in its suggested bounding box."
alignmode = Inside()
"Index of selected item"
i_selected = 1
i_selected = 0
"Selected item value"
selection = nothing
"Is the menu showing the available options"
Expand Down Expand Up @@ -51,6 +51,8 @@ function default_attributes(::Type{LMenu}, scene)
textcolor = :black
"The opening direction of the menu (:up or :down)"
direction = :down
"The default message prompting a selection when i == 0"
prompt = "Select..."
end
(attributes = attrs, documentation = docdict, defaults = defaultdict)
end
Expand Down Expand Up @@ -116,7 +118,7 @@ function LMenu(parent::Scene; bbox = nothing, kwargs...)
@extract attrs (halign, valign, i_selected, is_open, cell_color_hover,
cell_color_inactive_even, cell_color_inactive_odd, dropdown_arrow_color,
options, dropdown_arrow_size, textsize, selection, cell_color_active,
textpadding, selection_cell_color_inactive, textcolor, direction)
textpadding, selection_cell_color_inactive, textcolor, direction, prompt)

decorations = Dict{Symbol, Any}()

Expand Down Expand Up @@ -144,24 +146,114 @@ function LMenu(parent::Scene; bbox = nothing, kwargs...)

selectionrect = LRect(scene, width = nothing, height = nothing,
color = selection_cell_color_inactive[], strokewidth = 0)
selectiontext = LText(scene, "Select...", tellwidth = false, halign = :left,


optionstrings = Ref{Vector{String}}(optionlabel.(options[]))

selected_text = lift(prompt, i_selected) do prompt, i_selected
if i_selected == 0
prompt
else
optionstrings[][i_selected]
end
end


selectiontext = LText(scene, selected_text, tellwidth = false, halign = :left,
padding = textpadding, textsize = textsize, color = textcolor)


rects = [LRect(scene, width = nothing, height = nothing,
color = iseven(i) ? cell_color_inactive_even[] : cell_color_inactive_odd[], strokewidth = 0) for i in 1:length(options[])]
rects = Ref{Vector{LRect}}([])
texts = Ref{Vector{LText}}([])
allrects = Ref{Vector{LRect}}([])
alltexts = Ref{Vector{LText}}([])
mouseeventhandles = Ref{Vector{MouseEventHandle}}([])

function reassemble()
foreach(delete!, rects[])
foreach(delete!, texts[])
# remove all mouse actions previously connected to rects / texts
foreach(clear!, mouseeventhandles[])

trim!(contentgrid)

rects[] = [LRect(scene, width = nothing, height = nothing,
color = iseven(i) ? cell_color_inactive_even[] : cell_color_inactive_odd[],
strokewidth = 0)
for i in 1:length(options[])]

texts[] = [LText(scene, s, halign = :left, tellwidth = false,
textsize = textsize, color = textcolor,
padding = textpadding)
for s in optionstrings[]]

allrects[] = [selectionrect; rects[]]
alltexts[] = [selectiontext; texts[]]

contentgrid[1:length(allrects[]), 1] = allrects[]
contentgrid[1:length(alltexts[]), 1] = alltexts[]

strings = optionlabel.(options[])
rowgap!(contentgrid, 0)

texts = [LText(scene, s, halign = :left, tellwidth = false,
textsize = textsize, color = textcolor,
padding = textpadding) for s in strings]
mouseeventhandles[] = [addmouseevents!(scene, r.rect, t.textobject) for (r, t) in zip(allrects[], alltexts[])]

# create mouse events for each menu entry rect / text combo
for (i, (mouseeventhandle, r, t)) in enumerate(zip(mouseeventhandles[], allrects[], alltexts[]))
onmouseover(mouseeventhandle) do _
r.color = cell_color_hover[]
end

onmouseout(mouseeventhandle) do _
if i == 1
r.color = selection_cell_color_inactive[]
else
i_option = i - 1
r.color = iseven(i_option) ? cell_color_inactive_even[] : cell_color_inactive_odd[]
end
end

onmouseleftdown(mouseeventhandle) do _
r.color = cell_color_active[]
if is_open[]
# first item is already selected
if i > 1
i_selected[] = i - 1
end
end
is_open[] = !is_open[]
end
end

nothing
end

on(options) do options

allrects = [selectionrect; rects]
alltexts = [selectiontext; texts]
# update string ref before reassembly
optionstrings[] = optionlabel.(options)

reassemble()

new_i = 0 # default to nothing selected
# if there is a current selection, check if it still exists in the new options
if i_selected[] > 0
for (i, o) in enumerate(options)
# if one of the new options is equivalent to the old options, we choose it for continuity
if selection[] == optionvalue(o) && selected_text[] == optionlabel(o)
new_i = i
break
end
end
end

# trigger eventual selection actions
i_selected[] = new_i
end

# reassemble for the first time
reassemble()


dropdown_arrow = scatter!(scene,
lift(x -> [Point2f0(width(x) - 20, (top(x) + bottom(x)) / 2)], selectionrect.layoutobservables.computedbbox),
marker = @lift($is_open ? '' : ''),
Expand All @@ -174,7 +266,11 @@ function LMenu(parent::Scene; bbox = nothing, kwargs...)

onany(i_selected, is_open, contentgrid.layoutobservables.autosize) do i, open, gridautosize

h = texts[i].layoutobservables.autosize[][2]
h = if i == 0
selectiontext.layoutobservables.autosize[][2]
else
texts[][i].layoutobservables.autosize[][2]
end
layoutobservables.autosize[] = (nothing, h)
autosize = layoutobservables.autosize[]

Expand All @@ -187,72 +283,47 @@ function LMenu(parent::Scene; bbox = nothing, kwargs...)
translate!(scene, 0, 0, 10)

else
sceneheight[] = texts[1].layoutobservables.autosize[][2]
sceneheight[] = texts[][1].layoutobservables.autosize[][2]

# back to normal z
translate!(scene, 0, 0, 0)
# translate!(dropdown_arrow, 0, -top_border_offset, 1)
end
end

contentgrid[:v] = allrects
contentgrid[:v] = alltexts

on(direction) do d
if d == :down
contentgrid[:v] = allrects
contentgrid[:v] = alltexts
contentgrid[:v] = allrects[]
contentgrid[:v] = alltexts[]
elseif d == :up
contentgrid[:v] = reverse(allrects)
contentgrid[:v] = reverse(alltexts)
contentgrid[:v] = reverse(allrects[])
contentgrid[:v] = reverse(alltexts[])
else
error("Invalid direction $d. Possible values are :up and :down.")
end
end

on(i_selected) do i
h = selectiontext.layoutobservables.autosize[][2]
layoutobservables.autosize[] = (nothing, h)
end

# trigger size without triggering selection
i_selected[] = i_selected[]
is_open[] = is_open[]

on(i_selected) do i
# collect in case options is a zip or other generator without indexing
selectiontext.text = strings[i]
option = collect(options[])[i]
selection[] = optionvalue(option)
end

rowgap!(contentgrid, 0)

mouseevents_vector = [addmouseevents!(scene, r.rect, t.textobject) for (r, t) in zip(allrects, alltexts)]

for (i, (mouseevents, r, t)) in enumerate(zip(mouseevents_vector, allrects, alltexts))
onmouseover(mouseevents) do events
r.color = cell_color_hover[]
end

onmouseout(mouseevents) do events
if i == 1
r.color = selection_cell_color_inactive[]
else
i_option = i - 1
r.color = iseven(i_option) ? cell_color_inactive_even[] : cell_color_inactive_odd[]
end
end

onmouseleftdown(mouseevents) do events
r.color = cell_color_active[]
if is_open[]
# first item is already selected
if i > 1
i_selected[] = i - 1
end
if i == 0
selection[] = nothing
else
# collect in case options is a zip or other generator without indexing
option = collect(options[])[i]

# only update the selection value if the new value is actually different
# this is because i_selected can also be changed when the options themselves
# are mutated, and there could still be the same option in the list
# just at a different place, so that should not trigger a selection
newvalue = optionvalue(option)
if selection[] != newvalue
selection[] = newvalue
end
is_open[] = !is_open[]
end
end

Expand Down
36 changes: 26 additions & 10 deletions src/makielayout/mousestatemachine.jl
Original file line number Diff line number Diff line change
Expand Up @@ -54,20 +54,36 @@ struct MouseEvent
prev_px::Point2f0
end

struct MouseEventHandle
obs::Observable{MouseEvent}
observerfuncs::Vector{<:Observables.ObserverFunction}
end

"""
clear!(handle::MouseEventHandle)
Cut observable connections to the scene and remove any listeners to the mouse events.
"""
function clear!(handle::MouseEventHandle)
foreach(Observables.off, handle.observerfuncs)
empty!(handle.observerfuncs)
empty!(handle.obs.listeners)
nothing
end


for eventtype in instances(MouseEventType)
onfunctionname = Symbol("onmouse" * String(Symbol(eventtype)))
@eval begin

"""
Executes the function f whenever the `Node{MouseEvent}` statenode transitions
to `$($eventtype)`.
Executes the function f whenever the `MouseEventHandle`'s observable is set to
a MouseEvent with `event.type === $($eventtype)`.
"""
function $onfunctionname(f, statenode::Node{MouseEvent})
on(statenode) do state
if state.type === $eventtype
f(state)
function $onfunctionname(f, mev::MouseEventHandle)
on(mev.obs) do event
if event.type === $eventtype
f(event)
end
end
end
Expand All @@ -79,7 +95,7 @@ end
"""
addmouseevents!(scene, elements...)
Returns an `Observable{MouseEvent}` which is triggered by all mouse
Returns a `MouseEventHandle` with an observable inside which is triggered by all mouse
interactions with the `scene` and optionally restricted to all given
plot objects in `elements`.
Expand Down Expand Up @@ -120,7 +136,7 @@ function addmouseevents!(scene, elements...)


# react to mouse position changes
on(events(scene).mouseposition) do mp
mousepos_observerfunc = on(events(scene).mouseposition) do mp

t = time()
data = mouseposition(scene)
Expand Down Expand Up @@ -188,7 +204,7 @@ function addmouseevents!(scene, elements...)


# react to mouse button changes
on(events(scene).mousedrag) do mousedrag
mousedrag_observerfunc = on(events(scene).mousedrag) do mousedrag

t = time()
data = prev_data[]
Expand Down Expand Up @@ -309,5 +325,5 @@ function addmouseevents!(scene, elements...)
prev_t[] = t
end

mouseevent
MouseEventHandle(mouseevent, [mousepos_observerfunc, mousedrag_observerfunc])
end
2 changes: 1 addition & 1 deletion src/makielayout/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ mutable struct LAxis <: AbstractPlotting.AbstractScene
attributes::Attributes
block_limit_linking::Node{Bool}
decorations::Dict{Symbol, Any}
mouseevents::Observable{MouseEvent}
mouseeventhandle::MouseEventHandle
scrollevents::Observable{ScrollEvent}
keysevents::Observable{KeysEvent}
interactions::Dict{Symbol, Tuple{Bool, Any}}
Expand Down

0 comments on commit 6023fd5

Please sign in to comment.