refactor(tracker): Make Action have separate Doer and Enabler

This commit is contained in:
5684185+vsariola@users.noreply.github.com
2025-05-26 13:35:31 +03:00
parent d6badb97be
commit 036cb1f34d
7 changed files with 600 additions and 480 deletions

View File

@ -10,85 +10,174 @@ import (
) )
type ( type (
// Action describes a user action that can be performed on the model. It is // Action describes a user action that can be performed on the model, which
// usually a button press or a menu item. Action advertises whether it is // can be initiated by calling the Do() method. It is usually initiated by a
// allowed to be performed or not. // 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 { Action struct {
do func() doer Doer
allowed func() bool 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 // Action methods
func (e Action) Do() { // simple version for when both doer and enabler are the same
if e.allowed != nil && e.allowed() { func MakeAction(doerEnabler interface {
e.do() 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 { func (a Action) Enabled() bool {
return e.allowed != nil && e.allowed() if a.enabler == nil {
return true // no enabler, always allowed
}
return a.enabler.Enabled()
} }
func Allow(do func()) Action { // DoFunc
return Action{do: do, allowed: func() bool { return true }}
}
func Check(do func(), allowed func() bool) Action { func (d DoFunc) Do() { d() }
return Action{do: do, allowed: allowed}
}
// Model methods // AddTrack
func (m *Model) AddTrack() Action { func (m *Model) AddTrack() Action { return MakeAction((*AddTrack)(m)) }
return Action{ func (m *AddTrack) Enabled() bool { return m.d.Song.Score.NumVoices() < vm.MAX_VOICES }
allowed: func() bool { return m.d.Song.Score.NumVoices() < vm.MAX_VOICES }, func (m *AddTrack) Do() {
do: func() {
defer (*Model)(m).change("AddTrack", SongChange, MajorChange)() defer (*Model)(m).change("AddTrack", SongChange, MajorChange)()
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track) voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
p := sointu.Patch{defaultInstrument.Copy()} p := sointu.Patch{defaultInstrument.Copy()}
t := []sointu.Track{sointu.Track{NumVoices: 1}} t := []sointu.Track{{NumVoices: 1}}
_, _, ok := m.addVoices(voiceIndex, p, t, (*Model)(m).linkInstrTrack, true) _, _, ok := (*Model)(m).addVoices(voiceIndex, p, t, (*Model)(m).linkInstrTrack, true)
m.changeCancel = !ok m.changeCancel = !ok
},
}
} }
func (m *Model) DeleteTrack() Action { // DeleteTrack
return Action{
allowed: func() bool { return len(m.d.Song.Score.Tracks) > 0 },
do: func() { m.Tracks().List().DeleteElements(false) },
}
}
func (m *Model) AddInstrument() Action { func (m *Model) DeleteTrack() Action { return MakeAction((*DeleteTrack)(m)) }
return Action{ func (m *DeleteTrack) Enabled() bool { return len(m.d.Song.Score.Tracks) > 0 }
allowed: func() bool { return (*Model)(m).d.Song.Patch.NumVoices() < vm.MAX_VOICES }, func (m *DeleteTrack) Do() { (*Model)(m).Tracks().List().DeleteElements(false) }
do: func() {
// 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)() defer (*Model)(m).change("AddInstrument", SongChange, MajorChange)()
voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex) voiceIndex := m.d.Song.Patch.FirstVoiceForInstrument(m.d.InstrIndex)
p := sointu.Patch{defaultInstrument.Copy()} p := sointu.Patch{defaultInstrument.Copy()}
t := []sointu.Track{sointu.Track{NumVoices: 1}} t := []sointu.Track{{NumVoices: 1}}
_, _, ok := m.addVoices(voiceIndex, p, t, true, (*Model)(m).linkInstrTrack) _, _, ok := (*Model)(m).addVoices(voiceIndex, p, t, true, (*Model)(m).linkInstrTrack)
m.changeCancel = !ok m.changeCancel = !ok
},
}
} }
func (m *Model) DeleteInstrument() Action { // DeleteInstrument
return Action{
allowed: func() bool { return len((*Model)(m).d.Song.Patch) > 0 },
do: func() { m.Instruments().List().DeleteElements(false) },
}
}
func (m *Model) SplitTrack() Action { func (m *Model) DeleteInstrument() Action { return MakeAction((*DeleteInstrument)(m)) }
return Action{ func (m *DeleteInstrument) Enabled() bool { return len((*Model)(m).d.Song.Patch) > 0 }
allowed: func() bool { 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 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() { func (m *SplitTrack) Do() {
defer (*Model)(m).change("SplitTrack", SongChange, MajorChange)() defer (*Model)(m).change("SplitTrack", SongChange, MajorChange)()
voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track) voiceIndex := m.d.Song.Score.FirstVoiceForTrack(m.d.Cursor.Track)
middle := voiceIndex + (m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices+1)/2 middle := voiceIndex + (m.d.Song.Score.Tracks[m.d.Cursor.Track].NumVoices+1)/2
@ -106,16 +195,15 @@ func (m *Model) SplitTrack() Action {
newTrack := sointu.Track{NumVoices: end - middle} newTrack := sointu.Track{NumVoices: end - middle}
m.d.Song.Score.Tracks = append(left, newTrack) m.d.Song.Score.Tracks = append(left, newTrack)
m.d.Song.Score.Tracks = append(m.d.Song.Score.Tracks, right...) m.d.Song.Score.Tracks = append(m.d.Song.Score.Tracks, right...)
},
}
} }
func (m *Model) SplitInstrument() Action { // SplitInstrument
return Action{
allowed: func() bool { 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 return m.d.InstrIndex >= 0 && m.d.InstrIndex < len(m.d.Song.Patch) && m.d.Song.Patch[m.d.InstrIndex].NumVoices > 1
}, }
do: func() { func (m *SplitInstrument) Do() {
defer (*Model)(m).change("SplitInstrument", SongChange, MajorChange)() defer (*Model)(m).change("SplitInstrument", SongChange, MajorChange)()
voiceIndex := m.d.Song.Patch.Copy().FirstVoiceForInstrument(m.d.InstrIndex) voiceIndex := m.d.Song.Patch.Copy().FirstVoiceForInstrument(m.d.InstrIndex)
middle := voiceIndex + (m.d.Song.Patch[m.d.InstrIndex].NumVoices+1)/2 middle := voiceIndex + (m.d.Song.Patch[m.d.InstrIndex].NumVoices+1)/2
@ -131,16 +219,19 @@ func (m *Model) SplitInstrument() Action {
return return
} }
newInstrument := defaultInstrument.Copy() newInstrument := defaultInstrument.Copy()
m.assignUnitIDs(newInstrument.Units) (*Model)(m).assignUnitIDs(newInstrument.Units)
newInstrument.NumVoices = end - middle newInstrument.NumVoices = end - middle
m.d.Song.Patch = append(left, newInstrument) m.d.Song.Patch = append(left, newInstrument)
m.d.Song.Patch = append(m.d.Song.Patch, right...) m.d.Song.Patch = append(m.d.Song.Patch, right...)
},
}
} }
// AddUnit
func (m *Model) AddUnit(before bool) Action { func (m *Model) AddUnit(before bool) Action {
return Allow(func() { return MakeEnabledAction(AddUnit{Before: before, Model: m})
}
func (a AddUnit) Do() {
m := (*Model)(a.Model)
defer m.change("AddUnitAction", PatchChange, MajorChange)() defer m.change("AddUnitAction", PatchChange, MajorChange)()
if len(m.d.Song.Patch) == 0 { // no instruments, add one if len(m.d.Song.Patch) == 0 { // no instruments, add one
instr := sointu.Instrument{NumVoices: 1} instr := sointu.Instrument{NumVoices: 1}
@ -148,7 +239,7 @@ func (m *Model) AddUnit(before bool) Action {
m.d.Song.Patch = append(m.d.Song.Patch, instr) m.d.Song.Patch = append(m.d.Song.Patch, instr)
m.d.UnitIndex = 0 m.d.UnitIndex = 0
} else { } else {
if !before { if !a.Before {
m.d.UnitIndex++ m.d.UnitIndex++
} }
} }
@ -162,47 +253,39 @@ func (m *Model) AddUnit(before bool) Action {
m.assignUnitIDs(newUnits[m.d.UnitIndex : m.d.UnitIndex+1]) m.assignUnitIDs(newUnits[m.d.UnitIndex : m.d.UnitIndex+1])
m.d.Song.Patch[m.d.InstrIndex].Units = newUnits m.d.Song.Patch[m.d.InstrIndex].Units = newUnits
m.d.ParamIndex = 0 m.d.ParamIndex = 0
})
} }
func (m *Model) AddUnitAndThen(callback func()) Action { // DeleteUnit
return Allow(func() {
m.AddUnit(false).Do()
callback()
})
}
func (m *Model) DeleteUnit() Action { func (m *Model) DeleteUnit() Action { return MakeAction((*DeleteUnit)(m)) }
return Action{ func (m *DeleteUnit) Enabled() bool {
allowed: func() bool { i := (*Model)(m).d.InstrIndex
return len((*Model)(m).d.Song.Patch) > 0 && len((*Model)(m).d.Song.Patch[(*Model)(m).d.InstrIndex].Units) > 1 return i >= 0 && i < len((*Model)(m).d.Song.Patch) && len((*Model)(m).d.Song.Patch[i].Units) > 1
}, }
do: func() { func (m *DeleteUnit) Do() {
defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)() defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)()
m.Units().List().DeleteElements(true) (*Model)(m).Units().List().DeleteElements(true)
},
}
} }
func (m *Model) ClearUnit() Action { // ClearUnit
return Action{
do: func() { 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)() 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.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] = sointu.Unit{}
m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].ID = m.maxID() + 1 m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].ID = (*Model)(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
},
} }
}
func (m *Model) Undo() Action { // Undo
return Action{
allowed: func() bool { return len((*Model)(m).undoStack) > 0 }, func (m *Model) Undo() Action { return MakeAction((*Undo)(m)) }
do: func() { func (m *Undo) Enabled() bool { return len((*Model)(m).undoStack) > 0 }
func (m *Undo) Do() {
m.redoStack = append(m.redoStack, m.d.Copy()) m.redoStack = append(m.redoStack, m.d.Copy())
if len(m.redoStack) >= maxUndo { if len(m.redoStack) >= maxUndo {
copy(m.redoStack, m.redoStack[len(m.redoStack)-maxUndo:]) copy(m.redoStack, m.redoStack[len(m.redoStack)-maxUndo:])
@ -212,14 +295,13 @@ func (m *Model) Undo() Action {
m.undoStack = m.undoStack[:len(m.undoStack)-1] m.undoStack = m.undoStack[:len(m.undoStack)-1]
m.prevUndoKind = "" m.prevUndoKind = ""
TrySend(m.broker.ToPlayer, any(m.d.Song.Copy())) TrySend(m.broker.ToPlayer, any(m.d.Song.Copy()))
},
}
} }
func (m *Model) Redo() Action { // Redo
return Action{
allowed: func() bool { return len((*Model)(m).redoStack) > 0 }, func (m *Model) Redo() Action { return MakeAction((*Redo)(m)) }
do: func() { func (m *Redo) Enabled() bool { return len((*Model)(m).redoStack) > 0 }
func (m *Redo) Do() {
m.undoStack = append(m.undoStack, m.d.Copy()) m.undoStack = append(m.undoStack, m.d.Copy())
if len(m.undoStack) >= maxUndo { if len(m.undoStack) >= maxUndo {
copy(m.undoStack, m.undoStack[len(m.undoStack)-maxUndo:]) copy(m.undoStack, m.undoStack[len(m.undoStack)-maxUndo:])
@ -229,33 +311,38 @@ func (m *Model) Redo() Action {
m.redoStack = m.redoStack[:len(m.redoStack)-1] m.redoStack = m.redoStack[:len(m.redoStack)-1]
m.prevUndoKind = "" m.prevUndoKind = ""
TrySend(m.broker.ToPlayer, any(m.d.Song.Copy())) TrySend(m.broker.ToPlayer, any(m.d.Song.Copy()))
},
}
} }
func (m *Model) AddSemitone() Action { // AddSemiTone
return Allow(func() { Table{(*Notes)(m)}.Add(1) })
}
func (m *Model) SubtractSemitone() Action { func (m *Model) AddSemitone() Action { return MakeEnabledAction((*AddSemitone)(m)) }
return Allow(func() { Table{(*Notes)(m)}.Add(-1) }) func (m *AddSemitone) Do() { Table{(*Notes)(m)}.Add(1) }
}
func (m *Model) AddOctave() Action { // SubtractSemitone
return Allow(func() { Table{(*Notes)(m)}.Add(12) })
}
func (m *Model) SubtractOctave() Action { func (m *Model) SubtractSemitone() Action { return MakeEnabledAction((*SubtractSemitone)(m)) }
return Allow(func() { Table{(*Notes)(m)}.Add(-12) }) func (m *SubtractSemitone) Do() { Table{(*Notes)(m)}.Add(-1) }
}
func (m *Model) EditNoteOff() Action { // AddOctave
return Allow(func() { Table{(*Notes)(m)}.Fill(0) })
}
func (m *Model) RemoveUnused() Action { func (m *Model) AddOctave() Action { return MakeEnabledAction((*AddOctave)(m)) }
return Allow(func() { func (m *AddOctave) Do() { Table{(*Notes)(m)}.Add(12) }
defer m.change("RemoveUnusedAction", ScoreChange, MajorChange)()
// 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 { for trkIndex, trk := range m.d.Song.Score.Tracks {
// assign new indices to patterns // assign new indices to patterns
newIndex := map[int]int{} newIndex := map[int]int{}
@ -308,82 +395,80 @@ func (m *Model) RemoveUnused() Action {
trk.Patterns = newPatterns trk.Patterns = newPatterns
m.d.Song.Score.Tracks[trkIndex] = trk m.d.Song.Score.Tracks[trkIndex] = trk
} }
})
} }
func (m *Model) PlayCurrentPos() Action { // PlayCurrentPos
return Action{
allowed: func() bool { return !m.instrEnlarged }, func (m *Model) PlayCurrentPos() Action { return MakeAction((*PlayCurrentPos)(m)) }
do: func() { func (m *PlayCurrentPos) Enabled() bool { return !m.instrEnlarged }
m.setPanic(false) func (m *PlayCurrentPos) Do() {
m.setLoop(Loop{}) (*Model)(m).setPanic(false)
(*Model)(m).setLoop(Loop{})
m.playing = true m.playing = true
TrySend(m.broker.ToPlayer, any(StartPlayMsg{m.d.Cursor.SongPos})) TrySend(m.broker.ToPlayer, any(StartPlayMsg{m.d.Cursor.SongPos}))
},
}
} }
func (m *Model) PlaySongStart() Action { // PlaySongStart
return Action{
allowed: func() bool { return !m.instrEnlarged }, func (m *Model) PlaySongStart() Action { return MakeAction((*PlaySongStart)(m)) }
do: func() { func (m *PlaySongStart) Enabled() bool { return !m.instrEnlarged }
m.setPanic(false) func (m *PlaySongStart) Do() {
m.setLoop(Loop{}) (*Model)(m).setPanic(false)
(*Model)(m).setLoop(Loop{})
m.playing = true m.playing = true
TrySend(m.broker.ToPlayer, any(StartPlayMsg{})) TrySend(m.broker.ToPlayer, any(StartPlayMsg{}))
},
}
} }
func (m *Model) PlaySelected() Action { // PlaySelected
return Action{
allowed: func() bool { return !m.instrEnlarged }, func (m *Model) PlaySelected() Action { return MakeAction((*PlaySelected)(m)) }
do: func() { func (m *PlaySelected) Enabled() bool { return !m.instrEnlarged }
m.setPanic(false) func (m *PlaySelected) Do() {
(*Model)(m).setPanic(false)
m.playing = true m.playing = true
l := m.OrderRows().List() l := (*Model)(m).OrderRows().List()
r := l.listRange() r := l.listRange()
newLoop := Loop{r.Start, r.End - r.Start} newLoop := Loop{r.Start, r.End - r.Start}
m.setLoop(newLoop) (*Model)(m).setLoop(newLoop)
TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: r.Start, PatternRow: 0}})) TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: r.Start, PatternRow: 0}}))
},
}
} }
func (m *Model) PlayFromLoopStart() Action { // PlayFromLoopStart
return Action{
allowed: func() bool { return !m.instrEnlarged }, func (m *Model) PlayFromLoopStart() Action { return MakeAction((*PlayFromLoopStart)(m)) }
do: func() { func (m *PlayFromLoopStart) Enabled() bool { return !m.instrEnlarged }
m.setPanic(false) func (m *PlayFromLoopStart) Do() {
(*Model)(m).setPanic(false)
if m.loop == (Loop{}) { if m.loop == (Loop{}) {
m.PlaySelected().Do() (*Model)(m).PlaySelected().Do()
return return
} }
m.playing = true m.playing = true
TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: m.loop.Start, PatternRow: 0}})) TrySend(m.broker.ToPlayer, any(StartPlayMsg{sointu.SongPos{OrderRow: m.loop.Start, PatternRow: 0}}))
},
}
} }
func (m *Model) StopPlaying() Action { // StopPlaying
return Action{
allowed: func() bool { return true }, func (m *Model) StopPlaying() Action { return MakeEnabledAction((*StopPlaying)(m)) }
do: func() { func (m *StopPlaying) Do() {
if !m.playing { if !m.playing {
m.setPanic(true) (*Model)(m).setPanic(true)
m.setLoop(Loop{}) (*Model)(m).setLoop(Loop{})
return return
} }
m.playing = false m.playing = false
TrySend(m.broker.ToPlayer, any(IsPlayingMsg{false})) TrySend(m.broker.ToPlayer, any(IsPlayingMsg{false}))
},
}
} }
// AddOrderRow
func (m *Model) AddOrderRow(before bool) Action { func (m *Model) AddOrderRow(before bool) Action {
return Allow(func() { return MakeEnabledAction(AddOrderRow{Before: before, Model: m})
}
func (a AddOrderRow) Do() {
m := a.Model
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)() defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
if !before { if !a.Before {
m.d.Cursor.OrderRow++ m.d.Cursor.OrderRow++
} }
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
@ -397,11 +482,15 @@ func (m *Model) AddOrderRow(before bool) Action {
(*order)[from] = -1 (*order)[from] = -1
} }
} }
})
} }
// DeleteOrderRow
func (m *Model) DeleteOrderRow(backwards bool) Action { func (m *Model) DeleteOrderRow(backwards bool) Action {
return Allow(func() { return MakeEnabledAction(DeleteOrderRow{Backwards: backwards, Model: m})
}
func (d DeleteOrderRow) Do() {
m := d.Model
defer m.change("AddOrderRowAction", ScoreChange, MinorChange)() defer m.change("AddOrderRowAction", ScoreChange, MinorChange)()
from := m.d.Cursor.OrderRow from := m.d.Cursor.OrderRow
m.d.Song.Score.Length-- m.d.Song.Score.Length--
@ -412,47 +501,49 @@ func (m *Model) DeleteOrderRow(backwards bool) Action {
*order = (*order)[:len(*order)-1] *order = (*order)[:len(*order)-1]
} }
} }
if backwards { if d.Backwards {
if m.d.Cursor.OrderRow > 0 { if m.d.Cursor.OrderRow > 0 {
m.d.Cursor.OrderRow-- m.d.Cursor.OrderRow--
} }
} }
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
return
})
} }
func (m *Model) NewSong() Action { // NewSong
return Allow(func() {
func (m *Model) NewSong() Action { return MakeEnabledAction((*NewSong)(m)) }
func (m *NewSong) Do() {
m.dialog = NewSongChanges m.dialog = NewSongChanges
m.completeAction(true) (*Model)(m).completeAction(true)
})
} }
func (m *Model) OpenSong() Action { // OpenSong
return Allow(func() {
func (m *Model) OpenSong() Action { return MakeEnabledAction((*OpenSong)(m)) }
func (m *OpenSong) Do() {
m.dialog = OpenSongChanges m.dialog = OpenSongChanges
m.completeAction(true) (*Model)(m).completeAction(true)
})
} }
func (m *Model) RequestQuit() Action { // RequestQuit
return Allow(func() {
func (m *Model) RequestQuit() Action { return MakeEnabledAction((*RequestQuit)(m)) }
func (m *RequestQuit) Do() {
if !m.quitted { if !m.quitted {
m.dialog = QuitChanges m.dialog = QuitChanges
m.completeAction(true) (*Model)(m).completeAction(true)
} }
})
} }
func (m *Model) ForceQuit() Action { // ForceQuit
return Allow(func() {
m.quitted = true
})
}
func (m *Model) SaveSong() Action { func (m *Model) ForceQuit() Action { return MakeEnabledAction((*ForceQuit)(m)) }
return Allow(func() { 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 == "" { if m.d.FilePath == "" {
switch m.dialog { switch m.dialog {
case NoDialog: case NoDialog:
@ -468,30 +559,43 @@ func (m *Model) SaveSong() Action {
} }
f, err := os.Create(m.d.FilePath) f, err := os.Create(m.d.FilePath)
if err != nil { if err != nil {
m.Alerts().Add("Error creating file: "+err.Error(), Error) (*Model)(m).Alerts().Add("Error creating file: "+err.Error(), Error)
return return
} }
m.WriteSong(f) (*Model)(m).WriteSong(f)
m.d.ChangedSinceSave = false m.d.ChangedSinceSave = false
})
} }
func (m *Model) DiscardSong() Action { return Allow(func() { m.completeAction(false) }) } func (m *Model) DiscardSong() Action { return MakeEnabledAction((*DiscardSong)(m)) }
func (m *Model) SaveSongAs() Action { return Allow(func() { m.dialog = SaveAsExplorer }) } func (m *DiscardSong) Do() { (*Model)(m).completeAction(false) }
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) SaveSongAs() Action { return MakeEnabledAction((*SaveSongAs)(m)) }
func (m *Model) ExportFloat() Action { return Allow(func() { m.dialog = ExportFloatExplorer }) } func (m *SaveSongAs) Do() { m.dialog = SaveAsExplorer }
func (m *Model) ExportInt16() Action { return Allow(func() { m.dialog = ExportInt16Explorer }) }
func (m *Model) Cancel() Action { return MakeEnabledAction((*Cancel)(m)) }
func (m *Cancel) Do() { m.dialog = NoDialog }
func (m *Model) Export() Action { return MakeEnabledAction((*ExportAction)(m)) }
func (m *ExportAction) Do() { m.dialog = Export }
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) SelectMidiInput(item MIDIDevice) Action { func (m *Model) SelectMidiInput(item MIDIDevice) Action {
return Allow(func() { return MakeEnabledAction(SelectMidiInput{Item: item, Model: m})
if err := item.Open(); err == nil { }
message := fmt.Sprintf("Opened MIDI device: %s", item) 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) m.Alerts().Add(message, Info)
} else { } else {
message := fmt.Sprintf("Could not open MIDI device: %s", item) message := fmt.Sprintf("Could not open MIDI device: %s", s.Item)
m.Alerts().Add(message, Error) m.Alerts().Add(message, Error)
} }
})
} }
func (m *Model) completeAction(checkSave bool) { func (m *Model) completeAction(checkSave bool) {

View File

@ -69,7 +69,7 @@ func ActionIcon(gtx C, th *Theme, w *ActionClickable, icon []byte, tip string) T
for w.Clickable.Clicked(gtx) { for w.Clickable.Clicked(gtx) {
w.Action.Do() w.Action.Do()
} }
if !w.Action.Allowed() { if !w.Action.Enabled() {
ret.IconButtonStyle.Color = th.Button.Disabled.Color ret.IconButtonStyle.Color = th.Button.Disabled.Color
} }
return ret return ret
@ -119,7 +119,7 @@ func ActionButton(gtx C, th *Theme, style *ButtonStyle, w *ActionClickable, text
for w.Clickable.Clicked(gtx) { for w.Clickable.Clicked(gtx) {
w.Action.Do() w.Action.Do()
} }
if !w.Action.Allowed() { if !w.Action.Enabled() {
return Btn(th, &th.Button.Disabled, &w.Clickable, text) return Btn(th, &th.Button.Disabled, &w.Clickable, text)
} }
return Btn(th, style, &w.Clickable, text) return Btn(th, style, &w.Clickable, text)

View File

@ -92,7 +92,7 @@ func (d *Dialog) handleKeys(gtx C) {
for d.BtnCancel.Clicked(gtx) { for d.BtnCancel.Clicked(gtx) {
d.cancel.Do() d.cancel.Do()
} }
if d.alt.Allowed() { if d.alt.Enabled() {
d.handleKeysForButton(gtx, &d.BtnAlt, &d.BtnCancel, &d.BtnOk) d.handleKeysForButton(gtx, &d.BtnAlt, &d.BtnCancel, &d.BtnOk)
d.handleKeysForButton(gtx, &d.BtnCancel, &d.BtnOk, &d.BtnAlt) d.handleKeysForButton(gtx, &d.BtnCancel, &d.BtnOk, &d.BtnAlt)
d.handleKeysForButton(gtx, &d.BtnOk, &d.BtnAlt, &d.BtnCancel) 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 { layout.Rigid(func(gtx C) D {
return layout.E.Layout(gtx, func(gtx C) D { return layout.E.Layout(gtx, func(gtx C) D {
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(120)) 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, return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween}.Layout(gtx,
layout.Rigid(d.OkStyle.Layout), layout.Rigid(d.OkStyle.Layout),
layout.Rigid(d.AltStyle.Layout), layout.Rigid(d.AltStyle.Layout),

View File

@ -22,7 +22,8 @@ import (
"golang.org/x/exp/shiny/materialdesign/icons" "golang.org/x/exp/shiny/materialdesign/icons"
) )
type InstrumentEditor struct { type (
InstrumentEditor struct {
newInstrumentBtn *ActionClickable newInstrumentBtn *ActionClickable
enlargeBtn *BoolClickable enlargeBtn *BoolClickable
deleteInstrumentBtn *ActionClickable deleteInstrumentBtn *ActionClickable
@ -48,6 +49,8 @@ type InstrumentEditor struct {
presetMenuItems []MenuItem presetMenuItems []MenuItem
presetMenu Menu presetMenu Menu
addUnit tracker.Action
enlargeHint, shrinkHint string enlargeHint, shrinkHint string
addInstrumentHint string addInstrumentHint string
octaveHint string octaveHint string
@ -61,6 +64,9 @@ type InstrumentEditor struct {
splitInstrumentHint string splitInstrumentHint string
} }
AddUnitThenFocus InstrumentEditor
)
func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor { func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
ret := &InstrumentEditor{ ret := &InstrumentEditor{
newInstrumentBtn: NewActionClickable(model.AddInstrument()), newInstrumentBtn: NewActionClickable(model.AddInstrument()),
@ -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)}) ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: name, IconBytes: icons.ImageAudiotrack, Doer: model.LoadPreset(index)})
return true 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.enlargeHint = makeHint("Enlarge", " (%s)", "InstrEnlargedToggle")
ret.shrinkHint = makeHint("Shrink", " (%s)", "InstrEnlargedToggle") ret.shrinkHint = makeHint("Shrink", " (%s)", "InstrEnlargedToggle")
ret.addInstrumentHint = makeHint("Add\ninstrument", "\n(%s)", "AddInstrument") ret.addInstrumentHint = makeHint("Add\ninstrument", "\n(%s)", "AddInstrument")
@ -107,6 +114,15 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
return ret 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() { func (ie *InstrumentEditor) Focus() {
ie.unitDragList.Focus() ie.unitDragList.Focus()
} }

View File

@ -93,7 +93,7 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop() defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
var macro op.MacroOp var macro op.MacroOp
item := &items[i] 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) macro = op.Record(gtx.Ops)
} }
icon := widgetForIcon(item.IconBytes) 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) textLabel := Label(m.Theme, &m.Theme.Menu.Text, item.Text)
shortcutLabel := Label(m.Theme, &m.Theme.Menu.Text, item.ShortcutText) shortcutLabel := Label(m.Theme, &m.Theme.Menu.Text, item.ShortcutText)
shortcutLabel.Color = m.ShortCutColor shortcutLabel.Color = m.ShortCutColor
if !item.Doer.Allowed() { if !item.Doer.Enabled() {
iconColor = m.Disabled iconColor = m.Disabled
textLabel.Color = m.Disabled textLabel.Color = m.Disabled
shortcutLabel.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) 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() recording := macro.Stop()
paint.FillShape(gtx.Ops, m.HoverColor, clip.Rect{ paint.FillShape(gtx.Ops, m.HoverColor, clip.Rect{
Max: image.Pt(dims.Size.X, dims.Size.Y), Max: image.Pt(dims.Size.X, dims.Size.Y),
}.Op()) }.Op())
recording.Add(gtx.Ops) recording.Add(gtx.Ops)
} }
if item.Doer.Allowed() { if item.Doer.Enabled() {
rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y) rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y)
area := clip.Rect(rect).Push(gtx.Ops) area := clip.Rect(rect).Push(gtx.Ops)
event.Op(gtx.Ops, &m.Menu.tags[i]) event.Op(gtx.Ops, &m.Menu.tags[i])

View File

@ -332,11 +332,11 @@ func (p ParameterStyle) Layout(gtx C) D {
name, _, _, _ := p.tracker.Instruments().Item(i) name, _, _, _ := p.tracker.Instruments().Item(i)
instrItems[i].Text = name instrItems[i].Text = name
instrItems[i].IconBytes = icons.NavigationChevronRight 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 { if id, ok := p.tracker.Instruments().FirstID(i); ok {
tracker.Int{IntData: p.w.Parameter}.Set(id) tracker.Int{IntData: p.w.Parameter}.Set(id)
} }
}) }))
} }
var unitItems []MenuItem var unitItems []MenuItem
instrName := "<instr>" instrName := "<instr>"
@ -350,9 +350,9 @@ func (p ParameterStyle) Layout(gtx C) D {
id := unit.ID id := unit.ID
unitItems[j].Text = buildUnitLabel(j, unit) unitItems[j].Text = buildUnitLabel(j, unit)
unitItems[j].IconBytes = icons.NavigationChevronRight 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) tracker.Int{IntData: p.w.Parameter}.Set(id)
}) }))
} }
} }
defer pointer.PassOp{}.Push(gtx.Ops).Pop() defer pointer.PassOp{}.Push(gtx.Ops).Pop()

