feat: add ability to select & move multiple units (closes #71)

This commit is contained in:
5684185+vsariola@users.noreply.github.com 2023-07-21 00:39:29 +03:00
parent 5884a8d195
commit 61776f397a
5 changed files with 126 additions and 55 deletions

View File

@ -14,16 +14,17 @@ import (
) )
type DragList struct { type DragList struct {
SelectedItem int SelectedItem int
HoverItem int SelectedItem2 int
List *layout.List HoverItem int
drag bool List *layout.List
dragID pointer.ID drag bool
tags []bool dragID pointer.ID
swapped bool tags []bool
focused bool swapped bool
requestFocus bool focused bool
mainTag bool requestFocus bool
mainTag bool
} }
type FilledDragListStyle struct { type FilledDragListStyle struct {
@ -61,9 +62,9 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop() defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
keys := key.Set("↑|↓|Ctrl-↑|Ctrl-↓") keys := key.Set("↑|↓|Ctrl-↑|Ctrl-↓|Shift-↑|Shift-↓")
if s.dragList.List.Axis == layout.Horizontal { if s.dragList.List.Axis == layout.Horizontal {
keys = key.Set("←|→|Ctrl-←|Ctrl-→") keys = key.Set("←|→|Ctrl-←|Ctrl-→|Shift-←|Shift-→")
} }
key.InputOp{Tag: &s.dragList.mainTag, Keys: keys}.Add(gtx.Ops) key.InputOp{Tag: &s.dragList.mainTag, Keys: keys}.Add(gtx.Ops)
@ -78,6 +79,10 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops) key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops)
} }
if !s.dragList.focused {
s.dragList.SelectedItem2 = s.dragList.SelectedItem
}
for _, ke := range gtx.Events(&s.dragList.mainTag) { for _, ke := range gtx.Events(&s.dragList.mainTag) {
switch ke := ke.(type) { switch ke := ke.(type) {
case key.FocusEvent: case key.FocusEvent:
@ -102,6 +107,9 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
swap = delta swap = delta
} else { } else {
s.dragList.SelectedItem += delta s.dragList.SelectedItem += delta
if !ke.Modifiers.Contain(key.ModShift) {
s.dragList.SelectedItem2 = s.dragList.SelectedItem
}
} }
} }
} }
@ -119,6 +127,8 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
} else { } else {
color = s.SelectedColor color = s.SelectedColor
} }
} else if between(s.dragList.SelectedItem, index, s.dragList.SelectedItem2) {
color = s.SelectedColor
} else if s.dragList.HoverItem == index { } else if s.dragList.HoverItem == index {
color = s.HoverColor color = s.HoverColor
} }
@ -145,6 +155,9 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
break break
} }
s.dragList.SelectedItem = index s.dragList.SelectedItem = index
if !e.Modifiers.Contain(key.ModShift) {
s.dragList.SelectedItem2 = index
}
key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops) key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops)
} }
} }
@ -208,12 +221,41 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
) )
} }
dims := s.dragList.List.Layout(gtx, s.Count, listElem) dims := s.dragList.List.Layout(gtx, s.Count, listElem)
if !s.dragList.swapped && swap != 0 && s.dragList.SelectedItem+swap >= 0 && s.dragList.SelectedItem+swap < s.Count { a := intMin(s.dragList.SelectedItem, s.dragList.SelectedItem2)
s.swap(s.dragList.SelectedItem, s.dragList.SelectedItem+swap) b := intMax(s.dragList.SelectedItem, s.dragList.SelectedItem2)
if !s.dragList.swapped && swap != 0 && a+swap >= 0 && b+swap < s.Count {
if swap < 0 {
for i := a; i <= b; i++ {
s.swap(i, i+swap)
}
} else {
for i := b; i >= a; i-- {
s.swap(i, i+swap)
}
}
s.dragList.SelectedItem += swap s.dragList.SelectedItem += swap
s.dragList.SelectedItem2 += swap
s.dragList.swapped = true s.dragList.swapped = true
} else { } else {
s.dragList.swapped = false s.dragList.swapped = false
} }
return dims return dims
} }
func between(a, b, c int) bool {
return (a <= b && b <= c) || (c <= b && b <= a)
}
func intMax(a, b int) int {
if a > b {
return a
}
return b
}
func intMin(a, b int) int {
if a < b {
return a
}
return b
}

