diff --git a/tracker/action.go b/tracker/action.go index b8af6ec..c2f9d6c 100644 --- a/tracker/action.go +++ b/tracker/action.go @@ -10,488 +10,592 @@ import ( ) type ( - // Action describes a user action that can be performed on the model. It is - // usually a button press or a menu item. Action advertises whether it is - // allowed to be performed or not. + // Action describes a user action that can be performed on the model, which + // can be initiated by calling the Do() method. It is usually initiated by a + // button press or a menu item. Action advertises whether it is enabled, so + // UI can e.g. gray out buttons when the underlying action is not allowed. + // Action also implements the Doer and Enabler interfaces, but guarding that + // the underlying doer will never get called if enabler return false. That's + // why the doer and enabler are private fields, so that they cannot be + // called directly from outside, circumventing the Enabled() check. Action struct { - do func() - allowed func() bool + doer Doer + enabler Enabler + } + + // Doer is an interface that defines a single Do() method, which is called + // when an action is performed. + Doer interface { + Do() + } + + // Enabler is an interface that defines a single Enabled() method, which + // is used by the UI to check if UI Action/Bool/Int etc. is enabled or not. + Enabler interface { + Enabled() bool + } + + DoFunc func() + + AddTrack Model + DeleteTrack Model + SplitTrack Model + AddSemitone Model + SubtractSemitone Model + AddOctave Model + SubtractOctave Model + EditNoteOff Model + + AddInstrument Model + DeleteInstrument Model + SplitInstrument Model + + AddUnit struct { + Before bool + *Model + } + DeleteUnit Model + ClearUnit Model + + Undo Model + Redo Model + RemoveUnused Model + + PlayCurrentPos Model + PlaySongStart Model + PlaySelected Model + PlayFromLoopStart Model + StopPlaying Model + + AddOrderRow struct { + Before bool + *Model + } + DeleteOrderRow struct { + Backwards bool + *Model + } + + NewSong Model + OpenSong Model + SaveSong Model + RequestQuit Model + ForceQuit Model + Cancel Model + + DiscardSong Model + SaveSongAs Model + ExportAction Model + ExportFloat Model + ExportInt16 Model + SelectMidiInput struct { + Item MIDIDevice + *Model } ) // Action methods -func (e Action) Do() { - if e.allowed != nil && e.allowed() { - e.do() +// simple version for when both doer and enabler are the same +func MakeAction(doerEnabler interface { + Doer + Enabler +}) Action { + return Action{doer: doerEnabler, enabler: doerEnabler} +} + +// a version for cases where doer and enabler are different +func MakeAction2(doer Doer, enabler Enabler) Action { + return Action{doer: doer, enabler: enabler} +} + +func MakeEnabledAction(doer Doer) Action { + return Action{doer: doer, enabler: nil} +} + +func (a Action) Do() { + if a.enabler != nil && !a.enabler.Enabled() { + return + } + if a.doer != nil { + a.doer.Do() } } -func (e Action) Allowed() bool { - return e.allowed != nil && e.allowed() -} - -func Allow(do func()) Action { - return Action{do: do, allowed: func() bool { return true }} -} - -func Check(do func(), allowed func() bool) Action { - return Action{do: do, allowed: allowed} -} - -// Model methods - -func (m *Model) AddTrack() Action { - return Action{ - allowed: func() bool { return m.d.Song.Score.NumVoices() < vm.MAX_VOICES }, - do: func() { - defer (*Model)(m).change("AddTrack", SongChange, MajorChange)() - voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track) - p := sointu.Patch{defaultInstrument.Copy()} - t := []sointu.Track{sointu.Track{NumVoices: 1}} - _, _, ok := m.addVoices(voiceIndex, p, t, (*Model)(m).linkInstrTrack, true) - m.changeCancel = !ok - }, +func (a Action) Enabled() bool { + if a.enabler == nil { + return true // no enabler, always allowed } + return a.enabler.Enabled() } -func (m *Model) DeleteTrack() Action { - return Action{ - allowed: func() bool { return len(m.d.Song.Score.Tracks) > 0 }, - do: func() { m.Tracks().List().DeleteElements(false) }, - } +// DoFunc + +func (d DoFunc) Do() { d() } + +// AddTrack + +func (m *Model) AddTrack() Action { return MakeAction((*AddTrack)(m)) } +func (m *AddTrack) Enabled() bool { return m.d.Song.Score.NumVoices() < vm.MAX_VOICES } +func (m *AddTrack) Do() { + defer (*Model)(m).change("AddTrack", SongChange, MajorChange)() + voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track) + p := sointu.Patch{defaultInstrument.Copy()} + t := []sointu.Track{{NumVoices: 1}} + _, _, ok := (*Model)(m).addVoices(voiceIndex, p, t, (*Model)(m).linkInstrTrack, true) + m.changeCancel = !ok } -func (m *Model) AddInstrument() Action { - return Action{ - allowed: func() bool { return (*Model)(m).d.Song.Patch.NumVoices() < vm.MAX_VOICES }, - do: func() { - defer (*Model)(m).change("AddInstrument", SongChange, MajorChange)() - voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex) - p := sointu.Patch{defaultInstrument.Copy()} - t := []sointu.Track{sointu.Track{NumVoices: 1}} - _, _, ok := m.addVoices(voiceIndex, p, t, true, (*Model)(m).linkInstrTrack) - m.changeCancel = !ok - }, - } +// DeleteTrack + +func (m *Model) DeleteTrack() Action { return MakeAction((*DeleteTrack)(m)) } +func (m *DeleteTrack) Enabled() bool { return len(m.d.Song.Score.Tracks) > 0 } +func (m *DeleteTrack) Do() { (*Model)(m).Tracks().List().DeleteElements(false) } + +// AddInstrument + +func (m *Model) AddInstrument() Action { return MakeAction((*AddInstrument)(m)) } +func (m *AddInstrument) Enabled() bool { return (*Model)(m).d.Song.Patch.NumVoices() < vm.MAX_VOICES } +func (m *AddInstrument) Do() { + defer (*Model)(m).change("AddInstrument", SongChange, MajorChange)() + voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex) + p := sointu.Patch{defaultInstrument.Copy()} + t := []sointu.Track{{NumVoices: 1}} + _, _, ok := (*Model)(m).addVoices(voiceIndex, p, t, true, (*Model)(m).linkInstrTrack) + m.changeCancel = !ok } -func (m *Model) DeleteInstrument() Action { - return Action{ - allowed: func() bool { return len((*Model)(m).d.Song.Patch) > 0 }, - do: func() { m.Instruments().List().DeleteElements(false) }, +// DeleteInstrument + +func (m *Model) DeleteInstrument() Action { return MakeAction((*DeleteInstrument)(m)) } +func (m *DeleteInstrument) Enabled() bool { return len((*Model)(m).d.Song.Patch) > 0 } +func (m *DeleteInstrument) Do() { (*Model)(m).Instruments().List().DeleteElements(false) } + +// SplitTrack + +func (m *Model) SplitTrack() Action { return MakeAction((*SplitTrack)(m)) } +func (m *SplitTrack) Enabled() bool { + return m.d.Cursor.Track >= 0 && m.d.Cursor.Track < len(m.d.Song.Score.Tracks) && m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices > 1 +} +func (m *SplitTrack) Do() { + defer (*Model)(m).change("SplitTrack", SongChange, MajorChange)() + voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track) + middle := voiceIndex + (m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices+1)/2 + end := voiceIndex + m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices + left, ok := VoiceSlice(m.d.Song.Score.Tracks, Range{math.MinInt, middle}) + if !ok { + m.changeCancel = true + return } + right, ok := VoiceSlice(m.d.Song.Score.Tracks, Range{end, math.MaxInt}) + if !ok { + m.changeCancel = true + return + } + newTrack := sointu.Track{NumVoices: end - middle} + m.d.Song.Score.Tracks = append(left, newTrack) + m.d.Song.Score.Tracks = append(m.d.Song.Score.Tracks, right...) } -func (m *Model) SplitTrack() Action { - return Action{ - allowed: func() bool { - return m.d.Cursor.Track >= 0 && m.d.Cursor.Track < len(m.d.Song.Score.Tracks) && m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices > 1 - }, - do: func() { - defer (*Model)(m).change("SplitTrack", SongChange, MajorChange)() - voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track) - middle := voiceIndex + (m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices+1)/2 - end := voiceIndex + m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices - left, ok := VoiceSlice(m.d.Song.Score.Tracks, Range{math.MinInt, middle}) - if !ok { - m.changeCancel = true - return - } - right, ok := VoiceSlice(m.d.Song.Score.Tracks, Range{end, math.MaxInt}) - if !ok { - m.changeCancel = true - return - } - newTrack := sointu.Track{NumVoices: end - middle} - m.d.Song.Score.Tracks = append(left, newTrack) - m.d.Song.Score.Tracks = append(m.d.Song.Score.Tracks, right...) - }, +// SplitInstrument + +func (m *Model) SplitInstrument() Action { return MakeAction((*SplitInstrument)(m)) } +func (m *SplitInstrument) Enabled() bool { + return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) && m.d.Song.Patch[m.d.InstrIndex].NumVoices > 1 +} +func (m *SplitInstrument) Do() { + defer (*Model)(m).change("SplitInstrument", SongChange, MajorChange)() + voiceIndex := m.d.Song.Patch.Copy().FirstVoiceForInstrument(m.d.InstrIndex) + middle := voiceIndex + (m.d.Song.Patch[m.d.InstrIndex].NumVoices+1)/2 + end := voiceIndex + m.d.Song.Patch[m.d.InstrIndex].NumVoices + left, ok := VoiceSlice(m.d.Song.Patch, Range{math.MinInt, middle}) + if !ok { + m.changeCancel = true + return } + right, ok := VoiceSlice(m.d.Song.Patch, Range{end, math.MaxInt}) + if !ok { + m.changeCancel = true + return + } + newInstrument := defaultInstrument.Copy() + (*Model)(m).assignUnitIDs(newInstrument.Units) + newInstrument.NumVoices = end - middle + m.d.Song.Patch = append(left, newInstrument) + m.d.Song.Patch = append(m.d.Song.Patch, right...) } -func (m *Model) SplitInstrument() Action { - return Action{ - allowed: func() bool { - return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) && m.d.Song.Patch[m.d.InstrIndex].NumVoices > 1 - }, - do: func() { - defer (*Model)(m).change("SplitInstrument", SongChange, MajorChange)() - voiceIndex := m.d.Song.Patch.Copy().FirstVoiceForInstrument(m.d.InstrIndex) - middle := voiceIndex + (m.d.Song.Patch[m.d.InstrIndex].NumVoices+1)/2 - end := voiceIndex + m.d.Song.Patch[m.d.InstrIndex].NumVoices - left, ok := VoiceSlice(m.d.Song.Patch, Range{math.MinInt, middle}) - if !ok { - m.changeCancel = true - return - } - right, ok := VoiceSlice(m.d.Song.Patch, Range{end, math.MaxInt}) - if !ok { - m.changeCancel = true - return - } - newInstrument := defaultInstrument.Copy() - m.assignUnitIDs(newInstrument.Units) - newInstrument.NumVoices = end - middle - m.d.Song.Patch = append(left, newInstrument) - m.d.Song.Patch = append(m.d.Song.Patch, right...) - }, - } -} +// AddUnit func (m *Model) AddUnit(before bool) Action { - return Allow(func() { - defer m.change("AddUnitAction", PatchChange, MajorChange)() - if len(m.d.Song.Patch) == 0 { // no instruments, add one - instr := sointu.Instrument{NumVoices: 1} - instr.Units = make([]sointu.Unit, 0, 1) - m.d.Song.Patch = append(m.d.Song.Patch, instr) - m.d.UnitIndex = 0 - } else { - if !before { - m.d.UnitIndex++ - } + return MakeEnabledAction(AddUnit{Before: before, Model: m}) +} +func (a AddUnit) Do() { + m := (*Model)(a.Model) + defer m.change("AddUnitAction", PatchChange, MajorChange)() + if len(m.d.Song.Patch) == 0 { // no instruments, add one + instr := sointu.Instrument{NumVoices: 1} + instr.Units = make([]sointu.Unit, 0, 1) + m.d.Song.Patch = append(m.d.Song.Patch, instr) + m.d.UnitIndex = 0 + } else { + if !a.Before { + m.d.UnitIndex++ } - m.d.InstrIndex = max(min(m.d.InstrIndex, len(m.d.Song.Patch)-1), 0) - instr := m.d.Song.Patch[m.d.InstrIndex] - newUnits := make([]sointu.Unit, len(instr.Units)+1) - m.d.UnitIndex = clamp(m.d.UnitIndex, 0, len(newUnits)-1) - m.d.UnitIndex2 = m.d.UnitIndex - copy(newUnits, instr.Units[:m.d.UnitIndex]) - copy(newUnits[m.d.UnitIndex+1:], instr.Units[m.d.UnitIndex:]) - m.assignUnitIDs(newUnits[m.d.UnitIndex : m.d.UnitIndex+1]) - m.d.Song.Patch[m.d.InstrIndex].Units = newUnits - m.d.ParamIndex = 0 - }) -} - -func (m *Model) AddUnitAndThen(callback func()) Action { - return Allow(func() { - m.AddUnit(false).Do() - callback() - }) -} - -func (m *Model) DeleteUnit() Action { - return Action{ - allowed: func() bool { - return len((*Model)(m).d.Song.Patch) > 0 && len((*Model)(m).d.Song.Patch[(*Model)(m).d.InstrIndex].Units) > 1 - }, - do: func() { - defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)() - m.Units().List().DeleteElements(true) - }, } + m.d.InstrIndex = max(min(m.d.InstrIndex, len(m.d.Song.Patch)-1), 0) + instr := m.d.Song.Patch[m.d.InstrIndex] + newUnits := make([]sointu.Unit, len(instr.Units)+1) + m.d.UnitIndex = clamp(m.d.UnitIndex, 0, len(newUnits)-1) + m.d.UnitIndex2 = m.d.UnitIndex + copy(newUnits, instr.Units[:m.d.UnitIndex]) + copy(newUnits[m.d.UnitIndex+1:], instr.Units[m.d.UnitIndex:]) + m.assignUnitIDs(newUnits[m.d.UnitIndex : m.d.UnitIndex+1]) + m.d.Song.Patch[m.d.InstrIndex].Units = newUnits + m.d.ParamIndex = 0 } -func (m *Model) ClearUnit() Action { - return Action{ - do: func() { - defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)() - m.d.UnitIndex = max(min(m.d.UnitIndex, len(m.d.Song.Patch[m.d.InstrIndex].Units)-1), 0) - m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex] = sointu.Unit{} - m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].ID = m.maxID() + 1 - }, - allowed: func() bool { - return m.d.InstrIndex >= 0 && - m.d.InstrIndex < len(m.d.Song.Patch) && - len(m.d.Song.Patch[m.d.InstrIndex].Units) > 0 - }, +// DeleteUnit + +func (m *Model) DeleteUnit() Action { return MakeAction((*DeleteUnit)(m)) } +func (m *DeleteUnit) Enabled() bool { + i := (*Model)(m).d.InstrIndex + return i >= 0 && i < len((*Model)(m).d.Song.Patch) && len((*Model)(m).d.Song.Patch[i].Units) > 1 +} +func (m *DeleteUnit) Do() { + defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)() + (*Model)(m).Units().List().DeleteElements(true) +} + +// ClearUnit + +func (m *Model) ClearUnit() Action { return MakeAction((*ClearUnit)(m)) } +func (m *ClearUnit) Enabled() bool { + i := (*Model)(m).d.InstrIndex + return i >= 0 && i < len(m.d.Song.Patch) && len(m.d.Song.Patch[i].Units) > 0 +} +func (m *ClearUnit) Do() { + defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)() + m.d.UnitIndex = max(min(m.d.UnitIndex, len(m.d.Song.Patch[m.d.InstrIndex].Units)-1), 0) + m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex] = sointu.Unit{} + m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].ID = (*Model)(m).maxID() + 1 +} + +// Undo + +func (m *Model) Undo() Action { return MakeAction((*Undo)(m)) } +func (m *Undo) Enabled() bool { return len((*Model)(m).undoStack) > 0 } +func (m *Undo) Do() { + m.redoStack = append(m.redoStack, m.d.Copy()) + if len(m.redoStack) >= maxUndo { + copy(m.redoStack, m.redoStack[len(m.redoStack)-maxUndo:]) + m.redoStack = m.redoStack[:maxUndo] } + m.d = m.undoStack[len(m.undoStack)-1] + m.undoStack = m.undoStack[:len(m.undoStack)-1] + m.prevUndoKind = "" + TrySend(m.broker.ToPlayer, any(m.d.Song.Copy())) } -func (m *Model) Undo() Action { - return Action{ - allowed: func() bool { return len((*Model)(m).undoStack) > 0 }, - do: func() { - m.redoStack = append(m.redoStack, m.d.Copy()) - if len(m.redoStack) >= maxUndo { - copy(m.redoStack, m.redoStack[len(m.redoStack)-maxUndo:]) - m.redoStack = m.redoStack[:maxUndo] - } - m.d = m.undoStack[len(m.undoStack)-1] - m.undoStack = m.undoStack[:len(m.undoStack)-1] - m.prevUndoKind = "" - TrySend(m.broker.ToPlayer, any(m.d.Song.Copy())) - }, + +// Redo + +func (m *Model) Redo() Action { return MakeAction((*Redo)(m)) } +func (m *Redo) Enabled() bool { return len((*Model)(m).redoStack) > 0 } +func (m *Redo) Do() { + m.undoStack = append(m.undoStack, m.d.Copy()) + if len(m.undoStack) >= maxUndo { + copy(m.undoStack, m.undoStack[len(m.undoStack)-maxUndo:]) + m.undoStack = m.undoStack[:maxUndo] } + m.d = m.redoStack[len(m.redoStack)-1] + m.redoStack = m.redoStack[:len(m.redoStack)-1] + m.prevUndoKind = "" + TrySend(m.broker.ToPlayer, any(m.d.Song.Copy())) } -func (m *Model) Redo() Action { - return Action{ - allowed: func() bool { return len((*Model)(m).redoStack) > 0 }, - do: func() { - m.undoStack = append(m.undoStack, m.d.Copy()) - if len(m.undoStack) >= maxUndo { - copy(m.undoStack, m.undoStack[len(m.undoStack)-maxUndo:]) - m.undoStack = m.undoStack[:maxUndo] - } - m.d = m.redoStack[len(m.redoStack)-1] - m.redoStack = m.redoStack[:len(m.redoStack)-1] - m.prevUndoKind = "" - TrySend(m.broker.ToPlayer, any(m.d.Song.Copy())) - }, - } -} +// AddSemiTone -func (m *Model) AddSemitone() Action { - return Allow(func() { Table{(*Notes)(m)}.Add(1) }) -} +func (m *Model) AddSemitone() Action { return MakeEnabledAction((*AddSemitone)(m)) } +func (m *AddSemitone) Do() { Table{(*Notes)(m)}.Add(1) } -func (m *Model) SubtractSemitone() Action { - return Allow(func() { Table{(*Notes)(m)}.Add(-1) }) -} +// SubtractSemitone -func (m *Model) AddOctave() Action { - return Allow(func() { Table{(*Notes)(m)}.Add(12) }) -} +func (m *Model) SubtractSemitone() Action { return MakeEnabledAction((*SubtractSemitone)(m)) } +func (m *SubtractSemitone) Do() { Table{(*Notes)(m)}.Add(-1) } -func (m *Model) SubtractOctave() Action { - return Allow(func() { Table{(*Notes)(m)}.Add(-12) }) -} +// AddOctave -func (m *Model) EditNoteOff() Action { - return Allow(func() { Table{(*Notes)(m)}.Fill(0) }) -} +func (m *Model) AddOctave() Action { return MakeEnabledAction((*AddOctave)(m)) } +func (m *AddOctave) Do() { Table{(*Notes)(m)}.Add(12) } -func (m *Model) RemoveUnused() Action { - return Allow(func() { - defer m.change("RemoveUnusedAction", ScoreChange, MajorChange)() - for trkIndex, trk := range m.d.Song.Score.Tracks { - // assign new indices to patterns - newIndex := map[int]int{} - runningIndex := 0 - length := 0 - if len(trk.Order) > m.d.Song.Score.Length { - trk.Order = trk.Order[:m.d.Song.Score.Length] - } - for i, p := range trk.Order { - // if the pattern hasn't been considered and is within limits - if _, ok := newIndex[p]; !ok && p >= 0 && p < len(trk.Patterns) { - pat := trk.Patterns[p] - useful := false - for _, n := range pat { // patterns that have anything else than all holds are useful and to be kept - if n != 1 { - useful = true - break - } - } - if useful { - newIndex[p] = runningIndex - runningIndex++ - } else { - newIndex[p] = -1 +// SubtractOctave + +func (m *Model) SubtractOctave() Action { return MakeEnabledAction((*SubtractOctave)(m)) } +func (m *SubtractOctave) Do() { Table{(*Notes)(m)}.Add(-12) } + +// EditNoteOff + +func (m *Model) EditNoteOff() Action { return MakeEnabledAction((*EditNoteOff)(m)) } +func (m *EditNoteOff) Do() { Table{(*Notes)(m)}.Fill(0) } + +// RemoveUnused + +func (m *Model) RemoveUnused() Action { return MakeEnabledAction((*RemoveUnused)(m)) } +func (m *RemoveUnused) Do() { + defer (*Model)(m).change("RemoveUnusedAction", ScoreChange, MajorChange)() + for trkIndex, trk := range m.d.Song.Score.Tracks { + // assign new indices to patterns + newIndex := map[int]int{} + runningIndex := 0 + length := 0 + if len(trk.Order) > m.d.Song.Score.Length { + trk.Order = trk.Order[:m.d.Song.Score.Length] + } + for i, p := range trk.Order { + // if the pattern hasn't been considered and is within limits + if _, ok := newIndex[p]; !ok && p >= 0 && p < len(trk.Patterns) { + pat := trk.Patterns[p] + useful := false + for _, n := range pat { // patterns that have anything else than all holds are useful and to be kept + if n != 1 { + useful = true + break } } - if ind, ok := newIndex[p]; ok && ind > -1 { - length = i + 1 - trk.Order[i] = ind + if useful { + newIndex[p] = runningIndex + runningIndex++ } else { - trk.Order[i] = -1 + newIndex[p] = -1 } } - trk.Order = trk.Order[:length] - newPatterns := make([]sointu.Pattern, runningIndex) - for i, pat := range trk.Patterns { - if ind, ok := newIndex[i]; ok && ind > -1 { - patLength := 0 - for j, note := range pat { // find last note that is something else that hold - if note != 1 { - patLength = j + 1 - } - } - if patLength > m.d.Song.Score.RowsPerPattern { - patLength = m.d.Song.Score.RowsPerPattern - } - newPatterns[ind] = pat[:patLength] // crop to either RowsPerPattern or last row having something else than hold - } + if ind, ok := newIndex[p]; ok && ind > -1 { + length = i + 1 + trk.Order[i] = ind + } else { + trk.Order[i] = -1 } - trk.Patterns = newPatterns - m.d.Song.Score.Tracks[trkIndex] = trk } - }) -} - -func (m *Model) PlayCurrentPos() Action { - return Action{ - allowed: func() bool { return !m.instrEnlarged }, - do: func() { - m.setPanic(false) - m.setLoop(Loop{}) - m.playing = true - TrySend(m.broker.ToPlayer, any(StartPlayMsg{m.d.Cursor.SongPos})) - }, - } -} - -func (m *Model) PlaySongStart() Action { - return Action{ - allowed: func() bool { return !m.instrEnlarged }, - do: func() { - m.setPanic(false) - m.setLoop(Loop{}) - m.playing = true - TrySend(m.broker.ToPlayer, any(StartPlayMsg{})) - }, - } -} - -func (m *Model) PlaySelected() Action { - return Action{ - allowed: func() bool { return !m.instrEnlarged }, - do: func() { - m.setPanic(false) - m.playing = true - l := m.OrderRows().List() - r := l.listRange() - newLoop := Loop{r.Start, r.End - r.Start} - m.setLoop(newLoop) - TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: r.Start, PatternRow: 0}})) - }, - } -} - -func (m *Model) PlayFromLoopStart() Action { - return Action{ - allowed: func() bool { return !m.instrEnlarged }, - do: func() { - m.setPanic(false) - if m.loop == (Loop{}) { - m.PlaySelected().Do() - return + trk.Order = trk.Order[:length] + newPatterns := make([]sointu.Pattern, runningIndex) + for i, pat := range trk.Patterns { + if ind, ok := newIndex[i]; ok && ind > -1 { + patLength := 0 + for j, note := range pat { // find last note that is something else that hold + if note != 1 { + patLength = j + 1 + } + } + if patLength > m.d.Song.Score.RowsPerPattern { + patLength = m.d.Song.Score.RowsPerPattern + } + newPatterns[ind] = pat[:patLength] // crop to either RowsPerPattern or last row having something else than hold } - m.playing = true - TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: m.loop.Start, PatternRow: 0}})) - }, + } + trk.Patterns = newPatterns + m.d.Song.Score.Tracks[trkIndex] = trk } } -func (m *Model) StopPlaying() Action { - return Action{ - allowed: func() bool { return true }, - do: func() { - if !m.playing { - m.setPanic(true) - m.setLoop(Loop{}) - return - } - m.playing = false - TrySend(m.broker.ToPlayer, any(IsPlayingMsg{false})) - }, - } +// PlayCurrentPos + +func (m *Model) PlayCurrentPos() Action { return MakeAction((*PlayCurrentPos)(m)) } +func (m *PlayCurrentPos) Enabled() bool { return !m.instrEnlarged } +func (m *PlayCurrentPos) Do() { + (*Model)(m).setPanic(false) + (*Model)(m).setLoop(Loop{}) + m.playing = true + TrySend(m.broker.ToPlayer, any(StartPlayMsg{m.d.Cursor.SongPos})) } +// PlaySongStart + +func (m *Model) PlaySongStart() Action { return MakeAction((*PlaySongStart)(m)) } +func (m *PlaySongStart) Enabled() bool { return !m.instrEnlarged } +func (m *PlaySongStart) Do() { + (*Model)(m).setPanic(false) + (*Model)(m).setLoop(Loop{}) + m.playing = true + TrySend(m.broker.ToPlayer, any(StartPlayMsg{})) +} + +// PlaySelected + +func (m *Model) PlaySelected() Action { return MakeAction((*PlaySelected)(m)) } +func (m *PlaySelected) Enabled() bool { return !m.instrEnlarged } +func (m *PlaySelected) Do() { + (*Model)(m).setPanic(false) + m.playing = true + l := (*Model)(m).OrderRows().List() + r := l.listRange() + newLoop := Loop{r.Start, r.End - r.Start} + (*Model)(m).setLoop(newLoop) + TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: r.Start, PatternRow: 0}})) +} + +// PlayFromLoopStart + +func (m *Model) PlayFromLoopStart() Action { return MakeAction((*PlayFromLoopStart)(m)) } +func (m *PlayFromLoopStart) Enabled() bool { return !m.instrEnlarged } +func (m *PlayFromLoopStart) Do() { + (*Model)(m).setPanic(false) + if m.loop == (Loop{}) { + (*Model)(m).PlaySelected().Do() + return + } + m.playing = true + TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: m.loop.Start, PatternRow: 0}})) +} + +// StopPlaying + +func (m *Model) StopPlaying() Action { return MakeEnabledAction((*StopPlaying)(m)) } +func (m *StopPlaying) Do() { + if !m.playing { + (*Model)(m).setPanic(true) + (*Model)(m).setLoop(Loop{}) + return + } + m.playing = false + TrySend(m.broker.ToPlayer, any(IsPlayingMsg{false})) +} + +// AddOrderRow + func (m *Model) AddOrderRow(before bool) Action { - return Allow(func() { - defer m.change("AddOrderRowAction", ScoreChange, MinorChange)() - if !before { - m.d.Cursor.OrderRow++ - } - m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow - from := m.d.Cursor.OrderRow - m.d.Song.Score.Length++ - for i := range m.d.Song.Score.Tracks { - order := &m.d.Song.Score.Tracks[i].Order - if len(*order) > from { - *order = append(*order, -1) - copy((*order)[from+1:], (*order)[from:]) - (*order)[from] = -1 - } - } - }) + return MakeEnabledAction(AddOrderRow{Before: before, Model: m}) } +func (a AddOrderRow) Do() { + m := a.Model + defer m.change("AddOrderRowAction", ScoreChange, MinorChange)() + if !a.Before { + m.d.Cursor.OrderRow++ + } + m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow + from := m.d.Cursor.OrderRow + m.d.Song.Score.Length++ + for i := range m.d.Song.Score.Tracks { + order := &m.d.Song.Score.Tracks[i].Order + if len(*order) > from { + *order = append(*order, -1) + copy((*order)[from+1:], (*order)[from:]) + (*order)[from] = -1 + } + } +} + +// DeleteOrderRow func (m *Model) DeleteOrderRow(backwards bool) Action { - return Allow(func() { - defer m.change("AddOrderRowAction", ScoreChange, MinorChange)() - from := m.d.Cursor.OrderRow - m.d.Song.Score.Length-- - for i := range m.d.Song.Score.Tracks { - order := &m.d.Song.Score.Tracks[i].Order - if len(*order) > from { - copy((*order)[from:], (*order)[from+1:]) - *order = (*order)[:len(*order)-1] - } + return MakeEnabledAction(DeleteOrderRow{Backwards: backwards, Model: m}) +} +func (d DeleteOrderRow) Do() { + m := d.Model + defer m.change("AddOrderRowAction", ScoreChange, MinorChange)() + from := m.d.Cursor.OrderRow + m.d.Song.Score.Length-- + for i := range m.d.Song.Score.Tracks { + order := &m.d.Song.Score.Tracks[i].Order + if len(*order) > from { + copy((*order)[from:], (*order)[from+1:]) + *order = (*order)[:len(*order)-1] } - if backwards { - if m.d.Cursor.OrderRow > 0 { - m.d.Cursor.OrderRow-- - } + } + if d.Backwards { + if m.d.Cursor.OrderRow > 0 { + m.d.Cursor.OrderRow-- + } + } + m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow +} + +// NewSong + +func (m *Model) NewSong() Action { return MakeEnabledAction((*NewSong)(m)) } +func (m *NewSong) Do() { + m.dialog = NewSongChanges + (*Model)(m).completeAction(true) +} + +// OpenSong + +func (m *Model) OpenSong() Action { return MakeEnabledAction((*OpenSong)(m)) } +func (m *OpenSong) Do() { + m.dialog = OpenSongChanges + (*Model)(m).completeAction(true) +} + +// RequestQuit + +func (m *Model) RequestQuit() Action { return MakeEnabledAction((*RequestQuit)(m)) } +func (m *RequestQuit) Do() { + if !m.quitted { + m.dialog = QuitChanges + (*Model)(m).completeAction(true) + } +} + +// ForceQuit + +func (m *Model) ForceQuit() Action { return MakeEnabledAction((*ForceQuit)(m)) } +func (m *ForceQuit) Do() { m.quitted = true } + +// SaveSong + +func (m *Model) SaveSong() Action { return MakeEnabledAction((*SaveSong)(m)) } +func (m *SaveSong) Do() { + if m.d.FilePath == "" { + switch m.dialog { + case NoDialog: + m.dialog = SaveAsExplorer + case NewSongChanges: + m.dialog = NewSongSaveExplorer + case OpenSongChanges: + m.dialog = OpenSongSaveExplorer + case QuitChanges: + m.dialog = QuitSaveExplorer } - m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow return - }) + } + f, err := os.Create(m.d.FilePath) + if err != nil { + (*Model)(m).Alerts().Add("Error creating file: "+err.Error(), Error) + return + } + (*Model)(m).WriteSong(f) + m.d.ChangedSinceSave = false } -func (m *Model) NewSong() Action { - return Allow(func() { - m.dialog = NewSongChanges - m.completeAction(true) - }) -} +func (m *Model) DiscardSong() Action { return MakeEnabledAction((*DiscardSong)(m)) } +func (m *DiscardSong) Do() { (*Model)(m).completeAction(false) } -func (m *Model) OpenSong() Action { - return Allow(func() { - m.dialog = OpenSongChanges - m.completeAction(true) - }) -} +func (m *Model) SaveSongAs() Action { return MakeEnabledAction((*SaveSongAs)(m)) } +func (m *SaveSongAs) Do() { m.dialog = SaveAsExplorer } -func (m *Model) RequestQuit() Action { - return Allow(func() { - if !m.quitted { - m.dialog = QuitChanges - m.completeAction(true) - } - }) -} +func (m *Model) Cancel() Action { return MakeEnabledAction((*Cancel)(m)) } +func (m *Cancel) Do() { m.dialog = NoDialog } -func (m *Model) ForceQuit() Action { - return Allow(func() { - m.quitted = true - }) -} +func (m *Model) Export() Action { return MakeEnabledAction((*ExportAction)(m)) } +func (m *ExportAction) Do() { m.dialog = Export } -func (m *Model) SaveSong() Action { - return Allow(func() { - if m.d.FilePath == "" { - switch m.dialog { - case NoDialog: - m.dialog = SaveAsExplorer - case NewSongChanges: - m.dialog = NewSongSaveExplorer - case OpenSongChanges: - m.dialog = OpenSongSaveExplorer - case QuitChanges: - m.dialog = QuitSaveExplorer - } - return - } - f, err := os.Create(m.d.FilePath) - if err != nil { - m.Alerts().Add("Error creating file: "+err.Error(), Error) - return - } - m.WriteSong(f) - m.d.ChangedSinceSave = false - }) -} +func (m *Model) ExportFloat() Action { return MakeEnabledAction((*ExportFloat)(m)) } +func (m *ExportFloat) Do() { m.dialog = ExportFloatExplorer } + +func (m *Model) ExportInt16() Action { return MakeEnabledAction((*ExportInt16)(m)) } +func (m *ExportInt16) Do() { m.dialog = ExportInt16Explorer } -func (m *Model) DiscardSong() Action { return Allow(func() { m.completeAction(false) }) } -func (m *Model) SaveSongAs() Action { return Allow(func() { m.dialog = SaveAsExplorer }) } -func (m *Model) Cancel() Action { return Allow(func() { m.dialog = NoDialog }) } -func (m *Model) Export() Action { return Allow(func() { m.dialog = Export }) } -func (m *Model) ExportFloat() Action { return Allow(func() { m.dialog = ExportFloatExplorer }) } -func (m *Model) ExportInt16() Action { return Allow(func() { m.dialog = ExportInt16Explorer }) } func (m *Model) SelectMidiInput(item MIDIDevice) Action { - return Allow(func() { - if err := item.Open(); err == nil { - message := fmt.Sprintf("Opened MIDI device: %s", item) - m.Alerts().Add(message, Info) - } else { - message := fmt.Sprintf("Could not open MIDI device: %s", item) - m.Alerts().Add(message, Error) - } - }) + return MakeEnabledAction(SelectMidiInput{Item: item, Model: m}) +} +func (s SelectMidiInput) Do() { + m := s.Model + if err := s.Item.Open(); err == nil { + message := fmt.Sprintf("Opened MIDI device: %s", s.Item) + m.Alerts().Add(message, Info) + } else { + message := fmt.Sprintf("Could not open MIDI device: %s", s.Item) + m.Alerts().Add(message, Error) + } } func (m *Model) completeAction(checkSave bool) { diff --git a/tracker/gioui/buttons.go b/tracker/gioui/buttons.go index f5de38a..e9ea45d 100644 --- a/tracker/gioui/buttons.go +++ b/tracker/gioui/buttons.go @@ -69,7 +69,7 @@ func ActionIcon(gtx C, th *Theme, w *ActionClickable, icon []byte, tip string) T for w.Clickable.Clicked(gtx) { w.Action.Do() } - if !w.Action.Allowed() { + if !w.Action.Enabled() { ret.IconButtonStyle.Color = th.Button.Disabled.Color } return ret @@ -119,7 +119,7 @@ func ActionButton(gtx C, th *Theme, style *ButtonStyle, w *ActionClickable, text for w.Clickable.Clicked(gtx) { w.Action.Do() } - if !w.Action.Allowed() { + if !w.Action.Enabled() { return Btn(th, &th.Button.Disabled, &w.Clickable, text) } return Btn(th, style, &w.Clickable, text) diff --git a/tracker/gioui/dialog.go b/tracker/gioui/dialog.go index d7451d7..d012fca 100644 --- a/tracker/gioui/dialog.go +++ b/tracker/gioui/dialog.go @@ -92,7 +92,7 @@ func (d *Dialog) handleKeys(gtx C) { for d.BtnCancel.Clicked(gtx) { d.cancel.Do() } - if d.alt.Allowed() { + if d.alt.Enabled() { d.handleKeysForButton(gtx, &d.BtnAlt, &d.BtnCancel, &d.BtnOk) d.handleKeysForButton(gtx, &d.BtnCancel, &d.BtnOk, &d.BtnAlt) d.handleKeysForButton(gtx, &d.BtnOk, &d.BtnAlt, &d.BtnCancel) @@ -118,7 +118,7 @@ func (d *DialogStyle) Layout(gtx C) D { layout.Rigid(func(gtx C) D { return layout.E.Layout(gtx, func(gtx C) D { gtx.Constraints.Min.X = gtx.Dp(unit.Dp(120)) - if d.dialog.alt.Allowed() { + if d.dialog.alt.Enabled() { return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween}.Layout(gtx, layout.Rigid(d.OkStyle.Layout), layout.Rigid(d.AltStyle.Layout), diff --git a/tracker/gioui/instrument_editor.go b/tracker/gioui/instrument_editor.go index 7fbbc72..9e841df 100644 --- a/tracker/gioui/instrument_editor.go +++ b/tracker/gioui/instrument_editor.go @@ -22,44 +22,50 @@ import ( "golang.org/x/exp/shiny/materialdesign/icons" ) -type InstrumentEditor struct { - newInstrumentBtn *ActionClickable - enlargeBtn *BoolClickable - deleteInstrumentBtn *ActionClickable - linkInstrTrackBtn *BoolClickable - splitInstrumentBtn *ActionClickable - copyInstrumentBtn *TipClickable - saveInstrumentBtn *TipClickable - loadInstrumentBtn *TipClickable - addUnitBtn *ActionClickable - presetMenuBtn *TipClickable - commentExpandBtn *BoolClickable - soloBtn *BoolClickable - muteBtn *BoolClickable - commentEditor *Editor - commentString tracker.String - nameEditor *Editor - nameString tracker.String - searchEditor *Editor - instrumentDragList *DragList - unitDragList *DragList - unitEditor *UnitEditor - wasFocused bool - presetMenuItems []MenuItem - presetMenu Menu +type ( + InstrumentEditor struct { + newInstrumentBtn *ActionClickable + enlargeBtn *BoolClickable + deleteInstrumentBtn *ActionClickable + linkInstrTrackBtn *BoolClickable + splitInstrumentBtn *ActionClickable + copyInstrumentBtn *TipClickable + saveInstrumentBtn *TipClickable + loadInstrumentBtn *TipClickable + addUnitBtn *ActionClickable + presetMenuBtn *TipClickable + commentExpandBtn *BoolClickable + soloBtn *BoolClickable + muteBtn *BoolClickable + commentEditor *Editor + commentString tracker.String + nameEditor *Editor + nameString tracker.String + searchEditor *Editor + instrumentDragList *DragList + unitDragList *DragList + unitEditor *UnitEditor + wasFocused bool + presetMenuItems []MenuItem + presetMenu Menu - enlargeHint, shrinkHint string - addInstrumentHint string - octaveHint string - expandCommentHint string - collapseCommentHint string - deleteInstrumentHint string - muteHint, unmuteHint string - soloHint, unsoloHint string - linkDisabledHint string - linkEnabledHint string - splitInstrumentHint string -} + addUnit tracker.Action + + enlargeHint, shrinkHint string + addInstrumentHint string + octaveHint string + expandCommentHint string + collapseCommentHint string + deleteInstrumentHint string + muteHint, unmuteHint string + soloHint, unsoloHint string + linkDisabledHint string + linkEnabledHint string + splitInstrumentHint string + } + + AddUnitThenFocus InstrumentEditor +) func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor { ret := &InstrumentEditor{ @@ -89,7 +95,8 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor { ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: name, IconBytes: icons.ImageAudiotrack, Doer: model.LoadPreset(index)}) return true }) - ret.addUnitBtn = NewActionClickable(model.AddUnitAndThen(func() { ret.searchEditor.Focus() })) + ret.addUnit = model.AddUnit(false) + ret.addUnitBtn = NewActionClickable(tracker.MakeEnabledAction(ret.AddUnitThenFocus())) ret.enlargeHint = makeHint("Enlarge", " (%s)", "InstrEnlargedToggle") ret.shrinkHint = makeHint("Shrink", " (%s)", "InstrEnlargedToggle") ret.addInstrumentHint = makeHint("Add\ninstrument", "\n(%s)", "AddInstrument") @@ -107,6 +114,15 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor { return ret } +func (ie *InstrumentEditor) AddUnitThenFocus() tracker.Action { + return tracker.MakeAction((*AddUnitThenFocus)(ie)) +} +func (a *AddUnitThenFocus) Enabled() bool { return a.addUnit.Enabled() } +func (a *AddUnitThenFocus) Do() { + a.addUnit.Do() + a.searchEditor.Focus() +} + func (ie *InstrumentEditor) Focus() { ie.unitDragList.Focus() } diff --git a/tracker/gioui/menu.go b/tracker/gioui/menu.go index 8f1aadb..1413a21 100644 --- a/tracker/gioui/menu.go +++ b/tracker/gioui/menu.go @@ -93,7 +93,7 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D { defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() var macro op.MacroOp item := &items[i] - if i == m.Menu.hover-1 && item.Doer.Allowed() { + if i == m.Menu.hover-1 && item.Doer.Enabled() { macro = op.Record(gtx.Ops) } icon := widgetForIcon(item.IconBytes) @@ -102,7 +102,7 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D { textLabel := Label(m.Theme, &m.Theme.Menu.Text, item.Text) shortcutLabel := Label(m.Theme, &m.Theme.Menu.Text, item.ShortcutText) shortcutLabel.Color = m.ShortCutColor - if !item.Doer.Allowed() { + if !item.Doer.Enabled() { iconColor = m.Disabled textLabel.Color = m.Disabled shortcutLabel.Color = m.Disabled @@ -122,14 +122,14 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D { return shortcutInset.Layout(gtx, shortcutLabel.Layout) }), ) - if i == m.Menu.hover-1 && item.Doer.Allowed() { + if i == m.Menu.hover-1 && item.Doer.Enabled() { recording := macro.Stop() paint.FillShape(gtx.Ops, m.HoverColor, clip.Rect{ Max: image.Pt(dims.Size.X, dims.Size.Y), }.Op()) recording.Add(gtx.Ops) } - if item.Doer.Allowed() { + if item.Doer.Enabled() { rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y) area := clip.Rect(rect).Push(gtx.Ops) event.Op(gtx.Ops, &m.Menu.tags[i]) diff --git a/tracker/gioui/unit_editor.go b/tracker/gioui/unit_editor.go index ba20b1d..bdb9878 100644 --- a/tracker/gioui/unit_editor.go +++ b/tracker/gioui/unit_editor.go @@ -332,11 +332,11 @@ func (p ParameterStyle) Layout(gtx C) D { name, _, _, _ := p.tracker.Instruments().Item(i) instrItems[i].Text = name instrItems[i].IconBytes = icons.NavigationChevronRight - instrItems[i].Doer = tracker.Allow(func() { + instrItems[i].Doer = tracker.MakeEnabledAction((tracker.DoFunc)(func() { if id, ok := p.tracker.Instruments().FirstID(i); ok { tracker.Int{IntData: p.w.Parameter}.Set(id) } - }) + })) } var unitItems []MenuItem instrName := "" @@ -350,9 +350,9 @@ func (p ParameterStyle) Layout(gtx C) D { id := unit.ID unitItems[j].Text = buildUnitLabel(j, unit) unitItems[j].IconBytes = icons.NavigationChevronRight - unitItems[j].Doer = tracker.Allow(func() { + unitItems[j].Doer = tracker.MakeEnabledAction((tracker.DoFunc)(func() { tracker.Int{IntData: p.w.Parameter}.Set(id) - }) + })) } } defer pointer.PassOp{}.Push(gtx.Ops).Pop() diff --git a/tracker/presets.go b/tracker/presets.go index 25c2e67..beed73d 100644 --- a/tracker/presets.go +++ b/tracker/presets.go @@ -143,22 +143,22 @@ func NumPresets() int { // LoadPreset loads a preset from the list of instrument presets. The index // should be within the range of 0 to NumPresets()-1. + func (m *Model) LoadPreset(index int) Action { - return Action{do: func() { - defer m.change("LoadPreset", PatchChange, MajorChange)() - if m.d.InstrIndex < 0 { - m.d.InstrIndex = 0 - } - m.d.InstrIndex2 = m.d.InstrIndex - for m.d.InstrIndex >= len(m.d.Song.Patch) { - m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy()) - } - newInstr := instrumentPresets[index].Copy() - (*Model)(m).assignUnitIDs(newInstr.Units) - m.d.Song.Patch[m.d.InstrIndex] = newInstr - }, allowed: func() bool { - return true - }} + return MakeEnabledAction(LoadPreset{Index: index, Model: m}) +} +func (m LoadPreset) Do() { + defer m.change("LoadPreset", PatchChange, MajorChange)() + if m.d.InstrIndex < 0 { + m.d.InstrIndex = 0 + } + m.d.InstrIndex2 = m.d.InstrIndex + for m.d.InstrIndex >= len(m.d.Song.Patch) { + m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy()) + } + newInstr := instrumentPresets[m.Index].Copy() + m.Model.assignUnitIDs(newInstr.Units) + m.d.Song.Patch[m.d.InstrIndex] = newInstr } type instrumentPresetsSlice []sointu.Instrument