View File

@ -143,8 +143,11 @@ func NumPresets() int {
// LoadPreset loads a preset from the list of instrument presets. The index // LoadPreset loads a preset from the list of instrument presets. The index
// should be within the range of 0 to NumPresets()-1. // should be within the range of 0 to NumPresets()-1.
func (m *Model) LoadPreset(index int) Action { func (m *Model) LoadPreset(index int) Action {
return Action{do: func() { return MakeEnabledAction(LoadPreset{Index: index, Model: m})
}
func (m LoadPreset) Do() {
defer m.change("LoadPreset", PatchChange, MajorChange)() defer m.change("LoadPreset", PatchChange, MajorChange)()
if m.d.InstrIndex < 0 { if m.d.InstrIndex < 0 {
m.d.InstrIndex = 0 m.d.InstrIndex = 0
@ -153,12 +156,9 @@ func (m *Model) LoadPreset(index int) Action {
for m.d.InstrIndex >= len(m.d.Song.Patch) { for m.d.InstrIndex >= len(m.d.Song.Patch) {
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy()) m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
} }
newInstr := instrumentPresets[index].Copy() newInstr := instrumentPresets[m.Index].Copy()
(*Model)(m).assignUnitIDs(newInstr.Units) m.Model.assignUnitIDs(newInstr.Units)
m.d.Song.Patch[m.d.InstrIndex] = newInstr m.d.Song.Patch[m.d.InstrIndex] = newInstr
}, allowed: func() bool {
return true
}}
} }
type instrumentPresetsSlice []sointu.Instrument type instrumentPresetsSlice []sointu.Instrument