sointu/tracker/table.go
5684185+vsariola@users.noreply.github.com d92426a100 feat!: rewrote the GUI and model for better testability
The Model was getting unmaintanable mess. This is an attempt to refactor/rewrite the Model so that data of certain type is exposed in standardized way, offering certain standard manipulations for that data type, and on the GUI side, certain standard widgets to tied to that data.

This rewrite closes #72, #106 and #120.
2024-02-17 18:16:06 +02:00

633 lines
14 KiB
Go

package tracker
import (
"github.com/vsariola/sointu"
"gopkg.in/yaml.v3"
)
type (
Table struct {
TableData
}
TableData interface {
Cursor() Point
Cursor2() Point
SetCursor(Point)
SetCursor2(Point)
Width() int
Height() int
MoveCursor(dx, dy int) (ok bool)
clear(p Point)
set(p Point, value int)
add(rect Rect, delta int) (ok bool)
marshal(rect Rect) (data []byte, ok bool)
unmarshalAtCursor(data []byte) (ok bool)
unmarshalRange(rect Rect, data []byte) (ok bool)
change(kind string, severity ChangeSeverity) func()
cancel()
}
Point struct {
X, Y int
}
Rect struct {
TopLeft, BottomRight Point
}
Order Model
Notes Model
)
// Model methods
func (m *Model) Order() *Order { return (*Order)(m) }
func (m *Model) Notes() *Notes { return (*Notes)(m) }
// Rect methods
func (r *Rect) Contains(p Point) bool {
return r.TopLeft.X <= p.X && p.X <= r.BottomRight.X &&
r.TopLeft.Y <= p.Y && p.Y <= r.BottomRight.Y
}
func (r *Rect) Width() int {
return r.BottomRight.X - r.TopLeft.X + 1
}
func (r *Rect) Height() int {
return r.BottomRight.Y - r.TopLeft.Y + 1
}
func (r *Rect) Limit(width, height int) {
if r.TopLeft.X < 0 {
r.TopLeft.X = 0
}
if r.TopLeft.Y < 0 {
r.TopLeft.Y = 0
}
if r.BottomRight.X >= width {
r.BottomRight.X = width - 1
}
if r.BottomRight.Y >= height {
r.BottomRight.Y = height - 1
}
}
// Table methods
func (v Table) Range() (rect Rect) {
rect.TopLeft.X = intMin(v.Cursor().X, v.Cursor2().X)
rect.TopLeft.Y = intMin(v.Cursor().Y, v.Cursor2().Y)
rect.BottomRight.X = intMax(v.Cursor().X, v.Cursor2().X)
rect.BottomRight.Y = intMax(v.Cursor().Y, v.Cursor2().Y)
return
}
func (v Table) Copy() ([]byte, bool) {
ret, ok := v.marshal(v.Range())
if !ok {
return nil, false
}
return ret, true
}
func (v Table) Paste(data []byte) bool {
defer v.change("Paste", MajorChange)()
if v.Cursor() == v.Cursor2() {
return v.unmarshalAtCursor(data)
} else {
return v.unmarshalRange(v.Range(), data)
}
}
func (v Table) Clear() {
defer v.change("Clear", MajorChange)()
rect := v.Range()
rect.Limit(v.Width(), v.Height())
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
v.clear(Point{x, y})
}
}
}
func (v Table) Fill(value int) {
defer v.change("Fill", MajorChange)()
rect := v.Range()
rect.Limit(v.Width(), v.Height())
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
v.set(Point{x, y}, value)
}
}
}
func (v Table) Add(delta int) {
defer v.change("Add", MinorChange)()
if !v.add(v.Range(), delta) {
v.cancel()
}
}
func (v Table) SetCursorX(x int) {
p := v.Cursor()
p.X = x
v.SetCursor(p)
}
func (v Table) SetCursorY(y int) {
p := v.Cursor()
p.Y = y
v.SetCursor(p)
}
// Order methods
func (v *Order) Table() Table {
return Table{v}
}
func (m *Order) Cursor() Point {
t := intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
p := intMax(intMin(m.d.Cursor.OrderRow, m.d.Song.Score.Length-1), 0)
return Point{t, p}
}
func (m *Order) Cursor2() Point {
t := intMax(intMin(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0)
p := intMax(intMin(m.d.Cursor2.OrderRow, m.d.Song.Score.Length-1), 0)
return Point{t, p}
}
func (m *Order) SetCursor(p Point) {
m.d.Cursor.Track = intMax(intMin(p.X, len(m.d.Song.Score.Tracks)-1), 0)
y := intMax(intMin(p.Y, m.d.Song.Score.Length-1), 0)
if y != m.d.Cursor.OrderRow {
m.noteTracking = false
}
m.d.Cursor.OrderRow = y
m.updateCursorRows()
}
func (m *Order) SetCursor2(p Point) {
m.d.Cursor2.Track = intMax(intMin(p.X, len(m.d.Song.Score.Tracks)-1), 0)
m.d.Cursor2.OrderRow = intMax(intMin(p.Y, m.d.Song.Score.Length-1), 0)
m.updateCursorRows()
}
func (v *Order) updateCursorRows() {
if v.Cursor() == v.Cursor2() {
v.d.Cursor.PatternRow = 0
v.d.Cursor2.PatternRow = 0
return
}
if v.d.Cursor.OrderRow > v.d.Cursor2.OrderRow {
v.d.Cursor.PatternRow = v.d.Song.Score.RowsPerPattern - 1
v.d.Cursor2.PatternRow = 0
} else {
v.d.Cursor.PatternRow = 0
v.d.Cursor2.PatternRow = v.d.Song.Score.RowsPerPattern - 1
}
}
func (v *Order) Width() int {
return len((*Model)(v).d.Song.Score.Tracks)
}
func (v *Order) Height() int {
return (*Model)(v).d.Song.Score.Length
}
func (v *Order) MoveCursor(dx, dy int) (ok bool) {
p := v.Cursor()
p.X += dx
p.Y += dy
v.SetCursor(p)
return p == v.Cursor()
}
func (m *Order) clear(p Point) {
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, -1)
}
func (m *Order) set(p Point, value int) {
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, value)
}
func (v *Order) add(rect Rect, delta int) (ok bool) {
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
if !v.add1(Point{x, y}, delta) {
return false
}
}
}
return true
}
func (v *Order) add1(p Point, delta int) (ok bool) {
if p.X < 0 || p.X >= len(v.d.Song.Score.Tracks) {
return true
}
val := v.d.Song.Score.Tracks[p.X].Order.Get(p.Y)
if val < 0 {
return true
}
val += delta
if val < 0 || val > 36 {
return false
}
v.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val)
return true
}
type marshalOrder struct {
Order []int `yaml:",flow"`
}
type marshalTracks struct {
Tracks []marshalOrder
}
func (m *Order) marshal(rect Rect) (data []byte, ok bool) {
width := rect.BottomRight.X - rect.TopLeft.X + 1
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
var table = marshalTracks{Tracks: make([]marshalOrder, 0, width)}
for x := 0; x < width; x++ {
ax := x + rect.TopLeft.X
if ax < 0 || ax >= len(m.d.Song.Score.Tracks) {
continue
}
table.Tracks = append(table.Tracks, marshalOrder{Order: make([]int, 0, rect.BottomRight.Y-rect.TopLeft.Y+1)})
for y := 0; y < height; y++ {
table.Tracks[x].Order = append(table.Tracks[x].Order, m.d.Song.Score.Tracks[ax].Order.Get(y+rect.TopLeft.Y))
}
}
ret, err := yaml.Marshal(table)
if err != nil {
return nil, false
}
return ret, true
}
func (m *Order) unmarshal(data []byte) (marshalTracks, bool) {
var table marshalTracks
yaml.Unmarshal(data, &table)
if len(table.Tracks) == 0 {
return marshalTracks{}, false
}
for i := 0; i < len(table.Tracks); i++ {
if len(table.Tracks[i].Order) > 0 {
return table, true
}
}
return marshalTracks{}, false
}
func (v *Order) unmarshalAtCursor(data []byte) bool {
table, ok := v.unmarshal(data)
if !ok {
return false
}
for i := 0; i < len(table.Tracks); i++ {
for j, q := range table.Tracks[i].Order {
if table.Tracks[i].Order[j] < -1 || table.Tracks[i].Order[j] > 36 {
continue
}
x := i + v.Cursor().X
y := j + v.Cursor().Y
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length {
continue
}
v.d.Song.Score.Tracks[x].Order.Set(y, q)
}
}
return true
}
func (v *Order) unmarshalRange(rect Rect, data []byte) bool {
table, ok := v.unmarshal(data)
if !ok {
return false
}
for i := 0; i < rect.Width(); i++ {
for j := 0; j < rect.Height(); j++ {
k := i % len(table.Tracks)
l := j % len(table.Tracks[k].Order)
a := table.Tracks[k].Order[l]
if a < -1 || a > 36 {
continue
}
x := i + rect.TopLeft.X
y := j + rect.TopLeft.Y
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.Length {
continue
}
v.d.Song.Score.Tracks[x].Order.Set(y, a)
}
}
return true
}
func (v *Order) change(kind string, severity ChangeSeverity) func() {
return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity)
}
func (v *Order) cancel() {
v.changeCancel = true
}
func (m *Order) Value(p Point) int {
if p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
return -1
}
return m.d.Song.Score.Tracks[p.X].Order.Get(p.Y)
}
func (m *Order) SetValue(p Point, val int) {
defer (*Model)(m).change("OrderElement.SetValue", ScoreChange, MinorChange)()
m.d.Song.Score.Tracks[p.X].Order.Set(p.Y, val)
}
func (e *Order) Title(x int) (title string) {
title = "?"
if x < 0 || x >= len(e.d.Song.Score.Tracks) {
return
}
t := e.d.Song.Score.Tracks[x]
firstVoice := e.d.Song.Score.FirstVoiceForTrack(x)
lastVoice := firstVoice + t.NumVoices - 1
firstIndex, err := e.d.Song.Patch.InstrumentForVoice(firstVoice)
lastIndex, err2 := e.d.Song.Patch.InstrumentForVoice(lastVoice)
if err != nil || err2 != nil {
return
}
switch diff := lastIndex - firstIndex; diff {
case 0:
title = e.d.Song.Patch[firstIndex].Name
default:
n1 := e.d.Song.Patch[firstIndex].Name
n2 := e.d.Song.Patch[firstIndex+1].Name
if len(n1) > 0 {
n1 = string(n1[0])
} else {
n1 = "?"
}
if len(n2) > 0 {
n2 = string(n2[0])
} else {
n2 = "?"
}
if diff > 1 {
title = n1 + "/" + n2 + "..."
} else {
title = n1 + "/" + n2
}
}
return
}
// NoteTable
func (v *Notes) Table() Table {
return Table{v}
}
func (m *Notes) Cursor() Point {
t := intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
p := intMax(intMin(m.d.Song.Score.SongRow(m.d.Cursor.SongPos), m.d.Song.Score.LengthInRows()-1), 0)
return Point{t, p}
}
func (m *Notes) Cursor2() Point {
t := intMax(intMin(m.d.Cursor2.Track, len(m.d.Song.Score.Tracks)-1), 0)
p := intMax(intMin(m.d.Song.Score.SongRow(m.d.Cursor2.SongPos), m.d.Song.Score.LengthInRows()-1), 0)
return Point{t, p}
}
func (v *Notes) SetCursor(p Point) {
v.d.Cursor.Track = intMax(intMin(p.X, len(v.d.Song.Score.Tracks)-1), 0)
newPos := v.d.Song.Score.Wrap(sointu.SongPos{PatternRow: p.Y})
if newPos != v.d.Cursor.SongPos {
v.noteTracking = false
}
v.d.Cursor.SongPos = newPos
}
func (v *Notes) SetCursor2(p Point) {
v.d.Cursor2.Track = intMax(intMin(p.X, len(v.d.Song.Score.Tracks)-1), 0)
v.d.Cursor2.SongPos = v.d.Song.Score.Wrap(sointu.SongPos{PatternRow: p.Y})
}
func (v *Notes) Width() int {
return len((*Model)(v).d.Song.Score.Tracks)
}
func (v *Notes) Height() int {
return (*Model)(v).d.Song.Score.Length * (*Model)(v).d.Song.Score.RowsPerPattern
}
func (v *Notes) MoveCursor(dx, dy int) (ok bool) {
p := v.Cursor()
for dx < 0 {
if v.Effect(p.X) && v.d.LowNibble {
v.d.LowNibble = false
} else {
p.X--
v.d.LowNibble = true
}
dx++
}
for dx > 0 {
if v.Effect(p.X) && !v.d.LowNibble {
v.d.LowNibble = true
} else {
p.X++
v.d.LowNibble = false
}
dx--
}
p.Y += dy
v.SetCursor(p)
return p == v.Cursor()
}
func (v *Notes) clear(p Point) {
v.SetValue(p, 1)
}
func (v *Notes) set(p Point, value int) {
v.SetValue(p, byte(value))
}
func (v *Notes) add(rect Rect, delta int) (ok bool) {
for x := rect.BottomRight.X; x >= rect.TopLeft.X; x-- {
for y := rect.BottomRight.Y; y >= rect.TopLeft.Y; y-- {
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
continue
}
pos := v.d.Song.Score.SongPos(y)
note := v.d.Song.Score.Tracks[x].Note(pos)
if note <= 1 {
continue
}
newVal := int(note) + delta
if newVal < 2 {
newVal = 2
} else if newVal > 255 {
newVal = 255
}
// only do all sets after all gets, so we don't accidentally adjust single note multiple times
defer v.d.Song.Score.Tracks[x].SetNote(pos, byte(newVal))
}
}
return true
}
type noteTable struct {
Notes [][]byte `yaml:",flow"`
}
func (m *Notes) marshal(rect Rect) (data []byte, ok bool) {
width := rect.BottomRight.X - rect.TopLeft.X + 1
height := rect.BottomRight.Y - rect.TopLeft.Y + 1
var table = noteTable{Notes: make([][]byte, 0, width)}
for x := 0; x < width; x++ {
table.Notes = append(table.Notes, make([]byte, 0, rect.BottomRight.Y-rect.TopLeft.Y+1))
for y := 0; y < height; y++ {
pos := m.d.Song.Score.SongPos(y + rect.TopLeft.Y)
ax := x + rect.TopLeft.X
if ax < 0 || ax >= len(m.d.Song.Score.Tracks) {
continue
}
table.Notes[x] = append(table.Notes[x], m.d.Song.Score.Tracks[ax].Note(pos))
}
}
ret, err := yaml.Marshal(table)
if err != nil {
return nil, false
}
return ret, true
}
func (v *Notes) unmarshal(data []byte) (noteTable, bool) {
var table noteTable
yaml.Unmarshal(data, &table)
if len(table.Notes) == 0 {
return noteTable{}, false
}
for i := 0; i < len(table.Notes); i++ {
if len(table.Notes[i]) > 0 {
return table, true
}
}
return noteTable{}, false
}
func (v *Notes) unmarshalAtCursor(data []byte) bool {
table, ok := v.unmarshal(data)
if !ok {
return false
}
for i := 0; i < len(table.Notes); i++ {
for j, q := range table.Notes[i] {
x := i + v.Cursor().X
y := j + v.Cursor().Y
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
continue
}
pos := v.d.Song.Score.SongPos(y)
v.d.Song.Score.Tracks[x].SetNote(pos, q)
}
}
return true
}
func (v *Notes) unmarshalRange(rect Rect, data []byte) bool {
table, ok := v.unmarshal(data)
if !ok {
return false
}
for i := 0; i < rect.Width(); i++ {
for j := 0; j < rect.Height(); j++ {
k := i % len(table.Notes)
l := j % len(table.Notes[k])
a := table.Notes[k][l]
x := i + rect.TopLeft.X
y := j + rect.TopLeft.Y
if x < 0 || x >= len(v.d.Song.Score.Tracks) || y < 0 || y >= v.d.Song.Score.LengthInRows() {
continue
}
pos := v.d.Song.Score.SongPos(y)
v.d.Song.Score.Tracks[x].SetNote(pos, a)
}
}
return true
}
func (v *Notes) change(kind string, severity ChangeSeverity) func() {
return (*Model)(v).change("OrderTableView."+kind, ScoreChange, severity)
}
func (v *Notes) cancel() {
v.changeCancel = true
}
func (m *Notes) Value(p Point) byte {
if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
return 1
}
pos := m.d.Song.Score.SongPos(p.Y)
return m.d.Song.Score.Tracks[p.X].Note(pos)
}
func (m *Notes) Effect(x int) bool {
if x < 0 || x >= len(m.d.Song.Score.Tracks) {
return false
}
return m.d.Song.Score.Tracks[x].Effect
}
func (m *Notes) LowNibble() bool {
return m.d.LowNibble
}
func (m *Notes) Unique(t, p int) bool {
if t < 0 || t >= len(m.cachePatternUseCount) || p < 0 || p >= len(m.cachePatternUseCount[t]) {
return false
}
return m.cachePatternUseCount[t][p] == 1
}
func (m *Notes) SetValue(p Point, val byte) {
defer m.change("SetValue", MinorChange)()
if p.Y < 0 || p.X < 0 || p.X >= len(m.d.Song.Score.Tracks) {
return
}
track := &(m.d.Song.Score.Tracks[p.X])
pos := m.d.Song.Score.SongPos(p.Y)
(*track).SetNote(pos, val)
}
func (v *Notes) FillNibble(value byte, lowNibble bool) {
defer v.change("FillNibble", MajorChange)()
rect := Table{v}.Range()
for y := rect.TopLeft.Y; y <= rect.BottomRight.Y; y++ {
for x := rect.TopLeft.X; x <= rect.BottomRight.X; x++ {
val := v.Value(Point{x, y})
if val == 1 {
val = 0 // treat hold also as 0
}
if lowNibble {
val = (val & 0xf0) | byte(value&15)
} else {
val = (val & 0x0f) | byte((value&15)<<4)
}
v.SetValue(Point{x, y}, val)
}
}
}