diff --git a/patch.go b/patch.go index a8db243..1e16148 100644 --- a/patch.go +++ b/patch.go @@ -464,3 +464,22 @@ func (p Patch) FindUnit(id int) (instrIndex int, unitIndex int, err error) { } return 0, 0, fmt.Errorf("could not find a unit with id %v", id) } + +func FindParamForModulationPort(unitName string, index int) *UnitParameter { + // qm210: couldn't see a function yet that matches the parameter index to the modulateable param. + // Not sure whether *UnitParameters is good here, would this make them mutable? + unitType, ok := UnitTypes[unitName] + if !ok { + return nil + } + for _, param := range unitType { + if index == 0 { + return ¶m + } + if param.CanModulate { + index-- + } + } + // index outside range + return nil +} diff --git a/tracker/gioui/theme.go b/tracker/gioui/theme.go index f446bfd..bc93b00 100644 --- a/tracker/gioui/theme.go +++ b/tracker/gioui/theme.go @@ -72,3 +72,5 @@ var scrollBarColor = color.NRGBA{R: 255, G: 255, B: 255, A: 32} var warningColor = color.NRGBA{R: 251, G: 192, B: 45, A: 255} var dialogBgColor = color.NRGBA{R: 0, G: 0, B: 0, A: 224} + +var paramIsSendTargetColor = color.NRGBA{R: 120, G: 120, B: 210, A: 255} diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index 1277eda..f0adee4 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -3,10 +3,6 @@ package gioui import ( "bytes" "fmt" - "image" - "io" - "math" - "gioui.org/io/clipboard" "gioui.org/io/event" "gioui.org/io/key" @@ -17,10 +13,17 @@ import ( "gioui.org/unit" "gioui.org/widget" "gioui.org/widget/material" + "gioui.org/x/component" + sointu "github.com/vsariola/sointu" "github.com/vsariola/sointu/tracker" "golang.org/x/exp/shiny/materialdesign/icons" "golang.org/x/text/cases" "golang.org/x/text/language" + "image" + "io" + "iter" + "math" + "slices" ) type UnitEditor struct { @@ -240,6 +243,7 @@ type ParameterWidget struct { unitBtn widget.Clickable unitMenu Menu Parameter tracker.Parameter + tipArea component.TipArea } type ParameterStyle struct { @@ -247,17 +251,21 @@ type ParameterStyle struct { w *ParameterWidget Theme *material.Theme Focus bool + sends []sointu.Unit } func (t *Tracker) ParamStyle(th *material.Theme, paramWidget *ParameterWidget) ParameterStyle { + sends := slices.Collect(t.Model.CollectSendsTo(paramWidget.Parameter)) return ParameterStyle{ tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way Theme: th, w: paramWidget, + sends: sends, } } func (p ParameterStyle) Layout(gtx C) D { + sends := slices.Collect(p.findSends()) return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, layout.Rigid(func(gtx C) D { gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110)) @@ -288,6 +296,10 @@ func (p ParameterStyle) Layout(gtx C) D { } sliderStyle := material.Slider(p.Theme, &p.w.floatWidget) sliderStyle.Color = p.Theme.Fg + + if len(sends) > 0 { + sliderStyle.Color = paramIsSendTargetColor + } r := image.Rectangle{Max: gtx.Constraints.Min} defer clip.Rect(r).Push(gtx.Ops).Pop() defer pointer.PassOp{}.Push(gtx.Ops).Pop() @@ -336,15 +348,11 @@ func (p ParameterStyle) Layout(gtx C) D { targetInstrument := p.tracker.Instrument(targetI) instrName = targetInstrument.Name units := targetInstrument.Units - unitName = fmt.Sprintf("%d: %s %s", targetU, units[targetU].Type, units[targetU].Comment) + unitName = unitNameFor(targetU, units[targetU]) unitItems = make([]MenuItem, len(units)) for j, unit := range units { id := unit.ID - text := unit.Type - if unit.Comment != "" { - text = fmt.Sprintf("%s \"%s\"", text, unit.Comment) - } - unitItems[j].Text = fmt.Sprintf("%d: %s", j, text) + unitItems[j].Text = unitNameFor(j, unit) unitItems[j].IconBytes = icons.NavigationChevronRight unitItems[j].Doer = tracker.Allow(func() { tracker.Int{IntData: p.w.Parameter}.Set(id) @@ -365,9 +373,68 @@ func (p ParameterStyle) Layout(gtx C) D { }), layout.Rigid(func(gtx C) D { if p.w.Parameter.Type() != tracker.IDParameter { - return Label(p.w.Parameter.Hint(), white, p.tracker.Theme.Shaper)(gtx) + label := Label(p.w.Parameter.Hint(), white, p.tracker.Theme.Shaper) + info := p.buildTooltip(sends) + if info == "" { + return label(gtx) + } + tooltip := component.PlatformTooltip(p.Theme, info) + return p.w.tipArea.Layout(gtx, tooltip, label) } return D{} }), ) } + +func unitNameFor(index int, u sointu.Unit) string { + text := u.Type + if u.Comment != "" { + text = fmt.Sprintf("%s \"%s\"", text, u.Comment) + } + return fmt.Sprintf("%d: %s", index, text) +} + +func (p ParameterStyle) findSends() iter.Seq[sointu.Unit] { + return func(yield func(sointu.Unit) bool) { + param, ok := (p.w.Parameter).(tracker.NamedParameter) + if !ok { + return + } + for _, send := range p.sends { + port := send.Parameters["port"] + unitParam := sointu.FindParamForModulationPort(param.Unit().Type, port) + if unitParam.Name != param.Name() { + continue + } + if !yield(send) { + return + } + } + } +} + +func (p ParameterStyle) buildTooltip(sends []sointu.Unit) string { + if len(sends) == 0 { + return "" + } + targetParam := (p.w.Parameter).(tracker.NamedParameter) + targetInstr := p.tracker.Model.InstrumentForUnit(targetParam.Unit().ID) + amounts := "" + for i := 0; i < len(sends); i++ { + sourceInstr := p.tracker.Model.InstrumentForUnit(sends[0].ID) + sourceInfo := "" + if sourceInstr != targetInstr { + sourceInfo = fmt.Sprintf(" (%s)", sourceInstr.Name) + } + if amounts == "" { + amounts = fmt.Sprintf("x %d%s", sends[i].Parameters["amount"], sourceInfo) + } else { + amounts = fmt.Sprintf("%s, x %d%s", amounts, sends[i].Parameters["amount"], sourceInfo) + } + } + count := "1 send" + if len(sends) > 1 { + count = fmt.Sprintf("%d sends") + } + return fmt.Sprintf("%s [%s]", count, amounts) +} diff --git a/tracker/model.go b/tracker/model.go index 67c1a5f..5d759cc 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "fmt" + "iter" "os" "path/filepath" @@ -597,3 +598,38 @@ func clamp(a, min, max int) int { } return a } + +func (m *Model) CollectSendsTo(param Parameter) iter.Seq[sointu.Unit] { + return func(yield func(sointu.Unit) bool) { + p, ok := param.(NamedParameter) + if !ok { + return + } + for _, instr := range m.d.Song.Patch { + for _, unit := range instr.Units { + if unit.Type != "send" { + continue + } + targetId, ok := unit.Parameters["target"] + if !ok || targetId != p.Unit().ID { + continue + } + if !yield(unit) { + return + } + } + } + } +} + +func (m *Model) InstrumentForUnit(id int) *sointu.Instrument { + for _, instr := range m.d.Song.Patch { + for _, unit := range instr.Units { + if unit.ID == id { + return &instr + } + } + } + // ID does not exist + return nil +} diff --git a/tracker/params.go b/tracker/params.go index a376592..c6092cb 100644 --- a/tracker/params.go +++ b/tracker/params.go @@ -204,6 +204,10 @@ func (p NamedParameter) LargeStep() int { return 16 } +func (p NamedParameter) Unit() sointu.Unit { + return *p.parameter.unit +} + // GmDlsEntryParameter func (p GmDlsEntryParameter) Name() string { return "sample" }