mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-14 11:04:23 -04:00
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.
This commit is contained in:
parent
6d3c65e11d
commit
d92426a100
2
.gitignore
vendored
2
.gitignore
vendored
@ -31,3 +31,5 @@ actual_output/
|
||||
**/__debug_bin
|
||||
*.exe
|
||||
*.dll
|
||||
|
||||
**/testdata/fuzz/
|
@ -5,11 +5,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## Unreleased
|
||||
### Added
|
||||
- Massive rewrite of the GUI, in particular allowing better copying, pasting and
|
||||
scrolling of table-based data (order list and note data).
|
||||
- Dbgain unit, which allows defining the gain in decibels (-40 dB to +40dB)
|
||||
|
||||
### Fixed
|
||||
- 32-bit su_load_gmdls clobbered ebx, even though __stdcall demands it to be not
|
||||
touched
|
||||
- Spaces are allowed in instrument names (#120)
|
||||
|
||||
## v0.3.0
|
||||
### Added
|
||||
|
5
audio.go
5
audio.go
@ -66,7 +66,7 @@ type (
|
||||
|
||||
// Play plays the Song by first compiling the patch with the given Synther,
|
||||
// returning the stereo audio buffer as a result (and possible errors).
|
||||
func Play(synther Synther, song Song) (AudioBuffer, error) {
|
||||
func Play(synther Synther, song Song, progress func(float32)) (AudioBuffer, error) {
|
||||
err := song.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -125,6 +125,9 @@ func Play(synther Synther, song Song) (AudioBuffer, error) {
|
||||
return nil, fmt.Errorf("Song speed modulation likely so slow that row never advances; error at pattern %v, row %v", pattern, patternRow)
|
||||
}
|
||||
}
|
||||
if progress != nil {
|
||||
progress(float32(row+1) / float32(song.Score.LengthInRows()))
|
||||
}
|
||||
}
|
||||
return buffer, nil
|
||||
}
|
||||
|
@ -87,7 +87,7 @@ func main() {
|
||||
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
|
||||
}
|
||||
}
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song) // render the song to calculate its length
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song, nil) // render the song to calculate its length
|
||||
if err != nil {
|
||||
return fmt.Errorf("sointu.Play failed: %v", err)
|
||||
}
|
||||
|
@ -33,16 +33,16 @@ var memprofile = flag.String("memprofile", "", "write memory profile to `file`")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
var f *os.File
|
||||
if *cpuprofile != "" {
|
||||
f, err := os.Create(*cpuprofile)
|
||||
var err error
|
||||
f, err = os.Create(*cpuprofile)
|
||||
if err != nil {
|
||||
log.Fatal("could not create CPU profile: ", err)
|
||||
}
|
||||
defer f.Close() // error handling omitted for example
|
||||
if err := pprof.StartCPUProfile(f); err != nil {
|
||||
log.Fatal("could not start CPU profile: ", err)
|
||||
}
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
audioContext, err := oto.NewContext()
|
||||
if err != nil {
|
||||
@ -50,15 +50,12 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
defer audioContext.Close()
|
||||
modelMessages := make(chan interface{}, 1024)
|
||||
playerMessages := make(chan tracker.PlayerMessage, 1024)
|
||||
recoveryFile := ""
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
recoveryFile = filepath.Join(configDir, "Sointu", "sointu-track-recovery")
|
||||
}
|
||||
model := tracker.NewModel(modelMessages, playerMessages, recoveryFile)
|
||||
player := tracker.NewPlayer(cmd.MainSynther, playerMessages, modelMessages)
|
||||
tracker := gioui.NewTracker(model, cmd.MainSynther)
|
||||
model, player := tracker.NewModelPlayer(cmd.MainSynther, recoveryFile)
|
||||
tracker := gioui.NewTracker(model)
|
||||
output := audioContext.Output()
|
||||
defer output.Close()
|
||||
go func() {
|
||||
@ -71,6 +68,10 @@ func main() {
|
||||
}()
|
||||
go func() {
|
||||
tracker.Main()
|
||||
if *cpuprofile != "" {
|
||||
pprof.StopCPUProfile()
|
||||
f.Close()
|
||||
}
|
||||
if *memprofile != "" {
|
||||
f, err := os.Create(*memprofile)
|
||||
if err != nil {
|
||||
|
@ -54,19 +54,16 @@ func init() {
|
||||
version = int32(100)
|
||||
)
|
||||
vst2.PluginAllocator = func(h vst2.Host) (vst2.Plugin, vst2.Dispatcher) {
|
||||
modelMessages := make(chan interface{}, 1024)
|
||||
playerMessages := make(chan tracker.PlayerMessage, 1024)
|
||||
recoveryFile := ""
|
||||
if configDir, err := os.UserConfigDir(); err == nil {
|
||||
randBytes := make([]byte, 16)
|
||||
rand.Read(randBytes)
|
||||
recoveryFile = filepath.Join(configDir, "Sointu", "sointu-vsti-recovery-"+hex.EncodeToString(randBytes))
|
||||
}
|
||||
model := tracker.NewModel(modelMessages, playerMessages, recoveryFile)
|
||||
player := tracker.NewPlayer(cmd.MainSynther, playerMessages, modelMessages)
|
||||
tracker := gioui.NewTracker(model, cmd.MainSynther)
|
||||
tracker.SetInstrEnlarged(true) // start the vsti with the instrument editor enlarged
|
||||
go tracker.Main()
|
||||
model, player := tracker.NewModelPlayer(cmd.MainSynther, recoveryFile)
|
||||
t := gioui.NewTracker(model)
|
||||
tracker.Bool{BoolData: (*tracker.InstrEnlarged)(model)}.Set(true)
|
||||
go t.Main()
|
||||
context := VSTIProcessContext{host: h}
|
||||
buf := make(sointu.AudioBuffer, 1024)
|
||||
return vst2.Plugin{
|
||||
@ -110,14 +107,16 @@ func init() {
|
||||
}
|
||||
},
|
||||
CloseFunc: func() {
|
||||
tracker.Quit(true)
|
||||
tracker.WaitQuitted()
|
||||
t.Exec() <- func() { t.ForceQuit().Do() }
|
||||
t.WaitQuitted()
|
||||
},
|
||||
GetChunkFunc: func(isPreset bool) []byte {
|
||||
return tracker.SafeMarshalRecovery()
|
||||
retChn := make(chan []byte)
|
||||
t.Exec() <- func() { retChn <- t.MarshalRecovery() }
|
||||
return <-retChn
|
||||
},
|
||||
SetChunkFunc: func(data []byte, isPreset bool) {
|
||||
tracker.SafeUnmarshalRecovery(data)
|
||||
t.Exec() <- func() { t.UnmarshalRecovery(data) }
|
||||
},
|
||||
}
|
||||
|
||||
|
92
song.go
92
song.go
@ -67,8 +67,46 @@ type (
|
||||
// the slice only by necessary amount when a new item is added, filling the
|
||||
// unused slots with -1s.
|
||||
Order []int
|
||||
|
||||
// SongPos represents a position in a song, in terms of order row and
|
||||
// pattern row. The order row is the index of the pattern in the order list,
|
||||
// and the pattern row is the index of the row in the pattern.
|
||||
SongPos struct {
|
||||
OrderRow int
|
||||
PatternRow int
|
||||
}
|
||||
)
|
||||
|
||||
func (s *Score) SongPos(songRow int) SongPos {
|
||||
if s.RowsPerPattern == 0 {
|
||||
return SongPos{OrderRow: 0, PatternRow: 0}
|
||||
}
|
||||
orderRow := songRow / s.RowsPerPattern
|
||||
patternRow := songRow % s.RowsPerPattern
|
||||
return SongPos{OrderRow: orderRow, PatternRow: patternRow}
|
||||
}
|
||||
|
||||
func (s *Score) SongRow(songPos SongPos) int {
|
||||
return songPos.OrderRow*s.RowsPerPattern + songPos.PatternRow
|
||||
}
|
||||
|
||||
func (s *Score) Wrap(songPos SongPos) SongPos {
|
||||
ret := s.SongPos(s.SongRow(songPos))
|
||||
ret.OrderRow %= s.Length
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *Score) Clamp(songPos SongPos) SongPos {
|
||||
r := s.SongRow(songPos)
|
||||
if l := s.LengthInRows(); r >= l {
|
||||
r = l - 1
|
||||
}
|
||||
if r < 0 {
|
||||
r = 0
|
||||
}
|
||||
return s.SongPos(r)
|
||||
}
|
||||
|
||||
// Get returns the value at index; or -1 is the index is out of range
|
||||
func (s Order) Get(index int) int {
|
||||
if index < 0 || index >= len(s) {
|
||||
@ -85,6 +123,55 @@ func (s *Order) Set(index, value int) {
|
||||
(*s)[index] = value
|
||||
}
|
||||
|
||||
func (s Track) Note(pos SongPos) byte {
|
||||
if pos.OrderRow < 0 || pos.OrderRow >= len(s.Order) {
|
||||
return 1
|
||||
}
|
||||
pat := s.Order[pos.OrderRow]
|
||||
if pat < 0 || pat >= len(s.Patterns) {
|
||||
return 1
|
||||
}
|
||||
if pos.PatternRow < 0 || pos.PatternRow >= len(s.Patterns[pat]) {
|
||||
return 1
|
||||
}
|
||||
return s.Patterns[pat][pos.PatternRow]
|
||||
}
|
||||
|
||||
func (s *Track) SetNote(pos SongPos, note byte) {
|
||||
if pos.OrderRow < 0 || pos.PatternRow < 0 {
|
||||
return
|
||||
}
|
||||
pat := s.Order.Get(pos.OrderRow)
|
||||
if pat < 0 {
|
||||
if note == 1 {
|
||||
return
|
||||
}
|
||||
for _, o := range s.Order {
|
||||
if pat <= o {
|
||||
pat = o
|
||||
}
|
||||
}
|
||||
pat += 1
|
||||
if pat >= 36 {
|
||||
return
|
||||
}
|
||||
s.Order.Set(pos.OrderRow, pat)
|
||||
}
|
||||
if pat >= len(s.Patterns) && note == 1 {
|
||||
return
|
||||
}
|
||||
for pat >= len(s.Patterns) {
|
||||
s.Patterns = append(s.Patterns, Pattern{})
|
||||
}
|
||||
if pos.PatternRow >= len(s.Patterns[pat]) && note == 1 {
|
||||
return
|
||||
}
|
||||
for pos.PatternRow >= len(s.Patterns[pat]) {
|
||||
s.Patterns[pat] = append(s.Patterns[pat], 1)
|
||||
}
|
||||
s.Patterns[pat][pos.PatternRow] = note
|
||||
}
|
||||
|
||||
// Get returns the value at index; or 1 is the index is out of range
|
||||
func (s Pattern) Get(index int) byte {
|
||||
if index < 0 || index >= len(s) {
|
||||
@ -165,7 +252,10 @@ func (s *Song) Copy() Song {
|
||||
// Assuming 44100 Hz playback speed, return the number of samples of each row of
|
||||
// the song.
|
||||
func (s *Song) SamplesPerRow() int {
|
||||
return 44100 * 60 / (s.BPM * s.RowsPerBeat)
|
||||
if divisor := s.BPM * s.RowsPerBeat; divisor > 0 {
|
||||
return 44100 * 60 / divisor
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Validate checks if the Song looks like a valid song: BPM > 0, one or more
|
||||
|
412
tracker/action.go
Normal file
412
tracker/action.go
Normal file
@ -0,0 +1,412 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
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 struct {
|
||||
do func()
|
||||
allowed func() bool
|
||||
}
|
||||
)
|
||||
|
||||
// Action methods
|
||||
|
||||
func (e Action) Do() {
|
||||
if e.allowed != nil && e.allowed() {
|
||||
e.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("AddTrackAction", ScoreChange, MajorChange)()
|
||||
if len(m.d.Song.Score.Tracks) == 0 { // no instruments, add one
|
||||
m.d.Cursor.Track = 0
|
||||
} else {
|
||||
m.d.Cursor.Track++
|
||||
}
|
||||
m.d.Cursor.Track = intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)), 0)
|
||||
newTracks := make([]sointu.Track, len(m.d.Song.Score.Tracks)+1)
|
||||
copy(newTracks, m.d.Song.Score.Tracks[:m.d.Cursor.Track])
|
||||
copy(newTracks[m.d.Cursor.Track+1:], m.d.Song.Score.Tracks[m.d.Cursor.Track:])
|
||||
newTracks[m.d.Cursor.Track] = sointu.Track{
|
||||
NumVoices: 1,
|
||||
Patterns: []sointu.Pattern{},
|
||||
}
|
||||
m.d.Song.Score.Tracks = newTracks
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) DeleteTrack() Action {
|
||||
return Action{
|
||||
allowed: func() bool { return len(m.d.Song.Score.Tracks) > 0 },
|
||||
do: func() {
|
||||
defer (*Model)(m).change("DeleteTrackAction", ScoreChange, MajorChange)()
|
||||
m.d.Cursor.Track = intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||
newTracks := make([]sointu.Track, len(m.d.Song.Score.Tracks)-1)
|
||||
copy(newTracks, m.d.Song.Score.Tracks[:m.d.Cursor.Track])
|
||||
copy(newTracks[m.d.Cursor.Track:], m.d.Song.Score.Tracks[m.d.Cursor.Track+1:])
|
||||
m.d.Cursor.Track = intMax(intMin(m.d.Cursor.Track, len(m.d.Song.Score.Tracks)-1), 0)
|
||||
m.d.Song.Score.Tracks = newTracks
|
||||
m.d.Cursor2 = m.d.Cursor
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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("AddInstrumentAction", PatchChange, MajorChange)()
|
||||
if len(m.d.Song.Patch) == 0 { // no instruments, add one
|
||||
m.d.InstrIndex = 0
|
||||
} else {
|
||||
m.d.InstrIndex++
|
||||
}
|
||||
m.d.Song.Patch = append(m.d.Song.Patch, sointu.Instrument{})
|
||||
copy(m.d.Song.Patch[m.d.InstrIndex+1:], m.d.Song.Patch[m.d.InstrIndex:])
|
||||
newInstr := defaultInstrument.Copy()
|
||||
(*Model)(m).assignUnitIDs(newInstr.Units)
|
||||
m.d.Song.Patch[m.d.InstrIndex] = newInstr
|
||||
m.d.InstrIndex2 = m.d.InstrIndex
|
||||
m.d.UnitIndex = 0
|
||||
m.d.ParamIndex = 0
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) DeleteInstrument() Action {
|
||||
return Action{
|
||||
allowed: func() bool { return len((*Model)(m).d.Song.Patch) > 0 },
|
||||
do: func() {
|
||||
defer (*Model)(m).change("DeleteInstrumentAction", PatchChange, MajorChange)()
|
||||
m.d.Song.Patch = append(m.d.Song.Patch[:m.d.InstrIndex], m.d.Song.Patch[m.d.InstrIndex+1:]...)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) AddUnit(before bool) Action {
|
||||
return Allow(func() {
|
||||
defer (*Model)(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++
|
||||
}
|
||||
}
|
||||
m.d.InstrIndex = intMax(intMin(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:])
|
||||
(*Model)(m).assignUnitIDs(newUnits[m.d.UnitIndex : m.d.UnitIndex+1])
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units = newUnits
|
||||
m.d.ParamIndex = 0
|
||||
m.d.UnitSearchString = ""
|
||||
})
|
||||
}
|
||||
|
||||
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.UnitSearchString = m.Units().SelectedType()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) ClearUnit() Action {
|
||||
return Action{
|
||||
do: func() {
|
||||
defer (*Model)(m).change("DeleteUnitAction", PatchChange, MajorChange)()
|
||||
m.d.UnitIndex = intMax(intMin(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.UnitSearchString = ""
|
||||
},
|
||||
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 {
|
||||
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 = ""
|
||||
(*Model)(m).send(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 = ""
|
||||
(*Model)(m).send(m.d.Song.Copy())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) AddSemitone() Action {
|
||||
return Allow(func() { Table{(*Notes)(m)}.Add(1) })
|
||||
}
|
||||
|
||||
func (m *Model) SubtractSemitone() Action {
|
||||
return Allow(func() { Table{(*Notes)(m)}.Add(-1) })
|
||||
}
|
||||
|
||||
func (m *Model) AddOctave() Action {
|
||||
return Allow(func() { Table{(*Notes)(m)}.Add(12) })
|
||||
}
|
||||
|
||||
func (m *Model) SubtractOctave() Action {
|
||||
return Allow(func() { Table{(*Notes)(m)}.Add(-12) })
|
||||
}
|
||||
|
||||
func (m *Model) EditNoteOff() Action {
|
||||
return Allow(func() { Table{(*Notes)(m)}.Fill(0) })
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
if ind, ok := newIndex[p]; ok && ind > -1 {
|
||||
length = i + 1
|
||||
trk.Order[i] = ind
|
||||
} else {
|
||||
trk.Order[i] = -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
|
||||
}
|
||||
}
|
||||
trk.Patterns = newPatterns
|
||||
m.d.Song.Score.Tracks[trkIndex] = trk
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) Rewind() Action {
|
||||
return Action{
|
||||
allowed: func() bool {
|
||||
return m.playing || !m.instrEnlarged
|
||||
},
|
||||
do: func() {
|
||||
m.playing = true
|
||||
m.send(StartPlayMsg{})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
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]
|
||||
}
|
||||
}
|
||||
if backwards {
|
||||
if m.d.Cursor.OrderRow > 0 {
|
||||
m.d.Cursor.OrderRow--
|
||||
}
|
||||
}
|
||||
m.d.Cursor2.OrderRow = m.d.Cursor.OrderRow
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) NewSong() Action {
|
||||
return Allow(func() {
|
||||
m.dialog = NewSongChanges
|
||||
m.completeAction(true)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) OpenSong() Action {
|
||||
return Allow(func() {
|
||||
m.dialog = OpenSongChanges
|
||||
m.completeAction(true)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) Quit() Action {
|
||||
return Allow(func() {
|
||||
m.dialog = QuitChanges
|
||||
m.completeAction(true)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) ForceQuit() Action {
|
||||
return Allow(func() {
|
||||
m.quitted = true
|
||||
})
|
||||
}
|
||||
|
||||
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) 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) completeAction(checkSave bool) {
|
||||
if checkSave && m.d.ChangedSinceSave {
|
||||
return
|
||||
}
|
||||
switch m.dialog {
|
||||
case NewSongChanges, NewSongSaveExplorer:
|
||||
c := m.change("NewSong", SongChange, MajorChange)
|
||||
m.resetSong()
|
||||
c()
|
||||
m.d.ChangedSinceSave = false
|
||||
m.dialog = NoDialog
|
||||
case OpenSongChanges, OpenSongSaveExplorer:
|
||||
m.dialog = OpenSongOpenExplorer
|
||||
case QuitChanges, QuitSaveExplorer:
|
||||
m.quitted = true
|
||||
m.dialog = NoDialog
|
||||
default:
|
||||
m.dialog = NoDialog
|
||||
}
|
||||
}
|
110
tracker/alert.go
Normal file
110
tracker/alert.go
Normal file
@ -0,0 +1,110 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"time"
|
||||
)
|
||||
|
||||
const alertSpeed = 10 // units: fadeLevels per second
|
||||
const defaultAlertDuration = time.Second * 3
|
||||
|
||||
type (
|
||||
Alert struct {
|
||||
Name string
|
||||
Priority AlertPriority
|
||||
Message string
|
||||
Duration time.Duration
|
||||
FadeLevel float64
|
||||
}
|
||||
|
||||
AlertPriority int
|
||||
AlertYieldFunc func(alert Alert)
|
||||
Alerts Model
|
||||
)
|
||||
|
||||
const (
|
||||
None AlertPriority = iota
|
||||
Info
|
||||
Warning
|
||||
Error
|
||||
)
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) Alerts() *Alerts { return (*Alerts)(m) }
|
||||
|
||||
// Alerts methods
|
||||
|
||||
func (m *Alerts) Iterate(yield AlertYieldFunc) {
|
||||
for _, a := range m.alerts {
|
||||
yield(a)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Alerts) Update(d time.Duration) (animating bool) {
|
||||
for i := len(m.alerts) - 1; i >= 0; i-- {
|
||||
if m.alerts[i].Duration >= d {
|
||||
m.alerts[i].Duration -= d
|
||||
if m.alerts[i].FadeLevel < 1 {
|
||||
animating = true
|
||||
m.alerts[i].FadeLevel += float64(alertSpeed*d) / float64(time.Second)
|
||||
if m.alerts[i].FadeLevel > 1 {
|
||||
m.alerts[i].FadeLevel = 1
|
||||
}
|
||||
}
|
||||
} else {
|
||||
m.alerts[i].Duration = 0
|
||||
m.alerts[i].FadeLevel -= float64(alertSpeed*d) / float64(time.Second)
|
||||
animating = true
|
||||
if m.alerts[i].FadeLevel < 0 {
|
||||
heap.Remove(m, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Alerts) Add(message string, priority AlertPriority) {
|
||||
m.AddAlert(Alert{
|
||||
Priority: priority,
|
||||
Message: message,
|
||||
Duration: defaultAlertDuration,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Alerts) AddNamed(name, message string, priority AlertPriority) {
|
||||
m.AddAlert(Alert{
|
||||
Name: name,
|
||||
Priority: priority,
|
||||
Message: message,
|
||||
Duration: defaultAlertDuration,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Alerts) AddAlert(a Alert) {
|
||||
for i := range m.alerts {
|
||||
if n := m.alerts[i].Name; n != "" && n == a.Name {
|
||||
a.FadeLevel = m.alerts[i].FadeLevel
|
||||
m.alerts[i] = a
|
||||
heap.Fix(m, i)
|
||||
return
|
||||
}
|
||||
}
|
||||
heap.Push(m, a)
|
||||
}
|
||||
|
||||
func (m *Alerts) Push(x any) {
|
||||
m.alerts = append(m.alerts, x.(Alert))
|
||||
}
|
||||
|
||||
func (m *Alerts) Pop() any {
|
||||
old := m.alerts
|
||||
n := len(old)
|
||||
x := old[n-1]
|
||||
m.alerts = old[0 : n-1]
|
||||
return x
|
||||
}
|
||||
|
||||
func (m Alerts) Len() int { return len(m.alerts) }
|
||||
func (m Alerts) Less(i, j int) bool { return m.alerts[i].Priority < m.alerts[j].Priority }
|
||||
func (m Alerts) Swap(i, j int) { m.alerts[i], m.alerts[j] = m.alerts[j], m.alerts[i] }
|
114
tracker/bool.go
Normal file
114
tracker/bool.go
Normal file
@ -0,0 +1,114 @@
|
||||
package tracker
|
||||
|
||||
type (
|
||||
Bool struct {
|
||||
BoolData
|
||||
}
|
||||
|
||||
BoolData interface {
|
||||
Value() bool
|
||||
Enabled() bool
|
||||
setValue(bool)
|
||||
}
|
||||
|
||||
Panic Model
|
||||
IsRecording Model
|
||||
Playing Model
|
||||
InstrEnlarged Model
|
||||
Effect Model
|
||||
CommentExpanded Model
|
||||
NoteTracking Model
|
||||
)
|
||||
|
||||
func (v Bool) Toggle() {
|
||||
v.Set(!v.Value())
|
||||
}
|
||||
|
||||
func (v Bool) Set(value bool) {
|
||||
if v.Enabled() && v.Value() != value {
|
||||
v.setValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) Panic() *Panic { return (*Panic)(m) }
|
||||
func (m *Model) IsRecording() *IsRecording { return (*IsRecording)(m) }
|
||||
func (m *Model) Playing() *Playing { return (*Playing)(m) }
|
||||
func (m *Model) InstrEnlarged() *InstrEnlarged { return (*InstrEnlarged)(m) }
|
||||
func (m *Model) Effect() *Effect { return (*Effect)(m) }
|
||||
func (m *Model) CommentExpanded() *CommentExpanded { return (*CommentExpanded)(m) }
|
||||
func (m *Model) NoteTracking() *NoteTracking { return (*NoteTracking)(m) }
|
||||
|
||||
// Panic methods
|
||||
|
||||
func (m *Panic) Bool() Bool { return Bool{m} }
|
||||
func (m *Panic) Value() bool { return m.panic }
|
||||
func (m *Panic) setValue(val bool) {
|
||||
m.panic = val
|
||||
(*Model)(m).send(PanicMsg{val})
|
||||
}
|
||||
func (m *Panic) Enabled() bool { return true }
|
||||
|
||||
// IsRecording methods
|
||||
|
||||
func (m *IsRecording) Bool() Bool { return Bool{m} }
|
||||
func (m *IsRecording) Value() bool { return (*Model)(m).recording }
|
||||
func (m *IsRecording) setValue(val bool) {
|
||||
m.recording = val
|
||||
m.instrEnlarged = val
|
||||
(*Model)(m).send(RecordingMsg{val})
|
||||
}
|
||||
func (m *IsRecording) Enabled() bool { return true }
|
||||
|
||||
// Playing methods
|
||||
|
||||
func (m *Playing) Bool() Bool { return Bool{m} }
|
||||
func (m *Playing) Value() bool { return m.playing }
|
||||
func (m *Playing) setValue(val bool) {
|
||||
m.playing = val
|
||||
if m.playing {
|
||||
(*Model)(m).send(StartPlayMsg{m.d.Cursor.SongPos})
|
||||
} else {
|
||||
(*Model)(m).send(IsPlayingMsg{val})
|
||||
}
|
||||
}
|
||||
func (m *Playing) Enabled() bool { return m.playing || !m.instrEnlarged }
|
||||
|
||||
// InstrEnlarged methods
|
||||
|
||||
func (m *InstrEnlarged) Bool() Bool { return Bool{m} }
|
||||
func (m *InstrEnlarged) Value() bool { return m.instrEnlarged }
|
||||
func (m *InstrEnlarged) setValue(val bool) { m.instrEnlarged = val }
|
||||
func (m *InstrEnlarged) Enabled() bool { return true }
|
||||
|
||||
// CommentExpanded methods
|
||||
|
||||
func (m *CommentExpanded) Bool() Bool { return Bool{m} }
|
||||
func (m *CommentExpanded) Value() bool { return m.commentExpanded }
|
||||
func (m *CommentExpanded) setValue(val bool) { m.commentExpanded = val }
|
||||
func (m *CommentExpanded) Enabled() bool { return true }
|
||||
|
||||
// NoteTracking methods
|
||||
|
||||
func (m *NoteTracking) Bool() Bool { return Bool{m} }
|
||||
func (m *NoteTracking) Value() bool { return m.playing && m.noteTracking }
|
||||
func (m *NoteTracking) setValue(val bool) { m.noteTracking = val }
|
||||
func (m *NoteTracking) Enabled() bool { return m.playing }
|
||||
|
||||
// Effect methods
|
||||
|
||||
func (m *Effect) Bool() Bool { return Bool{m} }
|
||||
func (m *Effect) Value() bool {
|
||||
if m.d.Cursor.Track < 0 || m.d.Cursor.Track >= len(m.d.Song.Score.Tracks) {
|
||||
return false
|
||||
}
|
||||
return m.d.Song.Score.Tracks[m.d.Cursor.Track].Effect
|
||||
}
|
||||
func (m *Effect) setValue(val bool) {
|
||||
if m.d.Cursor.Track < 0 || m.d.Cursor.Track >= len(m.d.Song.Score.Tracks) {
|
||||
return
|
||||
}
|
||||
m.d.Song.Score.Tracks[m.d.Cursor.Track].Effect = val
|
||||
}
|
||||
func (m *Effect) Enabled() bool { return true }
|
179
tracker/files.go
Normal file
179
tracker/files.go
Normal file
@ -0,0 +1,179 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
func (m *Model) ReadSong(r io.ReadCloser) {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var song sointu.Song
|
||||
if errJSON := json.Unmarshal(b, &song); errJSON != nil {
|
||||
if errYaml := yaml.Unmarshal(b, &song); errYaml != nil {
|
||||
m.Alerts().Add(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error)
|
||||
return
|
||||
}
|
||||
}
|
||||
f := m.change("LoadSong", SongChange, MajorChange)
|
||||
m.d.Song = song
|
||||
if f, ok := r.(*os.File); ok {
|
||||
m.d.FilePath = f.Name()
|
||||
// when the song is loaded from a file, we are quite confident that the file is persisted and thus
|
||||
// we can close sointu without worrying about losing changes
|
||||
m.d.ChangedSinceSave = false
|
||||
}
|
||||
f()
|
||||
m.completeAction(false)
|
||||
}
|
||||
|
||||
func (m *Model) WriteSong(w io.WriteCloser) {
|
||||
path := ""
|
||||
var extension = filepath.Ext(path)
|
||||
var contents []byte
|
||||
var err error
|
||||
if extension == ".json" {
|
||||
contents, err = json.Marshal(m.d.Song)
|
||||
} else {
|
||||
contents, err = yaml.Marshal(m.d.Song)
|
||||
}
|
||||
if err != nil {
|
||||
m.Alerts().Add(fmt.Sprintf("Error marshaling a song file: %v", err), Error)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(contents); err != nil {
|
||||
m.Alerts().Add(fmt.Sprintf("Error writing to file: %v", err), Error)
|
||||
return
|
||||
}
|
||||
if f, ok := w.(*os.File); ok {
|
||||
path = f.Name()
|
||||
// when the song is saved to a file, we are quite confident that the file is persisted and thus
|
||||
// we can close sointu without worrying about losing changes
|
||||
m.d.ChangedSinceSave = false
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
m.Alerts().Add(fmt.Sprintf("Error rendering the song during export: %v", err), Error)
|
||||
return
|
||||
}
|
||||
m.d.FilePath = path
|
||||
m.completeAction(false)
|
||||
}
|
||||
|
||||
func (m *Model) WriteWav(w io.WriteCloser, pcm16 bool, execChan chan<- func()) {
|
||||
m.dialog = NoDialog
|
||||
song := m.d.Song.Copy()
|
||||
go func() {
|
||||
b := make([]byte, 32+2)
|
||||
rand.Read(b)
|
||||
name := fmt.Sprintf("%x", b)[2 : 32+2]
|
||||
data, err := sointu.Play(m.synther, song, func(p float32) {
|
||||
execChan <- func() {
|
||||
m.Alerts().AddNamed(name, fmt.Sprintf("Exporting song: %.0f%%", p*100), Info)
|
||||
}
|
||||
}) // render the song to calculate its length
|
||||
if err != nil {
|
||||
execChan <- func() {
|
||||
m.Alerts().Add(fmt.Sprintf("Error rendering the song during export: %v", err), Error)
|
||||
}
|
||||
return
|
||||
}
|
||||
buffer, err := data.Wav(pcm16)
|
||||
if err != nil {
|
||||
execChan <- func() {
|
||||
m.Alerts().Add(fmt.Sprintf("Error converting to .wav: %v", err), Error)
|
||||
}
|
||||
return
|
||||
}
|
||||
w.Write(buffer)
|
||||
w.Close()
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *Model) SaveInstrument(w io.WriteCloser) bool {
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
m.Alerts().Add("No instrument selected", Error)
|
||||
return false
|
||||
}
|
||||
path := ""
|
||||
if f, ok := w.(*os.File); ok {
|
||||
path = f.Name()
|
||||
}
|
||||
var extension = filepath.Ext(path)
|
||||
var contents []byte
|
||||
var err error
|
||||
if extension == ".json" {
|
||||
contents, err = json.Marshal(m.d.Song.Patch[m.d.InstrIndex])
|
||||
} else {
|
||||
contents, err = yaml.Marshal(m.d.Song.Patch[m.d.InstrIndex])
|
||||
}
|
||||
if err != nil {
|
||||
m.Alerts().Add(fmt.Sprintf("Error marshaling a ínstrument file: %v", err), Error)
|
||||
return false
|
||||
}
|
||||
w.Write(contents)
|
||||
w.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Model) LoadInstrument(r io.ReadCloser) bool {
|
||||
if m.d.InstrIndex < 0 {
|
||||
return false
|
||||
}
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var instrument sointu.Instrument
|
||||
var errJSON, errYaml, err4ki, err4kp error
|
||||
var patch sointu.Patch
|
||||
errJSON = json.Unmarshal(b, &instrument)
|
||||
if errJSON == nil {
|
||||
goto success
|
||||
}
|
||||
errYaml = yaml.Unmarshal(b, &instrument)
|
||||
if errYaml == nil {
|
||||
goto success
|
||||
}
|
||||
patch, err4kp = sointu.Read4klangPatch(bytes.NewReader(b))
|
||||
if err4kp == nil {
|
||||
defer m.change("LoadInstrument", PatchChange, MajorChange)()
|
||||
m.d.Song.Patch = patch
|
||||
return true
|
||||
}
|
||||
instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b))
|
||||
if err4ki == nil {
|
||||
goto success
|
||||
}
|
||||
m.Alerts().Add(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v / %v / %v", errYaml, errJSON, err4ki, err4kp), Error)
|
||||
return false
|
||||
success:
|
||||
if f, ok := r.(*os.File); ok {
|
||||
filename := f.Name()
|
||||
// the 4klang instrument names are junk, replace them with the filename without extension
|
||||
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
|
||||
}
|
||||
defer m.change("LoadInstrument", PatchChange, MajorChange)()
|
||||
for len(m.d.Song.Patch) <= m.d.InstrIndex {
|
||||
m.d.Song.Patch = append(m.d.Song.Patch, defaultInstrument.Copy())
|
||||
}
|
||||
m.d.Song.Patch[m.d.InstrIndex] = instrument
|
||||
if m.d.Song.Patch[m.d.InstrIndex].Comment != "" {
|
||||
m.commentExpanded = true
|
||||
}
|
||||
return true
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
message string
|
||||
alertType AlertType
|
||||
duration time.Duration
|
||||
showMessage string
|
||||
showAlertType AlertType
|
||||
showDuration time.Duration
|
||||
showTime time.Time
|
||||
pos float64
|
||||
lastUpdate time.Time
|
||||
shaper *text.Shaper
|
||||
}
|
||||
|
||||
type AlertType int
|
||||
|
||||
const (
|
||||
None AlertType = iota
|
||||
Notify
|
||||
Warning
|
||||
Error
|
||||
)
|
||||
|
||||
var alertSpeed = 150 * time.Millisecond
|
||||
var alertMargin = layout.UniformInset(unit.Dp(6))
|
||||
var alertInset = layout.UniformInset(unit.Dp(6))
|
||||
|
||||
func (a *Alert) Update(message string, alertType AlertType, duration time.Duration) {
|
||||
if a.alertType < alertType {
|
||||
a.message = message
|
||||
a.alertType = alertType
|
||||
a.duration = duration
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Alert) Layout(gtx C) D {
|
||||
now := time.Now()
|
||||
if a.alertType != None {
|
||||
a.showMessage = a.message
|
||||
a.showAlertType = a.alertType
|
||||
a.showTime = now
|
||||
a.showDuration = a.duration
|
||||
}
|
||||
a.alertType = None
|
||||
var targetPos float64 = 0.0
|
||||
if now.Sub(a.showTime) <= a.showDuration {
|
||||
targetPos = 1.0
|
||||
}
|
||||
delta := float64(now.Sub(a.lastUpdate)) / float64(alertSpeed)
|
||||
if a.pos < targetPos {
|
||||
a.pos += delta
|
||||
if a.pos > targetPos {
|
||||
a.pos = targetPos
|
||||
} else {
|
||||
op.InvalidateOp{At: now.Add(50 * time.Millisecond)}.Add(gtx.Ops)
|
||||
}
|
||||
} else if a.pos > targetPos {
|
||||
a.pos -= delta
|
||||
if a.pos < targetPos {
|
||||
a.pos = targetPos
|
||||
} else {
|
||||
op.InvalidateOp{At: now.Add(50 * time.Millisecond)}.Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
a.lastUpdate = now
|
||||
var color, textColor, shadeColor color.NRGBA
|
||||
switch a.showAlertType {
|
||||
case Warning:
|
||||
color = warningColor
|
||||
textColor = black
|
||||
case Error:
|
||||
color = errorColor
|
||||
textColor = black
|
||||
default:
|
||||
color = popupSurfaceColor
|
||||
textColor = white
|
||||
shadeColor = black
|
||||
}
|
||||
bgWidget := func(gtx C) D {
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
labelStyle := LabelStyle{Text: a.showMessage, Color: textColor, ShadeColor: shadeColor, Font: labelDefaultFont, Alignment: layout.Center, FontSize: unit.Sp(16), Shaper: a.shaper}
|
||||
return alertMargin.Layout(gtx, func(gtx C) D {
|
||||
return layout.S.Layout(gtx, func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
recording := op.Record(gtx.Ops)
|
||||
dims := layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Expanded(bgWidget),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return alertInset.Layout(gtx, labelStyle.Layout)
|
||||
}),
|
||||
)
|
||||
macro := recording.Stop()
|
||||
totalY := dims.Size.Y + gtx.Dp(alertMargin.Bottom)
|
||||
op.Offset(image.Point{0, int((1 - a.pos) * float64(totalY))}).Add((gtx.Ops))
|
||||
macro.Add(gtx.Ops)
|
||||
return dims
|
||||
})
|
||||
})
|
||||
}
|
@ -6,17 +6,43 @@ import (
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/component"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type TipClickable struct {
|
||||
type (
|
||||
TipClickable struct {
|
||||
Clickable widget.Clickable
|
||||
TipArea component.TipArea
|
||||
}
|
||||
|
||||
type TipIconButtonStyle struct {
|
||||
ActionClickable struct {
|
||||
Action tracker.Action
|
||||
TipClickable
|
||||
}
|
||||
|
||||
TipIconButtonStyle struct {
|
||||
TipArea *component.TipArea
|
||||
IconButtonStyle material.IconButtonStyle
|
||||
Tooltip component.Tooltip
|
||||
tipArea *component.TipArea
|
||||
}
|
||||
|
||||
BoolClickable struct {
|
||||
Clickable widget.Clickable
|
||||
TipArea component.TipArea
|
||||
Bool tracker.Bool
|
||||
}
|
||||
)
|
||||
|
||||
func NewActionClickable(a tracker.Action) *ActionClickable {
|
||||
return &ActionClickable{
|
||||
Action: a,
|
||||
}
|
||||
}
|
||||
|
||||
func NewBoolClickable(b tracker.Bool) *BoolClickable {
|
||||
return &BoolClickable{
|
||||
Bool: b,
|
||||
}
|
||||
}
|
||||
|
||||
func Tooltip(th *material.Theme, tip string) component.Tooltip {
|
||||
@ -25,24 +51,86 @@ func Tooltip(th *material.Theme, tip string) component.Tooltip {
|
||||
return tooltip
|
||||
}
|
||||
|
||||
func IconButton(th *material.Theme, w *TipClickable, icon []byte, enabled bool, tip string) TipIconButtonStyle {
|
||||
ret := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
||||
ret.Background = transparent
|
||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||
if enabled {
|
||||
ret.Color = primaryColor
|
||||
} else {
|
||||
ret.Color = disabledTextColor
|
||||
func ActionIcon(th *material.Theme, w *ActionClickable, icon []byte, tip string) TipIconButtonStyle {
|
||||
ret := TipIcon(th, &w.TipClickable, icon, tip)
|
||||
for w.Clickable.Clicked() {
|
||||
w.Action.Do()
|
||||
}
|
||||
if !w.Action.Allowed() {
|
||||
ret.IconButtonStyle.Color = disabledTextColor
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func TipIcon(th *material.Theme, w *TipClickable, icon []byte, tip string) TipIconButtonStyle {
|
||||
iconButtonStyle := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
||||
iconButtonStyle.Color = primaryColor
|
||||
iconButtonStyle.Background = transparent
|
||||
iconButtonStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
return TipIconButtonStyle{
|
||||
TipArea: &w.TipArea,
|
||||
IconButtonStyle: iconButtonStyle,
|
||||
Tooltip: Tooltip(th, tip),
|
||||
}
|
||||
}
|
||||
|
||||
func ToggleIcon(th *material.Theme, w *BoolClickable, offIcon, onIcon []byte, offTip, onTip string) TipIconButtonStyle {
|
||||
icon := offIcon
|
||||
tip := offTip
|
||||
if w.Bool.Value() {
|
||||
icon = onIcon
|
||||
tip = onTip
|
||||
}
|
||||
for w.Clickable.Clicked() {
|
||||
w.Bool.Toggle()
|
||||
}
|
||||
ibStyle := material.IconButton(th, &w.Clickable, widgetForIcon(icon), "")
|
||||
ibStyle.Background = transparent
|
||||
ibStyle.Inset = layout.UniformInset(unit.Dp(6))
|
||||
ibStyle.Color = primaryColor
|
||||
if !w.Bool.Enabled() {
|
||||
ibStyle.Color = disabledTextColor
|
||||
}
|
||||
return TipIconButtonStyle{
|
||||
IconButtonStyle: ret,
|
||||
TipArea: &w.TipArea,
|
||||
IconButtonStyle: ibStyle,
|
||||
Tooltip: Tooltip(th, tip),
|
||||
tipArea: &w.TipArea,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *TipIconButtonStyle) Layout(gtx C) D {
|
||||
return t.tipArea.Layout(gtx, t.Tooltip, t.IconButtonStyle.Layout)
|
||||
return t.TipArea.Layout(gtx, t.Tooltip, t.IconButtonStyle.Layout)
|
||||
}
|
||||
|
||||
func ActionButton(th *material.Theme, w *ActionClickable, text string) material.ButtonStyle {
|
||||
for w.Clickable.Clicked() {
|
||||
w.Action.Do()
|
||||
}
|
||||
ret := material.Button(th, &w.Clickable, text)
|
||||
ret.Color = th.Palette.Fg
|
||||
if !w.Action.Allowed() {
|
||||
ret.Color = disabledTextColor
|
||||
}
|
||||
ret.Background = transparent
|
||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||
return ret
|
||||
}
|
||||
|
||||
func ToggleButton(th *material.Theme, b *BoolClickable, text string) material.ButtonStyle {
|
||||
for b.Clickable.Clicked() {
|
||||
b.Bool.Toggle()
|
||||
}
|
||||
ret := material.Button(th, &b.Clickable, text)
|
||||
ret.Background = transparent
|
||||
ret.Inset = layout.UniformInset(unit.Dp(6))
|
||||
if b.Bool.Value() {
|
||||
ret.Color = th.Palette.ContrastFg
|
||||
ret.Background = th.Palette.Fg
|
||||
} else {
|
||||
ret.Color = th.Palette.Fg
|
||||
ret.Background = transparent
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func LowEmphasisButton(th *material.Theme, w *widget.Clickable, text string) material.ButtonStyle {
|
||||
|
@ -1,56 +1,82 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type Dialog struct {
|
||||
Visible bool
|
||||
BtnAlt widget.Clickable
|
||||
BtnOk widget.Clickable
|
||||
BtnCancel widget.Clickable
|
||||
BtnAlt *ActionClickable
|
||||
BtnOk *ActionClickable
|
||||
BtnCancel *ActionClickable
|
||||
tag bool
|
||||
}
|
||||
|
||||
type DialogStyle struct {
|
||||
dialog *Dialog
|
||||
Title string
|
||||
Text string
|
||||
Inset layout.Inset
|
||||
ShowAlt bool
|
||||
TextInset layout.Inset
|
||||
AltStyle material.ButtonStyle
|
||||
OkStyle material.ButtonStyle
|
||||
CancelStyle material.ButtonStyle
|
||||
Shaper *text.Shaper
|
||||
}
|
||||
|
||||
func ConfirmDialog(th *material.Theme, dialog *Dialog, text string, shaper *text.Shaper) DialogStyle {
|
||||
func NewDialog(ok, alt, cancel tracker.Action) *Dialog {
|
||||
return &Dialog{
|
||||
BtnOk: NewActionClickable(ok),
|
||||
BtnAlt: NewActionClickable(alt),
|
||||
BtnCancel: NewActionClickable(cancel),
|
||||
}
|
||||
}
|
||||
|
||||
func ConfirmDialog(th *material.Theme, dialog *Dialog, title, text string) DialogStyle {
|
||||
ret := DialogStyle{
|
||||
dialog: dialog,
|
||||
Title: title,
|
||||
Text: text,
|
||||
Inset: layout.Inset{Top: unit.Dp(12), Bottom: unit.Dp(12), Left: unit.Dp(20), Right: unit.Dp(20)},
|
||||
AltStyle: HighEmphasisButton(th, &dialog.BtnAlt, "Alt"),
|
||||
OkStyle: HighEmphasisButton(th, &dialog.BtnOk, "Ok"),
|
||||
CancelStyle: HighEmphasisButton(th, &dialog.BtnCancel, "Cancel"),
|
||||
Shaper: shaper,
|
||||
TextInset: layout.Inset{Top: unit.Dp(12), Bottom: unit.Dp(12)},
|
||||
AltStyle: ActionButton(th, dialog.BtnAlt, "Alt"),
|
||||
OkStyle: ActionButton(th, dialog.BtnOk, "Ok"),
|
||||
CancelStyle: ActionButton(th, dialog.BtnCancel, "Cancel"),
|
||||
Shaper: th.Shaper,
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (d *DialogStyle) Layout(gtx C) D {
|
||||
if d.dialog.Visible {
|
||||
if !d.dialog.BtnOk.Clickable.Focused() && !d.dialog.BtnCancel.Clickable.Focused() && !d.dialog.BtnAlt.Clickable.Focused() {
|
||||
d.dialog.BtnCancel.Clickable.Focus()
|
||||
}
|
||||
paint.Fill(gtx.Ops, dialogBgColor)
|
||||
text := func(gtx C) D {
|
||||
return d.TextInset.Layout(gtx, LabelStyle{Text: d.Text, Color: highEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(14), Shaper: d.Shaper}.Layout)
|
||||
}
|
||||
for _, e := range gtx.Events(&d.dialog.tag) {
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
d.command(e)
|
||||
}
|
||||
}
|
||||
visible := true
|
||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
||||
return Popup(&d.dialog.Visible).Layout(gtx, func(gtx C) D {
|
||||
return Popup(&visible).Layout(gtx, func(gtx C) D {
|
||||
key.InputOp{Tag: &d.dialog.tag, Keys: "⎋|←|→|Tab"}.Add(gtx.Ops)
|
||||
return d.Inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label(d.Text, highEmphasisTextColor, d.Shaper)),
|
||||
layout.Rigid(Label(d.Title, highEmphasisTextColor, d.Shaper)),
|
||||
layout.Rigid(text),
|
||||
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.ShowAlt {
|
||||
if d.dialog.BtnAlt.Action.Allowed() {
|
||||
return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween}.Layout(gtx,
|
||||
layout.Rigid(d.OkStyle.Layout),
|
||||
layout.Rigid(d.AltStyle.Layout),
|
||||
@ -61,11 +87,43 @@ func (d *DialogStyle) Layout(gtx C) D {
|
||||
layout.Rigid(d.OkStyle.Layout),
|
||||
layout.Rigid(d.CancelStyle.Layout),
|
||||
)
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
return D{}
|
||||
|
||||
func (d *DialogStyle) command(e key.Event) {
|
||||
switch e.Name {
|
||||
case key.NameEscape:
|
||||
d.dialog.BtnCancel.Action.Do()
|
||||
case key.NameLeftArrow:
|
||||
switch {
|
||||
case d.dialog.BtnOk.Clickable.Focused():
|
||||
d.dialog.BtnCancel.Clickable.Focus()
|
||||
case d.dialog.BtnCancel.Clickable.Focused():
|
||||
if d.dialog.BtnAlt.Action.Allowed() {
|
||||
d.dialog.BtnAlt.Clickable.Focus()
|
||||
} else {
|
||||
d.dialog.BtnOk.Clickable.Focus()
|
||||
}
|
||||
case d.dialog.BtnAlt.Clickable.Focused():
|
||||
d.dialog.BtnOk.Clickable.Focus()
|
||||
}
|
||||
case key.NameRightArrow, key.NameTab:
|
||||
switch {
|
||||
case d.dialog.BtnOk.Clickable.Focused():
|
||||
if d.dialog.BtnAlt.Action.Allowed() {
|
||||
d.dialog.BtnAlt.Clickable.Focus()
|
||||
} else {
|
||||
d.dialog.BtnCancel.Clickable.Focus()
|
||||
}
|
||||
case d.dialog.BtnCancel.Clickable.Focused():
|
||||
d.dialog.BtnOk.Clickable.Focus()
|
||||
case d.dialog.BtnAlt.Clickable.Focused():
|
||||
d.dialog.BtnCancel.Clickable.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,20 +4,23 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type DragList struct {
|
||||
SelectedItem int
|
||||
SelectedItem2 int
|
||||
TrackerList tracker.List
|
||||
HoverItem int
|
||||
List *layout.List
|
||||
ScrollBar *ScrollBar
|
||||
drag bool
|
||||
dragID pointer.ID
|
||||
tags []bool
|
||||
@ -32,20 +35,23 @@ type FilledDragListStyle struct {
|
||||
HoverColor color.NRGBA
|
||||
SelectedColor color.NRGBA
|
||||
CursorColor color.NRGBA
|
||||
Count int
|
||||
element func(gtx C, i int) D
|
||||
swap func(i, j int)
|
||||
ScrollBarWidth unit.Dp
|
||||
element, bg func(gtx C, i int) D
|
||||
}
|
||||
|
||||
func FilledDragList(th *material.Theme, dragList *DragList, count int, element func(gtx C, i int) D, swap func(i, j int)) FilledDragListStyle {
|
||||
func NewDragList(model tracker.List, axis layout.Axis) *DragList {
|
||||
return &DragList{TrackerList: model, List: &layout.List{Axis: axis}, HoverItem: -1, ScrollBar: &ScrollBar{Axis: axis}}
|
||||
}
|
||||
|
||||
func FilledDragList(th *material.Theme, dragList *DragList, element, bg func(gtx C, i int) D) FilledDragListStyle {
|
||||
return FilledDragListStyle{
|
||||
dragList: dragList,
|
||||
element: element,
|
||||
swap: swap,
|
||||
Count: count,
|
||||
bg: bg,
|
||||
HoverColor: dragListHoverColor,
|
||||
SelectedColor: dragListSelectedColor,
|
||||
CursorColor: cursorColor,
|
||||
ScrollBarWidth: unit.Dp(10),
|
||||
}
|
||||
}
|
||||
|
||||
@ -57,14 +63,18 @@ func (d *DragList) Focused() bool {
|
||||
return d.focused
|
||||
}
|
||||
|
||||
func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
func (s FilledDragListStyle) LayoutScrollBar(gtx C) D {
|
||||
return s.dragList.ScrollBar.Layout(gtx, s.ScrollBarWidth, s.dragList.TrackerList.Count(), &s.dragList.List.Position)
|
||||
}
|
||||
|
||||
func (s FilledDragListStyle) Layout(gtx C) D {
|
||||
swap := 0
|
||||
|
||||
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-↓|Shift-↑|Shift-↓")
|
||||
keys := key.Set("↑|↓|Ctrl-↑|Ctrl-↓|Shift-↑|Shift-↓|⇞|⇟|Ctrl-⇞|Ctrl-⇟|Ctrl-A|Ctrl-C|Ctrl-X|Ctrl-V|⌦|Ctrl-⌫")
|
||||
if s.dragList.List.Axis == layout.Horizontal {
|
||||
keys = key.Set("←|→|Ctrl-←|Ctrl-→|Shift-←|Shift-→")
|
||||
keys = key.Set("←|→|Ctrl-←|Ctrl-→|Shift-←|Shift-→|Home|End|Ctrl-Home|Ctrl-End|Ctrl-A|Ctrl-C|Ctrl-X|Ctrl-V|⌦|Ctrl-⌫")
|
||||
}
|
||||
key.InputOp{Tag: &s.dragList.mainTag, Keys: keys}.Add(gtx.Ops)
|
||||
|
||||
@ -79,65 +89,45 @@ 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:
|
||||
s.dragList.focused = ke.Focus
|
||||
if !s.dragList.focused {
|
||||
s.dragList.TrackerList.SetSelected2(s.dragList.TrackerList.Selected())
|
||||
}
|
||||
case key.Event:
|
||||
if !s.dragList.focused || ke.State != key.Press {
|
||||
break
|
||||
}
|
||||
delta := 0
|
||||
switch {
|
||||
case s.dragList.List.Axis == layout.Horizontal && ke.Name == key.NameLeftArrow && s.dragList.SelectedItem > 0:
|
||||
delta = -1
|
||||
case s.dragList.List.Axis == layout.Horizontal && ke.Name == key.NameRightArrow && s.dragList.SelectedItem < s.Count-1:
|
||||
delta = 1
|
||||
case s.dragList.List.Axis == layout.Vertical && ke.Name == key.NameUpArrow && s.dragList.SelectedItem > 0:
|
||||
delta = -1
|
||||
case s.dragList.List.Axis == layout.Vertical && ke.Name == key.NameDownArrow && s.dragList.SelectedItem < s.Count-1:
|
||||
delta = 1
|
||||
}
|
||||
if delta != 0 {
|
||||
if ke.Modifiers.Contain(key.ModShortcut) {
|
||||
swap = delta
|
||||
} else {
|
||||
s.dragList.SelectedItem += delta
|
||||
if !ke.Modifiers.Contain(key.ModShift) {
|
||||
s.dragList.SelectedItem2 = s.dragList.SelectedItem
|
||||
}
|
||||
}
|
||||
}
|
||||
s.dragList.command(gtx, ke)
|
||||
case clipboard.Event:
|
||||
s.dragList.TrackerList.PasteElements([]byte(ke.Text))
|
||||
}
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
_, isMutable := s.dragList.TrackerList.ListData.(tracker.MutableListData)
|
||||
|
||||
listElem := func(gtx C, index int) D {
|
||||
for len(s.dragList.tags) <= index {
|
||||
s.dragList.tags = append(s.dragList.tags, false)
|
||||
}
|
||||
bg := func(gtx C) D {
|
||||
cursorBg := func(gtx C) D {
|
||||
var color color.NRGBA
|
||||
if s.dragList.SelectedItem == index {
|
||||
if s.dragList.TrackerList.Selected() == index {
|
||||
if s.dragList.focused {
|
||||
color = s.CursorColor
|
||||
} else {
|
||||
color = s.SelectedColor
|
||||
}
|
||||
} else if between(s.dragList.SelectedItem, index, s.dragList.SelectedItem2) {
|
||||
} else if between(s.dragList.TrackerList.Selected(), index, s.dragList.TrackerList.Selected2()) {
|
||||
color = s.SelectedColor
|
||||
} else if s.dragList.HoverItem == index {
|
||||
color = s.HoverColor
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
|
||||
inputFg := func(gtx C) D {
|
||||
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
for _, ev := range gtx.Events(&s.dragList.tags[index]) {
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
@ -154,9 +144,9 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
if s.dragList.drag {
|
||||
break
|
||||
}
|
||||
s.dragList.SelectedItem = index
|
||||
s.dragList.TrackerList.SetSelected(index)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
s.dragList.SelectedItem2 = index
|
||||
s.dragList.TrackerList.SetSelected2(index)
|
||||
}
|
||||
key.FocusOp{Tag: &s.dragList.mainTag}.Add(gtx.Ops)
|
||||
}
|
||||
@ -167,7 +157,7 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
Types: pointer.Press | pointer.Enter | pointer.Leave,
|
||||
}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
if index == s.dragList.SelectedItem {
|
||||
if index == s.dragList.TrackerList.Selected() && isMutable {
|
||||
for _, ev := range gtx.Events(&s.dragList.focused) {
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
@ -212,29 +202,31 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
}
|
||||
return layout.Stack{Alignment: layout.W}.Layout(gtx,
|
||||
layout.Expanded(bg),
|
||||
layout.Expanded(inputFg),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return s.element(gtx, index)
|
||||
}),
|
||||
)
|
||||
}
|
||||
dims := s.dragList.List.Layout(gtx, s.Count, listElem)
|
||||
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)
|
||||
macro := op.Record(gtx.Ops)
|
||||
dims := s.element(gtx, index)
|
||||
call := macro.Stop()
|
||||
gtx.Constraints.Min = dims.Size
|
||||
if s.bg != nil {
|
||||
s.bg(gtx, index)
|
||||
}
|
||||
cursorBg(gtx)
|
||||
call.Add(gtx.Ops)
|
||||
if s.dragList.List.Axis == layout.Horizontal {
|
||||
dims.Size.Y = gtx.Constraints.Max.Y
|
||||
} else {
|
||||
for i := b; i >= a; i-- {
|
||||
s.swap(i, i+swap)
|
||||
dims.Size.X = gtx.Constraints.Max.X
|
||||
}
|
||||
return dims
|
||||
}
|
||||
count := s.dragList.TrackerList.Count()
|
||||
if count < 1 {
|
||||
count = 1 // draw at least one empty element to get the correct size
|
||||
}
|
||||
dims := s.dragList.List.Layout(gtx, count, listElem)
|
||||
if !s.dragList.swapped && swap != 0 {
|
||||
if s.dragList.TrackerList.MoveElements(swap) {
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
s.dragList.SelectedItem += swap
|
||||
s.dragList.SelectedItem2 += swap
|
||||
s.dragList.swapped = true
|
||||
} else {
|
||||
s.dragList.swapped = false
|
||||
@ -242,6 +234,88 @@ func (s *FilledDragListStyle) Layout(gtx C) D {
|
||||
return dims
|
||||
}
|
||||
|
||||
func (e *DragList) command(gtx layout.Context, k key.Event) {
|
||||
if k.Modifiers.Contain(key.ModShortcut) {
|
||||
switch k.Name {
|
||||
case "V":
|
||||
clipboard.ReadOp{Tag: &e.mainTag}.Add(gtx.Ops)
|
||||
return
|
||||
case "C", "X":
|
||||
data, ok := e.TrackerList.CopyElements()
|
||||
if ok && (k.Name == "C" || e.TrackerList.DeleteElements(false)) {
|
||||
clipboard.WriteOp{Text: string(data)}.Add(gtx.Ops)
|
||||
}
|
||||
return
|
||||
case "A":
|
||||
e.TrackerList.SetSelected(0)
|
||||
e.TrackerList.SetSelected2(e.TrackerList.Count() - 1)
|
||||
return
|
||||
}
|
||||
}
|
||||
delta := 0
|
||||
switch k.Name {
|
||||
case key.NameDeleteBackward:
|
||||
if k.Modifiers.Contain(key.ModShortcut) {
|
||||
e.TrackerList.DeleteElements(true)
|
||||
}
|
||||
return
|
||||
case key.NameDeleteForward:
|
||||
e.TrackerList.DeleteElements(false)
|
||||
return
|
||||
case key.NameLeftArrow:
|
||||
delta = -1
|
||||
case key.NameRightArrow:
|
||||
delta = 1
|
||||
case key.NameHome:
|
||||
delta = -1e6
|
||||
case key.NameEnd:
|
||||
delta = 1e6
|
||||
case key.NameUpArrow:
|
||||
delta = -1
|
||||
case key.NameDownArrow:
|
||||
delta = 1
|
||||
case key.NamePageUp:
|
||||
delta = -1e6
|
||||
case key.NamePageDown:
|
||||
delta = 1e6
|
||||
}
|
||||
if k.Modifiers.Contain(key.ModShortcut) {
|
||||
e.TrackerList.MoveElements(delta)
|
||||
} else {
|
||||
e.TrackerList.SetSelected(e.TrackerList.Selected() + delta)
|
||||
if !k.Modifiers.Contain(key.ModShift) {
|
||||
e.TrackerList.SetSelected2(e.TrackerList.Selected())
|
||||
}
|
||||
}
|
||||
e.EnsureVisible(e.TrackerList.Selected())
|
||||
}
|
||||
|
||||
func (l *DragList) EnsureVisible(item int) {
|
||||
first := l.List.Position.First
|
||||
last := l.List.Position.First + l.List.Position.Count - 1
|
||||
if item < first || (item == first && l.List.Position.Offset > 0) {
|
||||
l.List.ScrollTo(item)
|
||||
}
|
||||
if item > last || (item == last && l.List.Position.OffsetLast < 0) {
|
||||
o := -l.List.Position.OffsetLast + l.List.Position.Offset
|
||||
l.List.ScrollTo(item - l.List.Position.Count + 1)
|
||||
l.List.Position.Offset = o
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DragList) CenterOn(item int) {
|
||||
lenPerChildPx := l.List.Position.Length / l.TrackerList.Count()
|
||||
if lenPerChildPx == 0 {
|
||||
return
|
||||
}
|
||||
listLengthPx := l.List.Position.Count*l.List.Position.Length/l.TrackerList.Count() + l.List.Position.OffsetLast - l.List.Position.Offset
|
||||
lenBeforeItem := (listLengthPx - lenPerChildPx) / 2
|
||||
quot := lenBeforeItem / lenPerChildPx
|
||||
rem := lenBeforeItem % lenPerChildPx
|
||||
l.List.ScrollTo(item - quot - 1)
|
||||
l.List.Position.Offset = lenPerChildPx - rem
|
||||
}
|
||||
|
||||
func between(a, b, c int) bool {
|
||||
return (a <= b && b <= c) || (c <= b && b <= a)
|
||||
}
|
||||
|
@ -1,224 +0,0 @@
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
)
|
||||
|
||||
func (t *Tracker) OpenSongFile(forced bool) {
|
||||
if !forced && t.ChangedSinceSave() {
|
||||
t.ConfirmSongActionType = ConfirmLoad
|
||||
t.ConfirmSongDialog.Visible = true
|
||||
return
|
||||
}
|
||||
reader, err := t.Explorer.ChooseFile(".yml", ".json")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.loadSong(reader)
|
||||
}
|
||||
|
||||
func (t *Tracker) SaveSongFile() bool {
|
||||
if p := t.FilePath(); p != "" {
|
||||
if f, err := os.Create(p); err == nil {
|
||||
return t.saveSong(f)
|
||||
}
|
||||
}
|
||||
t.SaveSongAsFile()
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *Tracker) SaveSongAsFile() {
|
||||
p := t.FilePath()
|
||||
if p == "" {
|
||||
p = "song.yml"
|
||||
}
|
||||
writer, err := t.Explorer.CreateFile(p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.saveSong(writer)
|
||||
}
|
||||
|
||||
func (t *Tracker) ExportWav(pcm16 bool) {
|
||||
filename := "song.wav"
|
||||
if p := t.FilePath(); p != "" {
|
||||
filename = p[:len(p)-len(filepath.Ext(p))] + ".wav"
|
||||
}
|
||||
writer, err := t.Explorer.CreateFile(filename)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.exportWav(writer, pcm16)
|
||||
}
|
||||
|
||||
func (t *Tracker) LoadInstrument() {
|
||||
reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.loadInstrument(reader)
|
||||
}
|
||||
|
||||
func (t *Tracker) SaveInstrument() {
|
||||
writer, err := t.Explorer.CreateFile(t.Instrument().Name + ".yml")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
t.saveInstrument(writer)
|
||||
}
|
||||
|
||||
func (t *Tracker) loadSong(r io.ReadCloser) {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = r.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var song sointu.Song
|
||||
if errJSON := json.Unmarshal(b, &song); errJSON != nil {
|
||||
if errYaml := yaml.Unmarshal(b, &song); errYaml != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error, time.Second*3)
|
||||
}
|
||||
}
|
||||
if song.Score.Length <= 0 || len(song.Score.Tracks) == 0 || len(song.Patch) == 0 {
|
||||
t.Alert.Update("The song file is malformed", Error, time.Second*3)
|
||||
return
|
||||
}
|
||||
t.SetSong(song)
|
||||
path := ""
|
||||
if f, ok := r.(*os.File); ok {
|
||||
path = f.Name()
|
||||
}
|
||||
t.SetFilePath(path)
|
||||
t.ClearUndoHistory()
|
||||
t.SetChangedSinceSave(false)
|
||||
}
|
||||
|
||||
func (t *Tracker) saveSong(w io.WriteCloser) bool {
|
||||
path := ""
|
||||
if f, ok := w.(*os.File); ok {
|
||||
path = f.Name()
|
||||
}
|
||||
var extension = filepath.Ext(path)
|
||||
var contents []byte
|
||||
var err error
|
||||
if extension == ".json" {
|
||||
contents, err = json.Marshal(t.Song())
|
||||
} else {
|
||||
contents, err = yaml.Marshal(t.Song())
|
||||
}
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error marshaling a song file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
if _, err := w.Write(contents); err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error writing to file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error closing file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
t.SetFilePath(path)
|
||||
t.SetChangedSinceSave(false)
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Tracker) exportWav(w io.WriteCloser, pcm16 bool) {
|
||||
data, err := sointu.Play(t.synther, t.Song()) // render the song to calculate its length
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error rendering the song during export: %v", err), Error, time.Second*3)
|
||||
return
|
||||
}
|
||||
buffer, err := data.Wav(pcm16)
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error converting to .wav: %v", err), Error, time.Second*3)
|
||||
return
|
||||
}
|
||||
w.Write(buffer)
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func (t *Tracker) saveInstrument(w io.WriteCloser) bool {
|
||||
path := ""
|
||||
if f, ok := w.(*os.File); ok {
|
||||
path = f.Name()
|
||||
}
|
||||
var extension = filepath.Ext(path)
|
||||
var contents []byte
|
||||
var err error
|
||||
if extension == ".json" {
|
||||
contents, err = json.Marshal(t.Instrument())
|
||||
} else {
|
||||
contents, err = yaml.Marshal(t.Instrument())
|
||||
}
|
||||
if err != nil {
|
||||
t.Alert.Update(fmt.Sprintf("Error marshaling a ínstrument file: %v", err), Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
w.Write(contents)
|
||||
w.Close()
|
||||
return true
|
||||
}
|
||||
|
||||
func (t *Tracker) loadInstrument(r io.ReadCloser) bool {
|
||||
b, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var instrument sointu.Instrument
|
||||
var errJSON, errYaml, err4ki, err4kp error
|
||||
var patch sointu.Patch
|
||||
errJSON = json.Unmarshal(b, &instrument)
|
||||
if errJSON == nil {
|
||||
goto success
|
||||
}
|
||||
errYaml = yaml.Unmarshal(b, &instrument)
|
||||
if errYaml == nil {
|
||||
goto success
|
||||
}
|
||||
patch, err4kp = sointu.Read4klangPatch(bytes.NewReader(b))
|
||||
if err4kp == nil {
|
||||
song := t.Song()
|
||||
song.Score = t.Song().Score.Copy()
|
||||
song.Patch = patch
|
||||
t.SetSong(song)
|
||||
return true
|
||||
}
|
||||
instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b))
|
||||
if err4ki == nil {
|
||||
goto success
|
||||
}
|
||||
t.Alert.Update(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v / %v / %v", errYaml, errJSON, err4ki, err4kp), Error, time.Second*3)
|
||||
return false
|
||||
success:
|
||||
if f, ok := r.(*os.File); ok {
|
||||
filename := f.Name()
|
||||
// the 4klang instrument names are junk, replace them with the filename without extension
|
||||
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
|
||||
}
|
||||
if len(instrument.Units) == 0 {
|
||||
t.Alert.Update("The instrument file is malformed", Error, time.Second*3)
|
||||
return false
|
||||
}
|
||||
t.SetInstrument(instrument)
|
||||
if t.Instrument().Comment != "" {
|
||||
t.InstrumentEditor.ExpandComment()
|
||||
}
|
||||
return true
|
||||
}
|
461
tracker/gioui/instrument_editor.go
Normal file
461
tracker/gioui/instrument_editor.go
Normal file
@ -0,0 +1,461 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type InstrumentEditor struct {
|
||||
newInstrumentBtn *ActionClickable
|
||||
enlargeBtn *BoolClickable
|
||||
deleteInstrumentBtn *ActionClickable
|
||||
copyInstrumentBtn *TipClickable
|
||||
saveInstrumentBtn *TipClickable
|
||||
loadInstrumentBtn *TipClickable
|
||||
addUnitBtn *ActionClickable
|
||||
presetMenuBtn *TipClickable
|
||||
commentExpandBtn *BoolClickable
|
||||
commentEditor *widget.Editor
|
||||
commentString tracker.String
|
||||
nameEditor *widget.Editor
|
||||
nameString tracker.String
|
||||
searchEditor *widget.Editor
|
||||
instrumentDragList *DragList
|
||||
unitDragList *DragList
|
||||
presetDragList *DragList
|
||||
unitEditor *UnitEditor
|
||||
tag bool
|
||||
wasFocused bool
|
||||
presetMenuItems []MenuItem
|
||||
presetMenu Menu
|
||||
}
|
||||
|
||||
func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
|
||||
ret := &InstrumentEditor{
|
||||
newInstrumentBtn: NewActionClickable(model.AddInstrument()),
|
||||
enlargeBtn: NewBoolClickable(model.InstrEnlarged().Bool()),
|
||||
deleteInstrumentBtn: NewActionClickable(model.DeleteInstrument()),
|
||||
copyInstrumentBtn: new(TipClickable),
|
||||
saveInstrumentBtn: new(TipClickable),
|
||||
loadInstrumentBtn: new(TipClickable),
|
||||
addUnitBtn: NewActionClickable(model.AddUnit(false)),
|
||||
commentExpandBtn: NewBoolClickable(model.CommentExpanded().Bool()),
|
||||
presetMenuBtn: new(TipClickable),
|
||||
commentEditor: new(widget.Editor),
|
||||
nameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle},
|
||||
searchEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Start},
|
||||
commentString: model.InstrumentComment().String(),
|
||||
nameString: model.InstrumentName().String(),
|
||||
instrumentDragList: NewDragList(model.Instruments().List(), layout.Horizontal),
|
||||
unitDragList: NewDragList(model.Units().List(), layout.Vertical),
|
||||
unitEditor: NewUnitEditor(model),
|
||||
presetMenuItems: []MenuItem{},
|
||||
}
|
||||
model.IterateInstrumentPresets(func(index int, name string) bool {
|
||||
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: name, IconBytes: icons.ImageAudiotrack, Doer: model.LoadPreset(index)})
|
||||
return true
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Focus() {
|
||||
ie.unitDragList.Focus()
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Focused() bool {
|
||||
return ie.unitDragList.focused
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) ChildFocused() bool {
|
||||
return ie.unitEditor.sliderList.Focused() || ie.instrumentDragList.Focused() || ie.commentEditor.Focused() || ie.nameEditor.Focused() || ie.searchEditor.Focused() ||
|
||||
ie.addUnitBtn.Clickable.Focused() || ie.commentExpandBtn.Clickable.Focused() || ie.presetMenuBtn.Clickable.Focused() || ie.deleteInstrumentBtn.Clickable.Focused() || ie.copyInstrumentBtn.Clickable.Focused()
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
|
||||
ie.wasFocused = ie.Focused() || ie.ChildFocused()
|
||||
fullscreenBtnStyle := ToggleIcon(t.Theme, ie.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, "Enlarge (Ctrl+E)", "Shrink (Ctrl+E)")
|
||||
|
||||
octave := func(gtx C) D {
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
numStyle := NumericUpDown(t.Theme, t.OctaveNumberInput, "Octave down (<) or up (>)")
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
return dims
|
||||
}
|
||||
|
||||
newBtnStyle := ActionIcon(t.Theme, ie.newInstrumentBtn, icons.ContentAdd, "Add\ninstrument\n(Ctrl+I)")
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{}.Layout(
|
||||
gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return ie.layoutInstrumentList(gtx, t)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
inset := layout.UniformInset(unit.Dp(6))
|
||||
return inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("OCT:", white, t.Theme.Shaper)),
|
||||
layout.Rigid(octave),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, fullscreenBtnStyle.Layout)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, newBtnStyle.Layout)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return ie.layoutInstrumentHeader(gtx, t)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return ie.layoutUnitList(gtx, t)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return ie.unitEditor.Layout(gtx, t)
|
||||
}),
|
||||
)
|
||||
}))
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
|
||||
header := func(gtx C) D {
|
||||
commentExpandBtnStyle := ToggleIcon(t.Theme, ie.commentExpandBtn, icons.NavigationExpandMore, icons.NavigationExpandLess, "Expand comment", "Collapse comment")
|
||||
presetMenuBtnStyle := TipIcon(t.Theme, ie.presetMenuBtn, icons.NavigationMenu, "Load preset")
|
||||
copyInstrumentBtnStyle := TipIcon(t.Theme, ie.copyInstrumentBtn, icons.ContentContentCopy, "Copy instrument")
|
||||
saveInstrumentBtnStyle := TipIcon(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, "Save instrument")
|
||||
loadInstrumentBtnStyle := TipIcon(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, "Load instrument")
|
||||
deleteInstrumentBtnStyle := ActionIcon(t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, "Delete\ninstrument")
|
||||
|
||||
m := PopupMenu(&ie.presetMenu, t.Theme.Shaper)
|
||||
|
||||
for ie.copyInstrumentBtn.Clickable.Clicked() {
|
||||
if contents, ok := t.Instruments().List().CopyElements(); ok {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alerts().Add("Instrument copied to clipboard", tracker.Info)
|
||||
}
|
||||
}
|
||||
|
||||
for ie.saveInstrumentBtn.Clickable.Clicked() {
|
||||
writer, err := t.Explorer.CreateFile(t.InstrumentName().Value() + ".yml")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
t.SaveInstrument(writer)
|
||||
}
|
||||
|
||||
for ie.loadInstrumentBtn.Clickable.Clicked() {
|
||||
reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp")
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
t.LoadInstrument(reader)
|
||||
}
|
||||
|
||||
header := func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label("Voices: ", white, t.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
numStyle := NumericUpDown(t.Theme, t.InstrumentVoices, "Number of voices for this instrument")
|
||||
dims := numStyle.Layout(gtx)
|
||||
return dims
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(commentExpandBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
dims := presetMenuBtnStyle.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(500))
|
||||
gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180))
|
||||
m.Layout(gtx, ie.presetMenuItems...)
|
||||
return dims
|
||||
}),
|
||||
layout.Rigid(saveInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(loadInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(copyInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(deleteInstrumentBtnStyle.Layout))
|
||||
}
|
||||
|
||||
for ie.presetMenuBtn.Clickable.Clicked() {
|
||||
ie.presetMenu.Visible = true
|
||||
}
|
||||
|
||||
if ie.commentExpandBtn.Bool.Value() || ie.commentEditor.Focused() { // we draw once the widget after it manages to lose focus
|
||||
if ie.commentEditor.Text() != ie.commentString.Value() {
|
||||
ie.commentEditor.SetText(ie.commentString.Value())
|
||||
}
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(header),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &ie.unitDragList, Keys: globalKeys + "|⎋"}.Add(gtx.Ops)
|
||||
for _, event := range gtx.Events(&ie.unitDragList) {
|
||||
if e, ok := event.(key.Event); ok && e.State == key.Press && e.Name == key.NameEscape {
|
||||
ie.instrumentDragList.Focus()
|
||||
}
|
||||
}
|
||||
editorStyle := material.Editor(t.Theme, ie.commentEditor, "Comment")
|
||||
editorStyle.Color = highEmphasisTextColor
|
||||
return layout.UniformInset(unit.Dp(6)).Layout(gtx, editorStyle.Layout)
|
||||
}),
|
||||
)
|
||||
ie.commentString.Set(ie.commentEditor.Text())
|
||||
return ret
|
||||
}
|
||||
return header(gtx)
|
||||
}
|
||||
|
||||
return Surface{Gray: 37, Focus: ie.wasFocused}.Layout(gtx, header)
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layoutInstrumentList(gtx C, t *Tracker) D {
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(36))
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(30))
|
||||
grabhandle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(10), Alignment: layout.Center, Shaper: t.Theme.Shaper}
|
||||
if i == ie.instrumentDragList.TrackerList.Selected() {
|
||||
grabhandle.Text = ":::"
|
||||
}
|
||||
label := func(gtx C) D {
|
||||
name, level, ok := (*tracker.Instruments)(t.Model).Item(i)
|
||||
if !ok {
|
||||
labelStyle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
return layout.Center.Layout(gtx, labelStyle.Layout)
|
||||
}
|
||||
k := byte(255 - level*127)
|
||||
color := color.NRGBA{R: 255, G: k, B: 255, A: 255}
|
||||
if i == ie.instrumentDragList.TrackerList.Selected() {
|
||||
for _, ev := range ie.nameEditor.Events() {
|
||||
_, ok := ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
ie.instrumentDragList.Focus()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if n := name; n != ie.nameEditor.Text() {
|
||||
ie.nameEditor.SetText(n)
|
||||
}
|
||||
editor := material.Editor(t.Theme, ie.nameEditor, "Instr")
|
||||
editor.Color = color
|
||||
editor.HintColor = instrumentNameHintColor
|
||||
editor.TextSize = unit.Sp(12)
|
||||
editor.Font = labelDefaultFont
|
||||
dims := layout.Center.Layout(gtx, func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &ie.nameEditor, Keys: globalKeys}.Add(gtx.Ops)
|
||||
return editor.Layout(gtx)
|
||||
})
|
||||
ie.nameString.Set(ie.nameEditor.Text())
|
||||
return dims
|
||||
}
|
||||
if name == "" {
|
||||
name = "Instr"
|
||||
}
|
||||
labelStyle := LabelStyle{Text: name, ShadeColor: black, Color: color, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
return layout.Center.Layout(gtx, labelStyle.Layout)
|
||||
}
|
||||
return layout.Inset{Left: unit.Dp(6), Right: unit.Dp(6)}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(grabhandle.Layout),
|
||||
layout.Rigid(label),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
color := inactiveLightSurfaceColor
|
||||
if ie.wasFocused {
|
||||
color = activeLightSurfaceColor
|
||||
}
|
||||
instrumentList := FilledDragList(t.Theme, ie.instrumentDragList, element, nil)
|
||||
instrumentList.SelectedColor = color
|
||||
instrumentList.HoverColor = instrumentHoverColor
|
||||
instrumentList.ScrollBarWidth = unit.Dp(6)
|
||||
|
||||
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()
|
||||
key.InputOp{Tag: ie.instrumentDragList, Keys: "↓|⏎|⌤"}.Add(gtx.Ops)
|
||||
|
||||
for _, event := range gtx.Events(ie.instrumentDragList) {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameDownArrow:
|
||||
ie.unitDragList.Focus()
|
||||
case key.NameReturn, key.NameEnter:
|
||||
ie.nameEditor.Focus()
|
||||
l := len(ie.nameEditor.Text())
|
||||
ie.nameEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dims := instrumentList.Layout(gtx)
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
instrumentList.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}
|
||||
func (ie *InstrumentEditor) layoutUnitList(gtx C, t *Tracker) D {
|
||||
// TODO: how to ie.unitDragList.Focus()
|
||||
addUnitBtnStyle := ActionIcon(t.Theme, ie.addUnitBtn, icons.ContentAdd, "Add unit (Enter)")
|
||||
addUnitBtnStyle.IconButtonStyle.Color = t.Theme.ContrastFg
|
||||
addUnitBtnStyle.IconButtonStyle.Background = t.Theme.Fg
|
||||
addUnitBtnStyle.IconButtonStyle.Inset = layout.UniformInset(unit.Dp(4))
|
||||
|
||||
index := 0
|
||||
var units [256]tracker.UnitListItem
|
||||
(*tracker.Units)(t.Model).Iterate(func(item tracker.UnitListItem) (ok bool) {
|
||||
units[index] = item
|
||||
index++
|
||||
return index <= 256
|
||||
})
|
||||
count := intMin(ie.unitDragList.TrackerList.Count(), 256)
|
||||
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(unit.Dp(120)), gtx.Dp(unit.Dp(20))))
|
||||
if i < 0 || i >= count {
|
||||
return layout.Dimensions{Size: gtx.Constraints.Min}
|
||||
}
|
||||
u := units[i]
|
||||
var color color.NRGBA = white
|
||||
|
||||
var stackText string
|
||||
stackText = strconv.FormatInt(int64(u.StackAfter), 10)
|
||||
if u.StackNeed > u.StackBefore {
|
||||
color = errorColor
|
||||
(*tracker.Alerts)(t.Model).AddNamed("UnitNeedsInputs", fmt.Sprintf("%v needs at least %v input signals, got %v", u.Type, u.StackNeed, u.StackBefore), tracker.Error)
|
||||
} else if i == count-1 && u.StackAfter != 0 {
|
||||
color = warningColor
|
||||
(*tracker.Alerts)(t.Model).AddNamed("InstrumentLeavesSignals", fmt.Sprintf("Instrument leaves %v signal(s) on the stack", u.StackAfter), tracker.Warning)
|
||||
}
|
||||
|
||||
stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
rightMargin := layout.Inset{Right: unit.Dp(10)}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
if i == ie.unitDragList.TrackerList.Selected() {
|
||||
for _, ev := range ie.searchEditor.Events() {
|
||||
_, ok := ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
txt := ""
|
||||
ie.unitDragList.Focus()
|
||||
if text := ie.searchEditor.Text(); text != "" {
|
||||
for _, n := range sointu.UnitNames {
|
||||
if strings.HasPrefix(n, ie.searchEditor.Text()) {
|
||||
txt = n
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Units().SetSelectedType(txt)
|
||||
continue
|
||||
}
|
||||
}
|
||||
editor := material.Editor(t.Theme, ie.searchEditor, "---")
|
||||
editor.Color = color
|
||||
editor.HintColor = instrumentNameHintColor
|
||||
editor.TextSize = unit.Sp(12)
|
||||
editor.Font = labelDefaultFont
|
||||
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &ie.searchEditor, Keys: globalKeys}.Add(gtx.Ops)
|
||||
str := tracker.String{StringData: (*tracker.UnitSearch)(t.Model)}
|
||||
if ie.searchEditor.Text() != str.Value() {
|
||||
ie.searchEditor.SetText(str.Value())
|
||||
}
|
||||
ret := editor.Layout(gtx)
|
||||
str.Set(ie.searchEditor.Text())
|
||||
return ret
|
||||
} else {
|
||||
unitNameLabel := LabelStyle{Text: u.Type, ShadeColor: black, Color: color, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
if unitNameLabel.Text == "" {
|
||||
unitNameLabel.Text = "---"
|
||||
}
|
||||
return unitNameLabel.Layout(gtx)
|
||||
}
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return rightMargin.Layout(gtx, stackLabel.Layout)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
unitList := FilledDragList(t.Theme, ie.unitDragList, element, nil)
|
||||
return Surface{Gray: 30, Focus: ie.wasFocused}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Stack{Alignment: layout.SE}.Layout(gtx,
|
||||
layout.Expanded(func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: ie.unitDragList, Keys: "→|⏎|Ctrl-⏎|⌫|⎋"}.Add(gtx.Ops)
|
||||
for _, event := range gtx.Events(ie.unitDragList) {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameEscape:
|
||||
ie.instrumentDragList.Focus()
|
||||
case key.NameRightArrow:
|
||||
ie.unitEditor.sliderList.Focus()
|
||||
case key.NameDeleteBackward:
|
||||
t.Units().SetSelectedType("")
|
||||
ie.searchEditor.Focus()
|
||||
l := len(ie.searchEditor.Text())
|
||||
ie.searchEditor.SetCaret(l, l)
|
||||
case key.NameReturn:
|
||||
t.Model.AddUnit(e.Modifiers.Contain(key.ModCtrl)).Do()
|
||||
ie.searchEditor.SetText("")
|
||||
ie.searchEditor.Focus()
|
||||
l := len(ie.searchEditor.Text())
|
||||
ie.searchEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(unit.Dp(120)), gtx.Constraints.Max.Y))
|
||||
dims := unitList.Layout(gtx)
|
||||
unitList.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)}
|
||||
return margin.Layout(gtx, addUnitBtnStyle.Layout)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func clamp(i, min, max int) int {
|
||||
if i < min {
|
||||
return min
|
||||
}
|
||||
if i > max {
|
||||
return max
|
||||
}
|
||||
return i
|
||||
}
|
@ -1,578 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/eventx"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type InstrumentEditor struct {
|
||||
newInstrumentBtn *TipClickable
|
||||
enlargeBtn *TipClickable
|
||||
deleteInstrumentBtn *TipClickable
|
||||
copyInstrumentBtn *TipClickable
|
||||
saveInstrumentBtn *TipClickable
|
||||
loadInstrumentBtn *TipClickable
|
||||
addUnitBtn *TipClickable
|
||||
commentExpandBtn *TipClickable
|
||||
presetMenuBtn *TipClickable
|
||||
commentEditor *widget.Editor
|
||||
nameEditor *widget.Editor
|
||||
unitTypeEditor *widget.Editor
|
||||
instrumentDragList *DragList
|
||||
instrumentScrollBar *ScrollBar
|
||||
unitDragList *DragList
|
||||
unitScrollBar *ScrollBar
|
||||
confirmInstrDelete *Dialog
|
||||
paramEditor *ParamEditor
|
||||
stackUse []int
|
||||
tag bool
|
||||
wasFocused bool
|
||||
commentExpanded bool
|
||||
voiceLevels [vm.MAX_VOICES]float32
|
||||
presetMenuItems []MenuItem
|
||||
presetMenu Menu
|
||||
}
|
||||
|
||||
func NewInstrumentEditor() *InstrumentEditor {
|
||||
ret := &InstrumentEditor{
|
||||
newInstrumentBtn: new(TipClickable),
|
||||
enlargeBtn: new(TipClickable),
|
||||
deleteInstrumentBtn: new(TipClickable),
|
||||
copyInstrumentBtn: new(TipClickable),
|
||||
saveInstrumentBtn: new(TipClickable),
|
||||
loadInstrumentBtn: new(TipClickable),
|
||||
addUnitBtn: new(TipClickable),
|
||||
commentExpandBtn: new(TipClickable),
|
||||
presetMenuBtn: new(TipClickable),
|
||||
commentEditor: new(widget.Editor),
|
||||
nameEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Middle},
|
||||
unitTypeEditor: &widget.Editor{SingleLine: true, Submit: true, Alignment: text.Start},
|
||||
instrumentDragList: &DragList{List: &layout.List{Axis: layout.Horizontal}, HoverItem: -1},
|
||||
instrumentScrollBar: &ScrollBar{Axis: layout.Horizontal},
|
||||
unitDragList: &DragList{List: &layout.List{Axis: layout.Vertical}, HoverItem: -1},
|
||||
unitScrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
confirmInstrDelete: new(Dialog),
|
||||
paramEditor: NewParamEditor(),
|
||||
presetMenuItems: []MenuItem{},
|
||||
}
|
||||
for _, instr := range tracker.InstrumentPresets {
|
||||
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: instr.Name, IconBytes: icons.ImageAudiotrack})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (t *InstrumentEditor) ExpandComment() {
|
||||
t.commentExpanded = true
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Focus() {
|
||||
ie.unitDragList.Focus()
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Focused() bool {
|
||||
return ie.unitDragList.focused
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) ChildFocused() bool {
|
||||
return ie.paramEditor.Focused() || ie.instrumentDragList.Focused() || ie.commentEditor.Focused() || ie.nameEditor.Focused() || ie.unitTypeEditor.Focused() ||
|
||||
ie.addUnitBtn.Clickable.Focused() || ie.commentExpandBtn.Clickable.Focused() || ie.presetMenuBtn.Clickable.Focused() || ie.deleteInstrumentBtn.Clickable.Focused() || ie.copyInstrumentBtn.Clickable.Focused()
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
|
||||
ie.wasFocused = ie.Focused() || ie.ChildFocused()
|
||||
for _, e := range gtx.Events(&ie.tag) {
|
||||
switch e.(type) {
|
||||
case pointer.Event:
|
||||
ie.unitDragList.Focus()
|
||||
}
|
||||
}
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &ie.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
|
||||
enlargeTip := "Enlarge"
|
||||
icon := icons.NavigationFullscreen
|
||||
if t.InstrEnlarged() {
|
||||
icon = icons.NavigationFullscreenExit
|
||||
enlargeTip = "Shrink"
|
||||
}
|
||||
|
||||
fullscreenBtnStyle := IconButton(t.Theme, ie.enlargeBtn, icon, true, enlargeTip)
|
||||
for ie.enlargeBtn.Clickable.Clicked() {
|
||||
t.SetInstrEnlarged(!t.InstrEnlarged())
|
||||
}
|
||||
for ie.newInstrumentBtn.Clickable.Clicked() {
|
||||
t.AddInstrument(true)
|
||||
}
|
||||
octave := func(gtx C) D {
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
t.OctaveNumberInput.Value = t.Octave()
|
||||
numStyle := NumericUpDown(t.Theme, t.OctaveNumberInput, 0, 9, "Octave down (<) or up (>)")
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetOctave(t.OctaveNumberInput.Value)
|
||||
return dims
|
||||
}
|
||||
newBtnStyle := IconButton(t.Theme, ie.newInstrumentBtn, icons.ContentAdd, t.CanAddInstrument(), "Add\ninstrument")
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{}.Layout(
|
||||
gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return ie.layoutInstrumentNames(gtx, t)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return ie.instrumentScrollBar.Layout(gtx, unit.Dp(6), len(t.Song().Patch), &ie.instrumentDragList.List.Position)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
inset := layout.UniformInset(unit.Dp(6))
|
||||
return inset.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("OCT:", white, t.TextShaper)),
|
||||
layout.Rigid(octave),
|
||||
)
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, fullscreenBtnStyle.Layout)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.E.Layout(gtx, newBtnStyle.Layout)
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return ie.layoutInstrumentHeader(gtx, t)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return ie.layoutInstrumentEditor(gtx, t)
|
||||
}))
|
||||
return ret
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
|
||||
header := func(gtx C) D {
|
||||
collapseIcon := icons.NavigationExpandLess
|
||||
commentTip := "Collapse comment"
|
||||
if !ie.commentExpanded {
|
||||
collapseIcon = icons.NavigationExpandMore
|
||||
commentTip = "Expand comment"
|
||||
}
|
||||
|
||||
commentExpandBtnStyle := IconButton(t.Theme, ie.commentExpandBtn, collapseIcon, true, commentTip)
|
||||
presetMenuBtnStyle := IconButton(t.Theme, ie.presetMenuBtn, icons.NavigationMenu, true, "Load preset")
|
||||
copyInstrumentBtnStyle := IconButton(t.Theme, ie.copyInstrumentBtn, icons.ContentContentCopy, true, "Copy instrument")
|
||||
saveInstrumentBtnStyle := IconButton(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, true, "Save instrument")
|
||||
loadInstrumentBtnStyle := IconButton(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, true, "Load instrument")
|
||||
deleteInstrumentBtnStyle := IconButton(t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, t.CanDeleteInstrument(), "Delete\ninstrument")
|
||||
|
||||
m := t.PopupMenu(&ie.presetMenu)
|
||||
|
||||
for item, clicked := ie.presetMenu.Clicked(); clicked; item, clicked = ie.presetMenu.Clicked() {
|
||||
t.SetInstrument(tracker.InstrumentPresets[item])
|
||||
}
|
||||
|
||||
header := func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(Label("Voices: ", white, t.TextShaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
maxRemain := t.MaxInstrumentVoices()
|
||||
t.InstrumentVoices.Value = t.Instrument().NumVoices
|
||||
numStyle := NumericUpDown(t.Theme, t.InstrumentVoices, 0, maxRemain, "Number of voices for this instrument")
|
||||
dims := numStyle.Layout(gtx)
|
||||
t.SetInstrumentVoices(t.InstrumentVoices.Value)
|
||||
return dims
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(commentExpandBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
//defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
dims := presetMenuBtnStyle.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(500))
|
||||
gtx.Constraints.Max.X = gtx.Dp(unit.Dp(180))
|
||||
m.Layout(gtx, ie.presetMenuItems...)
|
||||
return dims
|
||||
}),
|
||||
layout.Rigid(saveInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(loadInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(copyInstrumentBtnStyle.Layout),
|
||||
layout.Rigid(deleteInstrumentBtnStyle.Layout))
|
||||
}
|
||||
|
||||
for ie.presetMenuBtn.Clickable.Clicked() {
|
||||
ie.presetMenu.Visible = true
|
||||
}
|
||||
|
||||
for ie.commentExpandBtn.Clickable.Clicked() {
|
||||
ie.commentExpanded = !ie.commentExpanded
|
||||
if !ie.commentExpanded {
|
||||
key.FocusOp{Tag: &ie.tag}.Add(gtx.Ops) // clear focus
|
||||
}
|
||||
}
|
||||
if ie.commentExpanded || ie.commentEditor.Focused() { // we draw once the widget after it manages to lose focus
|
||||
if ie.commentEditor.Text() != t.Instrument().Comment {
|
||||
ie.commentEditor.SetText(t.Instrument().Comment)
|
||||
}
|
||||
editorStyle := material.Editor(t.Theme, ie.commentEditor, "Comment")
|
||||
editorStyle.Color = highEmphasisTextColor
|
||||
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(header),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
spy, spiedGtx := eventx.Enspy(gtx)
|
||||
ret := layout.UniformInset(unit.Dp(6)).Layout(spiedGtx, editorStyle.Layout)
|
||||
for _, group := range spy.AllEvents() {
|
||||
for _, event := range group.Items {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
if e.Name == key.NameEscape {
|
||||
ie.instrumentDragList.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}),
|
||||
)
|
||||
t.SetInstrumentComment(ie.commentEditor.Text())
|
||||
return ret
|
||||
}
|
||||
return header(gtx)
|
||||
}
|
||||
|
||||
for ie.copyInstrumentBtn.Clickable.Clicked() {
|
||||
contents, err := yaml.Marshal(t.Instrument())
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Instrument copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
}
|
||||
for ie.deleteInstrumentBtn.Clickable.Clicked() {
|
||||
if t.CanDeleteInstrument() {
|
||||
dialogStyle := ConfirmDialog(t.Theme, ie.confirmInstrDelete, "Are you sure you want to delete this instrument?", t.TextShaper)
|
||||
ie.confirmInstrDelete.Visible = true
|
||||
t.ModalDialog = dialogStyle.Layout
|
||||
}
|
||||
}
|
||||
for ie.confirmInstrDelete.BtnOk.Clicked() {
|
||||
t.DeleteInstrument(false)
|
||||
t.ModalDialog = nil
|
||||
}
|
||||
for ie.confirmInstrDelete.BtnCancel.Clicked() {
|
||||
t.ModalDialog = nil
|
||||
}
|
||||
for ie.saveInstrumentBtn.Clickable.Clicked() {
|
||||
t.SaveInstrument()
|
||||
}
|
||||
|
||||
for ie.loadInstrumentBtn.Clickable.Clicked() {
|
||||
t.LoadInstrument()
|
||||
}
|
||||
return Surface{Gray: 37, Focus: ie.wasFocused}.Layout(gtx, header)
|
||||
}
|
||||
|
||||
func (ie *InstrumentEditor) layoutInstrumentNames(gtx C, t *Tracker) D {
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(30))
|
||||
grabhandle := LabelStyle{Text: "", ShadeColor: black, Color: white, FontSize: unit.Sp(10), Alignment: layout.Center, Shaper: t.TextShaper}
|
||||
if i == t.InstrIndex() {
|
||||
grabhandle.Text = ":::"
|
||||
}
|
||||
label := func(gtx C) D {
|
||||
c := float32(0.0)
|
||||
voice := t.Song().Patch.FirstVoiceForInstrument(i)
|
||||
loopMax := t.Song().Patch[i].NumVoices
|
||||
if loopMax > vm.MAX_VOICES {
|
||||
loopMax = vm.MAX_VOICES
|
||||
}
|
||||
for j := 0; j < loopMax; j++ {
|
||||
vc := ie.voiceLevels[voice]
|
||||
if c < vc {
|
||||
c = vc
|
||||
}
|
||||
voice++
|
||||
}
|
||||
k := byte(255 - c*127)
|
||||
color := color.NRGBA{R: 255, G: k, B: 255, A: 255}
|
||||
if i == t.InstrIndex() {
|
||||
for _, ev := range ie.nameEditor.Events() {
|
||||
_, ok := ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
ie.instrumentDragList.Focus()
|
||||
continue
|
||||
}
|
||||
}
|
||||
if n := t.Instrument().Name; n != ie.nameEditor.Text() {
|
||||
ie.nameEditor.SetText(n)
|
||||
}
|
||||
editor := material.Editor(t.Theme, ie.nameEditor, "Instr")
|
||||
editor.Color = color
|
||||
editor.HintColor = instrumentNameHintColor
|
||||
editor.TextSize = unit.Sp(12)
|
||||
dims := layout.Center.Layout(gtx, editor.Layout)
|
||||
t.SetInstrumentName(ie.nameEditor.Text())
|
||||
return dims
|
||||
}
|
||||
text := t.Song().Patch[i].Name
|
||||
if text == "" {
|
||||
text = "Instr"
|
||||
}
|
||||
labelStyle := LabelStyle{Text: text, ShadeColor: black, Color: color, FontSize: unit.Sp(12), Shaper: t.TextShaper}
|
||||
return layout.Center.Layout(gtx, labelStyle.Layout)
|
||||
}
|
||||
return layout.Inset{Left: unit.Dp(6), Right: unit.Dp(6)}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(grabhandle.Layout),
|
||||
layout.Rigid(label),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
color := inactiveLightSurfaceColor
|
||||
if ie.wasFocused {
|
||||
color = activeLightSurfaceColor
|
||||
}
|
||||
instrumentList := FilledDragList(t.Theme, ie.instrumentDragList, len(t.Song().Patch), element, t.SwapInstruments)
|
||||
instrumentList.SelectedColor = color
|
||||
instrumentList.HoverColor = instrumentHoverColor
|
||||
|
||||
ie.instrumentDragList.SelectedItem = t.InstrIndex()
|
||||
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()
|
||||
key.InputOp{Tag: ie.instrumentDragList, Keys: "↓|⏎|⌤"}.Add(gtx.Ops)
|
||||
|
||||
for _, event := range gtx.Events(ie.instrumentDragList) {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameDownArrow:
|
||||
ie.unitDragList.Focus()
|
||||
case key.NameReturn, key.NameEnter:
|
||||
ie.nameEditor.Focus()
|
||||
l := len(ie.nameEditor.Text())
|
||||
ie.nameEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dims := instrumentList.Layout(gtx)
|
||||
|
||||
if t.InstrIndex() != ie.instrumentDragList.SelectedItem {
|
||||
t.SetInstrIndex(ie.instrumentDragList.SelectedItem)
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
}
|
||||
return dims
|
||||
}
|
||||
func (ie *InstrumentEditor) layoutInstrumentEditor(gtx C, t *Tracker) D {
|
||||
for ie.addUnitBtn.Clickable.Clicked() {
|
||||
t.AddUnit(true)
|
||||
ie.unitDragList.Focus()
|
||||
}
|
||||
addUnitBtnStyle := IconButton(t.Theme, ie.addUnitBtn, icons.ContentAdd, true, "Add unit (Ctrl+Enter)")
|
||||
addUnitBtnStyle.IconButtonStyle.Color = t.Theme.ContrastFg
|
||||
addUnitBtnStyle.IconButtonStyle.Background = t.Theme.Fg
|
||||
addUnitBtnStyle.IconButtonStyle.Inset = layout.UniformInset(unit.Dp(4))
|
||||
|
||||
units := t.Instrument().Units
|
||||
for len(ie.stackUse) < len(units) {
|
||||
ie.stackUse = append(ie.stackUse, 0)
|
||||
}
|
||||
|
||||
stackHeight := 0
|
||||
for i, u := range units {
|
||||
stackHeight += u.StackChange()
|
||||
ie.stackUse[i] = stackHeight
|
||||
}
|
||||
|
||||
element := func(gtx C, i int) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(unit.Dp(120)), gtx.Dp(unit.Dp(20))))
|
||||
u := units[i]
|
||||
var color color.NRGBA = white
|
||||
|
||||
var stackText string
|
||||
if i < len(ie.stackUse) {
|
||||
stackText = strconv.FormatInt(int64(ie.stackUse[i]), 10)
|
||||
var prevStackUse int
|
||||
if i > 0 {
|
||||
prevStackUse = ie.stackUse[i-1]
|
||||
}
|
||||
if stackNeed := u.StackNeed(); stackNeed > prevStackUse {
|
||||
color = errorColor
|
||||
typeString := u.Type
|
||||
if u.Parameters["stereo"] == 1 {
|
||||
typeString += " (stereo)"
|
||||
}
|
||||
t.Alert.Update(fmt.Sprintf("%v needs at least %v input signals, got %v", typeString, stackNeed, prevStackUse), Error, 0)
|
||||
} else if i == len(units)-1 && ie.stackUse[i] != 0 {
|
||||
color = warningColor
|
||||
t.Alert.Update(fmt.Sprintf("Instrument leaves %v signal(s) on the stack", ie.stackUse[i]), Warning, 0)
|
||||
}
|
||||
}
|
||||
|
||||
var unitName layout.Widget
|
||||
if i == t.UnitIndex() {
|
||||
for _, ev := range ie.unitTypeEditor.Events() {
|
||||
_, ok := ev.(widget.SubmitEvent)
|
||||
if ok {
|
||||
ie.unitDragList.Focus()
|
||||
if text := ie.unitTypeEditor.Text(); text != "" {
|
||||
for _, n := range sointu.UnitNames {
|
||||
if strings.HasPrefix(n, ie.unitTypeEditor.Text()) {
|
||||
t.SetUnitType(n)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
t.SetUnitType("")
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !ie.unitTypeEditor.Focused() && !ie.paramEditor.Focused() && ie.unitTypeEditor.Text() != t.Unit().Type {
|
||||
ie.unitTypeEditor.SetText(t.Unit().Type)
|
||||
}
|
||||
editor := material.Editor(t.Theme, ie.unitTypeEditor, "---")
|
||||
editor.Color = color
|
||||
editor.HintColor = instrumentNameHintColor
|
||||
editor.TextSize = unit.Sp(12)
|
||||
editor.Font = labelDefaultFont
|
||||
unitName = editor.Layout
|
||||
} else {
|
||||
unitNameLabel := LabelStyle{Text: u.Type, ShadeColor: black, Color: color, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.TextShaper}
|
||||
if unitNameLabel.Text == "" {
|
||||
unitNameLabel.Text = "---"
|
||||
}
|
||||
unitName = unitNameLabel.Layout
|
||||
}
|
||||
|
||||
stackLabel := LabelStyle{Text: stackText, ShadeColor: black, Color: mediumEmphasisTextColor, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.TextShaper}
|
||||
rightMargin := layout.Inset{Right: unit.Dp(10)}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Flexed(1, unitName),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return rightMargin.Layout(gtx, stackLabel.Layout)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
unitList := FilledDragList(t.Theme, ie.unitDragList, len(units), element, t.SwapUnits)
|
||||
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 {
|
||||
return layout.Stack{Alignment: layout.SE}.Layout(gtx,
|
||||
layout.Expanded(func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: ie.unitDragList, Keys: "→|⏎|⌫|⌦|⎋|Ctrl-⏎|Ctrl-C|Ctrl-X"}.Add(gtx.Ops)
|
||||
for _, event := range gtx.Events(ie.unitDragList) {
|
||||
switch e := event.(type) {
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameEscape:
|
||||
ie.instrumentDragList.Focus()
|
||||
case key.NameRightArrow:
|
||||
ie.paramEditor.Focus()
|
||||
case key.NameDeleteBackward:
|
||||
t.SetUnitType("")
|
||||
ie.unitTypeEditor.Focus()
|
||||
l := len(ie.unitTypeEditor.Text())
|
||||
ie.unitTypeEditor.SetCaret(l, l)
|
||||
case key.NameDeleteForward:
|
||||
t.DeleteUnits(true, ie.unitDragList.SelectedItem, ie.unitDragList.SelectedItem2)
|
||||
ie.unitDragList.SelectedItem2 = t.UnitIndex()
|
||||
case "X":
|
||||
units := t.DeleteUnits(true, ie.unitDragList.SelectedItem, ie.unitDragList.SelectedItem2)
|
||||
ie.unitDragList.SelectedItem2 = t.UnitIndex()
|
||||
contents, err := yaml.Marshal(units)
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Unit(s) cut to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
case "C":
|
||||
a := clamp(ie.unitDragList.SelectedItem, 0, len(t.Instrument().Units)-1)
|
||||
b := clamp(ie.unitDragList.SelectedItem2, 0, len(t.Instrument().Units)-1)
|
||||
if a > b {
|
||||
a, b = b, a
|
||||
}
|
||||
units := t.Instrument().Units[a : b+1]
|
||||
contents, err := yaml.Marshal(units)
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Unit(s) copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
case key.NameReturn:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.AddUnit(true)
|
||||
ie.unitDragList.SelectedItem2 = ie.unitDragList.SelectedItem
|
||||
ie.unitTypeEditor.SetText("")
|
||||
}
|
||||
ie.unitTypeEditor.Focus()
|
||||
l := len(ie.unitTypeEditor.Text())
|
||||
ie.unitTypeEditor.SetCaret(l, l)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ie.unitDragList.SelectedItem = t.UnitIndex()
|
||||
dims := unitList.Layout(gtx)
|
||||
if t.UnitIndex() != ie.unitDragList.SelectedItem {
|
||||
t.SetUnitIndex(ie.unitDragList.SelectedItem)
|
||||
ie.unitTypeEditor.SetText(t.Unit().Type)
|
||||
}
|
||||
return dims
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
margin := layout.Inset{Right: unit.Dp(20), Bottom: unit.Dp(1)}
|
||||
return margin.Layout(gtx, addUnitBtnStyle.Layout)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return ie.unitScrollBar.Layout(gtx, unit.Dp(10), len(t.Instrument().Units), &ie.unitDragList.List.Position)
|
||||
}))
|
||||
}),
|
||||
layout.Rigid(ie.paramEditor.Bind(t)))
|
||||
})
|
||||
}
|
||||
|
||||
func clamp(i, min, max int) int {
|
||||
if i < min {
|
||||
return min
|
||||
}
|
||||
if i > max {
|
||||
return max
|
||||
}
|
||||
return i
|
||||
}
|
@ -1,15 +1,15 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/op"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// globalKeys is a list of keys that are handled globally by the app.
|
||||
// All Editors should capture these keys to prevent them flowing to the global handler.
|
||||
var globalKeys = key.Set("Space|\\|<|>|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|1|2|3|4|5|6|7|8|9|0|,|.")
|
||||
|
||||
var noteMap = map[string]int{
|
||||
"Z": -12,
|
||||
"S": -11,
|
||||
@ -49,15 +49,6 @@ var noteMap = map[string]int{
|
||||
func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) {
|
||||
if e.State == key.Press {
|
||||
switch e.Name {
|
||||
case "C":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
contents, err := yaml.Marshal(t.Song())
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(o)
|
||||
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
return
|
||||
}
|
||||
case "V":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
clipboard.ReadOp{Tag: t}.Add(o)
|
||||
@ -65,97 +56,118 @@ func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) {
|
||||
}
|
||||
case "Z":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Undo()
|
||||
t.Model.Undo().Do()
|
||||
return
|
||||
}
|
||||
case "Y":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Redo()
|
||||
t.Model.Redo().Do()
|
||||
return
|
||||
}
|
||||
case "N":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.NewSong(false)
|
||||
t.NewSong().Do()
|
||||
return
|
||||
}
|
||||
case "S":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.SaveSongFile()
|
||||
t.SaveSong().Do()
|
||||
return
|
||||
}
|
||||
case "O":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.OpenSongFile(false)
|
||||
t.OpenSong().Do()
|
||||
return
|
||||
}
|
||||
case "I":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.DeleteInstrument().Do()
|
||||
} else {
|
||||
t.AddInstrument().Do()
|
||||
}
|
||||
return
|
||||
}
|
||||
case "T":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.DeleteTrack().Do()
|
||||
} else {
|
||||
t.AddTrack().Do()
|
||||
}
|
||||
return
|
||||
}
|
||||
case "E":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.InstrEnlarged().Bool().Toggle()
|
||||
return
|
||||
}
|
||||
case "W":
|
||||
if e.Modifiers.Contain(key.ModShortcut) && canQuit {
|
||||
t.Quit().Do()
|
||||
return
|
||||
}
|
||||
case "F1":
|
||||
t.OrderEditor.Focus()
|
||||
t.OrderEditor.scrollTable.Focus()
|
||||
return
|
||||
case "F2":
|
||||
t.TrackEditor.Focus()
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
return
|
||||
case "F3":
|
||||
t.InstrumentEditor.Focus()
|
||||
return
|
||||
case "F4":
|
||||
t.TrackEditor.Focus()
|
||||
return
|
||||
case "F5":
|
||||
t.SetNoteTracking(true)
|
||||
startRow := t.Cursor().ScoreRow
|
||||
t.PlayFromPosition(startRow)
|
||||
t.SongPanel.RewindBtn.Action.Do()
|
||||
t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl))
|
||||
return
|
||||
case "F6":
|
||||
t.SetNoteTracking(false)
|
||||
startRow := t.Cursor().ScoreRow
|
||||
t.PlayFromPosition(startRow)
|
||||
case "F6", "Space":
|
||||
t.SongPanel.PlayingBtn.Bool.Toggle()
|
||||
t.SongPanel.NoteTracking.Bool.Set(!e.Modifiers.Contain(key.ModCtrl))
|
||||
return
|
||||
case "F7":
|
||||
t.SongPanel.RecordBtn.Bool.Toggle()
|
||||
return
|
||||
case "F8":
|
||||
t.SetPlaying(false)
|
||||
t.SongPanel.NoteTracking.Bool.Toggle()
|
||||
return
|
||||
case "F12":
|
||||
t.Panic().Bool().Toggle()
|
||||
return
|
||||
case "Space":
|
||||
if !t.Playing() && !t.InstrEnlarged() {
|
||||
t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut))
|
||||
startRow := t.Cursor().ScoreRow
|
||||
t.PlayFromPosition(startRow)
|
||||
} else {
|
||||
t.SetPlaying(false)
|
||||
}
|
||||
case `\`, `<`, `>`:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetOctave(t.Octave() + 1)
|
||||
t.OctaveNumberInput.Int.Add(1)
|
||||
} else {
|
||||
t.SetOctave(t.Octave() - 1)
|
||||
t.OctaveNumberInput.Int.Add(-1)
|
||||
}
|
||||
case key.NameTab:
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
switch {
|
||||
case t.OrderEditor.Focused():
|
||||
t.InstrumentEditor.paramEditor.Focus()
|
||||
case t.TrackEditor.Focused():
|
||||
t.OrderEditor.Focus()
|
||||
case t.OrderEditor.scrollTable.Focused():
|
||||
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
||||
case t.TrackEditor.scrollTable.Focused():
|
||||
t.OrderEditor.scrollTable.Focus()
|
||||
case t.InstrumentEditor.Focused():
|
||||
if t.InstrEnlarged() {
|
||||
t.InstrumentEditor.paramEditor.Focus()
|
||||
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
||||
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
||||
} else {
|
||||
t.TrackEditor.Focus()
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
}
|
||||
default:
|
||||
t.InstrumentEditor.Focus()
|
||||
}
|
||||
} else {
|
||||
switch {
|
||||
case t.OrderEditor.Focused():
|
||||
t.TrackEditor.Focus()
|
||||
case t.TrackEditor.Focused():
|
||||
case t.OrderEditor.scrollTable.Focused():
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
case t.TrackEditor.scrollTable.Focused():
|
||||
t.InstrumentEditor.Focus()
|
||||
case t.InstrumentEditor.Focused():
|
||||
t.InstrumentEditor.paramEditor.Focus()
|
||||
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
||||
default:
|
||||
if t.InstrEnlarged() {
|
||||
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
||||
t.InstrumentEditor.Focus()
|
||||
} else {
|
||||
t.OrderEditor.Focus()
|
||||
t.OrderEditor.scrollTable.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,28 +178,12 @@ func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) {
|
||||
}
|
||||
}
|
||||
|
||||
// NumberPressed handles incoming presses while in either of the hex number columns
|
||||
func (t *Tracker) NumberPressed(iv byte) {
|
||||
val := t.Note()
|
||||
if val == 1 {
|
||||
val = 0
|
||||
}
|
||||
if t.LowNibble() {
|
||||
val = (val & 0xF0) | (iv & 0xF)
|
||||
} else {
|
||||
val = ((iv & 0xF) << 4) | (val & 0xF)
|
||||
}
|
||||
t.SetNote(val)
|
||||
}
|
||||
|
||||
func (t *Tracker) JammingPressed(e key.Event) byte {
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
n := noteAsValue(t.OctaveNumberInput.Value, val)
|
||||
instr := t.InstrIndex()
|
||||
noteID := tracker.NoteIDInstr(instr, n)
|
||||
t.NoteOn(noteID)
|
||||
t.KeyPlaying[e.Name] = noteID
|
||||
n := noteAsValue(t.OctaveNumberInput.Int.Value(), val)
|
||||
instr := t.InstrumentEditor.instrumentDragList.TrackerList.Selected()
|
||||
t.KeyPlaying[e.Name] = t.InstrNoteOn(instr, n)
|
||||
return n
|
||||
}
|
||||
}
|
||||
@ -196,7 +192,7 @@ func (t *Tracker) JammingPressed(e key.Event) byte {
|
||||
|
||||
func (t *Tracker) JammingReleased(e key.Event) bool {
|
||||
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
t.NoteOff(noteID)
|
||||
noteID.NoteOff()
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
return true
|
||||
}
|
||||
|
@ -24,28 +24,25 @@ type LabelStyle struct {
|
||||
}
|
||||
|
||||
func (l LabelStyle) Layout(gtx layout.Context) layout.Dimensions {
|
||||
return layout.Stack{Alignment: l.Alignment}.Layout(gtx,
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
return l.Alignment.Layout(gtx, func(gtx C) D {
|
||||
gtx.Constraints.Min = image.Point{}
|
||||
paint.ColorOp{Color: l.ShadeColor}.Add(gtx.Ops)
|
||||
op.Offset(image.Pt(2, 2)).Add(gtx.Ops)
|
||||
offs := op.Offset(image.Pt(2, 2)).Push(gtx.Ops)
|
||||
widget.Label{
|
||||
Alignment: text.Start,
|
||||
MaxLines: 1,
|
||||
}.Layout(gtx, l.Shaper, l.Font, l.FontSize, l.Text, op.CallOp{})
|
||||
offs.Pop()
|
||||
paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
|
||||
dims := widget.Label{
|
||||
Alignment: text.Start,
|
||||
MaxLines: 1,
|
||||
}.Layout(gtx, l.Shaper, l.Font, l.FontSize, l.Text, op.CallOp{})
|
||||
return layout.Dimensions{
|
||||
Size: dims.Size.Add(image.Pt(2, 2)),
|
||||
Size: dims.Size,
|
||||
Baseline: dims.Baseline,
|
||||
}
|
||||
}),
|
||||
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
|
||||
paint.ColorOp{Color: l.Color}.Add(gtx.Ops)
|
||||
return widget.Label{
|
||||
Alignment: text.Start,
|
||||
MaxLines: 1,
|
||||
}.Layout(gtx, l.Shaper, l.Font, l.FontSize, l.Text, op.CallOp{})
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func Label(str string, color color.NRGBA, shaper *text.Shaper) layout.Widget {
|
||||
|
@ -1,121 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
|
||||
type C = layout.Context
|
||||
type D = layout.Dimensions
|
||||
|
||||
func (t *Tracker) Layout(gtx layout.Context, w *app.Window) {
|
||||
// this is the top level input handler for the whole app
|
||||
// it handles all the global key events and clipboard events
|
||||
// we need to tell gio that we handle tabs too; otherwise
|
||||
// it will steal them for focus switching
|
||||
key.InputOp{Tag: t, Keys: "Tab|Shift-Tab"}.Add(gtx.Ops)
|
||||
for _, ev := range gtx.Events(t) {
|
||||
switch e := ev.(type) {
|
||||
case key.Event:
|
||||
t.KeyEvent(e, gtx.Ops)
|
||||
case clipboard.Event:
|
||||
t.UnmarshalContent([]byte(e.Text))
|
||||
}
|
||||
}
|
||||
|
||||
paint.FillShape(gtx.Ops, backgroundColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
||||
if t.InstrEnlarged() {
|
||||
t.layoutTop(gtx)
|
||||
} else {
|
||||
t.VerticalSplit.Layout(gtx,
|
||||
t.layoutTop,
|
||||
t.layoutBottom)
|
||||
}
|
||||
t.Alert.Layout(gtx)
|
||||
dstyle := ConfirmDialog(t.Theme, t.ConfirmSongDialog, "Do you want to save your changes to the song? Your changes will be lost if you don't save them.", t.TextShaper)
|
||||
dstyle.ShowAlt = true
|
||||
dstyle.OkStyle.Text = "Save"
|
||||
dstyle.AltStyle.Text = "Don't save"
|
||||
dstyle.Layout(gtx)
|
||||
for t.ConfirmSongDialog.BtnOk.Clicked() {
|
||||
if t.SaveSongFile() {
|
||||
t.confirmedSongAction()
|
||||
}
|
||||
t.ConfirmSongDialog.Visible = false
|
||||
}
|
||||
for t.ConfirmSongDialog.BtnAlt.Clicked() {
|
||||
t.confirmedSongAction()
|
||||
t.ConfirmSongDialog.Visible = false
|
||||
}
|
||||
for t.ConfirmSongDialog.BtnCancel.Clicked() {
|
||||
t.ConfirmSongDialog.Visible = false
|
||||
}
|
||||
dstyle = ConfirmDialog(t.Theme, t.WaveTypeDialog, "Export .wav in int16 or float32 sample format?", t.TextShaper)
|
||||
dstyle.ShowAlt = true
|
||||
dstyle.OkStyle.Text = "Int16"
|
||||
dstyle.AltStyle.Text = "Float32"
|
||||
dstyle.Layout(gtx)
|
||||
for t.WaveTypeDialog.BtnOk.Clicked() {
|
||||
t.ExportWav(true)
|
||||
t.WaveTypeDialog.Visible = false
|
||||
}
|
||||
for t.WaveTypeDialog.BtnAlt.Clicked() {
|
||||
t.ExportWav(false)
|
||||
t.WaveTypeDialog.Visible = false
|
||||
}
|
||||
for t.WaveTypeDialog.BtnCancel.Clicked() {
|
||||
t.WaveTypeDialog.Visible = false
|
||||
}
|
||||
if t.ModalDialog != nil {
|
||||
t.ModalDialog(gtx)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) confirmedSongAction() {
|
||||
switch t.ConfirmSongActionType {
|
||||
case ConfirmLoad:
|
||||
t.OpenSongFile(true)
|
||||
case ConfirmNew:
|
||||
t.NewSong(true)
|
||||
case ConfirmQuit:
|
||||
t.Quit(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) NewSong(forced bool) {
|
||||
if !forced && t.ChangedSinceSave() {
|
||||
t.ConfirmSongActionType = ConfirmNew
|
||||
t.ConfirmSongDialog.Visible = true
|
||||
return
|
||||
}
|
||||
t.ResetSong()
|
||||
t.SetFilePath("")
|
||||
t.ClearUndoHistory()
|
||||
t.SetChangedSinceSave(false)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions {
|
||||
return t.BottomHorizontalSplit.Layout(gtx,
|
||||
func(gtx C) D {
|
||||
return t.OrderEditor.Layout(gtx, t)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return t.TrackEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions {
|
||||
return t.TopHorizontalSplit.Layout(gtx,
|
||||
t.layoutSongPanel,
|
||||
func(gtx C) D {
|
||||
return t.InstrumentEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
@ -12,6 +12,8 @@ import (
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type Menu struct {
|
||||
@ -40,7 +42,7 @@ type MenuItem struct {
|
||||
IconBytes []byte
|
||||
Text string
|
||||
ShortcutText string
|
||||
Disabled bool
|
||||
Doer tracker.Action
|
||||
}
|
||||
|
||||
func (m *Menu) Clicked() (int, bool) {
|
||||
@ -57,7 +59,7 @@ func (m *Menu) Clicked() (int, bool) {
|
||||
|
||||
func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
contents := func(gtx C) D {
|
||||
for i := range items {
|
||||
for i, item := range items {
|
||||
// make sure we have a tag for every item
|
||||
for len(m.Menu.tags) <= i {
|
||||
m.Menu.tags = append(m.Menu.tags, false)
|
||||
@ -70,7 +72,7 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
}
|
||||
switch e.Type {
|
||||
case pointer.Press:
|
||||
m.Menu.clicks = append(m.Menu.clicks, i)
|
||||
item.Doer.Do()
|
||||
m.Menu.Visible = false
|
||||
case pointer.Enter:
|
||||
m.Menu.hover = i + 1
|
||||
@ -89,17 +91,17 @@ 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.Disabled {
|
||||
if i == m.Menu.hover-1 && item.Doer.Allowed() {
|
||||
macro = op.Record(gtx.Ops)
|
||||
}
|
||||
icon := widgetForIcon(item.IconBytes)
|
||||
iconColor := m.IconColor
|
||||
if item.Disabled {
|
||||
if !item.Doer.Allowed() {
|
||||
iconColor = mediumEmphasisTextColor
|
||||
}
|
||||
iconInset := layout.Inset{Left: unit.Dp(12), Right: unit.Dp(6)}
|
||||
textLabel := LabelStyle{Text: item.Text, FontSize: m.FontSize, Color: m.TextColor, Shaper: m.Shaper}
|
||||
if item.Disabled {
|
||||
if !item.Doer.Allowed() {
|
||||
textLabel.Color = mediumEmphasisTextColor
|
||||
}
|
||||
shortcutLabel := LabelStyle{Text: item.ShortcutText, FontSize: m.FontSize, Color: m.ShortCutColor, Shaper: m.Shaper}
|
||||
@ -118,14 +120,14 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
return shortcutInset.Layout(gtx, shortcutLabel.Layout)
|
||||
}),
|
||||
)
|
||||
if i == m.Menu.hover-1 && !item.Disabled {
|
||||
if i == m.Menu.hover-1 && item.Doer.Allowed() {
|
||||
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.Disabled {
|
||||
if item.Doer.Allowed() {
|
||||
rect := image.Rect(0, 0, dims.Size.X, dims.Size.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &m.Menu.tags[i],
|
||||
@ -148,7 +150,7 @@ func (m *MenuStyle) Layout(gtx C, items ...MenuItem) D {
|
||||
return popup.Layout(gtx, contents)
|
||||
}
|
||||
|
||||
func (t *Tracker) PopupMenu(menu *Menu) MenuStyle {
|
||||
func PopupMenu(menu *Menu, shaper *text.Shaper) MenuStyle {
|
||||
return MenuStyle{
|
||||
Menu: menu,
|
||||
IconColor: white,
|
||||
@ -157,6 +159,26 @@ func (t *Tracker) PopupMenu(menu *Menu) MenuStyle {
|
||||
FontSize: unit.Sp(16),
|
||||
IconSize: unit.Dp(16),
|
||||
HoverColor: menuHoverColor,
|
||||
Shaper: t.TextShaper,
|
||||
Shaper: shaper,
|
||||
}
|
||||
}
|
||||
|
||||
func (tr *Tracker) layoutMenu(title string, clickable *widget.Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget {
|
||||
for clickable.Clicked() {
|
||||
menu.Visible = true
|
||||
}
|
||||
m := PopupMenu(menu, tr.Theme.Shaper)
|
||||
return func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
titleBtn := material.Button(tr.Theme, clickable, title)
|
||||
titleBtn.Color = white
|
||||
titleBtn.Background = transparent
|
||||
titleBtn.CornerRadius = unit.Dp(0)
|
||||
dims := titleBtn.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.X = gtx.Dp(width)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(1000))
|
||||
m.Layout(gtx, items...)
|
||||
return dims
|
||||
}
|
||||
}
|
||||
|
338
tracker/gioui/note_editor.go
Normal file
338
tracker/gioui/note_editor.go
Normal file
@ -0,0 +1,338 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
const trackRowHeight = unit.Dp(16)
|
||||
const trackColWidth = unit.Dp(54)
|
||||
const trackColTitleHeight = unit.Dp(16)
|
||||
const trackPatMarkWidth = unit.Dp(25)
|
||||
const trackRowMarkWidth = unit.Dp(25)
|
||||
|
||||
var noteStr [256]string
|
||||
var hexStr [256]string
|
||||
|
||||
func init() {
|
||||
// initialize these strings once, so we don't have to do it every time we draw the note editor
|
||||
hexStr[0] = "--"
|
||||
hexStr[1] = ".."
|
||||
noteStr[0] = "---"
|
||||
noteStr[1] = "..."
|
||||
for i := 2; i < 256; i++ {
|
||||
hexStr[i] = fmt.Sprintf("%02x", i)
|
||||
oNote := mod(i-baseNote, 12)
|
||||
octave := (i - oNote - baseNote) / 12
|
||||
switch {
|
||||
case octave < 0:
|
||||
noteStr[i] = fmt.Sprintf("%s%s", notes[oNote], string(byte('Z'+1+octave)))
|
||||
case octave >= 10:
|
||||
noteStr[i] = fmt.Sprintf("%s%s", notes[oNote], string(byte('A'+octave-10)))
|
||||
default:
|
||||
noteStr[i] = fmt.Sprintf("%s%d", notes[oNote], octave)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type NoteEditor struct {
|
||||
TrackVoices *NumberInput
|
||||
NewTrackBtn *ActionClickable
|
||||
DeleteTrackBtn *ActionClickable
|
||||
AddSemitoneBtn *ActionClickable
|
||||
SubtractSemitoneBtn *ActionClickable
|
||||
AddOctaveBtn *ActionClickable
|
||||
SubtractOctaveBtn *ActionClickable
|
||||
NoteOffBtn *ActionClickable
|
||||
EffectBtn *BoolClickable
|
||||
|
||||
scrollTable *ScrollTable
|
||||
tag struct{}
|
||||
}
|
||||
|
||||
func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
||||
return &NoteEditor{
|
||||
TrackVoices: NewNumberInput(model.TrackVoices().Int()),
|
||||
NewTrackBtn: NewActionClickable(model.AddTrack()),
|
||||
DeleteTrackBtn: NewActionClickable(model.DeleteTrack()),
|
||||
AddSemitoneBtn: NewActionClickable(model.AddSemitone()),
|
||||
SubtractSemitoneBtn: NewActionClickable(model.SubtractSemitone()),
|
||||
AddOctaveBtn: NewActionClickable(model.AddOctave()),
|
||||
SubtractOctaveBtn: NewActionClickable(model.SubtractOctave()),
|
||||
NoteOffBtn: NewActionClickable(model.EditNoteOff()),
|
||||
EffectBtn: NewBoolClickable(model.Effect().Bool()),
|
||||
scrollTable: NewScrollTable(
|
||||
model.Notes().Table(),
|
||||
model.Tracks().List(),
|
||||
model.NoteRows().List(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions {
|
||||
for _, e := range gtx.Events(&te.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.Event:
|
||||
if e.State == key.Release {
|
||||
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
noteID.NoteOff()
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
te.command(gtx, t, e)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
key.InputOp{Tag: &te.tag, Keys: "Ctrl-⌫|Ctrl-⌦|⏎|Ctrl-⏎|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|0|1|2|3|4|5|6|7|8|9|,|."}.Add(gtx.Ops)
|
||||
|
||||
return Surface{Gray: 24, Focus: te.scrollTable.Focused()}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return te.layoutButtons(gtx, t)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return te.layoutTracks(gtx, t)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
||||
return Surface{Gray: 37, Focus: te.scrollTable.Focused() || te.scrollTable.ChildFocused(), FitSize: true}.Layout(gtx, func(gtx C) D {
|
||||
addSemitoneBtnStyle := ActionButton(t.Theme, te.AddSemitoneBtn, "+1")
|
||||
subtractSemitoneBtnStyle := ActionButton(t.Theme, te.SubtractSemitoneBtn, "-1")
|
||||
addOctaveBtnStyle := ActionButton(t.Theme, te.AddOctaveBtn, "+12")
|
||||
subtractOctaveBtnStyle := ActionButton(t.Theme, te.SubtractOctaveBtn, "-12")
|
||||
noteOffBtnStyle := ActionButton(t.Theme, te.NoteOffBtn, "Note Off")
|
||||
deleteTrackBtnStyle := ActionIcon(t.Theme, te.DeleteTrackBtn, icons.ActionDelete, "Delete track\n(Ctrl+Shift+T)")
|
||||
newTrackBtnStyle := ActionIcon(t.Theme, te.NewTrackBtn, icons.ContentAdd, "Add track\n(Ctrl+T)")
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
voiceUpDown := func(gtx C) D {
|
||||
numStyle := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
|
||||
return in.Layout(gtx, numStyle.Layout)
|
||||
}
|
||||
effectBtnStyle := ToggleButton(t.Theme, te.EffectBtn, "Hex")
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
||||
layout.Rigid(addSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(subtractSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(addOctaveBtnStyle.Layout),
|
||||
layout.Rigid(subtractOctaveBtnStyle.Layout),
|
||||
layout.Rigid(noteOffBtnStyle.Layout),
|
||||
layout.Rigid(effectBtnStyle.Layout),
|
||||
layout.Rigid(Label(" Voices:", white, t.Theme.Shaper)),
|
||||
layout.Rigid(voiceUpDown),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(deleteTrackBtnStyle.Layout),
|
||||
layout.Rigid(newTrackBtnStyle.Layout))
|
||||
})
|
||||
}
|
||||
|
||||
const baseNote = 24
|
||||
|
||||
var notes = []string{
|
||||
"C-",
|
||||
"C#",
|
||||
"D-",
|
||||
"D#",
|
||||
"E-",
|
||||
"F-",
|
||||
"F#",
|
||||
"G-",
|
||||
"G#",
|
||||
"A-",
|
||||
"A#",
|
||||
"B-",
|
||||
}
|
||||
|
||||
func (te *NoteEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
||||
|
||||
beatMarkerDensity := t.RowsPerBeat().Value()
|
||||
switch beatMarkerDensity {
|
||||
case 0, 1, 2:
|
||||
beatMarkerDensity = 4
|
||||
}
|
||||
|
||||
playSongRow := t.PlaySongRow()
|
||||
pxWidth := gtx.Dp(trackColWidth)
|
||||
pxHeight := gtx.Dp(trackRowHeight)
|
||||
pxPatMarkWidth := gtx.Dp(trackPatMarkWidth)
|
||||
pxRowMarkWidth := gtx.Dp(trackRowMarkWidth)
|
||||
|
||||
colTitle := func(gtx C, i int) D {
|
||||
h := gtx.Dp(unit.Dp(trackColTitleHeight))
|
||||
title := ((*tracker.Order)(t.Model)).Title(i)
|
||||
gtx.Constraints = layout.Exact(image.Pt(pxWidth, h))
|
||||
LabelStyle{Alignment: layout.N, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.Theme.Shaper}.Layout(gtx)
|
||||
return D{Size: image.Pt(pxWidth, h)}
|
||||
}
|
||||
|
||||
rowTitleBg := func(gtx C, j int) D {
|
||||
if mod(j, beatMarkerDensity*2) == 0 {
|
||||
paint.FillShape(gtx.Ops, twoBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
||||
} else if mod(j, beatMarkerDensity) == 0 {
|
||||
paint.FillShape(gtx.Ops, oneBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
||||
}
|
||||
if t.SongPanel.PlayingBtn.Bool.Value() && j == playSongRow {
|
||||
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, pxHeight)}.Op())
|
||||
}
|
||||
return D{}
|
||||
}
|
||||
|
||||
rowTitle := func(gtx C, j int) D {
|
||||
rpp := intMax(t.RowsPerPattern().Value(), 1)
|
||||
pat := j / rpp
|
||||
row := j % rpp
|
||||
w := pxPatMarkWidth + pxRowMarkWidth
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
if row == 0 {
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", pat)), op.CallOp{})
|
||||
}
|
||||
defer op.Offset(image.Pt(pxPatMarkWidth, 0)).Push(gtx.Ops).Pop()
|
||||
paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", row)), op.CallOp{})
|
||||
return D{Size: image.Pt(w, pxHeight)}
|
||||
}
|
||||
|
||||
drawSelection := te.scrollTable.Table.Cursor() != te.scrollTable.Table.Cursor2()
|
||||
selection := te.scrollTable.Table.Range()
|
||||
|
||||
cell := func(gtx C, x, y int) D {
|
||||
// draw the background, to indicate selection
|
||||
color := transparent
|
||||
point := tracker.Point{X: x, Y: y}
|
||||
if drawSelection && selection.Contains(point) {
|
||||
color = inactiveSelectionColor
|
||||
if te.scrollTable.Focused() {
|
||||
color = selectionColor
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(0, 0), Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
||||
// draw the cursor
|
||||
if point == te.scrollTable.Table.Cursor() {
|
||||
cw := gtx.Constraints.Min.X
|
||||
cx := 0
|
||||
if t.Model.Notes().Effect(x) {
|
||||
cw /= 2
|
||||
if t.Model.Notes().LowNibble() {
|
||||
cx += cw
|
||||
}
|
||||
}
|
||||
c := inactiveSelectionColor
|
||||
if te.scrollTable.Focused() {
|
||||
c = cursorColor
|
||||
}
|
||||
paint.FillShape(gtx.Ops, c, clip.Rect{Min: image.Pt(cx, 0), Max: image.Pt(cx+cw, gtx.Constraints.Min.Y)}.Op())
|
||||
}
|
||||
// draw the pattern marker
|
||||
rpp := intMax(t.RowsPerPattern().Value(), 1)
|
||||
pat := y / rpp
|
||||
row := y % rpp
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
s := t.Model.Order().Value(tracker.Point{X: x, Y: pat})
|
||||
if row == 0 { // draw the pattern marker
|
||||
paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, patternIndexToString(s), op.CallOp{})
|
||||
}
|
||||
if row == 1 && t.Model.Notes().Unique(x, s) { // draw a * if the pattern is unique
|
||||
paint.ColorOp{Color: mediumEmphasisTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, "*", op.CallOp{})
|
||||
}
|
||||
if te.scrollTable.Table.Cursor() == point && te.scrollTable.Focused() {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops)
|
||||
}
|
||||
val := noteStr[byte(t.Model.Notes().Value(tracker.Point{X: x, Y: y}))]
|
||||
if t.Model.Notes().Effect(x) {
|
||||
val = hexStr[byte(t.Model.Notes().Value(tracker.Point{X: x, Y: y}))]
|
||||
}
|
||||
widget.Label{Alignment: text.Middle}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, val, op.CallOp{})
|
||||
return D{Size: image.Pt(pxWidth, pxHeight)}
|
||||
}
|
||||
table := FilledScrollTable(t.Theme, te.scrollTable, cell, colTitle, rowTitle, nil, rowTitleBg)
|
||||
table.RowTitleWidth = trackPatMarkWidth + trackRowMarkWidth
|
||||
table.ColumnTitleHeight = trackColTitleHeight
|
||||
table.CellWidth = trackColWidth
|
||||
table.CellHeight = trackRowHeight
|
||||
return table.Layout(gtx)
|
||||
}
|
||||
|
||||
func mod(x, d int) int {
|
||||
x = x % d
|
||||
if x >= 0 {
|
||||
return x
|
||||
}
|
||||
if d < 0 {
|
||||
return x - d
|
||||
}
|
||||
return x + d
|
||||
}
|
||||
|
||||
func noteAsValue(octave, note int) byte {
|
||||
return byte(baseNote + (octave * 12) + note)
|
||||
}
|
||||
|
||||
func (te *NoteEditor) command(gtx C, t *Tracker, e key.Event) {
|
||||
if e.Name == "A" || e.Name == "1" {
|
||||
t.Model.Notes().Table().Fill(0)
|
||||
te.scrollTable.EnsureCursorVisible()
|
||||
return
|
||||
}
|
||||
var n byte
|
||||
if t.Model.Notes().Effect(te.scrollTable.Table.Cursor().X) {
|
||||
if nibbleValue, err := strconv.ParseInt(e.Name, 16, 8); err == nil {
|
||||
n = t.Model.Notes().Value(te.scrollTable.Table.Cursor())
|
||||
t.Model.Notes().FillNibble(byte(nibbleValue), t.Model.Notes().LowNibble())
|
||||
goto validNote
|
||||
}
|
||||
} else {
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
n = noteAsValue(t.OctaveNumberInput.Int.Value(), val)
|
||||
t.Model.Notes().Table().Fill(int(n))
|
||||
goto validNote
|
||||
}
|
||||
}
|
||||
return
|
||||
validNote:
|
||||
te.scrollTable.EnsureCursorVisible()
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
trk := te.scrollTable.Table.Cursor().X
|
||||
t.KeyPlaying[e.Name] = t.TrackNoteOn(trk, n)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
case "+":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
te.AddOctaveBtn.Action.Do()
|
||||
} else {
|
||||
te.AddSemitoneBtn.Action.Do()
|
||||
}
|
||||
case "-":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
te.SubtractSemitoneBtn.Action.Do()
|
||||
} else {
|
||||
te.SubtractOctaveBtn.Action.Do()
|
||||
}
|
||||
}*/
|
@ -5,6 +5,7 @@ import (
|
||||
"image"
|
||||
"image/color"
|
||||
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
|
||||
"gioui.org/font"
|
||||
@ -23,7 +24,7 @@ import (
|
||||
)
|
||||
|
||||
type NumberInput struct {
|
||||
Value int
|
||||
Int tracker.Int
|
||||
dragStartValue int
|
||||
dragStartXY float32
|
||||
clickDecrease gesture.Click
|
||||
@ -33,8 +34,6 @@ type NumberInput struct {
|
||||
|
||||
type NumericUpDownStyle struct {
|
||||
NumberInput *NumberInput
|
||||
Min int
|
||||
Max int
|
||||
Color color.NRGBA
|
||||
Font font.Font
|
||||
TextSize unit.Sp
|
||||
@ -51,15 +50,17 @@ type NumericUpDownStyle struct {
|
||||
shaper text.Shaper
|
||||
}
|
||||
|
||||
func NumericUpDown(th *material.Theme, number *NumberInput, min, max int, tooltip string) NumericUpDownStyle {
|
||||
func NewNumberInput(v tracker.Int) *NumberInput {
|
||||
return &NumberInput{Int: v}
|
||||
}
|
||||
|
||||
func NumericUpDown(th *material.Theme, number *NumberInput, tooltip string) NumericUpDownStyle {
|
||||
bgColor := th.Palette.Fg
|
||||
bgColor.R /= 4
|
||||
bgColor.G /= 4
|
||||
bgColor.B /= 4
|
||||
return NumericUpDownStyle{
|
||||
NumberInput: number,
|
||||
Min: min,
|
||||
Max: max,
|
||||
Color: white,
|
||||
BorderColor: th.Palette.Fg,
|
||||
IconColor: th.Palette.ContrastFg,
|
||||
@ -104,12 +105,6 @@ func (s *NumericUpDownStyle) actualLayout(gtx C) D {
|
||||
layout.Flexed(1, s.layoutText),
|
||||
layout.Rigid(s.button(gtx.Constraints.Max.Y, widgetForIcon(icons.NavigationArrowForward), 1, &s.NumberInput.clickIncrease)),
|
||||
)
|
||||
if s.NumberInput.Value < s.Min {
|
||||
s.NumberInput.Value = s.Min
|
||||
}
|
||||
if s.NumberInput.Value > s.Max {
|
||||
s.NumberInput.Value = s.Max
|
||||
}
|
||||
off.Pop()
|
||||
c2.Pop()
|
||||
return layout.Dimensions{Size: size}
|
||||
@ -156,7 +151,7 @@ func (s *NumericUpDownStyle) layoutText(gtx C) D {
|
||||
}),
|
||||
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
|
||||
paint.ColorOp{Color: s.Color}.Add(gtx.Ops)
|
||||
return widget.Label{Alignment: text.Middle}.Layout(gtx, &s.shaper, s.Font, s.TextSize, fmt.Sprintf("%v", s.NumberInput.Value), op.CallOp{})
|
||||
return widget.Label{Alignment: text.Middle}.Layout(gtx, &s.shaper, s.Font, s.TextSize, fmt.Sprintf("%v", s.NumberInput.Int.Value()), op.CallOp{})
|
||||
}),
|
||||
layout.Expanded(s.layoutDrag),
|
||||
)
|
||||
@ -169,13 +164,13 @@ func (s *NumericUpDownStyle) layoutDrag(gtx layout.Context) layout.Dimensions {
|
||||
if e, ok := ev.(pointer.Event); ok {
|
||||
switch e.Type {
|
||||
case pointer.Press:
|
||||
s.NumberInput.dragStartValue = s.NumberInput.Value
|
||||
s.NumberInput.dragStartValue = s.NumberInput.Int.Value()
|
||||
s.NumberInput.dragStartXY = e.Position.X - e.Position.Y
|
||||
|
||||
case pointer.Drag:
|
||||
var deltaCoord float32
|
||||
deltaCoord = e.Position.X - e.Position.Y - s.NumberInput.dragStartXY
|
||||
s.NumberInput.Value = s.NumberInput.dragStartValue + int(deltaCoord/pxPerStep+0.5)
|
||||
s.NumberInput.Int.Set(s.NumberInput.dragStartValue + int(deltaCoord/pxPerStep+0.5))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -200,7 +195,7 @@ func (s *NumericUpDownStyle) layoutClick(gtx layout.Context, delta int, click *g
|
||||
for _, e := range click.Events(gtx) {
|
||||
switch e.Type {
|
||||
case gesture.TypeClick:
|
||||
s.NumberInput.Value += delta
|
||||
s.NumberInput.Int.Add(delta)
|
||||
}
|
||||
}
|
||||
// Avoid affecting the input tree with pointer events.
|
||||
|
156
tracker/gioui/order_editor.go
Normal file
156
tracker/gioui/order_editor.go
Normal file
@ -0,0 +1,156 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
const patternCellHeight = 16
|
||||
const patternCellWidth = 16
|
||||
const patternRowMarkerWidth = 30
|
||||
const orderTitleHeight = unit.Dp(52)
|
||||
|
||||
type OrderEditor struct {
|
||||
scrollTable *ScrollTable
|
||||
tag struct{}
|
||||
}
|
||||
|
||||
var patternIndexStrings [36]string
|
||||
|
||||
func init() {
|
||||
for i := 0; i < 10; i++ {
|
||||
patternIndexStrings[i] = string('0' + byte(i))
|
||||
}
|
||||
for i := 10; i < 36; i++ {
|
||||
patternIndexStrings[i] = string('A' + byte(i-10))
|
||||
}
|
||||
}
|
||||
|
||||
func NewOrderEditor(m *tracker.Model) *OrderEditor {
|
||||
return &OrderEditor{
|
||||
scrollTable: NewScrollTable(
|
||||
m.Order().Table(),
|
||||
m.Tracks().List(),
|
||||
m.OrderRows().List(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) Layout(gtx C, t *Tracker) D {
|
||||
if oe.scrollTable.CursorMoved() {
|
||||
cursor := t.TrackEditor.scrollTable.Table.Cursor()
|
||||
t.TrackEditor.scrollTable.ColTitleList.CenterOn(cursor.X)
|
||||
t.TrackEditor.scrollTable.RowTitleList.CenterOn(cursor.Y)
|
||||
}
|
||||
|
||||
for _, e := range gtx.Events(&oe.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.Event:
|
||||
if e.State != key.Press {
|
||||
continue
|
||||
}
|
||||
oe.command(gtx, t, e)
|
||||
}
|
||||
}
|
||||
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()
|
||||
key.InputOp{Tag: &oe.tag, Keys: "Ctrl-⌫|Ctrl-⌦|⏎|Ctrl-⏎|0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z"}.Add(gtx.Ops)
|
||||
|
||||
colTitle := func(gtx C, i int) D {
|
||||
h := gtx.Dp(orderTitleHeight)
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
defer op.Affine(f32.Affine2D{}.Rotate(f32.Pt(0, 0), -90*math.Pi/180).Offset(f32.Point{X: 0, Y: float32(h)})).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints = layout.Exact(image.Pt(1e6, 1e6))
|
||||
title := t.Model.Order().Title(i)
|
||||
LabelStyle{Alignment: layout.NW, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.Theme.Shaper}.Layout(gtx)
|
||||
return D{Size: image.Pt(patternCellWidth, h)}
|
||||
}
|
||||
|
||||
rowTitle := func(gtx C, j int) D {
|
||||
if playPos := t.PlayPosition(); t.SongPanel.PlayingBtn.Bool.Value() && j == playPos.OrderRow {
|
||||
paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op())
|
||||
}
|
||||
w := gtx.Dp(unit.Dp(30))
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
widget.Label{}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)), op.CallOp{})
|
||||
return D{Size: image.Pt(w, patternCellHeight)}
|
||||
}
|
||||
|
||||
selection := oe.scrollTable.Table.Range()
|
||||
|
||||
cell := func(gtx C, x, y int) D {
|
||||
val := patternIndexToString(t.Model.Order().Value(tracker.Point{X: x, Y: y}))
|
||||
color := patternCellColor
|
||||
point := tracker.Point{X: x, Y: y}
|
||||
if selection.Contains(point) {
|
||||
color = inactiveSelectionColor
|
||||
if oe.scrollTable.Focused() {
|
||||
color = selectionColor
|
||||
if point == oe.scrollTable.Table.Cursor() {
|
||||
color = cursorColor
|
||||
}
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Min: image.Pt(1, 1), Max: image.Pt(gtx.Constraints.Min.X-1, gtx.Constraints.Min.X-1)}.Op())
|
||||
paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops)
|
||||
defer op.Offset(image.Pt(0, -2)).Push(gtx.Ops).Pop()
|
||||
widget.Label{Alignment: text.Middle}.Layout(gtx, t.Theme.Shaper, trackerFont, trackerFontSize, val, op.CallOp{})
|
||||
return D{Size: image.Pt(patternCellWidth, patternCellHeight)}
|
||||
}
|
||||
|
||||
table := FilledScrollTable(t.Theme, oe.scrollTable, cell, colTitle, rowTitle, nil, nil)
|
||||
table.ColumnTitleHeight = orderTitleHeight
|
||||
|
||||
return table.Layout(gtx)
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) command(gtx C, t *Tracker, e key.Event) {
|
||||
switch e.Name {
|
||||
case key.NameDeleteBackward:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Model.DeleteOrderRow(true).Do()
|
||||
}
|
||||
case key.NameDeleteForward:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.Model.DeleteOrderRow(false).Do()
|
||||
}
|
||||
case key.NameReturn:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
oe.scrollTable.Table.MoveCursor(0, -1)
|
||||
oe.scrollTable.Table.SetCursor2(oe.scrollTable.Table.Cursor())
|
||||
}
|
||||
t.Model.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut)).Do()
|
||||
}
|
||||
if iv, err := strconv.Atoi(e.Name); err == nil {
|
||||
t.Model.Order().SetValue(oe.scrollTable.Table.Cursor(), iv)
|
||||
oe.scrollTable.EnsureCursorVisible()
|
||||
}
|
||||
if b := int(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 {
|
||||
t.Model.Order().SetValue(oe.scrollTable.Table.Cursor(), b+10)
|
||||
oe.scrollTable.EnsureCursorVisible()
|
||||
}
|
||||
}
|
||||
|
||||
func patternIndexToString(index int) string {
|
||||
if index < 0 {
|
||||
return ""
|
||||
} else if index < len(patternIndexStrings) {
|
||||
return patternIndexStrings[index]
|
||||
}
|
||||
return "?"
|
||||
}
|
@ -1,262 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
const patternCellHeight = 16
|
||||
const patternCellWidth = 16
|
||||
const patternRowMarkerWidth = 30
|
||||
|
||||
type OrderEditor struct {
|
||||
list *layout.List
|
||||
titleList *DragList
|
||||
scrollBar *ScrollBar
|
||||
tag bool
|
||||
focused bool
|
||||
requestFocus bool
|
||||
}
|
||||
|
||||
func NewOrderEditor() *OrderEditor {
|
||||
return &OrderEditor{
|
||||
list: &layout.List{Axis: layout.Vertical},
|
||||
titleList: &DragList{List: &layout.List{Axis: layout.Horizontal}},
|
||||
scrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
}
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) Focus() {
|
||||
oe.requestFocus = true
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) Focused() bool {
|
||||
return oe.focused
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) Layout(gtx C, t *Tracker) D {
|
||||
return Surface{Gray: 24, Focus: oe.focused}.Layout(gtx, func(gtx C) D {
|
||||
return oe.doLayout(gtx, t)
|
||||
})
|
||||
}
|
||||
|
||||
func (oe *OrderEditor) doLayout(gtx C, t *Tracker) D {
|
||||
for _, e := range gtx.Events(&oe.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
oe.focused = e.Focus
|
||||
case pointer.Event:
|
||||
if e.Type == pointer.Press {
|
||||
key.FocusOp{Tag: &oe.tag}.Add(gtx.Ops)
|
||||
}
|
||||
case key.Event:
|
||||
if e.State != key.Press {
|
||||
continue
|
||||
}
|
||||
switch e.Name {
|
||||
case key.NameDeleteForward, key.NameDeleteBackward:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.DeleteOrderRow(e.Name == key.NameDeleteForward)
|
||||
} else {
|
||||
t.DeletePatternSelection()
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
case "Space":
|
||||
if !t.Playing() {
|
||||
t.SetNoteTracking(!e.Modifiers.Contain(key.ModShortcut))
|
||||
startRow := t.Cursor().ScoreRow
|
||||
startRow.Row = 0
|
||||
t.PlayFromPosition(startRow)
|
||||
} else {
|
||||
t.SetPlaying(false)
|
||||
}
|
||||
case key.NameReturn:
|
||||
t.AddOrderRow(!e.Modifiers.Contain(key.ModShortcut))
|
||||
case key.NameUpArrow:
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.ScoreRow = tracker.ScoreRow{}
|
||||
} else {
|
||||
cursor.Row -= t.Song().Score.RowsPerPattern
|
||||
}
|
||||
t.SetNoteTracking(false)
|
||||
t.SetCursor(cursor)
|
||||
case key.NameDownArrow:
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Row = t.Song().Score.LengthInRows() - 1
|
||||
} else {
|
||||
cursor.Row += t.Song().Score.RowsPerPattern
|
||||
}
|
||||
t.SetNoteTracking(false)
|
||||
t.SetCursor(cursor)
|
||||
case key.NameLeftArrow:
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Track = 0
|
||||
} else {
|
||||
cursor.Track--
|
||||
}
|
||||
t.SetCursor(cursor)
|
||||
case key.NameRightArrow:
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Track = len(t.Song().Score.Tracks) - 1
|
||||
} else {
|
||||
cursor.Track++
|
||||
}
|
||||
t.SetCursor(cursor)
|
||||
case "+":
|
||||
t.AdjustPatternNumber(1, e.Modifiers.Contain(key.ModShortcut))
|
||||
continue
|
||||
case "-":
|
||||
t.AdjustPatternNumber(-1, e.Modifiers.Contain(key.ModShortcut))
|
||||
continue
|
||||
case key.NameHome:
|
||||
cursor := t.Cursor()
|
||||
cursor.Track = 0
|
||||
t.SetCursor(cursor)
|
||||
case key.NameEnd:
|
||||
cursor := t.Cursor()
|
||||
cursor.Track = len(t.Song().Score.Tracks) - 1
|
||||
t.SetCursor(cursor)
|
||||
}
|
||||
if (e.Name != key.NameLeftArrow &&
|
||||
e.Name != key.NameRightArrow &&
|
||||
e.Name != key.NameUpArrow &&
|
||||
e.Name != key.NameDownArrow) ||
|
||||
!e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
continue
|
||||
}
|
||||
if iv, err := strconv.Atoi(e.Name); err == nil {
|
||||
t.SetCurrentPattern(iv)
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
if b := int(e.Name[0]) - 'A'; len(e.Name) == 1 && b >= 0 && b < 26 {
|
||||
t.SetCurrentPattern(b + 10)
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddPatterns(1))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
if oe.requestFocus {
|
||||
oe.requestFocus = false
|
||||
key.FocusOp{Tag: &oe.tag}.Add(gtx.Ops)
|
||||
}
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
pointer.InputOp{Tag: &oe.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
|
||||
key.InputOp{Tag: &oe.tag, Keys: "←|→|↑|↓|Shift-←|Shift-→|Shift-↑|Shift-↓|⏎|⇱|⇲|⌫|⌦|Ctrl-⌫|Ctrl-⌦|+|-|Space|0|1|2|3|4|5|6|7|8|9|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z"}.Add(gtx.Ops)
|
||||
|
||||
patternRect := tracker.ScoreRect{
|
||||
Corner1: tracker.ScorePoint{ScoreRow: tracker.ScoreRow{Pattern: t.Cursor().Pattern}, Track: t.Cursor().Track},
|
||||
Corner2: tracker.ScorePoint{ScoreRow: tracker.ScoreRow{Pattern: t.SelectionCorner().Pattern}, Track: t.SelectionCorner().Track},
|
||||
}
|
||||
|
||||
// draw the single letter titles for tracks
|
||||
{
|
||||
gtx := gtx
|
||||
stack := op.Offset(image.Pt(patternRowMarkerWidth, 0)).Push(gtx.Ops)
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X-patternRowMarkerWidth, patternCellHeight))
|
||||
elem := func(gtx C, i int) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(patternCellWidth, patternCellHeight))
|
||||
instr, err := t.Song().Patch.InstrumentForVoice(t.Song().Score.FirstVoiceForTrack(i))
|
||||
var title string
|
||||
if err == nil && len(t.Song().Patch[instr].Name) > 0 {
|
||||
title = string(t.Song().Patch[instr].Name[0])
|
||||
} else {
|
||||
title = "?"
|
||||
}
|
||||
LabelStyle{Alignment: layout.N, Text: title, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.TextShaper}.Layout(gtx)
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
style := FilledDragList(t.Theme, oe.titleList, len(t.Song().Score.Tracks), elem, t.SwapTracks)
|
||||
style.HoverColor = transparent
|
||||
style.SelectedColor = transparent
|
||||
style.Layout(gtx)
|
||||
stack.Pop()
|
||||
}
|
||||
op.Offset(image.Pt(0, patternCellHeight)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.Y -= patternCellHeight
|
||||
gtx.Constraints.Min.Y -= patternCellHeight
|
||||
element := func(gtx C, j int) D {
|
||||
if playPos := t.PlayPosition(); t.Playing() && j == playPos.Pattern {
|
||||
paint.FillShape(gtx.Ops, patternPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}.Op())
|
||||
}
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)), op.CallOp{})
|
||||
stack := op.Offset(image.Pt(patternRowMarkerWidth, 0)).Push(gtx.Ops)
|
||||
for i, track := range t.Song().Score.Tracks {
|
||||
paint.FillShape(gtx.Ops, patternCellColor, clip.Rect{Min: image.Pt(1, 1), Max: image.Pt(patternCellWidth-1, patternCellHeight-1)}.Op())
|
||||
paint.ColorOp{Color: patternTextColor}.Add(gtx.Ops)
|
||||
if j >= 0 && j < len(track.Order) && track.Order[j] >= 0 {
|
||||
gtx := gtx
|
||||
gtx.Constraints.Max.X = patternCellWidth
|
||||
op.Offset(image.Pt(0, -2)).Add(gtx.Ops)
|
||||
widget.Label{Alignment: text.Middle}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, patternIndexToString(track.Order[j]), op.CallOp{})
|
||||
op.Offset(image.Pt(0, 2)).Add(gtx.Ops)
|
||||
}
|
||||
point := tracker.ScorePoint{Track: i, ScoreRow: tracker.ScoreRow{Pattern: j}}
|
||||
if oe.focused || t.TrackEditor.Focused() {
|
||||
if patternRect.Contains(point) {
|
||||
color := inactiveSelectionColor
|
||||
if oe.focused {
|
||||
color = selectionColor
|
||||
if point.Pattern == t.Cursor().Pattern && point.Track == t.Cursor().Track {
|
||||
color = cursorColor
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(patternCellWidth, patternCellHeight)}.Op())
|
||||
}
|
||||
}
|
||||
op.Offset(image.Pt(patternCellWidth, 0)).Add(gtx.Ops)
|
||||
}
|
||||
stack.Pop()
|
||||
return D{Size: image.Pt(gtx.Constraints.Max.X, patternCellHeight)}
|
||||
}
|
||||
|
||||
return layout.Stack{Alignment: layout.NE}.Layout(gtx,
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return oe.list.Layout(gtx, t.Song().Score.Length, element)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return oe.scrollBar.Layout(gtx, unit.Dp(10), t.Song().Score.Length, &oe.list.Position)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func patternIndexToString(index int) string {
|
||||
if index < 0 {
|
||||
return ""
|
||||
} else if index < 10 {
|
||||
return string('0' + byte(index))
|
||||
}
|
||||
return string('A' + byte(index-10))
|
||||
}
|
@ -1,256 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"github.com/vsariola/sointu"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type ParamEditor struct {
|
||||
list *layout.List
|
||||
scrollBar *ScrollBar
|
||||
Parameters []*ParameterWidget
|
||||
DeleteUnitBtn *TipClickable
|
||||
CopyUnitBtn *TipClickable
|
||||
ClearUnitBtn *TipClickable
|
||||
ChooseUnitTypeBtns []*widget.Clickable
|
||||
tag bool
|
||||
focused bool
|
||||
requestFocus bool
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) Focus() {
|
||||
pe.requestFocus = true
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) Focused() bool {
|
||||
return pe.focused
|
||||
}
|
||||
|
||||
func NewParamEditor() *ParamEditor {
|
||||
ret := &ParamEditor{
|
||||
DeleteUnitBtn: new(TipClickable),
|
||||
ClearUnitBtn: new(TipClickable),
|
||||
CopyUnitBtn: new(TipClickable),
|
||||
list: &layout.List{Axis: layout.Vertical},
|
||||
scrollBar: &ScrollBar{Axis: layout.Vertical},
|
||||
}
|
||||
for range sointu.UnitNames {
|
||||
ret.ChooseUnitTypeBtns = append(ret.ChooseUnitTypeBtns, new(widget.Clickable))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) Bind(t *Tracker) layout.Widget {
|
||||
return func(gtx C) D {
|
||||
for _, e := range gtx.Events(&pe.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
pe.focused = e.Focus
|
||||
case pointer.Event:
|
||||
if e.Type == pointer.Press {
|
||||
key.FocusOp{Tag: &pe.tag}.Add(gtx.Ops)
|
||||
}
|
||||
case key.Event:
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
continue
|
||||
}
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameUpArrow:
|
||||
t.SetParamIndex(t.ParamIndex() - 1)
|
||||
case key.NameDownArrow:
|
||||
t.SetParamIndex(t.ParamIndex() + 1)
|
||||
case key.NameLeftArrow:
|
||||
p, err := t.Param(t.ParamIndex())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetParam(p.Value - p.LargeStep)
|
||||
} else {
|
||||
t.SetParam(p.Value - 1)
|
||||
}
|
||||
case key.NameRightArrow:
|
||||
p, err := t.Param(t.ParamIndex())
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetParam(p.Value + p.LargeStep)
|
||||
} else {
|
||||
t.SetParam(p.Value + 1)
|
||||
}
|
||||
case key.NameEscape:
|
||||
t.InstrumentEditor.unitDragList.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if pe.requestFocus {
|
||||
pe.requestFocus = false
|
||||
key.FocusOp{Tag: &pe.tag}.Add(gtx.Ops)
|
||||
}
|
||||
editorFunc := pe.layoutUnitSliders
|
||||
if y := t.Unit().Type; y == "" || y != t.InstrumentEditor.unitTypeEditor.Text() {
|
||||
editorFunc = pe.layoutUnitTypeChooser
|
||||
}
|
||||
return Surface{Gray: 24, Focus: t.InstrumentEditor.wasFocused}.Layout(gtx, func(gtx C) D {
|
||||
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return editorFunc(gtx, t)
|
||||
}),
|
||||
layout.Rigid(pe.layoutUnitFooter(t)))
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
defer pointer.PassOp{}.Push(gtx.Ops).Pop()
|
||||
pointer.InputOp{Tag: &pe.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
key.InputOp{Tag: &pe.tag, Keys: "←|Shift-←|→|Shift-→|↑|↓|⎋"}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
return ret
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) layoutUnitSliders(gtx C, t *Tracker) D {
|
||||
numItems := t.NumParams()
|
||||
|
||||
for len(pe.Parameters) <= numItems {
|
||||
pe.Parameters = append(pe.Parameters, new(ParameterWidget))
|
||||
}
|
||||
|
||||
listItem := func(gtx C, index int) D {
|
||||
for pe.Parameters[index].Clicked() {
|
||||
if t.ParamIndex() != index {
|
||||
t.SetParamIndex(index)
|
||||
} else {
|
||||
t.ResetParam()
|
||||
}
|
||||
pe.Focus()
|
||||
}
|
||||
param, err := t.Param(index)
|
||||
if err != nil {
|
||||
return D{}
|
||||
}
|
||||
oldVal := param.Value
|
||||
paramStyle := t.ParamStyle(t.Theme, ¶m, pe.Parameters[index])
|
||||
paramStyle.Focus = pe.focused && t.ParamIndex() == index
|
||||
dims := paramStyle.Layout(gtx)
|
||||
if oldVal != param.Value {
|
||||
pe.Focus()
|
||||
t.SetParamIndex(index)
|
||||
t.SetParam(param.Value)
|
||||
}
|
||||
return dims
|
||||
}
|
||||
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return pe.list.Layout(gtx, numItems, listItem)
|
||||
}),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
gtx.Constraints.Min = gtx.Constraints.Max
|
||||
return pe.scrollBar.Layout(gtx, unit.Dp(10), numItems, &pe.list.Position)
|
||||
}))
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) layoutUnitFooter(t *Tracker) layout.Widget {
|
||||
return func(gtx C) D {
|
||||
for pe.ClearUnitBtn.Clickable.Clicked() {
|
||||
t.SetUnitType("")
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
t.InstrumentEditor.unitDragList.Focus()
|
||||
}
|
||||
for pe.DeleteUnitBtn.Clickable.Clicked() {
|
||||
t.DeleteUnits(false, t.UnitIndex(), t.UnitIndex())
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
t.InstrumentEditor.unitDragList.Focus()
|
||||
}
|
||||
for pe.CopyUnitBtn.Clickable.Clicked() {
|
||||
op.InvalidateOp{}.Add(gtx.Ops)
|
||||
contents, err := yaml.Marshal([]sointu.Unit{t.Unit()})
|
||||
if err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Unit copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
}
|
||||
copyUnitBtnStyle := IconButton(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, true, "Copy unit (Ctrl+C)")
|
||||
deleteUnitBtnStyle := IconButton(t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, t.CanDeleteUnit(), "Delete unit (Del)")
|
||||
text := t.Unit().Type
|
||||
if text == "" {
|
||||
text = "Choose unit type"
|
||||
} else {
|
||||
text = strings.Title(text)
|
||||
}
|
||||
hintText := Label(text, white, t.TextShaper)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(deleteUnitBtnStyle.Layout),
|
||||
layout.Rigid(copyUnitBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
var dims D
|
||||
if t.Unit().Type != "" {
|
||||
clearUnitBtnStyle := IconButton(t.Theme, pe.ClearUnitBtn, icons.ContentClear, true, "Clear unit")
|
||||
dims = clearUnitBtnStyle.Layout(gtx)
|
||||
}
|
||||
return D{Size: image.Pt(gtx.Dp(unit.Dp(48)), dims.Size.Y)}
|
||||
}),
|
||||
layout.Flexed(1, hintText),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func (pe *ParamEditor) layoutUnitTypeChooser(gtx C, t *Tracker) D {
|
||||
listElem := func(gtx C, i int) D {
|
||||
for pe.ChooseUnitTypeBtns[i].Clicked() {
|
||||
t.SetUnitType(sointu.UnitNames[i])
|
||||
t.InstrumentEditor.unitTypeEditor.SetText(sointu.UnitNames[i])
|
||||
}
|
||||
text := sointu.UnitNames[i]
|
||||
if t.InstrumentEditor.unitTypeEditor.Focused() && !strings.HasPrefix(text, t.InstrumentEditor.unitTypeEditor.Text()) {
|
||||
return D{}
|
||||
}
|
||||
labelStyle := LabelStyle{Text: text, ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.TextShaper}
|
||||
bg := func(gtx C) D {
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X, 20))
|
||||
var color color.NRGBA
|
||||
if pe.ChooseUnitTypeBtns[i].Hovered() {
|
||||
color = unitTypeListHighlightColor
|
||||
}
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{Max: image.Pt(gtx.Constraints.Min.X, gtx.Constraints.Min.Y)}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
leftMargin := layout.Inset{Left: unit.Dp(10)}
|
||||
return layout.Stack{Alignment: layout.W}.Layout(gtx,
|
||||
layout.Stacked(bg),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return pe.ChooseUnitTypeBtns[i].Layout(gtx, func(gtx C) D {
|
||||
return leftMargin.Layout(gtx, labelStyle.Layout)
|
||||
})
|
||||
}))
|
||||
}
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return pe.list.Layout(gtx, len(sointu.UnitNames), listElem)
|
||||
}),
|
||||
layout.Expanded(func(gtx C) D {
|
||||
return pe.scrollBar.Layout(gtx, unit.Dp(10), len(sointu.UnitNames), &pe.list.Position)
|
||||
}),
|
||||
)
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
type ParameterWidget struct {
|
||||
floatWidget widget.Float
|
||||
boolWidget widget.Bool
|
||||
labelBtn widget.Clickable
|
||||
instrBtn widget.Clickable
|
||||
instrMenu Menu
|
||||
unitBtn widget.Clickable
|
||||
unitMenu Menu
|
||||
}
|
||||
|
||||
type ParameterStyle struct {
|
||||
tracker *Tracker
|
||||
Parameter *tracker.Parameter
|
||||
ParameterWidget *ParameterWidget
|
||||
Theme *material.Theme
|
||||
Focus bool
|
||||
}
|
||||
|
||||
func (t *Tracker) ParamStyle(th *material.Theme, param *tracker.Parameter, paramWidget *ParameterWidget) ParameterStyle {
|
||||
return ParameterStyle{
|
||||
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way
|
||||
Parameter: param,
|
||||
Theme: th,
|
||||
ParameterWidget: paramWidget,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *ParameterWidget) Clicked() bool {
|
||||
return p.labelBtn.Clicked()
|
||||
}
|
||||
|
||||
func (p ParameterStyle) Layout(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return p.ParameterWidget.labelBtn.Layout(gtx, func(gtx C) D {
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110))
|
||||
return layout.E.Layout(gtx, Label(p.Parameter.Name, white, p.tracker.TextShaper))
|
||||
})
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
switch p.Parameter.Type {
|
||||
case tracker.IntegerParameter:
|
||||
for _, e := range gtx.Events(&p.ParameterWidget.floatWidget) {
|
||||
switch ev := e.(type) {
|
||||
case pointer.Event:
|
||||
if ev.Type == pointer.Scroll {
|
||||
delta := math.Min(math.Max(float64(ev.Scroll.Y), -1), 1)
|
||||
p.Parameter.Value += int(math.Round(delta))
|
||||
}
|
||||
}
|
||||
}
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
if !p.ParameterWidget.floatWidget.Dragging() {
|
||||
p.ParameterWidget.floatWidget.Value = float32(p.Parameter.Value)
|
||||
}
|
||||
sliderStyle := material.Slider(p.Theme, &p.ParameterWidget.floatWidget, float32(p.Parameter.Min), float32(p.Parameter.Max))
|
||||
sliderStyle.Color = p.Theme.Fg
|
||||
r := image.Rectangle{Max: gtx.Constraints.Min}
|
||||
area := clip.Rect(r).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &p.ParameterWidget.floatWidget, Types: pointer.Scroll, ScrollBounds: image.Rectangle{Min: image.Pt(0, -1e6), Max: image.Pt(0, 1e6)}}.Add(gtx.Ops)
|
||||
dims := sliderStyle.Layout(gtx)
|
||||
area.Pop()
|
||||
p.Parameter.Value = int(p.ParameterWidget.floatWidget.Value + 0.5)
|
||||
return dims
|
||||
case tracker.BoolParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(60))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
p.ParameterWidget.boolWidget.Value = p.Parameter.Value > p.Parameter.Min
|
||||
boolStyle := material.Switch(p.Theme, &p.ParameterWidget.boolWidget, "Toggle boolean parameter")
|
||||
boolStyle.Color.Disabled = p.Theme.Fg
|
||||
boolStyle.Color.Enabled = white
|
||||
dims := layout.Center.Layout(gtx, boolStyle.Layout)
|
||||
if p.ParameterWidget.boolWidget.Value {
|
||||
p.Parameter.Value = p.Parameter.Max
|
||||
} else {
|
||||
p.Parameter.Value = p.Parameter.Min
|
||||
}
|
||||
return dims
|
||||
case tracker.IDParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
if p.Focus {
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
}
|
||||
for clickedItem, hasClicked := p.ParameterWidget.instrMenu.Clicked(); hasClicked; {
|
||||
p.Parameter.Value = p.tracker.Song().Patch[clickedItem].Units[0].ID
|
||||
clickedItem, hasClicked = p.ParameterWidget.instrMenu.Clicked()
|
||||
}
|
||||
instrItems := make([]MenuItem, len(p.tracker.Song().Patch))
|
||||
for i, instr := range p.tracker.Song().Patch {
|
||||
instrItems[i].Text = instr.Name
|
||||
instrItems[i].IconBytes = icons.NavigationChevronRight
|
||||
}
|
||||
var unitItems []MenuItem
|
||||
instrName := "<instr>"
|
||||
unitName := "<unit>"
|
||||
targetI, targetU, err := p.tracker.Song().Patch.FindUnit(p.Parameter.Value)
|
||||
if err == nil {
|
||||
targetInstrument := p.tracker.Song().Patch[targetI]
|
||||
instrName = targetInstrument.Name
|
||||
units := targetInstrument.Units
|
||||
unitName = fmt.Sprintf("%v: %v", targetU, units[targetU].Type)
|
||||
unitItems = make([]MenuItem, len(units))
|
||||
for clickedItem, hasClicked := p.ParameterWidget.unitMenu.Clicked(); hasClicked; {
|
||||
p.Parameter.Value = units[clickedItem].ID
|
||||
clickedItem, hasClicked = p.ParameterWidget.unitMenu.Clicked()
|
||||
}
|
||||
for j, unit := range units {
|
||||
unitItems[j].Text = fmt.Sprintf("%v: %v", j, unit.Type)
|
||||
unitItems[j].IconBytes = icons.NavigationChevronRight
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(p.tracker.layoutMenu(instrName, &p.ParameterWidget.instrBtn, &p.ParameterWidget.instrMenu, unit.Dp(200),
|
||||
instrItems...,
|
||||
)),
|
||||
layout.Rigid(p.tracker.layoutMenu(unitName, &p.ParameterWidget.unitBtn, &p.ParameterWidget.unitMenu, unit.Dp(200),
|
||||
unitItems...,
|
||||
)),
|
||||
)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if p.Parameter.Type != tracker.IDParameter {
|
||||
return Label(p.Parameter.Hint, white, p.tracker.TextShaper)(gtx)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
func (t *Tracker) layoutParameter(gtx C, index int) D {
|
||||
u := t.Unit()
|
||||
ut, _ := sointu.UnitTypes[u.Type]
|
||||
|
||||
params := u.Parameters
|
||||
var name string
|
||||
var value, min, max int
|
||||
var valueText string
|
||||
if u.Type == "oscillator" && index == len(ut) {
|
||||
name = "sample"
|
||||
key := compiler.SampleOffset{Start: uint32(params["samplestart"]), LoopStart: uint16(params["loopstart"]), LoopLength: uint16(params["looplength"])}
|
||||
if v, ok := tracker.GmDlsEntryMap[key]; ok {
|
||||
value = v + 1
|
||||
valueText = fmt.Sprintf("%v / %v", value, tracker.GmDlsEntries[v].Name)
|
||||
} else {
|
||||
value = 0
|
||||
valueText = "0 / custom"
|
||||
}
|
||||
min, max = 0, len(tracker.GmDlsEntries)
|
||||
} else {
|
||||
if ut[index].MaxValue < ut[index].MinValue {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
name = ut[index].Name
|
||||
if u.Type == "oscillator" && (name == "samplestart" || name == "loopstart" || name == "looplength") {
|
||||
if params["type"] != sointu.Sample {
|
||||
return layout.Dimensions{}
|
||||
}
|
||||
}
|
||||
value = params[name]
|
||||
min, max = ut[index].MinValue, ut[index].MaxValue
|
||||
if u.Type == "send" && name == "voice" {
|
||||
max = t.Song().Patch.NumVoices()
|
||||
} else if u.Type == "send" && name == "unit" { // set the maximum values depending on the send target
|
||||
instrIndex, _, _ := t.Song().Patch.FindSendTarget(t.Unit().Parameters["target"])
|
||||
if instrIndex != -1 {
|
||||
max = len(t.Song().Patch[instrIndex].Units) - 1
|
||||
}
|
||||
} else if u.Type == "send" && name == "port" { // set the maximum values depending on the send target
|
||||
instrIndex, unitIndex, _ := t.Song().Patch.FindSendTarget(t.Unit().Parameters["target"])
|
||||
if instrIndex != -1 && unitIndex != -1 {
|
||||
max = len(sointu.Ports[t.Song().Patch[instrIndex].Units[unitIndex].Type]) - 1
|
||||
}
|
||||
}
|
||||
hint := t.Song().Patch.ParamHintString(t.InstrIndex(), t.UnitIndex(), name)
|
||||
if hint != "" {
|
||||
valueText = fmt.Sprintf("%v / %v", value, hint)
|
||||
} else {
|
||||
valueText = fmt.Sprintf("%v", value)
|
||||
}
|
||||
}
|
||||
|
||||
}*/
|
81
tracker/gioui/popup_alert.go
Normal file
81
tracker/gioui/popup_alert.go
Normal file
@ -0,0 +1,81 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type PopupAlert struct {
|
||||
alerts *tracker.Alerts
|
||||
prevUpdate time.Time
|
||||
shaper *text.Shaper
|
||||
}
|
||||
|
||||
var alertSpeed = 150 * time.Millisecond
|
||||
var alertMargin = layout.UniformInset(unit.Dp(6))
|
||||
var alertInset = layout.UniformInset(unit.Dp(6))
|
||||
|
||||
func NewPopupAlert(alerts *tracker.Alerts, shaper *text.Shaper) *PopupAlert {
|
||||
return &PopupAlert{alerts: alerts, shaper: shaper, prevUpdate: time.Now()}
|
||||
}
|
||||
|
||||
func (a *PopupAlert) Layout(gtx C) D {
|
||||
now := time.Now()
|
||||
if a.alerts.Update(now.Sub(a.prevUpdate)) {
|
||||
op.InvalidateOp{At: now.Add(50 * time.Millisecond)}.Add(gtx.Ops)
|
||||
}
|
||||
a.prevUpdate = now
|
||||
|
||||
var totalY float64
|
||||
a.alerts.Iterate(func(alert tracker.Alert) {
|
||||
var color, textColor, shadeColor color.NRGBA
|
||||
switch alert.Priority {
|
||||
case tracker.Warning:
|
||||
color = warningColor
|
||||
textColor = black
|
||||
case tracker.Error:
|
||||
color = errorColor
|
||||
textColor = black
|
||||
default:
|
||||
color = popupSurfaceColor
|
||||
textColor = white
|
||||
shadeColor = black
|
||||
}
|
||||
bgWidget := func(gtx C) D {
|
||||
paint.FillShape(gtx.Ops, color, clip.Rect{
|
||||
Max: gtx.Constraints.Min,
|
||||
}.Op())
|
||||
return D{Size: gtx.Constraints.Min}
|
||||
}
|
||||
labelStyle := LabelStyle{Text: alert.Message, Color: textColor, ShadeColor: shadeColor, Font: labelDefaultFont, Alignment: layout.Center, FontSize: unit.Sp(16), Shaper: a.shaper}
|
||||
alertMargin.Layout(gtx, func(gtx C) D {
|
||||
return layout.S.Layout(gtx, func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
recording := op.Record(gtx.Ops)
|
||||
dims := layout.Stack{Alignment: layout.Center}.Layout(gtx,
|
||||
layout.Expanded(bgWidget),
|
||||
layout.Stacked(func(gtx C) D {
|
||||
return alertInset.Layout(gtx, labelStyle.Layout)
|
||||
}),
|
||||
)
|
||||
macro := recording.Stop()
|
||||
delta := float64(dims.Size.Y + gtx.Dp(alertMargin.Bottom))
|
||||
op.Offset(image.Point{0, int(-totalY*alert.FadeLevel + delta*(1-alert.FadeLevel))}).Add((gtx.Ops))
|
||||
totalY += delta
|
||||
macro.Add(gtx.Ops)
|
||||
return dims
|
||||
})
|
||||
})
|
||||
})
|
||||
return D{}
|
||||
}
|
@ -1,70 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strings"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/widget"
|
||||
)
|
||||
|
||||
const rowMarkerWidth = 50
|
||||
|
||||
func (t *Tracker) layoutRowMarkers(gtx C) D {
|
||||
gtx.Constraints.Min.X = rowMarkerWidth
|
||||
paint.FillShape(gtx.Ops, rowMarkerSurfaceColor, clip.Rect{
|
||||
Max: gtx.Constraints.Max,
|
||||
}.Op())
|
||||
//defer op.Save(gtx.Ops).Load()
|
||||
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
||||
op.Offset(image.Pt(0, (gtx.Constraints.Max.Y-trackRowHeight)/2)).Add(gtx.Ops)
|
||||
cursorSongRow := t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row
|
||||
playPos := t.PlayPosition()
|
||||
playSongRow := playPos.Pattern*t.Song().Score.RowsPerPattern + playPos.Row
|
||||
op.Offset(image.Pt(0, (-1*trackRowHeight)*(cursorSongRow))).Add(gtx.Ops)
|
||||
beatMarkerDensity := t.Song().RowsPerBeat
|
||||
for beatMarkerDensity <= 2 {
|
||||
beatMarkerDensity *= 2
|
||||
}
|
||||
for i := 0; i < t.Song().Score.Length; i++ {
|
||||
for j := 0; j < t.Song().Score.RowsPerPattern; j++ {
|
||||
songRow := i*t.Song().Score.RowsPerPattern + j
|
||||
if mod(songRow, beatMarkerDensity*2) == 0 {
|
||||
paint.FillShape(gtx.Ops, twoBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
} else if mod(songRow, beatMarkerDensity) == 0 {
|
||||
paint.FillShape(gtx.Ops, oneBeatHighlight, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
}
|
||||
if t.Playing() && songRow == playSongRow {
|
||||
paint.FillShape(gtx.Ops, trackerPlayColor, clip.Rect{Max: image.Pt(gtx.Constraints.Max.X, trackRowHeight)}.Op())
|
||||
}
|
||||
if j == 0 {
|
||||
paint.ColorOp{Color: rowMarkerPatternTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", i)), op.CallOp{})
|
||||
}
|
||||
if t.TrackEditor.Focused() && songRow == cursorSongRow {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
paint.ColorOp{Color: rowMarkerRowTextColor}.Add(gtx.Ops)
|
||||
}
|
||||
op.Offset(image.Pt(rowMarkerWidth/2, 0)).Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(fmt.Sprintf("%02x", j)), op.CallOp{})
|
||||
op.Offset(image.Pt(-rowMarkerWidth/2, trackRowHeight)).Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
return layout.Dimensions{Size: image.Pt(rowMarkerWidth, gtx.Constraints.Max.Y)}
|
||||
}
|
||||
|
||||
func mod(a, b int) int {
|
||||
m := a % b
|
||||
if a < 0 && b < 0 {
|
||||
m -= b
|
||||
}
|
||||
if a < 0 && b > 0 {
|
||||
m += b
|
||||
}
|
||||
return m
|
||||
}
|
258
tracker/gioui/scroll_table.go
Normal file
258
tracker/gioui/scroll_table.go
Normal file
@ -0,0 +1,258 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
)
|
||||
|
||||
type ScrollTable struct {
|
||||
ColTitleList *DragList
|
||||
RowTitleList *DragList
|
||||
Table tracker.Table
|
||||
focused bool
|
||||
requestFocus bool
|
||||
tag bool
|
||||
colTag bool
|
||||
rowTag bool
|
||||
cursorMoved bool
|
||||
}
|
||||
|
||||
type ScrollTableStyle struct {
|
||||
RowTitleStyle FilledDragListStyle
|
||||
ColTitleStyle FilledDragListStyle
|
||||
ScrollTable *ScrollTable
|
||||
ScrollBarWidth unit.Dp
|
||||
RowTitleWidth unit.Dp
|
||||
ColumnTitleHeight unit.Dp
|
||||
CellWidth unit.Dp
|
||||
CellHeight unit.Dp
|
||||
element func(gtx C, x, y int) D
|
||||
}
|
||||
|
||||
func NewScrollTable(table tracker.Table, vertList, horizList tracker.List) *ScrollTable {
|
||||
return &ScrollTable{
|
||||
Table: table,
|
||||
ColTitleList: NewDragList(vertList, layout.Horizontal),
|
||||
RowTitleList: NewDragList(horizList, layout.Vertical),
|
||||
}
|
||||
}
|
||||
|
||||
func FilledScrollTable(th *material.Theme, scrollTable *ScrollTable, element func(gtx C, x, y int) D, colTitle, rowTitle, colTitleBg, rowTitleBg func(gtx C, i int) D) ScrollTableStyle {
|
||||
return ScrollTableStyle{
|
||||
RowTitleStyle: FilledDragList(th, scrollTable.RowTitleList, rowTitle, rowTitleBg),
|
||||
ColTitleStyle: FilledDragList(th, scrollTable.ColTitleList, colTitle, colTitleBg),
|
||||
ScrollTable: scrollTable,
|
||||
element: element,
|
||||
ScrollBarWidth: unit.Dp(14),
|
||||
RowTitleWidth: unit.Dp(30),
|
||||
ColumnTitleHeight: unit.Dp(16),
|
||||
CellWidth: unit.Dp(16),
|
||||
CellHeight: unit.Dp(16),
|
||||
}
|
||||
}
|
||||
|
||||
func (st *ScrollTable) CursorMoved() bool {
|
||||
ret := st.cursorMoved
|
||||
st.cursorMoved = false
|
||||
return ret
|
||||
}
|
||||
|
||||
func (st *ScrollTable) Focus() {
|
||||
st.requestFocus = true
|
||||
}
|
||||
|
||||
func (st *ScrollTable) Focused() bool {
|
||||
return st.focused
|
||||
}
|
||||
|
||||
func (st *ScrollTable) EnsureCursorVisible() {
|
||||
st.ColTitleList.EnsureVisible(st.Table.Cursor().X)
|
||||
st.RowTitleList.EnsureVisible(st.Table.Cursor().Y)
|
||||
}
|
||||
|
||||
func (st *ScrollTable) ChildFocused() bool {
|
||||
return st.ColTitleList.Focused() || st.RowTitleList.Focused()
|
||||
}
|
||||
|
||||
func (s ScrollTableStyle) Layout(gtx C) D {
|
||||
p := image.Pt(gtx.Dp(s.RowTitleWidth), gtx.Dp(s.ColumnTitleHeight))
|
||||
|
||||
for _, e := range gtx.Events(&s.ScrollTable.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
s.ScrollTable.focused = e.Focus
|
||||
case pointer.Event:
|
||||
if e.Position.X >= float32(p.X) && e.Position.Y >= float32(p.Y) {
|
||||
if e.Type == pointer.Press {
|
||||
key.FocusOp{Tag: &s.ScrollTable.tag}.Add(gtx.Ops)
|
||||
}
|
||||
dx := (int(e.Position.X) + s.ScrollTable.ColTitleList.List.Position.Offset - p.X) / gtx.Dp(s.CellWidth)
|
||||
dy := (int(e.Position.Y) + s.ScrollTable.RowTitleList.List.Position.Offset - p.Y) / gtx.Dp(s.CellHeight)
|
||||
x := dx + s.ScrollTable.ColTitleList.List.Position.First
|
||||
y := dy + s.ScrollTable.RowTitleList.List.Position.First
|
||||
s.ScrollTable.Table.SetCursor(
|
||||
tracker.Point{X: x, Y: y},
|
||||
)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
s.ScrollTable.Table.SetCursor2(s.ScrollTable.Table.Cursor())
|
||||
}
|
||||
s.ScrollTable.cursorMoved = true
|
||||
}
|
||||
case key.Event:
|
||||
if e.State == key.Press {
|
||||
s.ScrollTable.command(gtx, e)
|
||||
}
|
||||
case clipboard.Event:
|
||||
s.ScrollTable.Table.Paste([]byte(e.Text))
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range gtx.Events(&s.ScrollTable.rowTag) {
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
s.ScrollTable.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range gtx.Events(&s.ScrollTable.colTag) {
|
||||
if e, ok := e.(key.Event); ok && e.State == key.Press {
|
||||
s.ScrollTable.Focus()
|
||||
}
|
||||
}
|
||||
|
||||
return Surface{Gray: 24, Focus: s.ScrollTable.Focused() || s.ScrollTable.ChildFocused()}.Layout(gtx, func(gtx C) D {
|
||||
defer clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Push(gtx.Ops).Pop()
|
||||
pointer.InputOp{
|
||||
Tag: &s.ScrollTable.tag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
dims := gtx.Constraints.Max
|
||||
s.layoutColTitles(gtx, p)
|
||||
s.layoutRowTitles(gtx, p)
|
||||
defer op.Offset(p).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Constraints.Max.X-p.X, gtx.Constraints.Max.Y-p.Y))
|
||||
s.layoutTable(gtx, p)
|
||||
s.RowTitleStyle.LayoutScrollBar(gtx)
|
||||
s.ColTitleStyle.LayoutScrollBar(gtx)
|
||||
return D{Size: dims}
|
||||
})
|
||||
}
|
||||
|
||||
func (s ScrollTableStyle) layoutTable(gtx C, p image.Point) {
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Min}).Push(gtx.Ops).Pop()
|
||||
|
||||
if s.ScrollTable.requestFocus {
|
||||
s.ScrollTable.requestFocus = false
|
||||
key.FocusOp{Tag: &s.ScrollTable.tag}.Add(gtx.Ops)
|
||||
}
|
||||
key.InputOp{Tag: &s.ScrollTable.tag, Keys: "←|→|↑|↓|Shift-←|Shift-→|Shift-↑|Shift-↓|Ctrl-←|Ctrl-→|Ctrl-↑|Ctrl-↓|Ctrl-Shift-←|Ctrl-Shift-→|Ctrl-Shift-↑|Ctrl-Shift-↓|Alt-←|Alt-→|Alt-↑|Alt-↓|Alt-Shift-←|Alt-Shift-→|Alt-Shift-↑|Alt-Shift-↓|⇱|⇲|Shift-⇱|Shift-⇲|⌫|⌦|⇞|⇟|Shift-⇞|Shift-⇟|Ctrl-C|Ctrl-V|Ctrl-X|Shift-,|Shift-."}.Add(gtx.Ops)
|
||||
cellWidth := gtx.Dp(s.CellWidth)
|
||||
cellHeight := gtx.Dp(s.CellHeight)
|
||||
|
||||
gtx.Constraints = layout.Exact(image.Pt(cellWidth, cellHeight))
|
||||
|
||||
colP := s.ColTitleStyle.dragList.List.Position
|
||||
rowP := s.RowTitleStyle.dragList.List.Position
|
||||
defer op.Offset(image.Pt(-colP.Offset, -rowP.Offset)).Push(gtx.Ops).Pop()
|
||||
for x := colP.First; x < colP.First+colP.Count; x++ {
|
||||
offs := op.Offset(image.Point{}).Push(gtx.Ops)
|
||||
for y := rowP.First; y < rowP.First+rowP.Count; y++ {
|
||||
s.element(gtx, x, y)
|
||||
op.Offset(image.Pt(0, cellHeight)).Add(gtx.Ops)
|
||||
}
|
||||
offs.Pop()
|
||||
op.Offset(image.Pt(cellWidth, 0)).Add(gtx.Ops)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ScrollTableStyle) layoutRowTitles(gtx C, p image.Point) {
|
||||
defer op.Offset(image.Pt(0, p.Y)).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.X = p.X
|
||||
gtx.Constraints.Max.Y -= p.Y
|
||||
gtx.Constraints.Min.Y = gtx.Constraints.Max.Y
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &s.ScrollTable.rowTag, Keys: "→"}.Add(gtx.Ops)
|
||||
s.RowTitleStyle.Layout(gtx)
|
||||
}
|
||||
|
||||
func (s *ScrollTableStyle) layoutColTitles(gtx C, p image.Point) {
|
||||
defer op.Offset(image.Pt(p.X, 0)).Push(gtx.Ops).Pop()
|
||||
gtx.Constraints.Min.Y = p.Y
|
||||
gtx.Constraints.Max.X -= p.X
|
||||
gtx.Constraints.Min.X = gtx.Constraints.Max.X
|
||||
defer clip.Rect(image.Rectangle{Max: gtx.Constraints.Max}).Push(gtx.Ops).Pop()
|
||||
key.InputOp{Tag: &s.ScrollTable.colTag, Keys: "↓"}.Add(gtx.Ops)
|
||||
s.ColTitleStyle.Layout(gtx)
|
||||
}
|
||||
|
||||
func (s *ScrollTable) command(gtx C, e key.Event) {
|
||||
stepX := 1
|
||||
stepY := 1
|
||||
if e.Modifiers.Contain(key.ModAlt) {
|
||||
stepX = intMax(s.ColTitleList.List.Position.Count-3, 8)
|
||||
stepY = intMax(s.RowTitleList.List.Position.Count-3, 8)
|
||||
} else if e.Modifiers.Contain(key.ModCtrl) {
|
||||
stepX = 1e6
|
||||
stepY = 1e6
|
||||
}
|
||||
switch e.Name {
|
||||
case "X", "C":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
contents, ok := s.Table.Copy()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
if e.Name == "X" {
|
||||
s.Table.Clear()
|
||||
}
|
||||
return
|
||||
}
|
||||
case "V":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
clipboard.ReadOp{Tag: &s.tag}.Add(gtx.Ops)
|
||||
}
|
||||
return
|
||||
case key.NameDeleteBackward, key.NameDeleteForward:
|
||||
s.Table.Clear()
|
||||
return
|
||||
case key.NameUpArrow:
|
||||
if !s.Table.MoveCursor(0, -stepY) && stepY == 1 {
|
||||
s.ColTitleList.Focus()
|
||||
}
|
||||
case key.NameDownArrow:
|
||||
s.Table.MoveCursor(0, stepY)
|
||||
case key.NameLeftArrow:
|
||||
if !s.Table.MoveCursor(-stepX, 0) && stepX == 1 {
|
||||
s.RowTitleList.Focus()
|
||||
}
|
||||
case key.NameRightArrow:
|
||||
s.Table.MoveCursor(stepX, 0)
|
||||
case key.NamePageUp:
|
||||
s.Table.MoveCursor(0, -intMax(s.RowTitleList.List.Position.Count-3, 8))
|
||||
case key.NamePageDown:
|
||||
s.Table.MoveCursor(0, intMax(s.RowTitleList.List.Position.Count-3, 8))
|
||||
case key.NameHome:
|
||||
s.Table.SetCursorX(0)
|
||||
case key.NameEnd:
|
||||
s.Table.SetCursorX(s.Table.Width() - 1)
|
||||
case ".":
|
||||
s.Table.Add(1)
|
||||
case ",":
|
||||
s.Table.Add(-1)
|
||||
}
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
s.Table.SetCursor2(s.Table.Cursor())
|
||||
}
|
||||
s.ColTitleList.EnsureVisible(s.Table.Cursor().X)
|
||||
s.RowTitleList.EnsureVisible(s.Table.Cursor().Y)
|
||||
s.cursorMoved = true
|
||||
}
|
@ -2,222 +2,180 @@ package gioui
|
||||
|
||||
import (
|
||||
"image"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type SongPanel struct {
|
||||
MenuBar []widget.Clickable
|
||||
Menus []Menu
|
||||
BPM *NumberInput
|
||||
RowsPerPattern *NumberInput
|
||||
RowsPerBeat *NumberInput
|
||||
Step *NumberInput
|
||||
SongLength *NumberInput
|
||||
|
||||
RewindBtn *ActionClickable
|
||||
PlayingBtn *BoolClickable
|
||||
RecordBtn *BoolClickable
|
||||
NoteTracking *BoolClickable
|
||||
PanicBtn *BoolClickable
|
||||
|
||||
// File menu items
|
||||
fileMenuItems []MenuItem
|
||||
NewSong tracker.Action
|
||||
OpenSongFile tracker.Action
|
||||
SaveSongFile tracker.Action
|
||||
SaveSongAsFile tracker.Action
|
||||
ExportWav tracker.Action
|
||||
Quit tracker.Action
|
||||
|
||||
// Edit menu items
|
||||
editMenuItems []MenuItem
|
||||
}
|
||||
|
||||
func NewSongPanel(model *tracker.Model) *SongPanel {
|
||||
ret := &SongPanel{
|
||||
MenuBar: make([]widget.Clickable, 2),
|
||||
Menus: make([]Menu, 2),
|
||||
BPM: NewNumberInput(model.BPM().Int()),
|
||||
RowsPerPattern: NewNumberInput(model.RowsPerPattern().Int()),
|
||||
RowsPerBeat: NewNumberInput(model.RowsPerBeat().Int()),
|
||||
Step: NewNumberInput(model.Step().Int()),
|
||||
SongLength: NewNumberInput(model.SongLength().Int()),
|
||||
PanicBtn: NewBoolClickable(model.Panic().Bool()),
|
||||
RecordBtn: NewBoolClickable(model.IsRecording().Bool()),
|
||||
NoteTracking: NewBoolClickable(model.NoteTracking().Bool()),
|
||||
PlayingBtn: NewBoolClickable(model.Playing().Bool()),
|
||||
RewindBtn: NewActionClickable(model.Rewind()),
|
||||
}
|
||||
ret.fileMenuItems = []MenuItem{
|
||||
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N", Doer: model.NewSong()},
|
||||
{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O", Doer: model.OpenSong()},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S", Doer: model.SaveSong()},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song As...", Doer: model.SaveSongAs()},
|
||||
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav...", Doer: model.Export()},
|
||||
}
|
||||
if canQuit {
|
||||
ret.fileMenuItems = append(ret.fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit", Doer: model.Quit()})
|
||||
}
|
||||
ret.editMenuItems = []MenuItem{
|
||||
{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Doer: model.Undo()},
|
||||
{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: shortcutKey + "Y", Doer: model.Redo()},
|
||||
{IconBytes: icons.ImageCrop, Text: "Remove unused data", Doer: model.RemoveUnused()},
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
const shortcutKey = "Ctrl+"
|
||||
|
||||
var fileMenuItems []MenuItem = []MenuItem{
|
||||
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N"},
|
||||
{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O"},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S"},
|
||||
{IconBytes: icons.ContentSave, Text: "Save Song As..."},
|
||||
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav..."},
|
||||
}
|
||||
|
||||
func init() {
|
||||
if CAN_QUIT {
|
||||
fileMenuItems = append(fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit"})
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutSongPanel(gtx C) D {
|
||||
func (s *SongPanel) Layout(gtx C, t *Tracker) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(t.layoutMenuBar),
|
||||
layout.Rigid(t.layoutSongOptions),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return s.layoutMenuBar(gtx, t)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return s.layoutSongOptions(gtx, t)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutMenu(title string, clickable *widget.Clickable, menu *Menu, width unit.Dp, items ...MenuItem) layout.Widget {
|
||||
for clickable.Clicked() {
|
||||
menu.Visible = true
|
||||
}
|
||||
m := t.PopupMenu(menu)
|
||||
return func(gtx C) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
titleBtn := material.Button(t.Theme, clickable, title)
|
||||
titleBtn.Color = white
|
||||
titleBtn.Background = transparent
|
||||
titleBtn.CornerRadius = unit.Dp(0)
|
||||
dims := titleBtn.Layout(gtx)
|
||||
op.Offset(image.Pt(0, dims.Size.Y)).Add(gtx.Ops)
|
||||
gtx.Constraints.Max.X = gtx.Dp(width)
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(1000))
|
||||
m.Layout(gtx, items...)
|
||||
return dims
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutMenuBar(gtx C) D {
|
||||
func (t *SongPanel) layoutMenuBar(gtx C, tr *Tracker) D {
|
||||
gtx.Constraints.Max.Y = gtx.Dp(unit.Dp(36))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
|
||||
|
||||
for clickedItem, hasClicked := t.Menus[0].Clicked(); hasClicked; {
|
||||
switch clickedItem {
|
||||
case 0:
|
||||
t.NewSong(false)
|
||||
case 1:
|
||||
t.OpenSongFile(false)
|
||||
case 2:
|
||||
t.SaveSongFile()
|
||||
case 3:
|
||||
t.SaveSongAsFile()
|
||||
case 4:
|
||||
t.WaveTypeDialog.Visible = true
|
||||
case 5:
|
||||
t.Quit(false)
|
||||
}
|
||||
clickedItem, hasClicked = t.Menus[0].Clicked()
|
||||
}
|
||||
|
||||
for clickedItem, hasClicked := t.Menus[1].Clicked(); hasClicked; {
|
||||
switch clickedItem {
|
||||
case 0:
|
||||
t.Undo()
|
||||
case 1:
|
||||
t.Redo()
|
||||
case 2:
|
||||
if contents, err := yaml.Marshal(t.Song()); err == nil {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alert.Update("Song copied to clipboard", Notify, time.Second*3)
|
||||
}
|
||||
case 3:
|
||||
clipboard.ReadOp{Tag: t}.Add(gtx.Ops)
|
||||
case 4:
|
||||
t.RemoveUnusedData()
|
||||
}
|
||||
clickedItem, hasClicked = t.Menus[1].Clicked()
|
||||
}
|
||||
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(t.layoutMenu("File", &t.MenuBar[0], &t.Menus[0], unit.Dp(200),
|
||||
fileMenuItems...,
|
||||
)),
|
||||
layout.Rigid(t.layoutMenu("Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(200),
|
||||
MenuItem{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Disabled: !t.CanUndo()},
|
||||
MenuItem{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: shortcutKey + "Y", Disabled: !t.CanRedo()},
|
||||
MenuItem{IconBytes: icons.ContentContentCopy, Text: "Copy", ShortcutText: shortcutKey + "C"},
|
||||
MenuItem{IconBytes: icons.ContentContentPaste, Text: "Paste", ShortcutText: shortcutKey + "V"},
|
||||
MenuItem{IconBytes: icons.ImageCrop, Text: "Remove unused data"},
|
||||
)),
|
||||
layout.Rigid(tr.layoutMenu("File", &t.MenuBar[0], &t.Menus[0], unit.Dp(200), t.fileMenuItems...)),
|
||||
layout.Rigid(tr.layoutMenu("Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(200), t.editMenuItems...)),
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutSongOptions(gtx C) D {
|
||||
func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
|
||||
paint.FillShape(gtx.Ops, songSurfaceColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
||||
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
|
||||
var panicBtnStyle material.ButtonStyle
|
||||
if !t.Panic() {
|
||||
panicBtnStyle = LowEmphasisButton(t.Theme, t.PanicBtn, "Panic")
|
||||
} else {
|
||||
panicBtnStyle = HighEmphasisButton(t.Theme, t.PanicBtn, "Panic")
|
||||
}
|
||||
|
||||
for t.PanicBtn.Clicked() {
|
||||
t.SetPanic(!t.Panic())
|
||||
}
|
||||
|
||||
var recordBtnStyle material.ButtonStyle
|
||||
if !t.Recording() {
|
||||
recordBtnStyle = LowEmphasisButton(t.Theme, t.RecordBtn, "Record")
|
||||
} else {
|
||||
recordBtnStyle = HighEmphasisButton(t.Theme, t.RecordBtn, "Record")
|
||||
}
|
||||
|
||||
for t.RecordBtn.Clicked() {
|
||||
t.SetRecording(!t.Recording())
|
||||
}
|
||||
panicBtnStyle := ToggleButton(tr.Theme, t.PanicBtn, "Panic (F12)")
|
||||
rewindBtnStyle := ActionIcon(tr.Theme, t.RewindBtn, icons.AVFastRewind, "Rewind\n(F5)")
|
||||
playBtnStyle := ToggleIcon(tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F6 / Space)", "Stop (F6 / Space)")
|
||||
recordBtnStyle := ToggleIcon(tr.Theme, t.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, "Record (F7)", "Stop (F7)")
|
||||
noteTrackBtnStyle := ToggleIcon(tr.Theme, t.NoteTracking, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, "Follow\nOff\n(F8)", "Follow\nOn\n(F8)")
|
||||
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("LEN:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("LEN:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.SongLength.Value = t.Song().Score.Length
|
||||
numStyle := NumericUpDown(t.Theme, t.SongLength, 1, math.MaxInt32, "Song length")
|
||||
numStyle := NumericUpDown(tr.Theme, t.SongLength, "Song length")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetSongLength(t.SongLength.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("BPM:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("BPM:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.BPM.Value = t.Song().BPM
|
||||
numStyle := NumericUpDown(t.Theme, t.BPM, 1, 999, "Beats per minute")
|
||||
numStyle := NumericUpDown(tr.Theme, t.BPM, "Beats per minute")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetBPM(t.BPM.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("RPP:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("RPP:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.RowsPerPattern.Value = t.Song().Score.RowsPerPattern
|
||||
numStyle := NumericUpDown(t.Theme, t.RowsPerPattern, 1, 255, "Rows per pattern")
|
||||
numStyle := NumericUpDown(tr.Theme, t.RowsPerPattern, "Rows per pattern")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetRowsPerPattern(t.RowsPerPattern.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("RPB:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("RPB:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
t.RowsPerBeat.Value = t.Song().RowsPerBeat
|
||||
numStyle := NumericUpDown(t.Theme, t.RowsPerBeat, 1, 32, "Rows per beat")
|
||||
numStyle := NumericUpDown(tr.Theme, t.RowsPerBeat, "Rows per beat")
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(20))
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(70))
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
t.SetRowsPerBeat(t.RowsPerBeat.Value)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(Label("STP:", white, t.TextShaper)),
|
||||
layout.Rigid(Label("STP:", white, tr.Theme.Shaper)),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
numStyle := NumericUpDown(t.Theme, t.Step, 0, 8, "Cursor step")
|
||||
numStyle := NumericUpDown(tr.Theme, t.Step, "Cursor step")
|
||||
numStyle.UnitsPerStep = unit.Dp(20)
|
||||
dims := in.Layout(gtx, numStyle.Layout)
|
||||
return dims
|
||||
}),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(VuMeter{AverageVolume: tr.Model.AverageVolume(), PeakVolume: tr.Model.PeakVolume(), Range: 100}.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
gtx.Constraints.Min = image.Pt(0, 0)
|
||||
return panicBtnStyle.Layout(gtx)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(rewindBtnStyle.Layout),
|
||||
layout.Rigid(playBtnStyle.Layout),
|
||||
layout.Rigid(recordBtnStyle.Layout),
|
||||
layout.Rigid(noteTrackBtnStyle.Layout),
|
||||
)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
gtx.Constraints.Min = image.Pt(0, 0)
|
||||
return recordBtnStyle.Layout(gtx)
|
||||
}),
|
||||
layout.Rigid(VuMeter{AverageVolume: t.lastAvgVolume, PeakVolume: t.lastPeakVolume, Range: 100}.Layout),
|
||||
layout.Rigid(panicBtnStyle.Layout),
|
||||
)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"image/color"
|
||||
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
)
|
||||
@ -39,10 +40,13 @@ func (s Surface) Layout(gtx C, widget layout.Widget) D {
|
||||
return s.Inset.Layout(gtx, widget)
|
||||
}
|
||||
if s.FitSize {
|
||||
return layout.Stack{}.Layout(gtx,
|
||||
layout.Expanded(bg),
|
||||
layout.Stacked(fg),
|
||||
)
|
||||
macro := op.Record(gtx.Ops)
|
||||
dims := fg(gtx)
|
||||
call := macro.Stop()
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
bg(gtx)
|
||||
call.Add(gtx.Ops)
|
||||
return dims
|
||||
}
|
||||
gtxbg := gtx
|
||||
gtxbg.Constraints.Min = gtxbg.Constraints.Max
|
||||
|
@ -63,7 +63,7 @@ var inactiveLightSurfaceColor = color.NRGBA{R: 37, G: 37, B: 38, A: 255}
|
||||
var activeLightSurfaceColor = color.NRGBA{R: 45, G: 45, B: 45, A: 255}
|
||||
|
||||
var cursorColor = color.NRGBA{R: 100, G: 140, B: 255, A: 48}
|
||||
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 8}
|
||||
var selectionColor = color.NRGBA{R: 100, G: 140, B: 255, A: 12}
|
||||
var inactiveSelectionColor = color.NRGBA{R: 140, G: 140, B: 140, A: 16}
|
||||
|
||||
var errorColor = color.NRGBA{R: 207, G: 102, B: 121, A: 255}
|
||||
|
@ -1,500 +0,0 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
)
|
||||
|
||||
const trackRowHeight = 16
|
||||
const trackColWidth = 54
|
||||
const patmarkWidth = 16
|
||||
|
||||
type TrackEditor struct {
|
||||
TrackVoices *NumberInput
|
||||
NewTrackBtn *TipClickable
|
||||
DeleteTrackBtn *TipClickable
|
||||
AddSemitoneBtn *widget.Clickable
|
||||
SubtractSemitoneBtn *widget.Clickable
|
||||
AddOctaveBtn *widget.Clickable
|
||||
SubtractOctaveBtn *widget.Clickable
|
||||
NoteOffBtn *widget.Clickable
|
||||
trackPointerTag bool
|
||||
trackJumpPointerTag bool
|
||||
tag bool
|
||||
focused bool
|
||||
requestFocus bool
|
||||
}
|
||||
|
||||
func NewTrackEditor() *TrackEditor {
|
||||
return &TrackEditor{
|
||||
TrackVoices: new(NumberInput),
|
||||
NewTrackBtn: new(TipClickable),
|
||||
DeleteTrackBtn: new(TipClickable),
|
||||
AddSemitoneBtn: new(widget.Clickable),
|
||||
SubtractSemitoneBtn: new(widget.Clickable),
|
||||
AddOctaveBtn: new(widget.Clickable),
|
||||
SubtractOctaveBtn: new(widget.Clickable),
|
||||
NoteOffBtn: new(widget.Clickable),
|
||||
}
|
||||
}
|
||||
|
||||
func (te *TrackEditor) Focus() {
|
||||
te.requestFocus = true
|
||||
}
|
||||
|
||||
func (te *TrackEditor) Focused() bool {
|
||||
return te.focused || te.ChildFocused()
|
||||
}
|
||||
|
||||
func (te *TrackEditor) ChildFocused() bool {
|
||||
return te.AddOctaveBtn.Focused() || te.AddSemitoneBtn.Focused() || te.DeleteTrackBtn.Clickable.Focused() || te.NewTrackBtn.Clickable.Focused() || te.NoteOffBtn.Focused() || te.SubtractOctaveBtn.Focused() || te.SubtractSemitoneBtn.Focused() || te.SubtractSemitoneBtn.Focused() || te.SubtractSemitoneBtn.Focused()
|
||||
}
|
||||
|
||||
var trackerEditorKeys key.Set = "+|-|←|→|↑|↓|Ctrl-←|Ctrl-→|Ctrl-↑|Ctrl-↓|Shift-←|Shift-→|Shift-↑|Shift-↓|⏎|⇱|⇲|⌫|⌦|A|B|C|D|E|F|G|H|I|J|K|L|M|N|O|P|Q|R|S|T|U|V|W|X|Y|Z|0|1|2|3|4|5|6|7|8|9|,|."
|
||||
|
||||
func (te *TrackEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions {
|
||||
for _, e := range gtx.Events(te) {
|
||||
switch e := e.(type) {
|
||||
case key.FocusEvent:
|
||||
te.focused = e.Focus
|
||||
case pointer.Event:
|
||||
if e.Type == pointer.Press {
|
||||
key.FocusOp{Tag: te}.Add(gtx.Ops)
|
||||
}
|
||||
case key.Event:
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameDeleteForward, key.NameDeleteBackward:
|
||||
t.DeleteSelection()
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
case key.NameUpArrow, key.NameDownArrow:
|
||||
sign := -1
|
||||
if e.Name == key.NameDownArrow {
|
||||
sign = 1
|
||||
}
|
||||
cursor := t.Cursor()
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Row += t.Song().Score.RowsPerPattern * sign
|
||||
} else {
|
||||
if t.Step.Value > 0 {
|
||||
cursor.Row += t.Step.Value * sign
|
||||
} else {
|
||||
cursor.Row += sign
|
||||
}
|
||||
}
|
||||
t.SetNoteTracking(false)
|
||||
t.SetCursor(cursor)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
//scrollToView(t.PatternOrderList, t.Cursor().Pattern, t.Song().Score.Length)
|
||||
case key.NameLeftArrow:
|
||||
cursor := t.Cursor()
|
||||
if !t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor.Track--
|
||||
t.SetLowNibble(true)
|
||||
} else {
|
||||
t.SetLowNibble(false)
|
||||
}
|
||||
t.SetCursor(cursor)
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
case key.NameRightArrow:
|
||||
if t.LowNibble() || !t.Song().Score.Tracks[t.Cursor().Track].Effect || e.Modifiers.Contain(key.ModShortcut) {
|
||||
cursor := t.Cursor()
|
||||
cursor.Track++
|
||||
t.SetCursor(cursor)
|
||||
t.SetLowNibble(false)
|
||||
} else {
|
||||
t.SetLowNibble(true)
|
||||
}
|
||||
|
||||
if !e.Modifiers.Contain(key.ModShift) {
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
case "+":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.AdjustSelectionPitch(12)
|
||||
} else {
|
||||
t.AdjustSelectionPitch(1)
|
||||
}
|
||||
case "-":
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
t.AdjustSelectionPitch(-12)
|
||||
} else {
|
||||
t.AdjustSelectionPitch(-1)
|
||||
}
|
||||
}
|
||||
if e.Modifiers.Contain(key.ModShortcut) {
|
||||
continue
|
||||
}
|
||||
step := false
|
||||
if t.Song().Score.Tracks[t.Cursor().Track].Effect {
|
||||
if iv, err := strconv.ParseInt(e.Name, 16, 8); err == nil {
|
||||
t.NumberPressed(byte(iv))
|
||||
step = true
|
||||
}
|
||||
} else {
|
||||
if e.Name == "A" || e.Name == "1" {
|
||||
t.SetNote(0)
|
||||
step = true
|
||||
} else {
|
||||
if val, ok := noteMap[e.Name]; ok {
|
||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||
n := noteAsValue(t.OctaveNumberInput.Value, val)
|
||||
t.SetNote(n)
|
||||
step = true
|
||||
trk := t.Cursor().Track
|
||||
noteID := tracker.NoteIDTrack(trk, n)
|
||||
t.NoteOn(noteID)
|
||||
t.KeyPlaying[e.Name] = noteID
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if step && !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
case key.Release:
|
||||
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
||||
t.NoteOff(noteID)
|
||||
delete(t.KeyPlaying, e.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if te.requestFocus || te.ChildFocused() {
|
||||
te.requestFocus = false
|
||||
key.FocusOp{Tag: te}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
rowMarkers := layout.Rigid(t.layoutRowMarkers)
|
||||
|
||||
for te.NewTrackBtn.Clickable.Clicked() {
|
||||
t.AddTrack(true)
|
||||
}
|
||||
|
||||
for te.DeleteTrackBtn.Clickable.Clicked() {
|
||||
t.DeleteTrack(false)
|
||||
}
|
||||
|
||||
//t.TrackHexCheckBoxes[i2].Value = t.TrackShowHex[i2]
|
||||
//cbStyle := material.CheckBox(t.Theme, t.TrackHexCheckBoxes[i2], "hex")
|
||||
//cbStyle.Color = white
|
||||
//cbStyle.IconColor = t.Theme.Fg
|
||||
|
||||
for te.AddSemitoneBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(1)
|
||||
}
|
||||
|
||||
for te.SubtractSemitoneBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(-1)
|
||||
}
|
||||
|
||||
for te.NoteOffBtn.Clicked() {
|
||||
t.SetNote(0)
|
||||
if !(t.NoteTracking() && t.Playing()) && t.Step.Value > 0 {
|
||||
t.SetCursor(t.Cursor().AddRows(t.Step.Value))
|
||||
t.SetSelectionCorner(t.Cursor())
|
||||
}
|
||||
}
|
||||
|
||||
for te.AddOctaveBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(12)
|
||||
}
|
||||
|
||||
for te.SubtractOctaveBtn.Clicked() {
|
||||
t.AdjustSelectionPitch(-12)
|
||||
}
|
||||
|
||||
menu := func(gtx C) D {
|
||||
addSemitoneBtnStyle := LowEmphasisButton(t.Theme, te.AddSemitoneBtn, "+1")
|
||||
subtractSemitoneBtnStyle := LowEmphasisButton(t.Theme, te.SubtractSemitoneBtn, "-1")
|
||||
addOctaveBtnStyle := LowEmphasisButton(t.Theme, te.AddOctaveBtn, "+12")
|
||||
subtractOctaveBtnStyle := LowEmphasisButton(t.Theme, te.SubtractOctaveBtn, "-12")
|
||||
noteOffBtnStyle := LowEmphasisButton(t.Theme, te.NoteOffBtn, "Note Off")
|
||||
deleteTrackBtnStyle := IconButton(t.Theme, te.DeleteTrackBtn, icons.ActionDelete, t.CanDeleteTrack(), "Delete track")
|
||||
newTrackBtnStyle := IconButton(t.Theme, te.NewTrackBtn, icons.ContentAdd, t.CanAddTrack(), "Add track")
|
||||
n := t.Song().Score.Tracks[t.Cursor().Track].NumVoices
|
||||
te.TrackVoices.Value = n
|
||||
in := layout.UniformInset(unit.Dp(1))
|
||||
voiceUpDown := func(gtx C) D {
|
||||
numStyle := NumericUpDown(t.Theme, te.TrackVoices, 1, t.MaxTrackVoices(), "Number of voices for this track")
|
||||
return in.Layout(gtx, numStyle.Layout)
|
||||
}
|
||||
t.TrackHexCheckBox.Value = t.Song().Score.Tracks[t.Cursor().Track].Effect
|
||||
hexCheckBoxStyle := material.CheckBox(t.Theme, t.TrackHexCheckBox, "Hex")
|
||||
dims := layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D { return layout.Dimensions{Size: image.Pt(gtx.Dp(unit.Dp(12)), 0)} }),
|
||||
layout.Rigid(addSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(subtractSemitoneBtnStyle.Layout),
|
||||
layout.Rigid(addOctaveBtnStyle.Layout),
|
||||
layout.Rigid(subtractOctaveBtnStyle.Layout),
|
||||
layout.Rigid(noteOffBtnStyle.Layout),
|
||||
layout.Rigid(hexCheckBoxStyle.Layout),
|
||||
layout.Rigid(Label(" Voices:", white, t.TextShaper)),
|
||||
layout.Rigid(voiceUpDown),
|
||||
layout.Flexed(1, func(gtx C) D { return layout.Dimensions{Size: gtx.Constraints.Min} }),
|
||||
layout.Rigid(deleteTrackBtnStyle.Layout),
|
||||
layout.Rigid(newTrackBtnStyle.Layout))
|
||||
t.Song().Score.Tracks[t.Cursor().Track].Effect = t.TrackHexCheckBox.Value // TODO: we should not modify the model, but how should this be done
|
||||
t.SetTrackVoices(te.TrackVoices.Value)
|
||||
return dims
|
||||
}
|
||||
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: te,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
key.InputOp{Tag: te, Keys: trackerEditorKeys}.Add(gtx.Ops)
|
||||
|
||||
dims := Surface{Gray: 24, Focus: te.Focused()}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return Surface{Gray: 37, Focus: te.Focused(), FitSize: true}.Layout(gtx, menu)
|
||||
}),
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
rowMarkers,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return te.layoutTracks(gtx, t)
|
||||
}))
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
area.Pop()
|
||||
return dims
|
||||
}
|
||||
|
||||
const baseNote = 24
|
||||
|
||||
var notes = []string{
|
||||
"C-",
|
||||
"C#",
|
||||
"D-",
|
||||
"D#",
|
||||
"E-",
|
||||
"F-",
|
||||
"F#",
|
||||
"G-",
|
||||
"G#",
|
||||
"A-",
|
||||
"A#",
|
||||
"B-",
|
||||
}
|
||||
|
||||
func noteStr(val byte) string {
|
||||
if val == 1 {
|
||||
return "..." // hold
|
||||
}
|
||||
if val == 0 {
|
||||
return "---" // release
|
||||
}
|
||||
oNote := mod(int(val-baseNote), 12)
|
||||
octave := (int(val) - oNote - baseNote) / 12
|
||||
if octave < 0 {
|
||||
return fmt.Sprintf("%s%s", notes[oNote], string(byte('Z'+1+octave)))
|
||||
}
|
||||
if octave >= 10 {
|
||||
return fmt.Sprintf("%s%s", notes[oNote], string(byte('A'+octave-10)))
|
||||
}
|
||||
return fmt.Sprintf("%s%d", notes[oNote], octave)
|
||||
}
|
||||
|
||||
func noteAsValue(octave, note int) byte {
|
||||
return byte(baseNote + (octave * 12) + note)
|
||||
}
|
||||
|
||||
func (te *TrackEditor) layoutTracks(gtx C, t *Tracker) D {
|
||||
defer op.Offset(image.Point{}).Push(gtx.Ops).Pop()
|
||||
defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop()
|
||||
cursorSongRow := t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row
|
||||
for _, ev := range gtx.Events(&te.trackJumpPointerTag) {
|
||||
e, ok := ev.(pointer.Event)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if e.Type == pointer.Press {
|
||||
te.Focus()
|
||||
track := int(e.Position.X) / trackColWidth
|
||||
row := int((e.Position.Y-float32(gtx.Constraints.Max.Y-trackRowHeight)/2)/trackRowHeight + float32(cursorSongRow))
|
||||
cursor := tracker.ScorePoint{Track: track, ScoreRow: tracker.ScoreRow{Row: row}}.Clamp(t.Song().Score)
|
||||
t.SetCursor(cursor)
|
||||
t.SetSelectionCorner(cursor)
|
||||
cursorSongRow = cursor.Pattern*t.Song().Score.RowsPerPattern + cursor.Row
|
||||
}
|
||||
}
|
||||
rect := image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)
|
||||
area := clip.Rect(rect).Push(gtx.Ops)
|
||||
pointer.InputOp{Tag: &te.trackJumpPointerTag,
|
||||
Types: pointer.Press,
|
||||
}.Add(gtx.Ops)
|
||||
area.Pop()
|
||||
stack := op.Offset(image.Point{}).Push(gtx.Ops)
|
||||
curVoice := 0
|
||||
for _, trk := range t.Song().Score.Tracks {
|
||||
gtx := gtx
|
||||
instrName := "?"
|
||||
firstIndex, err := t.Song().Patch.InstrumentForVoice(curVoice)
|
||||
lastIndex, err2 := t.Song().Patch.InstrumentForVoice(curVoice + trk.NumVoices - 1)
|
||||
if err == nil && err2 == nil {
|
||||
switch diff := lastIndex - firstIndex; diff {
|
||||
case 0:
|
||||
instrName = t.Song().Patch[firstIndex].Name
|
||||
default:
|
||||
n1 := t.Song().Patch[firstIndex].Name
|
||||
n2 := t.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 {
|
||||
instrName = n1 + "/" + n2 + "..."
|
||||
} else {
|
||||
instrName = n1 + "/" + n2
|
||||
}
|
||||
}
|
||||
if len(instrName) > 7 {
|
||||
instrName = instrName[:7]
|
||||
}
|
||||
}
|
||||
gtx.Constraints.Max.X = trackColWidth
|
||||
LabelStyle{Alignment: layout.N, Text: instrName, FontSize: unit.Sp(12), Color: mediumEmphasisTextColor, Shaper: t.TextShaper}.Layout(gtx)
|
||||
op.Offset(image.Point{trackColWidth, 0}).Add(gtx.Ops)
|
||||
curVoice += trk.NumVoices
|
||||
}
|
||||
stack.Pop()
|
||||
op.Offset(image.Point{0, (gtx.Constraints.Max.Y - trackRowHeight) / 2}).Add(gtx.Ops)
|
||||
op.Offset(image.Point{0, int((-1 * trackRowHeight) * (cursorSongRow))}).Add(gtx.Ops)
|
||||
if te.Focused() || t.OrderEditor.Focused() {
|
||||
x1, y1 := t.Cursor().Track, t.Cursor().Pattern
|
||||
x2, y2 := t.SelectionCorner().Track, t.SelectionCorner().Pattern
|
||||
if x1 > x2 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
if y1 > y2 {
|
||||
y1, y2 = y2, y1
|
||||
}
|
||||
x2++
|
||||
y2++
|
||||
x1 *= trackColWidth
|
||||
y1 *= trackRowHeight * t.Song().Score.RowsPerPattern
|
||||
x2 *= trackColWidth
|
||||
y2 *= trackRowHeight * t.Song().Score.RowsPerPattern
|
||||
paint.FillShape(gtx.Ops, inactiveSelectionColor, clip.Rect{Min: image.Pt(x1, y1), Max: image.Pt(x2, y2)}.Op())
|
||||
}
|
||||
if te.Focused() {
|
||||
x1, y1 := t.Cursor().Track, t.Cursor().Pattern*t.Song().Score.RowsPerPattern+t.Cursor().Row
|
||||
x2, y2 := t.SelectionCorner().Track, t.SelectionCorner().Pattern*t.Song().Score.RowsPerPattern+t.SelectionCorner().Row
|
||||
if x1 > x2 {
|
||||
x1, x2 = x2, x1
|
||||
}
|
||||
if y1 > y2 {
|
||||
y1, y2 = y2, y1
|
||||
}
|
||||
x2++
|
||||
y2++
|
||||
x1 *= trackColWidth
|
||||
y1 *= trackRowHeight
|
||||
x2 *= trackColWidth
|
||||
y2 *= trackRowHeight
|
||||
paint.FillShape(gtx.Ops, selectionColor, clip.Rect{Min: image.Pt(x1, y1), Max: image.Pt(x2, y2)}.Op())
|
||||
cx := t.Cursor().Track * trackColWidth
|
||||
cy := (t.Cursor().Pattern*t.Song().Score.RowsPerPattern + t.Cursor().Row) * trackRowHeight
|
||||
cw := trackColWidth
|
||||
if t.Song().Score.Tracks[t.Cursor().Track].Effect {
|
||||
cw /= 2
|
||||
if t.LowNibble() {
|
||||
cx += cw
|
||||
}
|
||||
}
|
||||
paint.FillShape(gtx.Ops, cursorColor, clip.Rect{Min: image.Pt(cx, cy), Max: image.Pt(cx+cw, cy+trackRowHeight)}.Op())
|
||||
}
|
||||
delta := (gtx.Constraints.Max.Y/2 + trackRowHeight - 1) / trackRowHeight
|
||||
firstRow := cursorSongRow - delta
|
||||
lastRow := cursorSongRow + delta
|
||||
if firstRow < 0 {
|
||||
firstRow = 0
|
||||
}
|
||||
if l := t.Song().Score.LengthInRows(); lastRow >= l {
|
||||
lastRow = l - 1
|
||||
}
|
||||
op.Offset(image.Point{0, trackRowHeight * firstRow}).Add(gtx.Ops)
|
||||
for trkIndex, trk := range t.Song().Score.Tracks {
|
||||
stack := op.Offset(image.Point{}).Push(gtx.Ops)
|
||||
for row := firstRow; row <= lastRow; row++ {
|
||||
pat := row / t.Song().Score.RowsPerPattern
|
||||
patRow := row % t.Song().Score.RowsPerPattern
|
||||
s := trk.Order.Get(pat)
|
||||
if s < 0 {
|
||||
op.Offset(image.Point{0, trackRowHeight}).Add(gtx.Ops)
|
||||
continue
|
||||
}
|
||||
if s >= 0 && patRow == 0 {
|
||||
paint.ColorOp{Color: trackerPatMarker}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, patternIndexToString(s), op.CallOp{})
|
||||
}
|
||||
if s >= 0 && patRow == 1 && t.IsPatternUnique(trkIndex, s) {
|
||||
paint.ColorOp{Color: mediumEmphasisTextColor}.Add(gtx.Ops)
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, "*", op.CallOp{})
|
||||
}
|
||||
op.Offset(image.Point{patmarkWidth, 0}).Add(gtx.Ops)
|
||||
if te.Focused() && t.Cursor().Row == patRow && t.Cursor().Pattern == pat {
|
||||
paint.ColorOp{Color: trackerActiveTextColor}.Add(gtx.Ops)
|
||||
} else {
|
||||
paint.ColorOp{Color: trackerInactiveTextColor}.Add(gtx.Ops)
|
||||
}
|
||||
var c byte = 1
|
||||
if s >= 0 && s < len(trk.Patterns) {
|
||||
c = trk.Patterns[s].Get(patRow)
|
||||
}
|
||||
if trk.Effect {
|
||||
var text string
|
||||
switch c {
|
||||
case 0:
|
||||
text = "--"
|
||||
case 1:
|
||||
text = ".."
|
||||
default:
|
||||
text = fmt.Sprintf("%02x", c)
|
||||
}
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, strings.ToUpper(text), op.CallOp{})
|
||||
} else {
|
||||
widget.Label{}.Layout(gtx, t.TextShaper, trackerFont, trackerFontSize, noteStr(c), op.CallOp{})
|
||||
}
|
||||
op.Offset(image.Point{-patmarkWidth, trackRowHeight}).Add(gtx.Ops)
|
||||
}
|
||||
stack.Pop()
|
||||
op.Offset(image.Point{trackColWidth, 0}).Add(gtx.Ops)
|
||||
}
|
||||
return layout.Dimensions{Size: gtx.Constraints.Max}
|
||||
}
|
@ -1,24 +1,63 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gioui.org/app"
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/system"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/op/paint"
|
||||
"gioui.org/text"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"gioui.org/x/explorer"
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var canQuit = true // set to false in init() if plugin tag is enabled
|
||||
|
||||
type (
|
||||
Tracker struct {
|
||||
Theme *material.Theme
|
||||
OctaveNumberInput *NumberInput
|
||||
InstrumentVoices *NumberInput
|
||||
TopHorizontalSplit *Split
|
||||
BottomHorizontalSplit *Split
|
||||
VerticalSplit *Split
|
||||
KeyPlaying map[string]tracker.NoteID
|
||||
PopupAlert *PopupAlert
|
||||
|
||||
SaveChangesDialog *Dialog
|
||||
WaveTypeDialog *Dialog
|
||||
|
||||
ModalDialog layout.Widget
|
||||
InstrumentEditor *InstrumentEditor
|
||||
OrderEditor *OrderEditor
|
||||
TrackEditor *NoteEditor
|
||||
Explorer *explorer.Explorer
|
||||
Exploring bool
|
||||
SongPanel *SongPanel
|
||||
|
||||
filePathString tracker.String
|
||||
|
||||
quitWG sync.WaitGroup
|
||||
execChan chan func()
|
||||
|
||||
*tracker.Model
|
||||
}
|
||||
|
||||
C = layout.Context
|
||||
D = layout.Dimensions
|
||||
)
|
||||
|
||||
const (
|
||||
@ -27,140 +66,34 @@ const (
|
||||
ConfirmNew
|
||||
)
|
||||
|
||||
type Tracker struct {
|
||||
Theme *material.Theme
|
||||
MenuBar []widget.Clickable
|
||||
Menus []Menu
|
||||
OctaveNumberInput *NumberInput
|
||||
BPM *NumberInput
|
||||
RowsPerPattern *NumberInput
|
||||
RowsPerBeat *NumberInput
|
||||
Step *NumberInput
|
||||
InstrumentVoices *NumberInput
|
||||
SongLength *NumberInput
|
||||
PanicBtn *widget.Clickable
|
||||
RecordBtn *widget.Clickable
|
||||
AddUnitBtn *widget.Clickable
|
||||
TrackHexCheckBox *widget.Bool
|
||||
TopHorizontalSplit *Split
|
||||
BottomHorizontalSplit *Split
|
||||
VerticalSplit *Split
|
||||
KeyPlaying map[string]tracker.NoteID
|
||||
Alert Alert
|
||||
ConfirmSongDialog *Dialog
|
||||
WaveTypeDialog *Dialog
|
||||
ConfirmSongActionType int
|
||||
ModalDialog layout.Widget
|
||||
InstrumentEditor *InstrumentEditor
|
||||
OrderEditor *OrderEditor
|
||||
TrackEditor *TrackEditor
|
||||
Explorer *explorer.Explorer
|
||||
|
||||
TextShaper *text.Shaper
|
||||
|
||||
lastAvgVolume tracker.Volume
|
||||
lastPeakVolume tracker.Volume
|
||||
|
||||
wavFilePath string
|
||||
quitChannel chan struct{}
|
||||
quitWG sync.WaitGroup
|
||||
errorChannel chan error
|
||||
quitted bool
|
||||
unmarshalRecoveryChannel chan []byte
|
||||
marshalRecoveryChannel chan (chan []byte)
|
||||
synther sointu.Synther
|
||||
|
||||
*trackerModel
|
||||
}
|
||||
|
||||
type trackerModel = tracker.Model
|
||||
|
||||
func (t *Tracker) UnmarshalContent(bytes []byte) error {
|
||||
var units []sointu.Unit
|
||||
if errJSON := json.Unmarshal(bytes, &units); errJSON == nil {
|
||||
if len(units) == 0 {
|
||||
return nil
|
||||
}
|
||||
t.PasteUnits(units)
|
||||
// TODO: this is a bit hacky, but works for now. How to change the selection to the pasted units more elegantly?
|
||||
t.InstrumentEditor.unitDragList.SelectedItem = t.UnitIndex()
|
||||
t.InstrumentEditor.unitDragList.SelectedItem2 = t.UnitIndex() + len(units) - 1
|
||||
return nil
|
||||
}
|
||||
if errYaml := yaml.Unmarshal(bytes, &units); errYaml == nil {
|
||||
if len(units) == 0 {
|
||||
return nil
|
||||
}
|
||||
t.PasteUnits(units)
|
||||
t.InstrumentEditor.unitDragList.SelectedItem = t.UnitIndex()
|
||||
t.InstrumentEditor.unitDragList.SelectedItem2 = t.UnitIndex() + len(units) - 1
|
||||
return nil
|
||||
}
|
||||
var instr sointu.Instrument
|
||||
if errJSON := json.Unmarshal(bytes, &instr); errJSON == nil {
|
||||
if t.SetInstrument(instr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if errYaml := yaml.Unmarshal(bytes, &instr); errYaml == nil {
|
||||
if t.SetInstrument(instr) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
var song sointu.Song
|
||||
if errJSON := json.Unmarshal(bytes, &song); errJSON != nil {
|
||||
if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil {
|
||||
return fmt.Errorf("the song could not be parsed as .json (%v) or .yml (%v)", errJSON, errYaml)
|
||||
}
|
||||
}
|
||||
if song.BPM > 0 {
|
||||
t.SetSong(song)
|
||||
return nil
|
||||
}
|
||||
return errors.New("was able to unmarshal a song, but the bpm was 0")
|
||||
}
|
||||
|
||||
func NewTracker(model *tracker.Model, synther sointu.Synther) *Tracker {
|
||||
func NewTracker(model *tracker.Model) *Tracker {
|
||||
t := &Tracker{
|
||||
Theme: material.NewTheme(),
|
||||
BPM: new(NumberInput),
|
||||
OctaveNumberInput: &NumberInput{Value: 4},
|
||||
SongLength: new(NumberInput),
|
||||
RowsPerPattern: new(NumberInput),
|
||||
RowsPerBeat: new(NumberInput),
|
||||
Step: &NumberInput{Value: 1},
|
||||
InstrumentVoices: new(NumberInput),
|
||||
|
||||
PanicBtn: new(widget.Clickable),
|
||||
RecordBtn: new(widget.Clickable),
|
||||
TrackHexCheckBox: new(widget.Bool),
|
||||
Menus: make([]Menu, 2),
|
||||
MenuBar: make([]widget.Clickable, 2),
|
||||
quitChannel: make(chan struct{}, 1), // use non-blocking sends; no need to queue extra ticks if one is queued already
|
||||
OctaveNumberInput: NewNumberInput(model.Octave().Int()),
|
||||
InstrumentVoices: NewNumberInput(model.InstrumentVoices().Int()),
|
||||
|
||||
TopHorizontalSplit: &Split{Ratio: -.6},
|
||||
BottomHorizontalSplit: &Split{Ratio: -.6},
|
||||
VerticalSplit: &Split{Axis: layout.Vertical},
|
||||
|
||||
KeyPlaying: make(map[string]tracker.NoteID),
|
||||
ConfirmSongDialog: new(Dialog),
|
||||
WaveTypeDialog: new(Dialog),
|
||||
InstrumentEditor: NewInstrumentEditor(),
|
||||
OrderEditor: NewOrderEditor(),
|
||||
TrackEditor: NewTrackEditor(),
|
||||
SaveChangesDialog: NewDialog(model.SaveSong(), model.DiscardSong(), model.Cancel()),
|
||||
WaveTypeDialog: NewDialog(model.ExportInt16(), model.ExportFloat(), model.Cancel()),
|
||||
InstrumentEditor: NewInstrumentEditor(model),
|
||||
OrderEditor: NewOrderEditor(model),
|
||||
TrackEditor: NewNoteEditor(model),
|
||||
SongPanel: NewSongPanel(model),
|
||||
|
||||
errorChannel: make(chan error, 32),
|
||||
synther: synther,
|
||||
trackerModel: model,
|
||||
Model: model,
|
||||
|
||||
marshalRecoveryChannel: make(chan (chan []byte)),
|
||||
unmarshalRecoveryChannel: make(chan []byte),
|
||||
filePathString: model.FilePath().String(),
|
||||
execChan: make(chan func(), 1024),
|
||||
}
|
||||
t.TextShaper = text.NewShaper(text.WithCollection(fontCollection))
|
||||
t.Alert.shaper = t.TextShaper
|
||||
t.Theme.Shaper = text.NewShaper(text.WithCollection(fontCollection))
|
||||
t.PopupAlert = NewPopupAlert(model.Alerts(), t.Theme.Shaper)
|
||||
t.Theme.Palette.Fg = primaryColor
|
||||
t.Theme.Palette.ContrastFg = black
|
||||
t.TrackEditor.Focus()
|
||||
t.TrackEditor.scrollTable.Focus()
|
||||
t.quitWG.Add(1)
|
||||
return t
|
||||
}
|
||||
@ -171,19 +104,13 @@ func (t *Tracker) Main() {
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
app.Title("Sointu Tracker"),
|
||||
)
|
||||
t.InstrumentEditor.Focus()
|
||||
recoveryTicker := time.NewTicker(time.Second * 30)
|
||||
t.Explorer = explorer.NewExplorer(w)
|
||||
var ops op.Ops
|
||||
mainloop:
|
||||
for {
|
||||
if pos, playing := t.PlayPosition(), t.Playing(); t.NoteTracking() && playing {
|
||||
cursor := t.Cursor()
|
||||
cursor.ScoreRow = pos
|
||||
t.SetCursor(cursor)
|
||||
t.SetSelectionCorner(cursor)
|
||||
}
|
||||
if titleFooter != t.FilePath() {
|
||||
titleFooter = t.FilePath()
|
||||
if titleFooter != t.filePathString.Value() {
|
||||
titleFooter = t.filePathString.Value()
|
||||
if titleFooter != "" {
|
||||
w.Option(app.Title(fmt.Sprintf("Sointu Tracker - %v", titleFooter)))
|
||||
} else {
|
||||
@ -191,28 +118,16 @@ mainloop:
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-t.quitChannel:
|
||||
recoveryTicker.Stop()
|
||||
break mainloop
|
||||
case e := <-t.errorChannel:
|
||||
t.Alert.Update(e.Error(), Error, time.Second*5)
|
||||
w.Invalidate()
|
||||
case e := <-t.PlayerMessages:
|
||||
if err, ok := e.Inner.(tracker.PlayerCrashMessage); ok {
|
||||
t.Alert.Update(err.Error(), Error, time.Second*3)
|
||||
}
|
||||
if err, ok := e.Inner.(tracker.PlayerVolumeErrorMessage); ok {
|
||||
t.Alert.Update(err.Error(), Warning, time.Second*3)
|
||||
}
|
||||
t.lastAvgVolume = e.AverageVolume
|
||||
t.lastPeakVolume = e.PeakVolume
|
||||
t.InstrumentEditor.voiceLevels = e.VoiceLevels
|
||||
t.ProcessPlayerMessage(e)
|
||||
w.Invalidate()
|
||||
case e := <-w.Events():
|
||||
switch e := e.(type) {
|
||||
case system.DestroyEvent:
|
||||
if !t.Quit(false) {
|
||||
if canQuit {
|
||||
t.Quit().Do()
|
||||
}
|
||||
if !t.Quitted() {
|
||||
// TODO: uh oh, there's no way of canceling the destroyevent in gioui? so we create a new window just to show the dialog
|
||||
w = app.NewWindow(
|
||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||
@ -222,41 +137,146 @@ mainloop:
|
||||
}
|
||||
case system.FrameEvent:
|
||||
gtx := layout.NewContext(&ops, e)
|
||||
if t.SongPanel.PlayingBtn.Bool.Value() && t.SongPanel.NoteTracking.Bool.Value() {
|
||||
t.TrackEditor.scrollTable.RowTitleList.CenterOn(t.PlaySongRow())
|
||||
}
|
||||
t.Layout(gtx, w)
|
||||
e.Frame(gtx.Ops)
|
||||
}
|
||||
case <-recoveryTicker.C:
|
||||
t.SaveRecovery()
|
||||
case retChn := <-t.marshalRecoveryChannel:
|
||||
retChn <- t.MarshalRecovery()
|
||||
case bytes := <-t.unmarshalRecoveryChannel:
|
||||
t.UnmarshalRecovery(bytes)
|
||||
case f := <-t.execChan:
|
||||
f()
|
||||
}
|
||||
if t.Quitted() {
|
||||
break
|
||||
}
|
||||
}
|
||||
recoveryTicker.Stop()
|
||||
w.Perform(system.ActionClose)
|
||||
t.SaveRecovery()
|
||||
t.quitWG.Done()
|
||||
}
|
||||
|
||||
// thread safe, executed in the GUI thread
|
||||
func (t *Tracker) SafeMarshalRecovery() []byte {
|
||||
retChn := make(chan []byte)
|
||||
t.marshalRecoveryChannel <- retChn
|
||||
return <-retChn
|
||||
}
|
||||
|
||||
// thread safe, executed in the GUI thread
|
||||
func (t *Tracker) SafeUnmarshalRecovery(data []byte) {
|
||||
t.unmarshalRecoveryChannel <- data
|
||||
}
|
||||
|
||||
func (t *Tracker) sendQuit() {
|
||||
select {
|
||||
case t.quitChannel <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
func (t *Tracker) Exec() chan<- func() {
|
||||
return t.execChan
|
||||
}
|
||||
|
||||
func (t *Tracker) WaitQuitted() {
|
||||
t.quitWG.Wait()
|
||||
}
|
||||
|
||||
func (t *Tracker) Layout(gtx layout.Context, w *app.Window) {
|
||||
// this is the top level input handler for the whole app
|
||||
// it handles all the global key events and clipboard events
|
||||
// we need to tell gio that we handle tabs too; otherwise
|
||||
// it will steal them for focus switching
|
||||
key.InputOp{Tag: t, Keys: "Tab|Shift-Tab"}.Add(gtx.Ops)
|
||||
for _, ev := range gtx.Events(t) {
|
||||
switch e := ev.(type) {
|
||||
case key.Event:
|
||||
t.KeyEvent(e, gtx.Ops)
|
||||
case clipboard.Event:
|
||||
stringReader := strings.NewReader(e.Text)
|
||||
stringReadCloser := io.NopCloser(stringReader)
|
||||
t.ReadSong(stringReadCloser)
|
||||
}
|
||||
}
|
||||
|
||||
paint.FillShape(gtx.Ops, backgroundColor, clip.Rect(image.Rect(0, 0, gtx.Constraints.Max.X, gtx.Constraints.Max.Y)).Op())
|
||||
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
||||
t.layoutTop(gtx)
|
||||
} else {
|
||||
t.VerticalSplit.Layout(gtx,
|
||||
t.layoutTop,
|
||||
t.layoutBottom)
|
||||
}
|
||||
t.PopupAlert.Layout(gtx)
|
||||
t.showDialog(gtx)
|
||||
}
|
||||
|
||||
func (t *Tracker) showDialog(gtx C) {
|
||||
if t.Exploring {
|
||||
return
|
||||
}
|
||||
switch t.Dialog() {
|
||||
case tracker.NewSongChanges, tracker.OpenSongChanges, tracker.QuitChanges:
|
||||
dstyle := ConfirmDialog(t.Theme, t.SaveChangesDialog, "Save changes to song?", "Your changes will be lost if you don't save them.")
|
||||
dstyle.OkStyle.Text = "Save"
|
||||
dstyle.AltStyle.Text = "Don't save"
|
||||
dstyle.Layout(gtx)
|
||||
case tracker.Export:
|
||||
dstyle := ConfirmDialog(t.Theme, t.WaveTypeDialog, "", "Export .wav in int16 or float32 sample format?")
|
||||
dstyle.OkStyle.Text = "Int16"
|
||||
dstyle.AltStyle.Text = "Float32"
|
||||
dstyle.Layout(gtx)
|
||||
case tracker.OpenSongOpenExplorer:
|
||||
t.explorerChooseFile(t.ReadSong, ".yml", ".json")
|
||||
case tracker.NewSongSaveExplorer, tracker.OpenSongSaveExplorer, tracker.QuitSaveExplorer, tracker.SaveAsExplorer:
|
||||
filename := t.filePathString.Value()
|
||||
if filename == "" {
|
||||
filename = "song.yml"
|
||||
}
|
||||
t.explorerCreateFile(t.WriteSong, filename)
|
||||
case tracker.ExportFloatExplorer, tracker.ExportInt16Explorer:
|
||||
filename := "song.wav"
|
||||
if p := t.filePathString.Value(); p != "" {
|
||||
filename = p[:len(p)-len(filepath.Ext(p))] + ".wav"
|
||||
}
|
||||
t.explorerCreateFile(func(wc io.WriteCloser) {
|
||||
t.WriteWav(wc, t.Dialog() == tracker.ExportInt16Explorer, t.execChan)
|
||||
}, filename)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tracker) explorerChooseFile(success func(io.ReadCloser), extensions ...string) {
|
||||
t.Exploring = true
|
||||
go func() {
|
||||
file, err := t.Explorer.ChooseFile(extensions...)
|
||||
t.Exec() <- func() {
|
||||
t.Exploring = false
|
||||
if err == nil {
|
||||
success(file)
|
||||
} else {
|
||||
t.Cancel().Do()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *Tracker) explorerCreateFile(success func(io.WriteCloser), filename string) {
|
||||
t.Exploring = true
|
||||
go func() {
|
||||
file, err := t.Explorer.CreateFile(filename)
|
||||
t.Exec() <- func() {
|
||||
t.Exploring = false
|
||||
if err == nil {
|
||||
success(file)
|
||||
} else {
|
||||
t.Cancel().Do()
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions {
|
||||
return t.BottomHorizontalSplit.Layout(gtx,
|
||||
func(gtx C) D {
|
||||
return t.OrderEditor.Layout(gtx, t)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return t.TrackEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (t *Tracker) layoutTop(gtx layout.Context) layout.Dimensions {
|
||||
return t.TopHorizontalSplit.Layout(gtx,
|
||||
func(gtx C) D {
|
||||
return t.SongPanel.Layout(gtx, t)
|
||||
},
|
||||
func(gtx C) D {
|
||||
return t.InstrumentEditor.Layout(gtx, t)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
//go:build !plugin
|
||||
|
||||
package gioui
|
||||
|
||||
const CAN_QUIT = true
|
||||
|
||||
func (t *Tracker) Quit(forced bool) bool {
|
||||
if !forced && t.ChangedSinceSave() {
|
||||
t.ConfirmSongActionType = ConfirmQuit
|
||||
t.ConfirmSongDialog.Visible = true
|
||||
return false
|
||||
}
|
||||
t.sendQuit()
|
||||
return true
|
||||
}
|
@ -2,11 +2,6 @@
|
||||
|
||||
package gioui
|
||||
|
||||
const CAN_QUIT = false
|
||||
|
||||
func (t *Tracker) Quit(forced bool) bool {
|
||||
if forced {
|
||||
t.sendQuit()
|
||||
}
|
||||
return forced
|
||||
func init() {
|
||||
canQuit = false
|
||||
}
|
||||
|
321
tracker/gioui/unit_editor.go
Normal file
321
tracker/gioui/unit_editor.go
Normal file
@ -0,0 +1,321 @@
|
||||
package gioui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"math"
|
||||
|
||||
"gioui.org/io/clipboard"
|
||||
"gioui.org/io/key"
|
||||
"gioui.org/io/pointer"
|
||||
"gioui.org/layout"
|
||||
"gioui.org/op"
|
||||
"gioui.org/op/clip"
|
||||
"gioui.org/unit"
|
||||
"gioui.org/widget"
|
||||
"gioui.org/widget/material"
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
type UnitEditor struct {
|
||||
sliderList *DragList
|
||||
searchList *DragList
|
||||
Parameters []*ParameterWidget
|
||||
DeleteUnitBtn *ActionClickable
|
||||
CopyUnitBtn *TipClickable
|
||||
ClearUnitBtn *ActionClickable
|
||||
SelectTypeBtn *widget.Clickable
|
||||
tag bool
|
||||
caser cases.Caser
|
||||
}
|
||||
|
||||
func NewUnitEditor(m *tracker.Model) *UnitEditor {
|
||||
ret := &UnitEditor{
|
||||
DeleteUnitBtn: NewActionClickable(m.DeleteUnit()),
|
||||
ClearUnitBtn: NewActionClickable(m.ClearUnit()),
|
||||
CopyUnitBtn: new(TipClickable),
|
||||
SelectTypeBtn: new(widget.Clickable),
|
||||
sliderList: NewDragList(m.Params().List(), layout.Vertical),
|
||||
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
|
||||
}
|
||||
ret.caser = cases.Title(language.English)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) Layout(gtx C, t *Tracker) D {
|
||||
for _, e := range gtx.Events(&pe.tag) {
|
||||
switch e := e.(type) {
|
||||
case key.Event:
|
||||
if e.State == key.Press {
|
||||
pe.command(e, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
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()
|
||||
key.InputOp{Tag: &pe.tag, Keys: "←|Shift-←|→|Shift-→|⎋"}.Add(gtx.Ops)
|
||||
|
||||
editorFunc := pe.layoutSliders
|
||||
str := tracker.String{StringData: (*tracker.UnitSearch)(t.Model)}
|
||||
if str.Value() != t.Model.Units().SelectedType() || pe.sliderList.TrackerList.Count() == 0 {
|
||||
editorFunc = pe.layoutUnitTypeChooser
|
||||
}
|
||||
return Surface{Gray: 24, Focus: t.InstrumentEditor.wasFocused}.Layout(gtx, func(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Flexed(1, func(gtx C) D {
|
||||
return editorFunc(gtx, t)
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
return pe.layoutFooter(gtx, t)
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) layoutSliders(gtx C, t *Tracker) D {
|
||||
numItems := pe.sliderList.TrackerList.Count()
|
||||
|
||||
for len(pe.Parameters) < numItems {
|
||||
pe.Parameters = append(pe.Parameters, new(ParameterWidget))
|
||||
}
|
||||
|
||||
index := 0
|
||||
t.Model.Params().Iterate(func(param tracker.Parameter) {
|
||||
pe.Parameters[index].Parameter = param
|
||||
index++
|
||||
})
|
||||
|
||||
element := func(gtx C, index int) D {
|
||||
if index < 0 || index >= numItems {
|
||||
return D{}
|
||||
}
|
||||
paramStyle := t.ParamStyle(t.Theme, pe.Parameters[index])
|
||||
paramStyle.Focus = pe.sliderList.TrackerList.Selected() == index
|
||||
dims := paramStyle.Layout(gtx)
|
||||
return D{Size: image.Pt(gtx.Constraints.Max.X, dims.Size.Y)}
|
||||
}
|
||||
|
||||
fdl := FilledDragList(t.Theme, pe.sliderList, element, nil)
|
||||
dims := fdl.Layout(gtx)
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
fdl.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
|
||||
for pe.CopyUnitBtn.Clickable.Clicked() {
|
||||
if contents, ok := t.Units().List().CopyElements(); ok {
|
||||
clipboard.WriteOp{Text: string(contents)}.Add(gtx.Ops)
|
||||
t.Alerts().Add("Unit copied to clipboard", tracker.Info)
|
||||
}
|
||||
}
|
||||
copyUnitBtnStyle := TipIcon(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, "Copy unit (Ctrl+C)")
|
||||
deleteUnitBtnStyle := ActionIcon(t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)")
|
||||
text := t.Units().SelectedType()
|
||||
if text == "" {
|
||||
text = "Choose unit type"
|
||||
} else {
|
||||
text = pe.caser.String(text)
|
||||
}
|
||||
hintText := Label(text, white, t.Theme.Shaper)
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(deleteUnitBtnStyle.Layout),
|
||||
layout.Rigid(copyUnitBtnStyle.Layout),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
var dims D
|
||||
if t.Units().SelectedType() != "" {
|
||||
clearUnitBtnStyle := ActionIcon(t.Theme, pe.ClearUnitBtn, icons.ContentClear, "Clear unit")
|
||||
dims = clearUnitBtnStyle.Layout(gtx)
|
||||
}
|
||||
return D{Size: image.Pt(gtx.Dp(unit.Dp(48)), dims.Size.Y)}
|
||||
}),
|
||||
layout.Flexed(1, hintText),
|
||||
)
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) layoutUnitTypeChooser(gtx C, t *Tracker) D {
|
||||
var names [256]string
|
||||
index := 0
|
||||
t.Model.SearchResults().Iterate(func(item string) (ok bool) {
|
||||
names[index] = item
|
||||
index++
|
||||
return index <= 256
|
||||
})
|
||||
element := func(gtx C, i int) D {
|
||||
w := LabelStyle{Text: names[i], ShadeColor: black, Color: white, Font: labelDefaultFont, FontSize: unit.Sp(12), Shaper: t.Theme.Shaper}
|
||||
if i == pe.searchList.TrackerList.Selected() {
|
||||
for pe.SelectTypeBtn.Clicked() {
|
||||
t.Units().SetSelectedType(names[i])
|
||||
}
|
||||
return pe.SelectTypeBtn.Layout(gtx, w.Layout)
|
||||
}
|
||||
return w.Layout(gtx)
|
||||
}
|
||||
fdl := FilledDragList(t.Theme, pe.searchList, element, nil)
|
||||
dims := fdl.Layout(gtx)
|
||||
gtx.Constraints = layout.Exact(dims.Size)
|
||||
fdl.LayoutScrollBar(gtx)
|
||||
return dims
|
||||
}
|
||||
|
||||
func (pe *UnitEditor) command(e key.Event, t *Tracker) {
|
||||
params := (*tracker.Params)(t.Model)
|
||||
switch e.State {
|
||||
case key.Press:
|
||||
switch e.Name {
|
||||
case key.NameLeftArrow:
|
||||
sel := params.SelectedItem()
|
||||
if sel == nil {
|
||||
return
|
||||
}
|
||||
i := (&tracker.Int{IntData: sel})
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
i.Set(i.Value() - sel.LargeStep())
|
||||
} else {
|
||||
i.Set(i.Value() - 1)
|
||||
}
|
||||
case key.NameRightArrow:
|
||||
sel := params.SelectedItem()
|
||||
if sel == nil {
|
||||
return
|
||||
}
|
||||
i := (&tracker.Int{IntData: sel})
|
||||
if e.Modifiers.Contain(key.ModShift) {
|
||||
i.Set(i.Value() + sel.LargeStep())
|
||||
} else {
|
||||
i.Set(i.Value() + 1)
|
||||
}
|
||||
case key.NameEscape:
|
||||
t.InstrumentEditor.unitDragList.Focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ParameterWidget struct {
|
||||
floatWidget widget.Float
|
||||
boolWidget widget.Bool
|
||||
instrBtn widget.Clickable
|
||||
instrMenu Menu
|
||||
unitBtn widget.Clickable
|
||||
unitMenu Menu
|
||||
Parameter tracker.Parameter
|
||||
}
|
||||
|
||||
type ParameterStyle struct {
|
||||
tracker *Tracker
|
||||
w *ParameterWidget
|
||||
Theme *material.Theme
|
||||
Focus bool
|
||||
}
|
||||
|
||||
func (t *Tracker) ParamStyle(th *material.Theme, paramWidget *ParameterWidget) ParameterStyle {
|
||||
return ParameterStyle{
|
||||
tracker: t, // TODO: we need this to pull the instrument names for ID style parameters, find out another way
|
||||
Theme: th,
|
||||
w: paramWidget,
|
||||
}
|
||||
}
|
||||
|
||||
func (p ParameterStyle) Layout(gtx C) D {
|
||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
||||
layout.Rigid(func(gtx C) D {
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(110))
|
||||
return layout.E.Layout(gtx, Label(p.w.Parameter.Name(), white, p.tracker.Theme.Shaper))
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
switch p.w.Parameter.Type() {
|
||||
case tracker.IntegerParameter:
|
||||
for _, e := range gtx.Events(&p.w.floatWidget) {
|
||||
if ev, ok := e.(pointer.Event); ok && ev.Type == pointer.Scroll {
|
||||
delta := math.Min(math.Max(float64(ev.Scroll.Y), -1), 1)
|
||||
tracker.Int{IntData: p.w.Parameter}.Add(int(delta))
|
||||
}
|
||||
}
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
if !p.w.floatWidget.Dragging() {
|
||||
p.w.floatWidget.Value = float32(p.w.Parameter.Value())
|
||||
}
|
||||
ra := p.w.Parameter.Range()
|
||||
sliderStyle := material.Slider(p.Theme, &p.w.floatWidget, float32(ra.Min), float32(ra.Max))
|
||||
sliderStyle.Color = p.Theme.Fg
|
||||
r := image.Rectangle{Max: gtx.Constraints.Min}
|
||||
area := clip.Rect(r).Push(gtx.Ops)
|
||||
if p.Focus {
|
||||
pointer.InputOp{Tag: &p.w.floatWidget, Types: pointer.Scroll, ScrollBounds: image.Rectangle{Min: image.Pt(0, -1e6), Max: image.Pt(0, 1e6)}}.Add(gtx.Ops)
|
||||
}
|
||||
dims := sliderStyle.Layout(gtx)
|
||||
area.Pop()
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(int(p.w.floatWidget.Value + 0.5))
|
||||
return dims
|
||||
case tracker.BoolParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(60))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
ra := p.w.Parameter.Range()
|
||||
p.w.boolWidget.Value = p.w.Parameter.Value() > ra.Min
|
||||
boolStyle := material.Switch(p.Theme, &p.w.boolWidget, "Toggle boolean parameter")
|
||||
boolStyle.Color.Disabled = p.Theme.Fg
|
||||
boolStyle.Color.Enabled = white
|
||||
dims := layout.Center.Layout(gtx, boolStyle.Layout)
|
||||
if p.w.boolWidget.Value {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(ra.Max)
|
||||
} else {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(ra.Min)
|
||||
}
|
||||
return dims
|
||||
case tracker.IDParameter:
|
||||
gtx.Constraints.Min.X = gtx.Dp(unit.Dp(200))
|
||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(40))
|
||||
instrItems := make([]MenuItem, p.tracker.Instruments().Count())
|
||||
for i := range instrItems {
|
||||
i := i
|
||||
name, _, _ := p.tracker.Instruments().Item(i)
|
||||
instrItems[i].Text = name
|
||||
instrItems[i].IconBytes = icons.NavigationChevronRight
|
||||
instrItems[i].Doer = tracker.Allow(func() {
|
||||
if id, ok := p.tracker.Instruments().FirstID(i); ok {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(id)
|
||||
}
|
||||
})
|
||||
}
|
||||
var unitItems []MenuItem
|
||||
instrName := "<instr>"
|
||||
unitName := "<unit>"
|
||||
targetI, targetU, err := p.tracker.FindUnit(p.w.Parameter.Value())
|
||||
if err == nil {
|
||||
targetInstrument := p.tracker.Instrument(targetI)
|
||||
instrName = targetInstrument.Name
|
||||
units := targetInstrument.Units
|
||||
unitName = fmt.Sprintf("%v: %v", targetU, units[targetU].Type)
|
||||
unitItems = make([]MenuItem, len(units))
|
||||
for j, unit := range units {
|
||||
id := unit.ID
|
||||
unitItems[j].Text = fmt.Sprintf("%v: %v", j, unit.Type)
|
||||
unitItems[j].IconBytes = icons.NavigationChevronRight
|
||||
unitItems[j].Doer = tracker.Allow(func() {
|
||||
tracker.Int{IntData: p.w.Parameter}.Set(id)
|
||||
})
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||
layout.Rigid(p.tracker.layoutMenu(instrName, &p.w.instrBtn, &p.w.instrMenu, unit.Dp(200),
|
||||
instrItems...,
|
||||
)),
|
||||
layout.Rigid(p.tracker.layoutMenu(unitName, &p.w.unitBtn, &p.w.unitMenu, unit.Dp(200),
|
||||
unitItems...,
|
||||
)),
|
||||
)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
layout.Rigid(func(gtx C) D {
|
||||
if p.w.Parameter.Type() != tracker.IDParameter {
|
||||
return Label(p.w.Parameter.Hint(), white, p.tracker.Theme.Shaper)(gtx)
|
||||
}
|
||||
return D{}
|
||||
}),
|
||||
)
|
||||
}
|
191
tracker/int.go
Normal file
191
tracker/int.go
Normal file
@ -0,0 +1,191 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type (
|
||||
Int struct {
|
||||
IntData
|
||||
}
|
||||
|
||||
IntData interface {
|
||||
Value() int
|
||||
Range() intRange
|
||||
|
||||
setValue(int)
|
||||
change(kind string) func()
|
||||
}
|
||||
|
||||
intRange struct {
|
||||
Min, Max int
|
||||
}
|
||||
|
||||
InstrumentVoices Model
|
||||
TrackVoices Model
|
||||
SongLength Model
|
||||
BPM Model
|
||||
RowsPerPattern Model
|
||||
RowsPerBeat Model
|
||||
Step Model
|
||||
Octave Model
|
||||
)
|
||||
|
||||
func (v Int) Add(delta int) (ok bool) {
|
||||
r := v.Range()
|
||||
value := r.Clamp(v.Value() + delta)
|
||||
if value == v.Value() || value < r.Min || value > r.Max {
|
||||
return false
|
||||
}
|
||||
defer v.change("Add")()
|
||||
v.setValue(value)
|
||||
return true
|
||||
}
|
||||
|
||||
func (v Int) Set(value int) (ok bool) {
|
||||
r := v.Range()
|
||||
value = v.Range().Clamp(value)
|
||||
if value == v.Value() || value < r.Min || value > r.Max {
|
||||
return false
|
||||
}
|
||||
defer v.change("Set")()
|
||||
v.setValue(value)
|
||||
return true
|
||||
}
|
||||
|
||||
func (r intRange) Clamp(value int) int {
|
||||
return intMax(intMin(value, r.Max), r.Min)
|
||||
}
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) InstrumentVoices() *InstrumentVoices { return (*InstrumentVoices)(m) }
|
||||
func (m *Model) TrackVoices() *TrackVoices { return (*TrackVoices)(m) }
|
||||
func (m *Model) SongLength() *SongLength { return (*SongLength)(m) }
|
||||
func (m *Model) BPM() *BPM { return (*BPM)(m) }
|
||||
func (m *Model) RowsPerPattern() *RowsPerPattern { return (*RowsPerPattern)(m) }
|
||||
func (m *Model) RowsPerBeat() *RowsPerBeat { return (*RowsPerBeat)(m) }
|
||||
func (m *Model) Step() *Step { return (*Step)(m) }
|
||||
func (m *Model) Octave() *Octave { return (*Octave)(m) }
|
||||
|
||||
// BeatsPerMinuteInt
|
||||
|
||||
func (v *BPM) Int() Int { return Int{v} }
|
||||
func (v *BPM) Value() int { return v.d.Song.BPM }
|
||||
func (v *BPM) setValue(value int) { v.d.Song.BPM = value }
|
||||
func (v *BPM) Range() intRange { return intRange{1, 999} }
|
||||
func (v *BPM) change(kind string) func() {
|
||||
return (*Model)(v).change("BPMInt."+kind, SongChange, MinorChange)
|
||||
}
|
||||
|
||||
// RowsPerPatternInt
|
||||
|
||||
func (v *RowsPerPattern) Int() Int { return Int{v} }
|
||||
func (v *RowsPerPattern) Value() int { return v.d.Song.Score.RowsPerPattern }
|
||||
func (v *RowsPerPattern) setValue(value int) { v.d.Song.Score.RowsPerPattern = value }
|
||||
func (v *RowsPerPattern) Range() intRange { return intRange{1, 256} }
|
||||
func (v *RowsPerPattern) change(kind string) func() {
|
||||
return (*Model)(v).change("RowsPerPatternInt."+kind, SongChange, MinorChange)
|
||||
}
|
||||
|
||||
// SongLengthInt
|
||||
|
||||
func (v *SongLength) Int() Int { return Int{v} }
|
||||
func (v *SongLength) Value() int { return v.d.Song.Score.Length }
|
||||
func (v *SongLength) setValue(value int) { v.d.Song.Score.Length = value }
|
||||
func (v *SongLength) Range() intRange { return intRange{1, math.MaxInt32} }
|
||||
func (v *SongLength) change(kind string) func() {
|
||||
return (*Model)(v).change("SongLengthInt."+kind, SongChange, MinorChange)
|
||||
}
|
||||
|
||||
// StepInt
|
||||
|
||||
func (v *Step) Int() Int { return Int{v} }
|
||||
func (v *Step) Value() int { return v.d.Step }
|
||||
func (v *Step) setValue(value int) { v.d.Step = value }
|
||||
func (v *Step) Range() intRange { return intRange{0, 8} }
|
||||
func (v *Step) change(kind string) func() {
|
||||
return (*Model)(v).change("StepInt"+kind, NoChange, MinorChange)
|
||||
}
|
||||
|
||||
// OctaveInt
|
||||
|
||||
func (v *Octave) Int() Int { return Int{v} }
|
||||
func (v *Octave) Value() int { return v.d.Octave }
|
||||
func (v *Octave) setValue(value int) { v.d.Octave = value }
|
||||
func (v *Octave) Range() intRange { return intRange{0, 9} }
|
||||
func (v *Octave) change(kind string) func() { return func() {} }
|
||||
|
||||
// RowsPerBeatInt
|
||||
|
||||
func (v *RowsPerBeat) Int() Int { return Int{v} }
|
||||
func (v *RowsPerBeat) Value() int { return v.d.Song.RowsPerBeat }
|
||||
func (v *RowsPerBeat) setValue(value int) { v.d.Song.RowsPerBeat = value }
|
||||
func (v *RowsPerBeat) Range() intRange { return intRange{1, 32} }
|
||||
func (v *RowsPerBeat) change(kind string) func() {
|
||||
return (*Model)(v).change("RowsPerBeatInt."+kind, SongChange, MinorChange)
|
||||
}
|
||||
|
||||
// InstrumentVoicesInt
|
||||
|
||||
func (v *InstrumentVoices) Int() Int {
|
||||
return Int{v}
|
||||
}
|
||||
|
||||
func (v *InstrumentVoices) Value() int {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return 1
|
||||
}
|
||||
return intMax(v.d.Song.Patch[v.d.InstrIndex].NumVoices, 1)
|
||||
}
|
||||
|
||||
func (v *InstrumentVoices) setValue(value int) {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
v.d.Song.Patch[v.d.InstrIndex].NumVoices = value
|
||||
}
|
||||
|
||||
func (v *InstrumentVoices) Range() intRange {
|
||||
return intRange{1, vm.MAX_VOICES - v.d.Song.Patch.NumVoices() + v.Value()}
|
||||
}
|
||||
|
||||
func (v *InstrumentVoices) change(kind string) func() {
|
||||
return (*Model)(v).change("InstrumentVoicesInt."+kind, PatchChange, MinorChange)
|
||||
}
|
||||
|
||||
// TrackVoicesInt
|
||||
|
||||
func (v *TrackVoices) Int() Int {
|
||||
return Int{v}
|
||||
}
|
||||
|
||||
func (v *TrackVoices) Value() int {
|
||||
t := v.d.Cursor.Track
|
||||
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
||||
return 1
|
||||
}
|
||||
return intMax(v.d.Song.Score.Tracks[t].NumVoices, 1)
|
||||
}
|
||||
|
||||
func (v *TrackVoices) setValue(value int) {
|
||||
t := v.d.Cursor.Track
|
||||
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
||||
return
|
||||
}
|
||||
v.d.Song.Score.Tracks[t].NumVoices = value
|
||||
}
|
||||
|
||||
func (v *TrackVoices) Range() intRange {
|
||||
t := v.d.Cursor.Track
|
||||
if t < 0 || t >= len(v.d.Song.Score.Tracks) {
|
||||
return intRange{1, 1}
|
||||
}
|
||||
return intRange{1, vm.MAX_VOICES - v.d.Song.Score.NumVoices() + v.d.Song.Score.Tracks[t].NumVoices}
|
||||
}
|
||||
|
||||
func (v *TrackVoices) change(kind string) func() {
|
||||
return (*Model)(v).change("TrackVoicesInt."+kind, ScoreChange, MinorChange)
|
||||
}
|
765
tracker/list.go
Normal file
765
tracker/list.go
Normal file
@ -0,0 +1,765 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
type (
|
||||
List struct {
|
||||
ListData
|
||||
}
|
||||
|
||||
ListData interface {
|
||||
Selected() int
|
||||
Selected2() int
|
||||
SetSelected(int)
|
||||
SetSelected2(int)
|
||||
Count() int
|
||||
}
|
||||
|
||||
MutableListData interface {
|
||||
change(kind string, severity ChangeSeverity) func()
|
||||
cancel()
|
||||
swap(i, j int) (ok bool)
|
||||
delete(i int) (ok bool)
|
||||
marshal(from, to int) ([]byte, error)
|
||||
unmarshal([]byte) (from, to int, err error)
|
||||
}
|
||||
|
||||
UnitListItem struct {
|
||||
Type string
|
||||
StackNeed, StackBefore, StackAfter int
|
||||
}
|
||||
|
||||
UnitYieldFunc func(item UnitListItem) (ok bool)
|
||||
UnitSearchYieldFunc func(item string) (ok bool)
|
||||
|
||||
Instruments Model // Instruments is a list of instruments, implementing ListData & MutableListData interfaces
|
||||
Units Model // Units is a list of all the units in the selected instrument, implementing ListData & MutableListData interfaces
|
||||
Tracks Model // Tracks is a list of all the tracks, implementing ListData & MutableListData interfaces
|
||||
OrderRows Model // OrderRows is a list of all the order rows, implementing ListData & MutableListData interfaces
|
||||
NoteRows Model // NoteRows is a list of all the note rows, implementing ListData & MutableListData interfaces
|
||||
SearchResults Model // SearchResults is a unmutable list of all the search results, implementing ListData interface
|
||||
Presets Model // Presets is a unmutable list of all the presets, implementing ListData interface
|
||||
)
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) Instruments() *Instruments { return (*Instruments)(m) }
|
||||
func (m *Model) Units() *Units { return (*Units)(m) }
|
||||
func (m *Model) Tracks() *Tracks { return (*Tracks)(m) }
|
||||
func (m *Model) OrderRows() *OrderRows { return (*OrderRows)(m) }
|
||||
func (m *Model) NoteRows() *NoteRows { return (*NoteRows)(m) }
|
||||
func (m *Model) SearchResults() *SearchResults { return (*SearchResults)(m) }
|
||||
|
||||
// MoveElements moves the selected elements in a list by delta. If delta is
|
||||
// negative, the elements move up, otherwise down. The list must implement the
|
||||
// MutableListData interface.
|
||||
func (v List) MoveElements(delta int) (ok bool) {
|
||||
if delta == 0 {
|
||||
return false
|
||||
}
|
||||
s, ok := v.ListData.(MutableListData)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
defer s.change("MoveElements", MajorChange)()
|
||||
a, b := v.listRange()
|
||||
if a+delta < 0 {
|
||||
delta = -a
|
||||
}
|
||||
if b+delta >= v.Count() {
|
||||
delta = v.Count() - 1 - b
|
||||
}
|
||||
if delta < 0 {
|
||||
for i := a; i <= b; i++ {
|
||||
if !s.swap(i, i+delta) {
|
||||
s.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i := b; i >= a; i-- {
|
||||
if !s.swap(i, i+delta) {
|
||||
s.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
v.SetSelected(v.Selected() + delta)
|
||||
v.SetSelected2(v.Selected2() + delta)
|
||||
return true
|
||||
}
|
||||
|
||||
// DeleteElements deletes the selected elements in a list. The list must
|
||||
// implement the MutableListData interface.
|
||||
func (v List) DeleteElements(backwards bool) (ok bool) {
|
||||
d, ok := v.ListData.(MutableListData)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
defer d.change("DeleteElements", MajorChange)()
|
||||
a, b := v.listRange()
|
||||
for i := b; i >= a; i-- {
|
||||
if !d.delete(i) {
|
||||
d.cancel()
|
||||
return false
|
||||
}
|
||||
}
|
||||
if backwards && a > 0 {
|
||||
a--
|
||||
}
|
||||
v.SetSelected(a)
|
||||
v.SetSelected2(a)
|
||||
return true
|
||||
}
|
||||
|
||||
// CopyElements copies the selected elements in a list. The list must implement
|
||||
// the MutableListData interface. Returns the copied data, marshaled into byte
|
||||
// slice, and true if successful.
|
||||
func (v List) CopyElements() ([]byte, bool) {
|
||||
a, b := v.listRange()
|
||||
m, ok := v.ListData.(MutableListData)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
ret, err := m.marshal(a, b)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
return ret, true
|
||||
}
|
||||
|
||||
// PasteElements pastes the data into the list. The data is unmarshaled from the
|
||||
// byte slice. The list must implement the MutableListData interface. Returns
|
||||
// true if successful.
|
||||
func (v List) PasteElements(data []byte) (ok bool) {
|
||||
m, ok := v.ListData.(MutableListData)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
defer m.change("PasteElements", MajorChange)()
|
||||
from, to, err := m.unmarshal(data)
|
||||
if err != nil {
|
||||
m.cancel()
|
||||
return false
|
||||
}
|
||||
v.SetSelected(from)
|
||||
v.SetSelected2(to)
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *List) listRange() (lower, higher int) {
|
||||
lower = intMin(v.Selected(), v.Selected2())
|
||||
higher = intMax(v.Selected(), v.Selected2())
|
||||
return
|
||||
}
|
||||
|
||||
// Instruments methods
|
||||
|
||||
func (v *Instruments) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (v *Instruments) Item(i int) (name string, maxLevel float32, ok bool) {
|
||||
if i < 0 || i >= len(v.d.Song.Patch) {
|
||||
return "", 0, false
|
||||
}
|
||||
name = v.d.Song.Patch[i].Name
|
||||
start := v.d.Song.Patch.FirstVoiceForInstrument(i)
|
||||
end := start + v.d.Song.Patch[i].NumVoices
|
||||
if end >= vm.MAX_VOICES {
|
||||
end = vm.MAX_VOICES
|
||||
}
|
||||
if start < end {
|
||||
for _, level := range v.voiceLevels[start:end] {
|
||||
if maxLevel < level {
|
||||
maxLevel = level
|
||||
}
|
||||
}
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
func (v *Instruments) FirstID(i int) (id int, ok bool) {
|
||||
if i < 0 || i >= len(v.d.Song.Patch) {
|
||||
return 0, false
|
||||
}
|
||||
if len(v.d.Song.Patch[i].Units) == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return v.d.Song.Patch[i].Units[0].ID, true
|
||||
}
|
||||
|
||||
func (v *Instruments) Selected() int {
|
||||
return intMax(intMin(v.d.InstrIndex, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Instruments) Selected2() int {
|
||||
return intMax(intMin(v.d.InstrIndex2, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Instruments) SetSelected(value int) {
|
||||
v.d.InstrIndex = intMax(intMin(value, v.Count()-1), 0)
|
||||
v.d.UnitIndex = 0
|
||||
v.d.UnitIndex2 = 0
|
||||
}
|
||||
|
||||
func (v *Instruments) SetSelected2(value int) {
|
||||
v.d.InstrIndex2 = intMax(intMin(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Instruments) swap(i, j int) (ok bool) {
|
||||
if i < 0 || j < 0 || i >= len(v.d.Song.Patch) || j >= len(v.d.Song.Patch) || i == j {
|
||||
return false
|
||||
}
|
||||
instr := v.d.Song.Patch
|
||||
instr[i], instr[j] = instr[j], instr[i]
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Instruments) delete(i int) (ok bool) {
|
||||
if i < 0 || i >= len(v.d.Song.Patch) {
|
||||
return false
|
||||
}
|
||||
v.d.Song.Patch = append(v.d.Song.Patch[:i], v.d.Song.Patch[i+1:]...)
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Instruments) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("InstrumentListView."+n, PatchChange, severity)
|
||||
}
|
||||
|
||||
func (v *Instruments) cancel() {
|
||||
v.changeCancel = true
|
||||
}
|
||||
|
||||
func (v *Instruments) Count() int {
|
||||
return len(v.d.Song.Patch)
|
||||
}
|
||||
|
||||
func (v *Instruments) marshal(from, to int) ([]byte, error) {
|
||||
if from < 0 || to >= len(v.d.Song.Patch) || from > to {
|
||||
return nil, fmt.Errorf("InstrumentListView.marshal: index out of range: %d, %d", from, to)
|
||||
}
|
||||
ret, err := yaml.Marshal(struct{ Patch sointu.Patch }{v.d.Song.Patch[from : to+1]})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("InstrumentListView.marshal: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (v *Instruments) unmarshal(data []byte) (from, to int, err error) {
|
||||
var newInstr struct{ Patch sointu.Patch }
|
||||
if err := yaml.Unmarshal(data, &newInstr); err != nil {
|
||||
return 0, 0, fmt.Errorf("InstrumentListView.unmarshal: %v", err)
|
||||
}
|
||||
if len(newInstr.Patch) == 0 {
|
||||
return 0, 0, errors.New("InstrumentListView.unmarshal: no instruments")
|
||||
}
|
||||
if v.d.Song.Patch.NumVoices()+newInstr.Patch.NumVoices() > vm.MAX_VOICES {
|
||||
return 0, 0, fmt.Errorf("InstrumentListView.unmarshal: too many voices: %d", v.d.Song.Patch.NumVoices()+newInstr.Patch.NumVoices())
|
||||
}
|
||||
patch := append(v.d.Song.Patch, make([]sointu.Instrument, len(newInstr.Patch))...)
|
||||
sel := v.Selected()
|
||||
copy(patch[sel+len(newInstr.Patch):], patch[sel:])
|
||||
copy(patch[sel:sel+len(newInstr.Patch)], newInstr.Patch)
|
||||
v.d.Song.Patch = patch
|
||||
from = sel
|
||||
to = sel + len(newInstr.Patch) - 1
|
||||
return
|
||||
}
|
||||
|
||||
// Units methods
|
||||
|
||||
func (v *Units) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (m *Units) SelectedType() string {
|
||||
if m.d.InstrIndex < 0 ||
|
||||
m.d.InstrIndex >= len(m.d.Song.Patch) ||
|
||||
m.d.UnitIndex < 0 ||
|
||||
m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
||||
return ""
|
||||
}
|
||||
return m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Type
|
||||
}
|
||||
|
||||
func (m *Units) SetSelectedType(t string) {
|
||||
if m.d.InstrIndex < 0 ||
|
||||
m.d.InstrIndex >= len(m.d.Song.Patch) ||
|
||||
m.d.UnitIndex < 0 ||
|
||||
m.d.UnitIndex >= len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
||||
return
|
||||
}
|
||||
unit, ok := defaultUnits[t]
|
||||
if !ok { // if the type is invalid, we just set it to empty unit
|
||||
unit = sointu.Unit{Parameters: make(map[string]int)}
|
||||
} else {
|
||||
unit = unit.Copy()
|
||||
}
|
||||
oldUnit := m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex]
|
||||
if oldUnit.Type == unit.Type {
|
||||
return
|
||||
}
|
||||
defer m.change("SetSelectedType", MajorChange)()
|
||||
m.d.UnitSearchString = unit.Type
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex] = unit
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].ID = oldUnit.ID // keep the ID of the replaced unit
|
||||
}
|
||||
|
||||
func (v *Units) Iterate(yield UnitYieldFunc) {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
stackBefore := 0
|
||||
for _, unit := range v.d.Song.Patch[v.d.InstrIndex].Units {
|
||||
stackAfter := stackBefore + unit.StackChange()
|
||||
if !yield(UnitListItem{unit.Type, unit.StackNeed(), stackBefore, stackAfter}) {
|
||||
break
|
||||
}
|
||||
stackBefore = stackAfter
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Units) Selected() int {
|
||||
return intMax(intMin(v.d.UnitIndex, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Units) Selected2() int {
|
||||
return intMax(intMin(v.d.UnitIndex2, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Units) SetSelected(value int) {
|
||||
m := (*Model)(v)
|
||||
m.d.UnitIndex = intMax(intMin(value, v.Count()-1), 0)
|
||||
m.d.ParamIndex = 0
|
||||
if m.d.UnitIndex >= 0 && m.d.UnitIndex < len(m.d.Song.Patch[m.d.InstrIndex].Units) {
|
||||
m.d.UnitSearchString = m.d.Song.Patch[m.d.InstrIndex].Units[m.d.UnitIndex].Type
|
||||
}
|
||||
}
|
||||
|
||||
func (v *Units) SetSelected2(value int) {
|
||||
(*Model)(v).d.UnitIndex2 = intMax(intMin(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Units) Count() int {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return 0
|
||||
}
|
||||
return len(m.d.Song.Patch[(*Model)(v).d.InstrIndex].Units)
|
||||
}
|
||||
|
||||
func (v *Units) swap(i, j int) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return false
|
||||
}
|
||||
units := m.d.Song.Patch[m.d.InstrIndex].Units
|
||||
if i < 0 || j < 0 || i >= len(units) || j >= len(units) || i == j {
|
||||
return false
|
||||
}
|
||||
units[i], units[j] = units[j], units[i]
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Units) delete(i int) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return false
|
||||
}
|
||||
units := m.d.Song.Patch[m.d.InstrIndex].Units
|
||||
if i < 0 || i >= len(units) {
|
||||
return false
|
||||
}
|
||||
units = append(units[:i], units[i+1:]...)
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units = units
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Units) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("UnitListView."+n, PatchChange, severity)
|
||||
}
|
||||
|
||||
func (v *Units) cancel() {
|
||||
(*Model)(v).changeCancel = true
|
||||
}
|
||||
|
||||
func (v *Units) marshal(from, to int) ([]byte, error) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return nil, errors.New("UnitListView.marshal: no instruments")
|
||||
}
|
||||
if from < 0 || to >= len(m.d.Song.Patch[m.d.InstrIndex].Units) || from > to {
|
||||
return nil, fmt.Errorf("UnitListView.marshal: index out of range: %d, %d", from, to)
|
||||
}
|
||||
ret, err := yaml.Marshal(struct{ Units []sointu.Unit }{m.d.Song.Patch[m.d.InstrIndex].Units[from : to+1]})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("UnitListView.marshal: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (v *Units) unmarshal(data []byte) (from, to int, err error) {
|
||||
m := (*Model)(v)
|
||||
if m.d.InstrIndex < 0 || m.d.InstrIndex >= len(m.d.Song.Patch) {
|
||||
return 0, 0, errors.New("UnitListView.unmarshal: no instruments")
|
||||
}
|
||||
var pastedUnits struct{ Units []sointu.Unit }
|
||||
if err := yaml.Unmarshal(data, &pastedUnits); err != nil {
|
||||
return 0, 0, fmt.Errorf("UnitListView.unmarshal: %v", err)
|
||||
}
|
||||
if len(pastedUnits.Units) == 0 {
|
||||
return 0, 0, errors.New("UnitListView.unmarshal: no units")
|
||||
}
|
||||
m.assignUnitIDs(pastedUnits.Units)
|
||||
sel := v.Selected()
|
||||
units := append(m.d.Song.Patch[m.d.InstrIndex].Units, make([]sointu.Unit, len(pastedUnits.Units))...)
|
||||
copy(units[sel+len(pastedUnits.Units):], units[sel:])
|
||||
copy(units[sel:], pastedUnits.Units)
|
||||
m.d.Song.Patch[m.d.InstrIndex].Units = units
|
||||
from = sel
|
||||
to = sel + len(pastedUnits.Units) - 1
|
||||
return
|
||||
}
|
||||
|
||||
// Tracks methods
|
||||
|
||||
func (v *Tracks) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (v *Tracks) Selected() int {
|
||||
return intMax(intMin(v.d.Cursor.Track, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Tracks) Selected2() int {
|
||||
return intMax(intMin(v.d.Cursor2.Track, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Tracks) SetSelected(value int) {
|
||||
v.d.Cursor.Track = intMax(intMin(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Tracks) SetSelected2(value int) {
|
||||
v.d.Cursor2.Track = intMax(intMin(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *Tracks) swap(i, j int) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if i < 0 || j < 0 || i >= len(m.d.Song.Score.Tracks) || j >= len(m.d.Song.Score.Tracks) || i == j {
|
||||
return false
|
||||
}
|
||||
tracks := m.d.Song.Score.Tracks
|
||||
tracks[i], tracks[j] = tracks[j], tracks[i]
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Tracks) delete(i int) (ok bool) {
|
||||
m := (*Model)(v)
|
||||
if i < 0 || i >= len(m.d.Song.Score.Tracks) {
|
||||
return false
|
||||
}
|
||||
m.d.Song.Score.Tracks = append(m.d.Song.Score.Tracks[:i], m.d.Song.Score.Tracks[i+1:]...)
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *Tracks) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("TrackList."+n, ScoreChange, severity)
|
||||
}
|
||||
|
||||
func (v *Tracks) cancel() {
|
||||
v.changeCancel = true
|
||||
}
|
||||
|
||||
func (v *Tracks) Count() int {
|
||||
return len((*Model)(v).d.Song.Score.Tracks)
|
||||
}
|
||||
|
||||
func (v *Tracks) marshal(from, to int) ([]byte, error) {
|
||||
m := (*Model)(v)
|
||||
if from < 0 || to >= len(m.d.Song.Score.Tracks) || from > to {
|
||||
return nil, fmt.Errorf("TrackListView.marshal: index out of range: %d, %d", from, to)
|
||||
}
|
||||
ret, err := yaml.Marshal(struct{ Score sointu.Score }{sointu.Score{Tracks: m.d.Song.Score.Tracks[from : to+1]}})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TrackListView.marshal: %v", err)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (v *Tracks) unmarshal(data []byte) (from, to int, err error) {
|
||||
m := (*Model)(v)
|
||||
var newTracks struct{ Score sointu.Score }
|
||||
if err := yaml.Unmarshal(data, &newTracks); err != nil {
|
||||
return 0, 0, fmt.Errorf("TrackListView.unmarshal: %v", err)
|
||||
}
|
||||
if len(newTracks.Score.Tracks) == 0 {
|
||||
return 0, 0, errors.New("TrackListView.unmarshal: no tracks")
|
||||
}
|
||||
if v.d.Song.Score.NumVoices()+newTracks.Score.NumVoices() > vm.MAX_VOICES {
|
||||
return 0, 0, fmt.Errorf("InstrumentListView.unmarshal: too many voices: %d", v.d.Song.Patch.NumVoices()+newTracks.Score.NumVoices())
|
||||
}
|
||||
from = m.d.Cursor.Track
|
||||
to = m.d.Cursor.Track + len(newTracks.Score.Tracks) - 1
|
||||
tracks := m.d.Song.Score.Tracks
|
||||
newTracks.Score.Tracks = append(newTracks.Score.Tracks, tracks[m.d.Cursor.Track:]...)
|
||||
tracks = append(tracks[:m.d.Cursor.Track], newTracks.Score.Tracks...)
|
||||
m.d.Song.Score.Tracks = tracks
|
||||
return
|
||||
}
|
||||
|
||||
// OrderRows methods
|
||||
|
||||
func (v *OrderRows) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (v *OrderRows) Selected() int {
|
||||
p := v.d.Cursor.OrderRow
|
||||
p = intMax(intMin(p, v.Count()-1), 0)
|
||||
return p
|
||||
}
|
||||
|
||||
func (v *OrderRows) Selected2() int {
|
||||
p := v.d.Cursor2.OrderRow
|
||||
p = intMax(intMin(p, v.Count()-1), 0)
|
||||
return p
|
||||
}
|
||||
|
||||
func (v *OrderRows) SetSelected(value int) {
|
||||
y := intMax(intMin(value, v.Count()-1), 0)
|
||||
if y != v.d.Cursor.OrderRow {
|
||||
v.noteTracking = false
|
||||
}
|
||||
v.d.Cursor.OrderRow = y
|
||||
}
|
||||
|
||||
func (v *OrderRows) SetSelected2(value int) {
|
||||
v.d.Cursor2.OrderRow = intMax(intMin(value, v.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (v *OrderRows) swap(x, y int) (ok bool) {
|
||||
for i := range v.d.Song.Score.Tracks {
|
||||
track := &v.d.Song.Score.Tracks[i]
|
||||
a, b := track.Order.Get(x), track.Order.Get(y)
|
||||
track.Order.Set(x, b)
|
||||
track.Order.Set(y, a)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *OrderRows) delete(i int) (ok bool) {
|
||||
for _, track := range v.d.Song.Score.Tracks {
|
||||
if i < len(track.Order) {
|
||||
track.Order = append(track.Order[:i], track.Order[i+1:]...)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *OrderRows) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("OrderRowList."+n, ScoreChange, severity)
|
||||
}
|
||||
|
||||
func (v *OrderRows) cancel() {
|
||||
v.changeCancel = true
|
||||
}
|
||||
|
||||
func (v *OrderRows) Count() int {
|
||||
return v.d.Song.Score.Length
|
||||
}
|
||||
|
||||
type marshalOrderRows struct {
|
||||
Columns [][]int `yaml:",flow"`
|
||||
}
|
||||
|
||||
func (v *OrderRows) marshal(from, to int) ([]byte, error) {
|
||||
var table marshalOrderRows
|
||||
for i := range v.d.Song.Score.Tracks {
|
||||
table.Columns = append(table.Columns, make([]int, to-from+1))
|
||||
for j := 0; j < to-from+1; j++ {
|
||||
table.Columns[i][j] = v.d.Song.Score.Tracks[i].Order.Get(from + j)
|
||||
}
|
||||
}
|
||||
return yaml.Marshal(table)
|
||||
}
|
||||
|
||||
func (v *OrderRows) unmarshal(data []byte) (from, to int, err error) {
|
||||
var table marshalOrderRows
|
||||
err = yaml.Unmarshal(data, &table)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(table.Columns) == 0 {
|
||||
err = errors.New("OrderRowList.unmarshal: no rows")
|
||||
return
|
||||
}
|
||||
from = v.d.Cursor.OrderRow
|
||||
to = v.d.Cursor.OrderRow + len(table.Columns[0]) - 1
|
||||
for i := range v.d.Song.Score.Tracks {
|
||||
if i >= len(table.Columns) {
|
||||
break
|
||||
}
|
||||
order := &v.d.Song.Score.Tracks[i].Order
|
||||
for j := 0; j < from-len(*order); j++ {
|
||||
*order = append(*order, -1)
|
||||
}
|
||||
if len(*order) > from {
|
||||
table.Columns[i] = append(table.Columns[i], (*order)[from:]...)
|
||||
*order = (*order)[:from]
|
||||
}
|
||||
*order = append(*order, table.Columns[i]...)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// NoteRows methods
|
||||
|
||||
func (v *NoteRows) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (v *NoteRows) Selected() int {
|
||||
return v.d.Song.Score.SongRow(v.d.Song.Score.Clamp(v.d.Cursor.SongPos))
|
||||
}
|
||||
|
||||
func (v *NoteRows) Selected2() int {
|
||||
return v.d.Song.Score.SongRow(v.d.Song.Score.Clamp(v.d.Cursor2.SongPos))
|
||||
}
|
||||
|
||||
func (v *NoteRows) SetSelected(value int) {
|
||||
if value != v.d.Song.Score.SongRow(v.d.Cursor.SongPos) {
|
||||
v.noteTracking = false
|
||||
}
|
||||
v.d.Cursor.SongPos = v.d.Song.Score.Clamp(v.d.Song.Score.SongPos(value))
|
||||
}
|
||||
|
||||
func (v *NoteRows) SetSelected2(value int) {
|
||||
v.d.Cursor2.SongPos = v.d.Song.Score.Clamp(v.d.Song.Score.SongPos(value))
|
||||
|
||||
}
|
||||
|
||||
func (v *NoteRows) swap(i, j int) (ok bool) {
|
||||
ipos := v.d.Song.Score.SongPos(i)
|
||||
jpos := v.d.Song.Score.SongPos(j)
|
||||
for _, track := range v.d.Song.Score.Tracks {
|
||||
n1 := track.Note(ipos)
|
||||
n2 := track.Note(jpos)
|
||||
track.SetNote(ipos, n2)
|
||||
track.SetNote(jpos, n1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *NoteRows) delete(i int) (ok bool) {
|
||||
if i < 0 || i >= v.Count() {
|
||||
return
|
||||
}
|
||||
pos := v.d.Song.Score.SongPos(i)
|
||||
for _, track := range v.d.Song.Score.Tracks {
|
||||
track.SetNote(pos, 1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (v *NoteRows) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(v).change("NoteRowList."+n, ScoreChange, severity)
|
||||
}
|
||||
|
||||
func (v *NoteRows) cancel() {
|
||||
(*Model)(v).changeCancel = true
|
||||
}
|
||||
|
||||
func (v *NoteRows) Count() int {
|
||||
return (*Model)(v).d.Song.Score.Length * v.d.Song.Score.RowsPerPattern
|
||||
}
|
||||
|
||||
type marshalNoteRows struct {
|
||||
NoteRows [][]byte `yaml:",flow"`
|
||||
}
|
||||
|
||||
func (v *NoteRows) marshal(from, to int) ([]byte, error) {
|
||||
var table marshalNoteRows
|
||||
for i, track := range v.d.Song.Score.Tracks {
|
||||
table.NoteRows = append(table.NoteRows, make([]byte, to-from+1))
|
||||
for j := 0; j < to-from+1; j++ {
|
||||
row := from + j
|
||||
pos := v.d.Song.Score.SongPos(row)
|
||||
table.NoteRows[i][j] = track.Note(pos)
|
||||
}
|
||||
}
|
||||
return yaml.Marshal(table)
|
||||
}
|
||||
|
||||
func (v *NoteRows) unmarshal(data []byte) (from, to int, err error) {
|
||||
var table marshalNoteRows
|
||||
if err := yaml.Unmarshal(data, &table); err != nil {
|
||||
return 0, 0, fmt.Errorf("NoteRowList.unmarshal: %v", err)
|
||||
}
|
||||
if len(table.NoteRows) < 1 {
|
||||
return 0, 0, errors.New("NoteRowList.unmarshal: no tracks")
|
||||
}
|
||||
from = v.d.Song.Score.SongRow(v.d.Cursor.SongPos)
|
||||
for i, arr := range table.NoteRows {
|
||||
if i >= len(v.d.Song.Score.Tracks) {
|
||||
continue
|
||||
}
|
||||
to = from + len(arr) - 1
|
||||
for j, note := range arr {
|
||||
y := j + from
|
||||
pos := v.d.Song.Score.SongPos(y)
|
||||
v.d.Song.Score.Tracks[i].SetNote(pos, note)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SearchResults
|
||||
|
||||
func (v *SearchResults) List() List {
|
||||
return List{v}
|
||||
}
|
||||
|
||||
func (l *SearchResults) Iterate(yield UnitSearchYieldFunc) {
|
||||
for _, name := range sointu.UnitNames {
|
||||
if !strings.HasPrefix(name, l.d.UnitSearchString) {
|
||||
continue
|
||||
}
|
||||
if !yield(name) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *SearchResults) Selected() int {
|
||||
return intMax(intMin(l.d.UnitSearchIndex, l.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (l *SearchResults) Selected2() int {
|
||||
return intMax(intMin(l.d.UnitSearchIndex, l.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (l *SearchResults) SetSelected(value int) {
|
||||
l.d.UnitSearchIndex = intMax(intMin(value, l.Count()-1), 0)
|
||||
}
|
||||
|
||||
func (l *SearchResults) SetSelected2(value int) {
|
||||
}
|
||||
|
||||
func (l *SearchResults) Count() (count int) {
|
||||
for _, n := range sointu.UnitNames {
|
||||
if strings.HasPrefix(n, l.d.UnitSearchString) {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
1585
tracker/model.go
1585
tracker/model.go
File diff suppressed because it is too large
Load Diff
252
tracker/model_test.go
Normal file
252
tracker/model_test.go
Normal file
@ -0,0 +1,252 @@
|
||||
package tracker_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/vsariola/sointu/tracker"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type NullContext struct{}
|
||||
|
||||
func (NullContext) NextEvent() (event tracker.MIDINoteEvent, ok bool) {
|
||||
return tracker.MIDINoteEvent{}, false
|
||||
}
|
||||
|
||||
func (NullContext) BPM() (bpm float64, ok bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
type modelFuzzState struct {
|
||||
model *tracker.Model
|
||||
clipboard []byte
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) Iterate(yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
// Ints
|
||||
s.IterateInt("InstrumentVoices", s.model.InstrumentVoices().Int(), yield, seed)
|
||||
s.IterateInt("TrackVoices", s.model.TrackVoices().Int(), yield, seed)
|
||||
s.IterateInt("SongLength", s.model.SongLength().Int(), yield, seed)
|
||||
s.IterateInt("BPM", s.model.BPM().Int(), yield, seed)
|
||||
s.IterateInt("RowsPerPattern", s.model.RowsPerPattern().Int(), yield, seed)
|
||||
s.IterateInt("RowsPerBeat", s.model.RowsPerBeat().Int(), yield, seed)
|
||||
s.IterateInt("Step", s.model.Step().Int(), yield, seed)
|
||||
s.IterateInt("Octave", s.model.Octave().Int(), yield, seed)
|
||||
// Lists
|
||||
s.IterateList("Instruments", s.model.Instruments().List(), yield, seed)
|
||||
s.IterateList("Units", s.model.Units().List(), yield, seed)
|
||||
s.IterateList("Tracks", s.model.Tracks().List(), yield, seed)
|
||||
s.IterateList("OrderRows", s.model.OrderRows().List(), yield, seed)
|
||||
s.IterateList("NoteRows", s.model.NoteRows().List(), yield, seed)
|
||||
s.IterateList("UnitSearchResults", s.model.SearchResults().List(), yield, seed)
|
||||
s.IterateBool("Panic", s.model.Panic().Bool(), yield, seed)
|
||||
s.IterateBool("Recording", s.model.IsRecording().Bool(), yield, seed)
|
||||
s.IterateBool("Playing", s.model.Playing().Bool(), yield, seed)
|
||||
s.IterateBool("InstrEnlarged", s.model.InstrEnlarged().Bool(), yield, seed)
|
||||
s.IterateBool("Effect", s.model.Effect().Bool(), yield, seed)
|
||||
s.IterateBool("CommentExpanded", s.model.CommentExpanded().Bool(), yield, seed)
|
||||
s.IterateBool("NoteTracking", s.model.NoteTracking().Bool(), yield, seed)
|
||||
// Strings
|
||||
s.IterateString("FilePath", s.model.FilePath().String(), yield, seed)
|
||||
s.IterateString("InstrumentName", s.model.InstrumentName().String(), yield, seed)
|
||||
s.IterateString("InstrumentComment", s.model.InstrumentComment().String(), yield, seed)
|
||||
s.IterateString("UnitSearchText", s.model.UnitSearch().String(), yield, seed)
|
||||
// Actions
|
||||
s.IterateAction("AddTrack", s.model.AddTrack(), yield, seed)
|
||||
s.IterateAction("DeleteTrack", s.model.DeleteTrack(), yield, seed)
|
||||
s.IterateAction("AddInstrument", s.model.AddInstrument(), yield, seed)
|
||||
s.IterateAction("DeleteInstrument", s.model.DeleteInstrument(), yield, seed)
|
||||
s.IterateAction("AddUnitAfter", s.model.AddUnit(false), yield, seed)
|
||||
s.IterateAction("AddUnitBefore", s.model.AddUnit(true), yield, seed)
|
||||
s.IterateAction("DeleteUnit", s.model.DeleteUnit(), yield, seed)
|
||||
s.IterateAction("ClearUnit", s.model.ClearUnit(), yield, seed)
|
||||
s.IterateAction("Undo", s.model.Undo(), yield, seed)
|
||||
s.IterateAction("Redo", s.model.Redo(), yield, seed)
|
||||
s.IterateAction("RemoveUnused", s.model.RemoveUnused(), yield, seed)
|
||||
s.IterateAction("AddSemitone", s.model.AddSemitone(), yield, seed)
|
||||
s.IterateAction("SubtractSemitone", s.model.SubtractSemitone(), yield, seed)
|
||||
s.IterateAction("AddOctave", s.model.AddOctave(), yield, seed)
|
||||
s.IterateAction("SubtractOctave", s.model.SubtractOctave(), yield, seed)
|
||||
s.IterateAction("EditNoteOff", s.model.EditNoteOff(), yield, seed)
|
||||
s.IterateAction("Rewind", s.model.Rewind(), yield, seed)
|
||||
s.IterateAction("AddOrderRowAfter", s.model.AddOrderRow(false), yield, seed)
|
||||
s.IterateAction("AddOrderRowBefore", s.model.AddOrderRow(true), yield, seed)
|
||||
s.IterateAction("DeleteOrderRowForward", s.model.DeleteOrderRow(false), yield, seed)
|
||||
s.IterateAction("DeleteOrderRowBackward", s.model.DeleteOrderRow(true), yield, seed)
|
||||
// Tables
|
||||
s.IterateTable("Order", s.model.Order().Table(), yield, seed)
|
||||
s.IterateTable("Notes", s.model.Notes().Table(), yield, seed)
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateInt(name string, i tracker.Int, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
r := i.Range()
|
||||
yield(name+".Set", func(p string, t *testing.T) {
|
||||
i.Set(seed%(r.Max-r.Min+10) - 5 + r.Min)
|
||||
})
|
||||
yield(name+".Value", func(p string, t *testing.T) {
|
||||
if v := i.Value(); v < r.Min || v > r.Max {
|
||||
r := i.Range()
|
||||
t.Errorf("Path: %s %s value out of range [%d,%d]: %d", p, name, r.Min, r.Max, v)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateAction(name string, a tracker.Action, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
yield(name+".Do", func(p string, t *testing.T) {
|
||||
a.Do()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateBool(name string, b tracker.Bool, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
yield(name+".Set", func(p string, t *testing.T) {
|
||||
b.Set(seed%2 == 0)
|
||||
})
|
||||
yield(name+".Toggle", func(p string, t *testing.T) {
|
||||
b.Toggle()
|
||||
})
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateString(name string, str tracker.String, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
yield(name+".Set", func(p string, t *testing.T) {
|
||||
str.Set(fmt.Sprintf("%d", seed))
|
||||
})
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateList(name string, l tracker.List, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
yield(name+".SetSelected", func(p string, t *testing.T) {
|
||||
l.SetSelected(seed%50 - 16)
|
||||
})
|
||||
yield(name+".Count", func(p string, t *testing.T) {
|
||||
if c := l.Count(); c > 0 {
|
||||
if l.Selected() < 0 || l.Selected() >= c {
|
||||
t.Errorf("Path: %s %s selected out of range: %d", p, name, l.Selected())
|
||||
}
|
||||
} else {
|
||||
if l.Selected() != 0 {
|
||||
t.Errorf("Path: %s %s selected out of range: %d", p, name, l.Selected())
|
||||
}
|
||||
}
|
||||
})
|
||||
yield(name+".SetSelected2", func(p string, t *testing.T) {
|
||||
l.SetSelected2(seed%50 - 16)
|
||||
})
|
||||
yield(name+".Count2", func(p string, t *testing.T) {
|
||||
if c := l.Count(); c > 0 {
|
||||
if l.Selected2() < 0 || l.Selected2() >= c {
|
||||
t.Errorf("Path: %s List selected2 out of range: %d", p, l.Selected2())
|
||||
}
|
||||
} else {
|
||||
if l.Selected2() != 0 {
|
||||
t.Errorf("Path: %s List selected2 out of range: %d", p, l.Selected2())
|
||||
}
|
||||
}
|
||||
})
|
||||
yield(name+".MoveElements", func(p string, t *testing.T) {
|
||||
l.MoveElements(seed%2*2 - 1)
|
||||
})
|
||||
yield(name+".DeleteElementsForward", func(p string, t *testing.T) {
|
||||
l.DeleteElements(false)
|
||||
})
|
||||
yield(name+".DeleteElementsBackward", func(p string, t *testing.T) {
|
||||
l.DeleteElements(true)
|
||||
})
|
||||
yield(name+".CopyElements", func(p string, t *testing.T) {
|
||||
s.clipboard, _ = l.CopyElements()
|
||||
})
|
||||
yield(name+".PasteElements", func(p string, t *testing.T) {
|
||||
l.PasteElements(s.clipboard)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *modelFuzzState) IterateTable(name string, table tracker.Table, yield func(string, func(p string, t *testing.T)) bool, seed int) {
|
||||
yield(name+".SetCursor", func(p string, t *testing.T) {
|
||||
table.SetCursor(tracker.Point{seed % 16, seed * 1337 % 16})
|
||||
})
|
||||
yield(name+".SetCursor2", func(p string, t *testing.T) {
|
||||
table.SetCursor2(tracker.Point{seed % 16, seed * 1337 % 16})
|
||||
})
|
||||
yield(name+".Cursor", func(p string, t *testing.T) {
|
||||
if c := table.Cursor(); c.X < 0 || (c.X >= table.Width() && table.Width() > 0) || c.Y < 0 || (c.Y >= table.Height() && table.Height() > 0) {
|
||||
t.Errorf("Path: %s Table cursor out of range: %v", p, c)
|
||||
}
|
||||
})
|
||||
yield(name+".Cursor2", func(p string, t *testing.T) {
|
||||
if c := table.Cursor2(); c.X < 0 || (c.X >= table.Width() && table.Width() > 0) || c.Y < 0 || (c.Y >= table.Height() && table.Height() > 0) {
|
||||
t.Errorf("Path: %s Table cursor2 out of range: %v", p, c)
|
||||
}
|
||||
})
|
||||
yield(name+".SetCursorX", func(p string, t *testing.T) {
|
||||
table.SetCursorX(seed % 16)
|
||||
})
|
||||
yield(name+".SetCursorY", func(p string, t *testing.T) {
|
||||
table.SetCursorY(seed % 16)
|
||||
})
|
||||
yield(name+".MoveCursor", func(p string, t *testing.T) {
|
||||
table.MoveCursor(seed%2*2-1, seed%2*2-1)
|
||||
})
|
||||
yield(name+".Copy", func(p string, t *testing.T) {
|
||||
s.clipboard, _ = table.Copy()
|
||||
})
|
||||
yield(name+".Paste", func(p string, t *testing.T) {
|
||||
table.Paste(s.clipboard)
|
||||
})
|
||||
yield(name+".Clear", func(p string, t *testing.T) {
|
||||
table.Clear()
|
||||
})
|
||||
yield(name+".Fill", func(p string, t *testing.T) {
|
||||
table.Fill(seed % 16)
|
||||
})
|
||||
yield(name+".Add", func(p string, t *testing.T) {
|
||||
table.Add(seed % 16)
|
||||
})
|
||||
}
|
||||
|
||||
func FuzzModel(f *testing.F) {
|
||||
seed := make([]byte, 1)
|
||||
for i := range seed {
|
||||
seed[i] = byte(i)
|
||||
}
|
||||
f.Add(seed)
|
||||
f.Fuzz(func(t *testing.T, slice []byte) {
|
||||
reader := bytes.NewReader(slice)
|
||||
synther := vm.GoSynther{}
|
||||
model, player := tracker.NewModelPlayer(synther, "")
|
||||
buf := make([][2]float32, 2048)
|
||||
closeChan := make(chan struct{})
|
||||
go func() {
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-closeChan:
|
||||
break loop
|
||||
default:
|
||||
ctx := NullContext{}
|
||||
player.Process(buf, ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
state := modelFuzzState{model: model}
|
||||
count := 0
|
||||
state.Iterate(func(n string, f func(p string, t *testing.T)) bool {
|
||||
count++
|
||||
return true
|
||||
}, 0)
|
||||
totalPath := ""
|
||||
for m, err := binary.ReadVarint(reader); err == nil; m, err = binary.ReadVarint(reader) {
|
||||
seed := int(m)
|
||||
index := seed % count
|
||||
state.Iterate(func(n string, f func(p string, t *testing.T)) bool {
|
||||
if index == 0 {
|
||||
totalPath += n + ". "
|
||||
f(totalPath, t)
|
||||
}
|
||||
index--
|
||||
return index > 0
|
||||
}, seed)
|
||||
}
|
||||
closeChan <- struct{}{}
|
||||
})
|
||||
}
|
345
tracker/params.go
Normal file
345
tracker/params.go
Normal file
@ -0,0 +1,345 @@
|
||||
package tracker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
"github.com/vsariola/sointu"
|
||||
"github.com/vsariola/sointu/vm"
|
||||
)
|
||||
|
||||
type (
|
||||
Parameter interface {
|
||||
IntData
|
||||
Type() ParameterType
|
||||
Name() string
|
||||
Hint() string
|
||||
LargeStep() int
|
||||
Reset()
|
||||
}
|
||||
|
||||
parameter struct {
|
||||
m *Model
|
||||
unit *sointu.Unit
|
||||
}
|
||||
|
||||
NamedParameter struct {
|
||||
parameter
|
||||
up *sointu.UnitParameter
|
||||
}
|
||||
|
||||
DelayTimeParameter struct {
|
||||
parameter
|
||||
index int
|
||||
}
|
||||
|
||||
DelayLinesParameter struct{ parameter }
|
||||
GmDlsEntryParameter struct{ parameter }
|
||||
ReverbParameter struct{ parameter }
|
||||
|
||||
Params Model
|
||||
|
||||
ParamYieldFunc func(Parameter)
|
||||
|
||||
ParameterType int
|
||||
)
|
||||
|
||||
const (
|
||||
IntegerParameter ParameterType = iota
|
||||
BoolParameter
|
||||
IDParameter
|
||||
)
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) Params() *Params { return (*Params)(m) }
|
||||
|
||||
// parameter methods
|
||||
|
||||
func (p parameter) change(kind string) func() {
|
||||
return p.m.change("Parameter."+kind, PatchChange, MinorChange)
|
||||
}
|
||||
|
||||
// ParamList
|
||||
|
||||
func (pl *Params) List() List { return List{pl} }
|
||||
func (pl *Params) Selected() int { return pl.d.ParamIndex }
|
||||
func (pl *Params) Selected2() int { return pl.Selected() }
|
||||
func (pl *Params) SetSelected(value int) { pl.d.ParamIndex = intMax(intMin(value, pl.Count()-1), 0) }
|
||||
func (pl *Params) SetSelected2(value int) {}
|
||||
func (pl *Params) cancel() { (*Model)(pl).changeCancel = true }
|
||||
|
||||
func (pl *Params) change(n string, severity ChangeSeverity) func() {
|
||||
return (*Model)(pl).change("ParamList."+n, PatchChange, severity)
|
||||
}
|
||||
|
||||
func (pl *Params) Count() int {
|
||||
count := 0
|
||||
pl.Iterate(func(p Parameter) {
|
||||
count++
|
||||
})
|
||||
return count
|
||||
}
|
||||
|
||||
func (pl *Params) SelectedItem() (ret Parameter) {
|
||||
index := pl.Selected()
|
||||
pl.Iterate(func(param Parameter) {
|
||||
if index == 0 {
|
||||
ret = param
|
||||
}
|
||||
index--
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (pl *Params) Iterate(yield ParamYieldFunc) {
|
||||
if pl.d.InstrIndex < 0 || pl.d.InstrIndex >= len(pl.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
if pl.d.UnitIndex < 0 || pl.d.UnitIndex >= len(pl.d.Song.Patch[pl.d.InstrIndex].Units) {
|
||||
return
|
||||
}
|
||||
unit := &pl.d.Song.Patch[pl.d.InstrIndex].Units[pl.d.UnitIndex]
|
||||
unitType, ok := sointu.UnitTypes[unit.Type]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for i := range unitType {
|
||||
if !unitType[i].CanSet {
|
||||
continue
|
||||
}
|
||||
if unit.Type == "oscillator" && unit.Parameters["type"] != sointu.Sample && i >= 11 {
|
||||
break // don't show the sample related params unless necessary
|
||||
}
|
||||
yield(NamedParameter{
|
||||
parameter: parameter{m: (*Model)(pl), unit: unit},
|
||||
up: &unitType[i],
|
||||
})
|
||||
}
|
||||
if unit.Type == "oscillator" && unit.Parameters["type"] == sointu.Sample {
|
||||
yield(GmDlsEntryParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
|
||||
}
|
||||
switch {
|
||||
case unit.Type == "delay":
|
||||
if unit.Parameters["stereo"] == 1 && len(unit.VarArgs)%2 == 1 {
|
||||
unit.VarArgs = append(unit.VarArgs, 1)
|
||||
}
|
||||
yield(ReverbParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
|
||||
yield(DelayLinesParameter{parameter: parameter{m: (*Model)(pl), unit: unit}})
|
||||
for i := range unit.VarArgs {
|
||||
yield(DelayTimeParameter{parameter: parameter{m: (*Model)(pl), unit: unit}, index: i})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NamedParameter
|
||||
|
||||
func (p NamedParameter) Name() string { return p.up.Name }
|
||||
func (p NamedParameter) Range() intRange { return intRange{Min: p.up.MinValue, Max: p.up.MaxValue} }
|
||||
func (p NamedParameter) Value() int { return p.unit.Parameters[p.up.Name] }
|
||||
func (p NamedParameter) setValue(value int) { p.unit.Parameters[p.up.Name] = value }
|
||||
|
||||
func (p NamedParameter) Reset() {
|
||||
v, ok := defaultUnits[p.unit.Type].Parameters[p.up.Name]
|
||||
if !ok || p.unit.Parameters[p.up.Name] == v {
|
||||
return
|
||||
}
|
||||
defer p.parameter.change("Reset")()
|
||||
p.unit.Parameters[p.up.Name] = v
|
||||
}
|
||||
|
||||
func (p NamedParameter) Type() ParameterType {
|
||||
if p.unit.Type == "send" && p.up.Name == "target" {
|
||||
return IDParameter
|
||||
}
|
||||
if p.up.MinValue == 0 && p.up.MaxValue == 1 {
|
||||
return BoolParameter
|
||||
}
|
||||
return IntegerParameter
|
||||
}
|
||||
|
||||
func (p NamedParameter) Hint() string {
|
||||
val := p.Value()
|
||||
text := p.m.d.Song.Patch.ParamHintString(p.m.d.InstrIndex, p.m.d.UnitIndex, p.up.Name)
|
||||
if text != "" {
|
||||
text = fmt.Sprintf("%v / %v", val, text)
|
||||
} else {
|
||||
text = strconv.Itoa(val)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func (p NamedParameter) LargeStep() int {
|
||||
if p.up.Name == "transpose" {
|
||||
return 12
|
||||
}
|
||||
return 16
|
||||
}
|
||||
|
||||
// GmDlsEntryParameter
|
||||
|
||||
func (p GmDlsEntryParameter) Name() string { return "sample" }
|
||||
func (p GmDlsEntryParameter) Type() ParameterType { return IntegerParameter }
|
||||
func (p GmDlsEntryParameter) Range() intRange { return intRange{Min: 0, Max: len(GmDlsEntries)} }
|
||||
func (p GmDlsEntryParameter) LargeStep() int { return 16 }
|
||||
func (p GmDlsEntryParameter) Reset() { return }
|
||||
|
||||
func (p GmDlsEntryParameter) Value() int {
|
||||
key := vm.SampleOffset{Start: uint32(p.unit.Parameters["samplestart"]), LoopStart: uint16(p.unit.Parameters["loopstart"]), LoopLength: uint16(p.unit.Parameters["looplength"])}
|
||||
if v, ok := gmDlsEntryMap[key]; ok {
|
||||
return v + 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (p GmDlsEntryParameter) setValue(v int) {
|
||||
if v < 1 || v > len(GmDlsEntries) {
|
||||
return
|
||||
}
|
||||
e := GmDlsEntries[v-1]
|
||||
p.unit.Parameters["samplestart"] = e.Start
|
||||
p.unit.Parameters["loopstart"] = e.LoopStart
|
||||
p.unit.Parameters["looplength"] = e.LoopLength
|
||||
p.unit.Parameters["transpose"] = 64 + e.SuggestedTranspose
|
||||
}
|
||||
|
||||
func (p GmDlsEntryParameter) Hint() string {
|
||||
if v := p.Value(); v > 0 {
|
||||
return fmt.Sprintf("%v / %v", v, GmDlsEntries[v-1].Name)
|
||||
}
|
||||
return "0 / custom"
|
||||
}
|
||||
|
||||
// DelayTimeParameter
|
||||
|
||||
func (p DelayTimeParameter) Name() string { return "delaytime" }
|
||||
func (p DelayTimeParameter) Type() ParameterType { return IntegerParameter }
|
||||
func (p DelayTimeParameter) LargeStep() int { return 16 }
|
||||
func (p DelayTimeParameter) Reset() { return }
|
||||
|
||||
func (p DelayTimeParameter) Value() int {
|
||||
if p.index < 0 || p.index >= len(p.unit.VarArgs) {
|
||||
return 1
|
||||
}
|
||||
return p.unit.VarArgs[p.index]
|
||||
}
|
||||
|
||||
func (p DelayTimeParameter) setValue(v int) {
|
||||
p.unit.VarArgs[p.index] = v
|
||||
}
|
||||
|
||||
func (p DelayTimeParameter) Range() intRange {
|
||||
if p.unit.Parameters["notetracking"] == 2 {
|
||||
return intRange{Min: 1, Max: 576}
|
||||
}
|
||||
return intRange{Min: 1, Max: 65535}
|
||||
}
|
||||
|
||||
func (p DelayTimeParameter) Hint() string {
|
||||
val := p.Value()
|
||||
var text string
|
||||
switch p.unit.Parameters["notetracking"] {
|
||||
default:
|
||||
case 0:
|
||||
text = fmt.Sprintf("%v / %.3f rows", val, float32(val)/float32(p.m.d.Song.SamplesPerRow()))
|
||||
case 1:
|
||||
relPitch := float64(val) / 10787
|
||||
semitones := -math.Log2(relPitch) * 12
|
||||
text = fmt.Sprintf("%v / %.3f st", val, semitones)
|
||||
case 2:
|
||||
k := 0
|
||||
v := val
|
||||
for v&1 == 0 { // divide val by 2 until it is odd
|
||||
v >>= 1
|
||||
k++
|
||||
}
|
||||
switch v {
|
||||
case 1:
|
||||
if k <= 7 {
|
||||
text = fmt.Sprintf(" (1/%d triplet)", 1<<(7-k))
|
||||
}
|
||||
case 3:
|
||||
if k <= 6 {
|
||||
text = fmt.Sprintf(" (1/%d)", 1<<(6-k))
|
||||
}
|
||||
break
|
||||
case 9:
|
||||
if k <= 5 {
|
||||
text = fmt.Sprintf(" (1/%d dotted)", 1<<(5-k))
|
||||
}
|
||||
}
|
||||
text = fmt.Sprintf("%v / %.3f beats%s", val, float32(val)/48.0, text)
|
||||
}
|
||||
if p.unit.Parameters["stereo"] == 1 {
|
||||
if p.index < len(p.unit.VarArgs)/2 {
|
||||
text += " R"
|
||||
} else {
|
||||
text += " L"
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
// DelayLinesParameter
|
||||
|
||||
func (p DelayLinesParameter) Name() string { return "delaylines" }
|
||||
func (p DelayLinesParameter) Type() ParameterType { return IntegerParameter }
|
||||
func (p DelayLinesParameter) Range() intRange { return intRange{Min: 1, Max: 32} }
|
||||
func (p DelayLinesParameter) LargeStep() int { return 4 }
|
||||
func (p DelayLinesParameter) Reset() { return }
|
||||
func (p DelayLinesParameter) Hint() string { return strconv.Itoa(p.Value()) }
|
||||
|
||||
func (p DelayLinesParameter) Value() int {
|
||||
val := len(p.unit.VarArgs)
|
||||
if p.unit.Parameters["stereo"] == 1 {
|
||||
val /= 2
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func (p DelayLinesParameter) setValue(v int) {
|
||||
targetLines := v
|
||||
if p.unit.Parameters["stereo"] == 1 {
|
||||
targetLines *= 2
|
||||
}
|
||||
for len(p.unit.VarArgs) < targetLines {
|
||||
p.unit.VarArgs = append(p.unit.VarArgs, 1)
|
||||
}
|
||||
p.unit.VarArgs = p.unit.VarArgs[:targetLines]
|
||||
}
|
||||
|
||||
// ReverbParameter
|
||||
|
||||
func (p ReverbParameter) Name() string { return "reverb" }
|
||||
func (p ReverbParameter) Type() ParameterType { return IntegerParameter }
|
||||
func (p ReverbParameter) Range() intRange { return intRange{Min: 0, Max: len(reverbs)} }
|
||||
func (p ReverbParameter) LargeStep() int { return 1 }
|
||||
func (p ReverbParameter) Reset() { return }
|
||||
|
||||
func (p ReverbParameter) Value() int {
|
||||
i := slices.IndexFunc(reverbs, func(d delayPreset) bool {
|
||||
return d.stereo == p.unit.Parameters["stereo"] && p.unit.Parameters["notetracking"] == 0 && slices.Equal(d.varArgs, p.unit.VarArgs)
|
||||
})
|
||||
return i + 1
|
||||
}
|
||||
|
||||
func (p ReverbParameter) setValue(v int) {
|
||||
if v < 1 || v > len(reverbs) {
|
||||
return
|
||||
}
|
||||
entry := reverbs[v-1]
|
||||
p.unit.Parameters["stereo"] = entry.stereo
|
||||
p.unit.Parameters["notetracking"] = 0
|
||||
p.unit.VarArgs = make([]int, len(entry.varArgs))
|
||||
copy(p.unit.VarArgs, entry.varArgs)
|
||||
}
|
||||
|
||||
func (p ReverbParameter) Hint() string {
|
||||
i := p.Value()
|
||||
if i > 0 {
|
||||
return fmt.Sprintf("%v / %v", i, reverbs[i-1].name)
|
||||
}
|
||||
return "0 / custom"
|
||||
}
|
@ -19,7 +19,7 @@ type (
|
||||
song sointu.Song // the song being played
|
||||
playing bool // is the player playing the score or not
|
||||
rowtime int // how many samples have been played in the current row
|
||||
position ScoreRow // the current position in the score
|
||||
songPos sointu.SongPos // the current position in the score
|
||||
avgVolumeMeter VolumeAnalyzer // the volume analyzer used to calculate the average volume
|
||||
peakVolumeMeter VolumeAnalyzer // the volume analyzer used to calculate the peak volume
|
||||
voiceLevels [vm.MAX_VOICES]float32 // a level that can be used to visualize the volume of each voice
|
||||
@ -29,8 +29,8 @@ type (
|
||||
recording Recording // the recorded MIDI events and BPM
|
||||
|
||||
synther sointu.Synther // the synther used to create new synths
|
||||
playerMessages chan<- PlayerMessage
|
||||
modelMessages <-chan interface{}
|
||||
playerMsgs chan<- PlayerMsg
|
||||
modelMsgs <-chan interface{}
|
||||
}
|
||||
|
||||
// PlayerProcessContext is the context given to the player when processing
|
||||
@ -50,29 +50,19 @@ type (
|
||||
Note byte
|
||||
}
|
||||
|
||||
// PlayerMessage is a message sent from the player to the model. The Inner
|
||||
// PlayerMsg is a message sent from the player to the model. The Inner
|
||||
// field can contain any message. Panic, AverageVolume, PeakVolume, SongRow
|
||||
// and VoiceStates transmitted frequently, with every message, so they are
|
||||
// treated specially, to avoid boxing. All the rest messages can be boxed to
|
||||
// Inner interface{}
|
||||
PlayerMessage struct {
|
||||
PlayerMsg struct {
|
||||
Panic bool
|
||||
AverageVolume Volume
|
||||
PeakVolume Volume
|
||||
SongRow ScoreRow
|
||||
SongPosition sointu.SongPos
|
||||
VoiceLevels [vm.MAX_VOICES]float32
|
||||
Inner interface{}
|
||||
}
|
||||
|
||||
// PlayerCrashMessage is sent to the model when the player crashes.
|
||||
PlayerCrashMessage struct {
|
||||
error
|
||||
}
|
||||
|
||||
// PlayerVolumeErrorMessage is sent to the model there is an error in the volume analyzer. The error is not fatal.
|
||||
PlayerVolumeErrorMessage struct {
|
||||
error
|
||||
}
|
||||
)
|
||||
|
||||
type (
|
||||
@ -93,20 +83,6 @@ const (
|
||||
|
||||
const numRenderTries = 10000
|
||||
|
||||
// NewPlayer creates a new player. The playerMessages channel is used to send
|
||||
// messages to the model. The modelMessages channel is used to receive messages
|
||||
// from the model. The synther is used to create new synths.
|
||||
func NewPlayer(synther sointu.Synther, playerMessages chan<- PlayerMessage, modelMessages <-chan interface{}) *Player {
|
||||
p := &Player{
|
||||
playerMessages: playerMessages,
|
||||
modelMessages: modelMessages,
|
||||
synther: synther,
|
||||
avgVolumeMeter: VolumeAnalyzer{Attack: 0.3, Release: 0.3, Min: -100, Max: 20},
|
||||
peakVolumeMeter: VolumeAnalyzer{Attack: 1e-4, Release: 1, Min: -100, Max: 20},
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// Process renders audio to the given buffer, trying to fill it completely. If
|
||||
// the buffer is not filled, the synth is destroyed and an error is sent to the
|
||||
// model. context tells the player which MIDI events happen during the current
|
||||
@ -152,6 +128,9 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
|
||||
if p.playing {
|
||||
timeUntilRowAdvance = p.song.SamplesPerRow() - p.rowtime
|
||||
}
|
||||
if timeUntilRowAdvance < 0 {
|
||||
timeUntilRowAdvance = 0
|
||||
}
|
||||
var rendered, timeAdvanced int
|
||||
var err error
|
||||
if p.synth != nil {
|
||||
@ -169,7 +148,7 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
|
||||
}
|
||||
if err != nil {
|
||||
p.synth = nil
|
||||
p.send(PlayerCrashMessage{fmt.Errorf("synth.Render: %w", err)})
|
||||
p.send(Alert{Message: fmt.Sprintf("synth.Render: %s", err.Error()), Priority: Error, Name: "PlayerCrash"})
|
||||
}
|
||||
buffer = buffer[rendered:]
|
||||
frame += rendered
|
||||
@ -189,47 +168,37 @@ func (p *Player) Process(buffer sointu.AudioBuffer, context PlayerProcessContext
|
||||
if len(buffer) == 0 {
|
||||
err := p.avgVolumeMeter.Update(oldBuffer)
|
||||
err2 := p.peakVolumeMeter.Update(oldBuffer)
|
||||
var msg interface{}
|
||||
if err != nil {
|
||||
msg = PlayerCrashMessage{err}
|
||||
p.synth = nil
|
||||
p.sendAlert("PlayerVolume", err.Error(), Warning)
|
||||
return
|
||||
}
|
||||
if err2 != nil {
|
||||
msg = PlayerCrashMessage{err}
|
||||
p.synth = nil
|
||||
p.sendAlert("PlayerVolume", err2.Error(), Warning)
|
||||
return
|
||||
}
|
||||
p.send(msg)
|
||||
p.send(nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
// we were not able to fill the buffer with NUM_RENDER_TRIES attempts, destroy synth and throw an error
|
||||
p.synth = nil
|
||||
p.send(PlayerCrashMessage{fmt.Errorf("synth did not fill the audio buffer even with %d render calls", numRenderTries)})
|
||||
p.sendAlert("PlayerCrash", fmt.Sprintf("synth did not fill the audio buffer even with %d render calls", numRenderTries), Error)
|
||||
}
|
||||
|
||||
func (p *Player) advanceRow() {
|
||||
if p.song.Score.Length == 0 || p.song.Score.RowsPerPattern == 0 {
|
||||
return
|
||||
}
|
||||
p.position.Row++ // advance row (this is why we subtracted one in Play())
|
||||
p.position = p.position.Wrap(p.song.Score)
|
||||
p.songPos.PatternRow++ // advance row (this is why we subtracted one in Play())
|
||||
p.songPos = p.song.Score.Wrap(p.songPos)
|
||||
p.send(nil) // just send volume and song row information
|
||||
lastVoice := 0
|
||||
for i, t := range p.song.Score.Tracks {
|
||||
start := lastVoice
|
||||
lastVoice = start + t.NumVoices
|
||||
if p.position.Pattern < 0 || p.position.Pattern >= len(t.Order) {
|
||||
continue
|
||||
}
|
||||
o := t.Order[p.position.Pattern]
|
||||
if o < 0 || o >= len(t.Patterns) {
|
||||
continue
|
||||
}
|
||||
pat := t.Patterns[o]
|
||||
if p.position.Row < 0 || p.position.Row >= len(pat) {
|
||||
continue
|
||||
}
|
||||
n := pat[p.position.Row]
|
||||
n := t.Note(p.songPos)
|
||||
switch {
|
||||
case n == 0:
|
||||
p.releaseTrack(i)
|
||||
@ -245,9 +214,9 @@ func (p *Player) processMessages(context PlayerProcessContext) {
|
||||
loop:
|
||||
for { // process new message
|
||||
select {
|
||||
case msg := <-p.modelMessages:
|
||||
case msg := <-p.modelMsgs:
|
||||
switch m := msg.(type) {
|
||||
case ModelPanicMessage:
|
||||
case PanicMsg:
|
||||
if m.bool {
|
||||
p.synth = nil
|
||||
} else {
|
||||
@ -261,23 +230,23 @@ loop:
|
||||
p.compileOrUpdateSynth()
|
||||
case sointu.Score:
|
||||
p.song.Score = m
|
||||
case ModelPlayingChangedMessage:
|
||||
case IsPlayingMsg:
|
||||
p.playing = bool(m.bool)
|
||||
if !p.playing {
|
||||
for i := range p.song.Score.Tracks {
|
||||
p.releaseTrack(i)
|
||||
}
|
||||
}
|
||||
case ModelBPMChangedMessage:
|
||||
case BPMMsg:
|
||||
p.song.BPM = m.int
|
||||
p.compileOrUpdateSynth()
|
||||
case ModelRowsPerBeatChangedMessage:
|
||||
case RowsPerBeatMsg:
|
||||
p.song.RowsPerBeat = m.int
|
||||
p.compileOrUpdateSynth()
|
||||
case ModelPlayFromPositionMessage:
|
||||
case StartPlayMsg:
|
||||
p.playing = true
|
||||
p.position = m.ScoreRow
|
||||
p.position.Row--
|
||||
p.songPos = m.SongPos
|
||||
p.songPos.PatternRow--
|
||||
p.rowtime = math.MaxInt
|
||||
for i, t := range p.song.Score.Tracks {
|
||||
if !t.Effect {
|
||||
@ -285,19 +254,19 @@ loop:
|
||||
p.releaseTrack(i)
|
||||
}
|
||||
}
|
||||
case ModelNoteOnMessage:
|
||||
case NoteOnMsg:
|
||||
if m.IsInstr {
|
||||
p.triggerInstrument(m.Instr, m.Note)
|
||||
} else {
|
||||
p.triggerTrack(m.Track, m.Note)
|
||||
}
|
||||
case ModelNoteOffMessage:
|
||||
case NoteOffMsg:
|
||||
if m.IsInstr {
|
||||
p.releaseInstrument(m.Instr, m.Note)
|
||||
} else {
|
||||
p.releaseTrack(m.Track)
|
||||
}
|
||||
case ModelRecordingMessage:
|
||||
case RecordingMsg:
|
||||
if m.bool {
|
||||
p.recState = recStateWaitingForNote
|
||||
p.recording = Recording{}
|
||||
@ -317,6 +286,15 @@ loop:
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Player) sendAlert(name, message string, priority AlertPriority) {
|
||||
p.send(Alert{
|
||||
Name: name,
|
||||
Priority: priority,
|
||||
Message: message,
|
||||
Duration: defaultAlertDuration,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *Player) compileOrUpdateSynth() {
|
||||
if p.song.BPM <= 0 {
|
||||
return // bpm not set yet
|
||||
@ -325,7 +303,7 @@ func (p *Player) compileOrUpdateSynth() {
|
||||
err := p.synth.Update(p.song.Patch, p.song.BPM)
|
||||
if err != nil {
|
||||
p.synth = nil
|
||||
p.send(PlayerCrashMessage{fmt.Errorf("synth.Update: %w", err)})
|
||||
p.sendAlert("PlayerCrash", fmt.Sprintf("synth.Update: %v", err), Error)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
@ -333,7 +311,7 @@ func (p *Player) compileOrUpdateSynth() {
|
||||
p.synth, err = p.synther.Synth(p.song.Patch, p.song.BPM)
|
||||
if err != nil {
|
||||
p.synth = nil
|
||||
p.send(PlayerCrashMessage{fmt.Errorf("synther.Synth: %w", err)})
|
||||
p.sendAlert("PlayerCrash", fmt.Sprintf("synther.Synth: %v", err), Error)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -342,7 +320,7 @@ func (p *Player) compileOrUpdateSynth() {
|
||||
// all sends from player are always non-blocking, to ensure that the player thread cannot end up in a dead-lock
|
||||
func (p *Player) send(message interface{}) {
|
||||
select {
|
||||
case p.playerMessages <- PlayerMessage{Panic: p.synth == nil, AverageVolume: p.avgVolumeMeter.Level, PeakVolume: p.peakVolumeMeter.Level, SongRow: p.position, VoiceLevels: p.voiceLevels, Inner: message}:
|
||||
case p.playerMsgs <- PlayerMsg{Panic: p.synth == nil, AverageVolume: p.avgVolumeMeter.Level, PeakVolume: p.peakVolumeMeter.Level, SongPosition: p.songPos, VoiceLevels: p.voiceLevels, Inner: message}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
@ -12,8 +12,9 @@ import (
|
||||
|
||||
//go:generate go run generate/main.go
|
||||
|
||||
type (
|
||||
// GmDlsEntry is a single sample entry from the gm.dls file
|
||||
type GmDlsEntry struct {
|
||||
GmDlsEntry struct {
|
||||
Start int // sample start offset in words
|
||||
LoopStart int // loop start offset in words
|
||||
LoopLength int // loop length in words
|
||||
@ -21,14 +22,21 @@ type GmDlsEntry struct {
|
||||
Name string // sample name
|
||||
}
|
||||
|
||||
// GmDlsEntryMap is a reverse map, to find the index of the GmDlsEntry in the
|
||||
InstrumentPresetYieldFunc func(index int, item string) (ok bool)
|
||||
LoadPreset struct {
|
||||
Index int
|
||||
*Model
|
||||
}
|
||||
)
|
||||
|
||||
// gmDlsEntryMap is a reverse map, to find the index of the GmDlsEntry in the
|
||||
// GmDlsEntries list based on the sample offset. Do not modify during runtime.
|
||||
var GmDlsEntryMap = make(map[vm.SampleOffset]int)
|
||||
var gmDlsEntryMap = make(map[vm.SampleOffset]int)
|
||||
|
||||
func init() {
|
||||
for i, e := range GmDlsEntries {
|
||||
key := vm.SampleOffset{Start: uint32(e.Start), LoopStart: uint16(e.LoopStart), LoopLength: uint16(e.LoopLength)}
|
||||
GmDlsEntryMap[key] = i
|
||||
gmDlsEntryMap[key] = i
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,12 +111,6 @@ var defaultSong = sointu.Song{
|
||||
}}},
|
||||
}
|
||||
|
||||
type delayPreset struct {
|
||||
name string
|
||||
stereo int
|
||||
varArgs []int
|
||||
}
|
||||
|
||||
var reverbs = []delayPreset{
|
||||
{"stereo", 1, []int{1116, 1188, 1276, 1356, 1422, 1492, 1556, 1618,
|
||||
1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642,
|
||||
@ -117,11 +119,41 @@ var reverbs = []delayPreset{
|
||||
{"right", 0, []int{1140, 1212, 1300, 1380, 1446, 1516, 1580, 1642}},
|
||||
}
|
||||
|
||||
type instrumentPresets []sointu.Instrument
|
||||
type delayPreset struct {
|
||||
name string
|
||||
stereo int
|
||||
varArgs []int
|
||||
}
|
||||
|
||||
func (m *Model) IterateInstrumentPresets(yield InstrumentPresetYieldFunc) {
|
||||
for index, instr := range instrumentPresets {
|
||||
if !yield(index, instr.Name) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
m.d.Song.Patch[m.d.InstrIndex] = instrumentPresets[index].Copy()
|
||||
}, allowed: func() bool {
|
||||
return true
|
||||
}}
|
||||
}
|
||||
|
||||
type instrumentPresetsSlice []sointu.Instrument
|
||||
|
||||
//go:embed presets/*
|
||||
var instrumentPresetFS embed.FS
|
||||
var InstrumentPresets instrumentPresets
|
||||
var instrumentPresets instrumentPresetsSlice
|
||||
|
||||
func init() {
|
||||
fs.WalkDir(instrumentPresetFS, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
@ -139,20 +171,12 @@ func init() {
|
||||
if yaml.Unmarshal(data, &instr) != nil {
|
||||
return nil
|
||||
}
|
||||
InstrumentPresets = append(InstrumentPresets, instr)
|
||||
instrumentPresets = append(instrumentPresets, instr)
|
||||
return nil
|
||||
})
|
||||
sort.Sort(InstrumentPresets)
|
||||
sort.Sort(instrumentPresets)
|
||||
}
|
||||
|
||||
func (p instrumentPresets) Len() int {
|
||||
return len(p)
|
||||
}
|
||||
|
||||
func (p instrumentPresets) Less(i, j int) bool {
|
||||
return p[i].Name < p[j].Name
|
||||
}
|
||||
|
||||
func (p instrumentPresets) Swap(i, j int) {
|
||||
p[i], p[j] = p[j], p[i]
|
||||
}
|
||||
func (p instrumentPresetsSlice) Len() int { return len(p) }
|
||||
func (p instrumentPresetsSlice) Less(i, j int) bool { return p[i].Name < p[j].Name }
|
||||
func (p instrumentPresetsSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
|
@ -22,9 +22,9 @@ type recordingNote struct {
|
||||
|
||||
var ErrInvalidRows = errors.New("rows per beat and rows per pattern must be greater than 1")
|
||||
|
||||
func (recording *Recording) Song(patch sointu.Patch, rowsPerBeat, rowsPerPattern int) (sointu.Song, error) {
|
||||
func (recording *Recording) Score(patch sointu.Patch, rowsPerBeat, rowsPerPattern int) (sointu.Score, error) {
|
||||
if rowsPerBeat <= 1 || rowsPerPattern <= 1 {
|
||||
return sointu.Song{}, ErrInvalidRows
|
||||
return sointu.Score{}, ErrInvalidRows
|
||||
}
|
||||
channelNotes := make([][]recordingNote, 0)
|
||||
// find the length of each note and assign it to its respective channel
|
||||
@ -88,15 +88,18 @@ func (recording *Recording) Song(patch sointu.Patch, rowsPerBeat, rowsPerPattern
|
||||
flatPattern[k] = 1 // set all notes as holds at first
|
||||
}
|
||||
for _, n := range t {
|
||||
flatPattern.Set(n.startRow, n.note)
|
||||
if n.startRow >= songLengthRows {
|
||||
continue
|
||||
}
|
||||
flatPattern[n.startRow] = n.note
|
||||
if n.endRow < songLengthRows {
|
||||
for l := n.startRow + 1; l < n.endRow; l++ {
|
||||
flatPattern.Set(l, 1)
|
||||
flatPattern[l] = 1
|
||||
}
|
||||
flatPattern.Set(n.endRow, 0)
|
||||
flatPattern[n.endRow] = 0
|
||||
} else {
|
||||
for l := n.startRow + 1; l < songLengthRows; l++ {
|
||||
flatPattern.Set(l, 1)
|
||||
flatPattern[l] = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -136,7 +139,7 @@ func (recording *Recording) Song(patch sointu.Patch, rowsPerBeat, rowsPerPattern
|
||||
}
|
||||
}
|
||||
score := sointu.Score{Length: songLengthPatterns, RowsPerPattern: rowsPerPattern, Tracks: songTracks}
|
||||
return sointu.Song{BPM: int(recording.BPM + 0.5), RowsPerBeat: rowsPerBeat, Score: score, Patch: patch.Copy()}, nil
|
||||
return score, nil
|
||||
}
|
||||
|
||||
func frameToRow(BPM float64, rowsPerBeat, frame int) int {
|
||||
|
@ -1,106 +0,0 @@
|
||||
package tracker
|
||||
|
||||
import "github.com/vsariola/sointu"
|
||||
|
||||
type (
|
||||
// ScoreRow identifies a row of the song score.
|
||||
ScoreRow struct {
|
||||
Pattern int
|
||||
Row int
|
||||
}
|
||||
|
||||
// ScorePoint identifies a row and a track in a song score.
|
||||
ScorePoint struct {
|
||||
Track int
|
||||
ScoreRow
|
||||
}
|
||||
|
||||
// ScoreRect identifies a rectangular area in a song score.
|
||||
ScoreRect struct {
|
||||
Corner1 ScorePoint
|
||||
Corner2 ScorePoint
|
||||
}
|
||||
)
|
||||
|
||||
func (r ScoreRow) AddRows(rows int) ScoreRow {
|
||||
return ScoreRow{Row: r.Row + rows, Pattern: r.Pattern}
|
||||
}
|
||||
|
||||
func (r ScoreRow) AddPatterns(patterns int) ScoreRow {
|
||||
return ScoreRow{Row: r.Row, Pattern: r.Pattern + patterns}
|
||||
}
|
||||
|
||||
func (r ScoreRow) Wrap(score sointu.Score) ScoreRow {
|
||||
totalRow := r.Pattern*score.RowsPerPattern + r.Row
|
||||
r.Row = mod(totalRow, score.RowsPerPattern)
|
||||
r.Pattern = mod((totalRow-r.Row)/score.RowsPerPattern, score.Length)
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ScoreRow) Clamp(score sointu.Score) ScoreRow {
|
||||
totalRow := r.Pattern*score.RowsPerPattern + r.Row
|
||||
if totalRow < 0 {
|
||||
totalRow = 0
|
||||
}
|
||||
if totalRow >= score.LengthInRows() {
|
||||
totalRow = score.LengthInRows() - 1
|
||||
}
|
||||
r.Row = totalRow % score.RowsPerPattern
|
||||
r.Pattern = ((totalRow - r.Row) / score.RowsPerPattern) % score.Length
|
||||
return r
|
||||
}
|
||||
|
||||
func (r ScorePoint) AddRows(rows int) ScorePoint {
|
||||
return ScorePoint{Track: r.Track, ScoreRow: r.ScoreRow.AddRows(rows)}
|
||||
}
|
||||
|
||||
func (r ScorePoint) AddPatterns(patterns int) ScorePoint {
|
||||
return ScorePoint{Track: r.Track, ScoreRow: r.ScoreRow.AddPatterns(patterns)}
|
||||
}
|
||||
|
||||
func (p ScorePoint) Wrap(score sointu.Score) ScorePoint {
|
||||
p.Track = mod(p.Track, len(score.Tracks))
|
||||
p.ScoreRow = p.ScoreRow.Wrap(score)
|
||||
return p
|
||||
}
|
||||
|
||||
func (p ScorePoint) Clamp(score sointu.Score) ScorePoint {
|
||||
if p.Track < 0 {
|
||||
p.Track = 0
|
||||
} else if l := len(score.Tracks); p.Track >= l {
|
||||
p.Track = l - 1
|
||||
}
|
||||
p.ScoreRow = p.ScoreRow.Clamp(score)
|
||||
return p
|
||||
}
|
||||
|
||||
func (r *ScoreRect) Contains(p ScorePoint) bool {
|
||||
track1, track2 := r.Corner1.Track, r.Corner2.Track
|
||||
if track2 < track1 {
|
||||
track1, track2 = track2, track1
|
||||
}
|
||||
if p.Track < track1 || p.Track > track2 {
|
||||
return false
|
||||
}
|
||||
pattern1, row1, pattern2, row2 := r.Corner1.Pattern, r.Corner1.Row, r.Corner2.Pattern, r.Corner2.Row
|
||||
if pattern2 < pattern1 || (pattern1 == pattern2 && row2 < row1) {
|
||||
pattern1, row1, pattern2, row2 = pattern2, row2, pattern1, row1
|
||||
}
|
||||
if p.Pattern < pattern1 || p.Pattern > pattern2 {
|
||||
return false
|
||||
}
|
||||
if p.Pattern == pattern1 && p.Row < row1 {
|
||||
return false
|
||||
}
|
||||
if p.Pattern == pattern2 && p.Row > row2 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func mod(a, b int) int {
|
||||
if a < 0 {
|
||||
return b - 1 - mod(-a-1, b)
|
||||
}
|
||||
return a % b
|
||||
}
|
94
tracker/string.go
Normal file
94
tracker/string.go
Normal file
@ -0,0 +1,94 @@
|
||||
package tracker
|
||||
|
||||
type (
|
||||
String struct {
|
||||
StringData
|
||||
}
|
||||
|
||||
StringData interface {
|
||||
Value() string
|
||||
setValue(string)
|
||||
change(kind string) func()
|
||||
}
|
||||
|
||||
FilePath Model
|
||||
InstrumentName Model
|
||||
InstrumentComment Model
|
||||
UnitSearch Model
|
||||
)
|
||||
|
||||
func (v String) Set(value string) {
|
||||
if v.Value() != value {
|
||||
defer v.change("Set")()
|
||||
v.setValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
// Model methods
|
||||
|
||||
func (m *Model) FilePath() *FilePath { return (*FilePath)(m) }
|
||||
func (m *Model) InstrumentName() *InstrumentName { return (*InstrumentName)(m) }
|
||||
func (m *Model) InstrumentComment() *InstrumentComment { return (*InstrumentComment)(m) }
|
||||
func (m *Model) UnitSearch() *UnitSearch { return (*UnitSearch)(m) }
|
||||
|
||||
// FilePathString
|
||||
|
||||
func (v *FilePath) String() String { return String{v} }
|
||||
func (v *FilePath) Value() string { return v.d.FilePath }
|
||||
func (v *FilePath) setValue(value string) { v.d.FilePath = value }
|
||||
func (v *FilePath) change(kind string) func() { return func() {} }
|
||||
|
||||
// UnitSearchString
|
||||
|
||||
func (v *UnitSearch) String() String { return String{v} }
|
||||
func (v *UnitSearch) Value() string { return v.d.UnitSearchString }
|
||||
func (v *UnitSearch) setValue(value string) { v.d.UnitSearchString = value }
|
||||
func (v *UnitSearch) change(kind string) func() { return func() {} }
|
||||
|
||||
// InstrumentNameString
|
||||
|
||||
func (v *InstrumentName) String() String {
|
||||
return String{v}
|
||||
}
|
||||
|
||||
func (v *InstrumentName) Value() string {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return ""
|
||||
}
|
||||
return v.d.Song.Patch[v.d.InstrIndex].Name
|
||||
}
|
||||
|
||||
func (v *InstrumentName) setValue(value string) {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
v.d.Song.Patch[v.d.InstrIndex].Name = value
|
||||
}
|
||||
|
||||
func (v *InstrumentName) change(kind string) func() {
|
||||
return (*Model)(v).change("InstrumentNameString."+kind, PatchChange, MinorChange)
|
||||
}
|
||||
|
||||
// InstrumentComment
|
||||
|
||||
func (v *InstrumentComment) String() String {
|
||||
return String{v}
|
||||
}
|
||||
|
||||
func (v *InstrumentComment) Value() string {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return ""
|
||||
}
|
||||
return v.d.Song.Patch[v.d.InstrIndex].Comment
|
||||
}
|
||||
|
||||
func (v *InstrumentComment) setValue(value string) {
|
||||
if v.d.InstrIndex < 0 || v.d.InstrIndex >= len(v.d.Song.Patch) {
|
||||
return
|
||||
}
|
||||
v.d.Song.Patch[v.d.InstrIndex].Comment = value
|
||||
}
|
||||
|
||||
func (v *InstrumentComment) change(kind string) func() {
|
||||
return (*Model)(v).change("InstrumentComment."+kind, PatchChange, MinorChange)
|
||||
}
|
632
tracker/table.go
Normal file
632
tracker/table.go
Normal file
@ -0,0 +1,632 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -40,7 +40,7 @@ func TestOscillatSine(t *testing.T) {
|
||||
}}}
|
||||
tracks := []sointu.Track{{NumVoices: 1, Order: []int{0}, Patterns: []sointu.Pattern{{64, 0, 68, 0, 32, 0, 0, 0, 75, 0, 78, 0, 0, 0, 0, 0}}}}
|
||||
song := sointu.Song{BPM: 100, RowsPerBeat: 4, Score: sointu.Score{RowsPerPattern: 16, Length: 1, Tracks: tracks}, Patch: patch}
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song)
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Render failed: %v", err)
|
||||
}
|
||||
@ -95,7 +95,7 @@ func TestAllRegressionTests(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse the .yml file: %v", err)
|
||||
}
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song)
|
||||
buffer, err := sointu.Play(bridge.NativeSynther{}, song, nil)
|
||||
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()] // extend to the nominal length always.
|
||||
if err != nil {
|
||||
t.Fatalf("Play failed: %v", err)
|
||||
|
@ -14,13 +14,8 @@ func flattenSequence(t sointu.Track, songLength int, rowsPerPattern int, release
|
||||
notes := make([]int, sumLen)
|
||||
k := 0
|
||||
for i := 0; i < songLength; i++ {
|
||||
patIndex := t.Order.Get(i)
|
||||
var pattern sointu.Pattern = nil
|
||||
if patIndex >= 0 && patIndex < len(t.Patterns) {
|
||||
pattern = t.Patterns[patIndex]
|
||||
}
|
||||
for j := 0; j < rowsPerPattern; j++ {
|
||||
note := int(pattern.Get(j))
|
||||
note := int(t.Note(sointu.SongPos{OrderRow: i, PatternRow: j}))
|
||||
if releaseFirst && i == 0 && j == 0 && note == 1 {
|
||||
note = 0
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ func TestAllRegressionTests(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("could not parse the .yml file: %v", err)
|
||||
}
|
||||
buffer, err := sointu.Play(vm.GoSynther{}, song)
|
||||
buffer, err := sointu.Play(vm.GoSynther{}, song, nil)
|
||||
buffer = buffer[:song.Score.LengthInRows()*song.SamplesPerRow()] // extend to the nominal length always.
|
||||
if err != nil {
|
||||
t.Fatalf("Play failed: %v", err)
|
||||
|
Reference in New Issue
Block a user