View File

@ -453,7 +453,6 @@ func (ie *InstrumentEditor) layoutInstrumentEditor(gtx C, t *Tracker) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
unitList := FilledDragList(t.Theme, ie.unitDragList, len(units), element, t.SwapUnits) unitList := FilledDragList(t.Theme, ie.unitDragList, len(units), element, t.SwapUnits)
ie.unitDragList.SelectedItem = t.UnitIndex()
return Surface{Gray: 30, Focus: ie.wasFocused}.Layout(gtx, func(gtx C) D { return Surface{Gray: 30, Focus: ie.wasFocused}.Layout(gtx, func(gtx C) D {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(func(gtx C) D { layout.Rigid(func(gtx C) D {
@ -477,24 +476,23 @@ func (ie *InstrumentEditor) layoutInstrumentEditor(gtx C, t *Tracker) D {
l := len(ie.unitTypeEditor.Text()) l := len(ie.unitTypeEditor.Text())
ie.unitTypeEditor.SetCaret(l, l) ie.unitTypeEditor.SetCaret(l, l)
case key.NameDeleteForward: case key.NameDeleteForward:
t.DeleteUnit(true) t.DeleteUnits(true, ie.unitDragList.SelectedItem, ie.unitDragList.SelectedItem2)
case "C": case "C", "X":
contents, err := yaml.Marshal(t.Unit()) units := t.DeleteUnits(true, ie.unitDragList.SelectedItem, ie.unitDragList.SelectedItem2)
contents, err := yaml.Marshal(units)
if err == nil { if err == nil {
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops) clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
t.Alert.Update("Unit copied to clipboard", Notify, time.Second*3) alertText := "Unit(s) copied to clipboard"
if e.Name == "X" {
alertText = "Unit(s) cut to clipboard"
}
t.Alert.Update(alertText, Notify, time.Second*3)
} }
case "X": ie.unitDragList.SelectedItem2 = t.UnitIndex()
contents, err := yaml.Marshal(t.Unit())
if err == nil {
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
t.Alert.Update("Unit cut to clipboard", Notify, time.Second*3)
}
t.DeleteUnit(true)
case key.NameReturn: case key.NameReturn:
if e.Modifiers.Contain(key.ModShortcut) { if e.Modifiers.Contain(key.ModShortcut) {
t.AddUnit(true) t.AddUnit(true)
ie.unitDragList.SelectedItem = t.UnitIndex() ie.unitDragList.SelectedItem2 = ie.unitDragList.SelectedItem
ie.unitTypeEditor.SetText("") ie.unitTypeEditor.SetText("")
} }
ie.unitTypeEditor.Focus() ie.unitTypeEditor.Focus()
@ -504,7 +502,7 @@ func (ie *InstrumentEditor) layoutInstrumentEditor(gtx C, t *Tracker) D {
} }
} }
} }
ie.unitDragList.SelectedItem = t.UnitIndex()
dims := unitList.Layout(gtx) dims := unitList.Layout(gtx)
if t.UnitIndex() != ie.unitDragList.SelectedItem { if t.UnitIndex() != ie.unitDragList.SelectedItem {
t.SetUnitIndex(ie.unitDragList.SelectedItem) t.SetUnitIndex(ie.unitDragList.SelectedItem)

View File

@ -179,7 +179,7 @@ func (pe *ParamEditor) layoutUnitFooter(t *Tracker) layout.Widget {
t.InstrumentEditor.unitDragList.Focus() t.InstrumentEditor.unitDragList.Focus()
} }
for pe.DeleteUnitBtn.Clickable.Clicked() { for pe.DeleteUnitBtn.Clickable.Clicked() {
t.DeleteUnit(false) t.DeleteUnits(false, t.UnitIndex(), t.UnitIndex())
op.InvalidateOp{}.Add(gtx.Ops) op.InvalidateOp{}.Add(gtx.Ops)
t.InstrumentEditor.unitDragList.Focus() t.InstrumentEditor.unitDragList.Focus()
} }

View File

@ -67,13 +67,13 @@ type Tracker struct {
} }
func (t *Tracker) UnmarshalContent(bytes []byte) error { func (t *Tracker) UnmarshalContent(bytes []byte) error {
var unit sointu.Unit var units []sointu.Unit
if errJSON := json.Unmarshal(bytes, &unit); errJSON == nil { if errJSON := json.Unmarshal(bytes, &units); errJSON == nil {
t.PasteUnit(unit) t.PasteUnits(units)
return nil return nil
} }
if errYaml := yaml.Unmarshal(bytes, &unit); errYaml == nil { if errYaml := yaml.Unmarshal(bytes, &units); errYaml == nil {
t.PasteUnit(unit) t.PasteUnits(units)
return nil return nil
} }
var instr sointu.Instrument var instr sointu.Instrument

View File

@ -641,18 +641,20 @@ func (m *Model) SetUnitType(t string) {
m.notifyPatchChange() m.notifyPatchChange()
} }
func (m *Model) PasteUnit(unit sointu.Unit) { func (m *Model) PasteUnits(units []sointu.Unit) {
m.saveUndo("PasteUnit", 0) m.saveUndo("PasteUnits", 0)
newUnits := make([]sointu.Unit, len(m.Instrument().Units)+1) newUnits := make([]sointu.Unit, len(m.Instrument().Units)+len(units))
m.unitIndex++ m.unitIndex++
copy(newUnits, m.Instrument().Units[:m.unitIndex]) copy(newUnits, m.Instrument().Units[:m.unitIndex])
copy(newUnits[m.unitIndex+1:], m.Instrument().Units[m.unitIndex:]) copy(newUnits[m.unitIndex+len(units):], m.Instrument().Units[m.unitIndex:])
if _, ok := m.usedIDs[unit.ID]; ok { for _, unit := range units {
m.maxID++ if _, ok := m.usedIDs[unit.ID]; ok {
unit.ID = m.maxID m.maxID++
unit.ID = m.maxID
}
m.usedIDs[unit.ID] = true
} }
m.usedIDs[unit.ID] = true copy(newUnits[m.unitIndex:m.unitIndex+len(units)], units)
newUnits[m.unitIndex] = unit
m.song.Patch[m.instrIndex].Units = newUnits m.song.Patch[m.instrIndex].Units = newUnits
m.paramIndex = 0 m.paramIndex = 0
m.clampPositions() m.clampPositions()
@ -724,23 +726,38 @@ func (m *Model) DeleteOrderRow(forward bool) {
m.notifyScoreChange() m.notifyScoreChange()
} }
func (m *Model) DeleteUnit(forward bool) { func (m *Model) DeleteUnits(forward bool, a, b int) []sointu.Unit {
if !m.CanDeleteUnit() {
return
}
instr := m.Instrument() instr := m.Instrument()
m.saveUndo("DeleteUnit", 0) m.saveUndo("DeleteUnits", 0)
delete(m.usedIDs, instr.Units[m.unitIndex].ID) a, b = intMin(a, b), intMax(a, b)
newUnits := make([]sointu.Unit, len(instr.Units)-1) if a < 0 {
copy(newUnits, instr.Units[:m.unitIndex]) a = 0
copy(newUnits[m.unitIndex:], instr.Units[m.unitIndex+1:])
m.song.Patch[m.instrIndex].Units = newUnits
if !forward && m.unitIndex > 0 {
m.unitIndex--
} }
if b > len(instr.Units)-1 {
b = len(instr.Units) - 1
}
for i := a; i <= b; i++ {
delete(m.usedIDs, instr.Units[i].ID)
}
var newUnits []sointu.Unit
if a == 0 && b == len(instr.Units)-1 {
newUnits = make([]sointu.Unit, 1)
m.unitIndex = 0
} else {
newUnits = make([]sointu.Unit, len(instr.Units)-(b-a+1))
copy(newUnits, instr.Units[:a])
copy(newUnits[a:], instr.Units[b+1:])
m.unitIndex = a
if forward {
m.unitIndex--
}
}
deletedUnits := instr.Units[a : b+1]
m.song.Patch[m.instrIndex].Units = newUnits
m.paramIndex = 0 m.paramIndex = 0
m.clampPositions() m.clampPositions()
m.notifyPatchChange() m.notifyPatchChange()
return deletedUnits
} }
func (m *Model) CanDeleteUnit() bool { func (m *Model) CanDeleteUnit() bool {
@ -1377,3 +1394,17 @@ func clamp(a, min, max int) int {
} }
return a return a
} }
func intMax(a, b int) int {
if a > b {
return a
}
return b
}
func intMin(a, b int) int {
if a < b {
return a
}
return b
}