diff --git a/tracker/gioui/draglist.go b/tracker/gioui/draglist.go index 7557b89..260df7d 100644 --- a/tracker/gioui/draglist.go +++ b/tracker/gioui/draglist.go @@ -14,16 +14,17 @@ import ( ) type DragList struct { - SelectedItem int - HoverItem int - List *layout.List - drag bool - dragID pointer.ID - tags []bool - swapped bool - focused bool - requestFocus bool - mainTag bool + SelectedItem int + SelectedItem2 int + HoverItem int + List *layout.List + drag bool + dragID pointer.ID + tags []bool + swapped bool + focused bool + requestFocus bool + mainTag bool } type FilledDragListStyle struct { @@ -61,9 +62,9 @@ func (s *FilledDragListStyle) Layout(gtx C) D { 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() - keys := key.Set("↑|↓|Ctrl-↑|Ctrl-↓") + keys := key.Set("↑|↓|Ctrl-↑|Ctrl-↓|Shift-↑|Shift-↓") 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) @@ -78,6 +79,10 @@ func (s *FilledDragListStyle) Layout(gtx C) D { 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) { switch ke := ke.(type) { case key.FocusEvent: @@ -102,6 +107,9 @@ func (s *FilledDragListStyle) Layout(gtx C) D { swap = delta } else { 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 { color = s.SelectedColor } + } else if between(s.dragList.SelectedItem, index, s.dragList.SelectedItem2) { + color = s.SelectedColor } else if s.dragList.HoverItem == index { color = s.HoverColor } @@ -145,6 +155,9 @@ func (s *FilledDragListStyle) Layout(gtx C) D { break } s.dragList.SelectedItem = index + if !e.Modifiers.Contain(key.ModShift) { + s.dragList.SelectedItem2 = index + } 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) - if !s.dragList.swapped && swap != 0 && s.dragList.SelectedItem+swap >= 0 && s.dragList.SelectedItem+swap < s.Count { - s.swap(s.dragList.SelectedItem, s.dragList.SelectedItem+swap) + a := intMin(s.dragList.SelectedItem, s.dragList.SelectedItem2) + 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.SelectedItem2 += swap s.dragList.swapped = true } else { s.dragList.swapped = false } 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 +} diff --git a/tracker/gioui/instrumenteditor.go b/tracker/gioui/instrumenteditor.go index 8c27fa3..ec9f258 100644 --- a/tracker/gioui/instrumenteditor.go +++ b/tracker/gioui/instrumenteditor.go @@ -453,7 +453,6 @@ func (ie *InstrumentEditor) layoutInstrumentEditor(gtx C, t *Tracker) D { defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() 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 layout.Flex{Axis: layout.Horizontal}.Layout(gtx, layout.Rigid(func(gtx C) D { @@ -477,24 +476,23 @@ func (ie *InstrumentEditor) layoutInstrumentEditor(gtx C, t *Tracker) D { l := len(ie.unitTypeEditor.Text()) ie.unitTypeEditor.SetCaret(l, l) case key.NameDeleteForward: - t.DeleteUnit(true) - case "C": - contents, err := yaml.Marshal(t.Unit()) + t.DeleteUnits(true, ie.unitDragList.SelectedItem, ie.unitDragList.SelectedItem2) + case "C", "X": + units := t.DeleteUnits(true, ie.unitDragList.SelectedItem, ie.unitDragList.SelectedItem2) + contents, err := yaml.Marshal(units) if err == nil { 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": - 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) + ie.unitDragList.SelectedItem2 = t.UnitIndex() case key.NameReturn: if e.Modifiers.Contain(key.ModShortcut) { t.AddUnit(true) - ie.unitDragList.SelectedItem = t.UnitIndex() + ie.unitDragList.SelectedItem2 = ie.unitDragList.SelectedItem ie.unitTypeEditor.SetText("") } 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) if t.UnitIndex() != ie.unitDragList.SelectedItem { t.SetUnitIndex(ie.unitDragList.SelectedItem) diff --git a/tracker/gioui/parameditor.go b/tracker/gioui/parameditor.go index 657057d..b36f497 100644 --- a/tracker/gioui/parameditor.go +++ b/tracker/gioui/parameditor.go @@ -179,7 +179,7 @@ func (pe *ParamEditor) layoutUnitFooter(t *Tracker) layout.Widget { t.InstrumentEditor.unitDragList.Focus() } for pe.DeleteUnitBtn.Clickable.Clicked() { - t.DeleteUnit(false) + t.DeleteUnits(false, t.UnitIndex(), t.UnitIndex()) op.InvalidateOp{}.Add(gtx.Ops) t.InstrumentEditor.unitDragList.Focus() } diff --git a/tracker/gioui/tracker.go b/tracker/gioui/tracker.go index db75640..93a00ea 100644 --- a/tracker/gioui/tracker.go +++ b/tracker/gioui/tracker.go @@ -67,13 +67,13 @@ type Tracker struct { } func (t *Tracker) UnmarshalContent(bytes []byte) error { - var unit sointu.Unit - if errJSON := json.Unmarshal(bytes, &unit); errJSON == nil { - t.PasteUnit(unit) + var units []sointu.Unit + if errJSON := json.Unmarshal(bytes, &units); errJSON == nil { + t.PasteUnits(units) return nil } - if errYaml := yaml.Unmarshal(bytes, &unit); errYaml == nil { - t.PasteUnit(unit) + if errYaml := yaml.Unmarshal(bytes, &units); errYaml == nil { + t.PasteUnits(units) return nil } var instr sointu.Instrument diff --git a/tracker/model.go b/tracker/model.go index e86b82d..bd83955 100644 --- a/tracker/model.go +++ b/tracker/model.go @@ -641,18 +641,20 @@ func (m *Model) SetUnitType(t string) { m.notifyPatchChange() } -func (m *Model) PasteUnit(unit sointu.Unit) { - m.saveUndo("PasteUnit", 0) - newUnits := make([]sointu.Unit, len(m.Instrument().Units)+1) +func (m *Model) PasteUnits(units []sointu.Unit) { + m.saveUndo("PasteUnits", 0) + newUnits := make([]sointu.Unit, len(m.Instrument().Units)+len(units)) m.unitIndex++ copy(newUnits, m.Instrument().Units[:m.unitIndex]) - copy(newUnits[m.unitIndex+1:], m.Instrument().Units[m.unitIndex:]) - if _, ok := m.usedIDs[unit.ID]; ok { - m.maxID++ - unit.ID = m.maxID + copy(newUnits[m.unitIndex+len(units):], m.Instrument().Units[m.unitIndex:]) + for _, unit := range units { + if _, ok := m.usedIDs[unit.ID]; ok { + m.maxID++ + unit.ID = m.maxID + } + m.usedIDs[unit.ID] = true } - m.usedIDs[unit.ID] = true - newUnits[m.unitIndex] = unit + copy(newUnits[m.unitIndex:m.unitIndex+len(units)], units) m.song.Patch[m.instrIndex].Units = newUnits m.paramIndex = 0 m.clampPositions() @@ -724,23 +726,38 @@ func (m *Model) DeleteOrderRow(forward bool) { m.notifyScoreChange() } -func (m *Model) DeleteUnit(forward bool) { - if !m.CanDeleteUnit() { - return - } +func (m *Model) DeleteUnits(forward bool, a, b int) []sointu.Unit { instr := m.Instrument() - m.saveUndo("DeleteUnit", 0) - delete(m.usedIDs, instr.Units[m.unitIndex].ID) - newUnits := make([]sointu.Unit, len(instr.Units)-1) - copy(newUnits, instr.Units[:m.unitIndex]) - copy(newUnits[m.unitIndex:], instr.Units[m.unitIndex+1:]) - m.song.Patch[m.instrIndex].Units = newUnits - if !forward && m.unitIndex > 0 { - m.unitIndex-- + m.saveUndo("DeleteUnits", 0) + a, b = intMin(a, b), intMax(a, b) + if a < 0 { + a = 0 } + 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.clampPositions() m.notifyPatchChange() + return deletedUnits } func (m *Model) CanDeleteUnit() bool { @@ -1377,3 +1394,17 @@ func clamp(a, min, max int) int { } 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 +}