feat(tracker): make keybindings user configurable

Closes #94, closes #151.
This commit is contained in:
5684185+vsariola@users.noreply.github.com
2024-10-12 21:08:30 +03:00
parent 5c51932f60
commit a6bb5c2afc
8 changed files with 520 additions and 309 deletions

View File

@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- User can define own keybindings in
`os.UserConfigDir()/sointu/keybindings.yaml` ([#94][i94], [#151][i151])
- A small number above the instrument name identifies the MIDI channel /
instrument number, with numbering starting from 1 ([#154][i154])
- The filter unit frequency parameter is displayed in Hz, corresponding roughly
@ -224,6 +226,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
[0.1.0]: https://github.com/vsariola/sointu/compare/4klang-3.11...v0.1.0
[i65]: https://github.com/vsariola/sointu/issues/65
[i68]: https://github.com/vsariola/sointu/issues/68
[i94]: https://github.com/vsariola/sointu/issues/94
[i112]: https://github.com/vsariola/sointu/issues/112
[i116]: https://github.com/vsariola/sointu/issues/116
[i120]: https://github.com/vsariola/sointu/issues/120

View File

@ -51,6 +51,13 @@ type InstrumentEditor struct {
commentKeyFilters []event.Filter
searchkeyFilters []event.Filter
nameKeyFilters []event.Filter
enlargeHint, shrinkHint string
addInstrumentHint string
octaveHint string
expandCommentHint string
collapseCommentHint string
deleteInstrumentHint string
}
func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
@ -78,10 +85,13 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: name, IconBytes: icons.ImageAudiotrack, Doer: model.LoadPreset(index)})
return true
})
for k := range noteMap {
ret.commentKeyFilters = append(ret.commentKeyFilters, key.Filter{Name: k, Focus: ret.commentEditor})
ret.searchkeyFilters = append(ret.searchkeyFilters, key.Filter{Name: k, Focus: ret.searchEditor})
ret.nameKeyFilters = append(ret.nameKeyFilters, key.Filter{Name: k, Focus: ret.nameEditor})
for k, a := range keyBindingMap {
if len(a) < 4 || a[:4] != "Note" {
continue
}
ret.commentKeyFilters = append(ret.commentKeyFilters, key.Filter{Name: k.Name, Required: k.Modifiers, Focus: ret.commentEditor})
ret.searchkeyFilters = append(ret.searchkeyFilters, key.Filter{Name: k.Name, Required: k.Modifiers, Focus: ret.searchEditor})
ret.nameKeyFilters = append(ret.nameKeyFilters, key.Filter{Name: k.Name, Required: k.Modifiers, Focus: ret.nameEditor})
}
ret.commentKeyFilters = append(ret.commentKeyFilters, key.Filter{Name: key.NameEscape, Focus: ret.commentEditor})
ret.searchkeyFilters = append(ret.searchkeyFilters, key.Filter{Name: key.NameEscape, Focus: ret.searchEditor})
@ -89,6 +99,13 @@ func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
ret.commentKeyFilters = append(ret.commentKeyFilters, key.Filter{Name: key.NameSpace, Focus: ret.commentEditor})
ret.searchkeyFilters = append(ret.searchkeyFilters, key.Filter{Name: key.NameSpace, Focus: ret.searchEditor})
ret.nameKeyFilters = append(ret.nameKeyFilters, key.Filter{Name: key.NameSpace, Focus: ret.nameEditor})
ret.enlargeHint = makeHint("Enlarge", " (%s)", "InstrEnlargedToggle")
ret.shrinkHint = makeHint("Shrink", " (%s)", "InstrEnlargedToggle")
ret.addInstrumentHint = makeHint("Add\ninstrument", "\n(%s)", "AddInstrument")
ret.octaveHint = makeHint("Octave down", " (%s)", "OctaveNumberInputSubtract") + makeHint(" or up", " (%s)", "OctaveNumberInputAdd")
ret.expandCommentHint = makeHint("Expand comment", " (%s)", "CommentExpandedToggle")
ret.collapseCommentHint = makeHint("Collapse comment", " (%s)", "CommentExpandedToggle")
ret.deleteInstrumentHint = makeHint("Delete\ninstrument", "\n(%s)", "DeleteInstrument")
return ret
}
@ -109,16 +126,16 @@ func (ie *InstrumentEditor) childFocused(gtx C) bool {
func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
ie.wasFocused = ie.Focused() || ie.childFocused(gtx)
fullscreenBtnStyle := ToggleIcon(gtx, t.Theme, ie.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, "Enlarge (Ctrl+E)", "Shrink (Ctrl+E)")
fullscreenBtnStyle := ToggleIcon(gtx, t.Theme, ie.enlargeBtn, icons.NavigationFullscreen, icons.NavigationFullscreenExit, ie.enlargeHint, ie.shrinkHint)
octave := func(gtx C) D {
in := layout.UniformInset(unit.Dp(1))
numStyle := NumericUpDown(t.Theme, t.OctaveNumberInput, "Octave down (<) or up (>)")
numStyle := NumericUpDown(t.Theme, t.OctaveNumberInput, ie.octaveHint)
dims := in.Layout(gtx, numStyle.Layout)
return dims
}
newBtnStyle := ActionIcon(gtx, t.Theme, ie.newInstrumentBtn, icons.ContentAdd, "Add\ninstrument\n(Ctrl+I)")
newBtnStyle := ActionIcon(gtx, t.Theme, ie.newInstrumentBtn, icons.ContentAdd, ie.addInstrumentHint)
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
return layout.Flex{}.Layout(
@ -161,12 +178,12 @@ func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
header := func(gtx C) D {
commentExpandBtnStyle := ToggleIcon(gtx, t.Theme, ie.commentExpandBtn, icons.NavigationExpandMore, icons.NavigationExpandLess, "Expand comment", "Collapse comment")
commentExpandBtnStyle := ToggleIcon(gtx, t.Theme, ie.commentExpandBtn, icons.NavigationExpandMore, icons.NavigationExpandLess, ie.expandCommentHint, ie.collapseCommentHint)
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(gtx, t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, "Delete\ninstrument")
deleteInstrumentBtnStyle := ActionIcon(gtx, t.Theme, ie.deleteInstrumentBtn, icons.ActionDelete, ie.deleteInstrumentHint)
m := PopupMenu(&ie.presetMenu, t.Theme.Shaper)

View File

@ -0,0 +1,78 @@
- {key: "C", shortcut: true, action: "Copy"}
- {key: "V", shortcut: true, action: "Paste"}
- {key: "A", shortcut: true, action: "SelectAll"}
- {key: "X", shortcut: true, action: "Cut"}
- {key: "Z", shortcut: true, action: "Undo"}
- {key: "Y", shortcut: true, action: "Redo"}
- {key: "D", shortcut: true, action: "UnitDisabledToggle"}
- {key: "L", shortcut: true, action: "LoopToggle"}
- {key: "N", shortcut: true, action: "NewSong"}
- {key: "S", shortcut: true, action: "SaveSong"}
- {key: "O", shortcut: true, action: "OpenSong"}
- {key: "I", shortcut: true, shift: true, action: "DeleteInstrument"}
- {key: "I", shortcut: true, action: "AddInstrument"}
- {key: "T", shortcut: true, shift: true, action: "DeleteTrack"}
- {key: "T", shortcut: true, action: "AddTrack"}
- {key: "E", shortcut: true, action: "InstrEnlargedToggle"}
- {key: "W", shortcut: true, action: "Quit"}
- {key: "Space", action: "PlayingToggleUnfollow"}
- {key: "Space", shift: true, action: "PlayingToggleFollow"}
- {key: "F1", action: "OrderEditorFocus"}
- {key: "F2", action: "TrackEditorFocus"}
- {key: "F3", action: "InstrumentEditorFocus"}
- {key: "F5", action: "PlayCurrentPosUnfollow"}
- {key: "F5", shift: true, action: "PlayCurrentPosFollow"}
- {key: "F5", shortcut: true, action: "PlaySongStartUnfollow"}
- {key: "F5", shortcut: true, shift: true, action: "PlaySongStartFollow"}
- {key: "F6", action: "PlaySelectedUnfollow"}
- {key: "F6", shift: true, action: "PlaySelectedFollow"}
- {key: "F6", shortcut: true, action: "PlayLoopUnfollow"}
- {key: "F6", shortcut: true, shift: true, action: "PlayLoopFollow"}
- {key: "F7", action: "RecordingToggle"}
- {key: "F8", action: "StopPlaying"}
- {key: "F9", action: "FollowToggle"}
- {key: "F12", action: "PanicToggle"}
- {key: "\\", shift: true, action: "OctaveAdd"}
- {key: "\\", action: "OctaveSubtract"}
- {key: ">", shift: true, action: "OctaveAdd"}
- {key: ">", action: "OctaveSubtract"}
- {key: "<", shift: true, action: "OctaveAdd"}
- {key: "<", action: "OctaveSubtract"}
- {key: "Tab", shift: true, action: "FocusPrev"}
- {key: "Tab", action: "FocusNext"}
- {key: "A", action: "NoteOff"}
- {key: "1", action: "NoteOff"}
- {key: "Z", action: "Note0"}
- {key: "S", action: "Note1"}
- {key: "X", action: "Note2"}
- {key: "D", action: "Note3"}
- {key: "C", action: "Note4"}
- {key: "V", action: "Note5"}
- {key: "G", action: "Note6"}
- {key: "B", action: "Note7"}
- {key: "H", action: "Note8"}
- {key: "N", action: "Note9"}
- {key: "J", action: "Note10"}
- {key: "M", action: "Note11"}
- {key: ",", action: "Note12"}
- {key: "L", action: "Note13"}
- {key: ".", action: "Note14"}
- {key: "Q", action: "Note12"}
- {key: "2", action: "Note13"}
- {key: "W", action: "Note14"}
- {key: "3", action: "Note15"}
- {key: "E", action: "Note16"}
- {key: "R", action: "Note17"}
- {key: "5", action: "Note18"}
- {key: "T", action: "Note19"}
- {key: "6", action: "Note20"}
- {key: "Y", action: "Note21"}
- {key: "7", action: "Note22"}
- {key: "U", action: "Note23"}
- {key: "I", action: "Note24"}
- {key: "9", action: "Note25"}
- {key: "O", action: "Note26"}
- {key: "0", action: "Note27"}
- {key: "P", action: "Note28"}
- {key: "+", action: "Increase"}
- {key: "-", action: "Decrease"}

View File

@ -1,211 +1,314 @@
package gioui
import (
_ "embed"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"gioui.org/io/clipboard"
"gioui.org/io/key"
"gopkg.in/yaml.v2"
)
var noteMap = map[key.Name]int{
"Z": -12,
"S": -11,
"X": -10,
"D": -9,
"C": -8,
"V": -7,
"G": -6,
"B": -5,
"H": -4,
"N": -3,
"J": -2,
"M": -1,
",": 0,
"L": 1,
".": 2,
"Q": 0,
"2": 1,
"W": 2,
"3": 3,
"E": 4,
"R": 5,
"5": 6,
"T": 7,
"6": 8,
"Y": 9,
"7": 10,
"U": 11,
"I": 12,
"9": 13,
"O": 14,
"0": 15,
"P": 16,
type (
KeyAction string
KeyBinding struct {
Key string
Shortcut, Ctrl, Command, Shift, Alt, Super bool
Action string
}
)
var keyBindingMap = map[key.Event]string{}
var keyActionMap = map[KeyAction]string{} // holds an informative string of the first key bound to an action
func loadCustomKeyBindings() []KeyBinding {
var keyBindings []KeyBinding
configDir, err := os.UserConfigDir()
if err != nil {
return nil
}
path := filepath.Join(configDir, "sointu", "keybindings.yaml")
bytes, err := os.ReadFile(path)
if err != nil {
return nil
}
err = yaml.Unmarshal(bytes, &keyBindings)
if err != nil {
return nil
}
if len(keyBindings) == 0 {
return nil
}
return keyBindings
}
//go:embed keybindings.yaml
var defaultKeyBindingsYaml []byte
func loadDefaultKeyBindings() []KeyBinding {
var keyBindings []KeyBinding
err := yaml.Unmarshal(defaultKeyBindingsYaml, &keyBindings)
if err != nil {
panic(fmt.Errorf("failed to unmarshal keybindings: %w", err))
}
return keyBindings
}
func init() {
keyBindings := loadCustomKeyBindings()
if keyBindings == nil {
keyBindings = loadDefaultKeyBindings()
}
for _, kb := range keyBindings {
var mods key.Modifiers
if kb.Shortcut {
mods |= key.ModShortcut
}
if kb.Ctrl {
mods |= key.ModCtrl
}
if kb.Command {
mods |= key.ModCommand
}
if kb.Shift {
mods |= key.ModShift
}
if kb.Alt {
mods |= key.ModAlt
}
if kb.Super {
mods |= key.ModSuper
}
keyBindingMap[key.Event{Name: key.Name(kb.Key), Modifiers: mods, State: key.Press}] = kb.Action
if _, ok := keyActionMap[KeyAction(kb.Action)]; !ok {
modString := strings.Replace(mods.String(), "-", "+", -1)
text := kb.Key
if modString != "" {
text = modString + "+" + text
}
keyActionMap[KeyAction(kb.Action)] = text
}
}
}
func makeHint(hint, format, action string) string {
if keyActionMap[KeyAction(action)] != "" {
return hint + fmt.Sprintf(format, keyActionMap[KeyAction(action)])
}
return hint
}
// KeyEvent handles incoming key events and returns true if repaint is needed.
func (t *Tracker) KeyEvent(e key.Event, gtx C) {
if e.State == key.Press {
switch e.Name {
case "V":
if e.Modifiers.Contain(key.ModShortcut) {
gtx.Execute(clipboard.ReadCmd{Tag: t})
return
}
case "Z":
if e.Modifiers.Contain(key.ModShortcut) {
t.Model.Undo().Do()
return
}
case "Y":
if e.Modifiers.Contain(key.ModShortcut) {
t.Model.Redo().Do()
return
}
case "D":
if e.Modifiers.Contain(key.ModShortcut) {
t.Model.UnitDisabled().Bool().Toggle()
return
}
case "L":
if e.Modifiers.Contain(key.ModShortcut) {
t.Model.LoopToggle().Bool().Toggle()
return
}
case "N":
if e.Modifiers.Contain(key.ModShortcut) {
t.NewSong().Do()
return
}
case "S":
if e.Modifiers.Contain(key.ModShortcut) {
t.SaveSong().Do()
return
}
case "O":
if e.Modifiers.Contain(key.ModShortcut) {
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":
if e.State == key.Release {
t.JammingReleased(e)
return
}
action, ok := keyBindingMap[e]
if !ok {
return
}
switch action {
// Actions
case "AddTrack":
t.AddTrack().Do()
case "DeleteTrack":
t.DeleteTrack().Do()
case "AddInstrument":
t.AddInstrument().Do()
case "DeleteInstrument":
t.DeleteInstrument().Do()
case "AddUnitAfter":
t.AddUnit(false).Do()
case "AddUnitBefore":
t.AddUnit(true).Do()
case "DeleteUnit":
t.DeleteUnit().Do()
case "ClearUnit":
t.ClearUnit().Do()
case "Undo":
t.Undo().Do()
case "Redo":
t.Redo().Do()
case "AddSemitone":
t.AddSemitone().Do()
case "SubtractSemitone":
t.SubtractSemitone().Do()
case "AddOctave":
t.AddOctave().Do()
case "SubtractOctave":
t.SubtractOctave().Do()
case "EditNoteOff":
t.EditNoteOff().Do()
case "RemoveUnused":
t.RemoveUnused().Do()
case "PlayCurrentPosFollow":
t.NoteTracking().Bool().Set(true)
t.PlayFromCurrentPosition().Do()
case "PlayCurrentPosUnfollow":
t.NoteTracking().Bool().Set(false)
t.PlayFromCurrentPosition().Do()
case "PlaySongStartFollow":
t.NoteTracking().Bool().Set(true)
t.PlayFromSongStart().Do()
case "PlaySongStartUnfollow":
t.NoteTracking().Bool().Set(false)
t.PlayFromSongStart().Do()
case "PlaySelectedFollow":
t.NoteTracking().Bool().Set(true)
t.PlaySelected().Do()
case "PlaySelectedUnfollow":
t.NoteTracking().Bool().Set(false)
t.PlaySelected().Do()
case "PlayLoopFollow":
t.NoteTracking().Bool().Set(true)
t.PlayFromLoopStart().Do()
case "PlayLoopUnfollow":
t.NoteTracking().Bool().Set(false)
t.PlayFromLoopStart().Do()
case "StopPlaying":
t.StopPlaying().Do()
case "AddOrderRowBefore":
t.AddOrderRow(true).Do()
case "AddOrderRowAfter":
t.AddOrderRow(false).Do()
case "DeleteOrderRowBackwards":
t.DeleteOrderRow(true).Do()
case "DeleteOrderRowForwards":
t.DeleteOrderRow(false).Do()
case "NewSong":
t.NewSong().Do()
case "OpenSong":
t.OpenSong().Do()
case "Quit":
if canQuit {
t.Quit().Do()
}
case "SaveSong":
t.SaveSong().Do()
case "SaveSongAs":
t.SaveSongAs().Do()
case "ExportWav":
t.Export().Do()
case "ExportFloat":
t.ExportFloat().Do()
case "ExportInt16":
t.ExportInt16().Do()
// Booleans
case "PanicToggle":
t.Panic().Bool().Toggle()
case "RecordingToggle":
t.IsRecording().Bool().Toggle()
case "PlayingToggleFollow":
t.NoteTracking().Bool().Set(true)
t.Playing().Bool().Toggle()
case "PlayingToggleUnfollow":
t.NoteTracking().Bool().Set(false)
t.Playing().Bool().Toggle()
case "InstrEnlargedToggle":
t.InstrEnlarged().Bool().Toggle()
case "CommentExpandedToggle":
t.CommentExpanded().Bool().Toggle()
case "FollowToggle":
t.NoteTracking().Bool().Toggle()
case "UnitDisabledToggle":
t.UnitDisabled().Bool().Toggle()
case "LoopToggle":
t.LoopToggle().Bool().Toggle()
// Integers
case "InstrumentVoicesAdd":
t.Model.InstrumentVoices().Int().Add(1)
case "InstrumentVoicesSubtract":
t.Model.InstrumentVoices().Int().Add(-1)
case "TrackVoicesAdd":
t.TrackVoices().Int().Add(1)
case "TrackVoicesSubtract":
t.TrackVoices().Int().Add(-1)
case "SongLengthAdd":
t.SongLength().Int().Add(1)
case "SongLengthSubtract":
t.SongLength().Int().Add(-1)
case "BPMAdd":
t.BPM().Int().Add(1)
case "BPMSubtract":
t.BPM().Int().Add(-1)
case "RowsPerPatternAdd":
t.RowsPerPattern().Int().Add(1)
case "RowsPerPatternSubtract":
t.RowsPerPattern().Int().Add(-1)
case "RowsPerBeatAdd":
t.RowsPerBeat().Int().Add(1)
case "RowsPerBeatSubtract":
t.RowsPerBeat().Int().Add(-1)
case "StepAdd":
t.Step().Int().Add(1)
case "StepSubtract":
t.Step().Int().Add(-1)
case "OctaveAdd":
t.Octave().Int().Add(1)
case "OctaveSubtract":
t.Octave().Int().Add(-1)
// Other miscellaneous
case "Paste":
gtx.Execute(clipboard.ReadCmd{Tag: t})
case "OrderEditorFocus":
t.OrderEditor.scrollTable.Focus()
case "TrackEditorFocus":
t.TrackEditor.scrollTable.Focus()
case "InstrumentEditorFocus":
t.InstrumentEditor.Focus()
case "FocusPrev":
switch {
case t.OrderEditor.scrollTable.Focused():
t.InstrumentEditor.unitEditor.sliderList.Focus()
case t.TrackEditor.scrollTable.Focused():
t.OrderEditor.scrollTable.Focus()
return
case "F2":
t.TrackEditor.scrollTable.Focus()
return
case "F3":
case t.InstrumentEditor.Focused():
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
t.InstrumentEditor.unitEditor.sliderList.Focus()
} else {
t.TrackEditor.scrollTable.Focus()
}
default:
t.InstrumentEditor.Focus()
return
case "Space":
t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift))
t.Playing().Bool().Toggle()
return
case "F5":
t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift))
if e.Modifiers.Contain(key.ModCtrl) {
t.Model.PlayFromSongStart().Do()
}
case "FocusNext":
switch {
case t.OrderEditor.scrollTable.Focused():
t.TrackEditor.scrollTable.Focus()
case t.TrackEditor.scrollTable.Focused():
t.InstrumentEditor.Focus()
case t.InstrumentEditor.Focused():
t.InstrumentEditor.unitEditor.sliderList.Focus()
default:
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
t.InstrumentEditor.Focus()
} else {
t.Model.PlayFromCurrentPosition().Do()
}
return
case "F6":
t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift))
if e.Modifiers.Contain(key.ModCtrl) {
t.Model.PlayFromLoopStart().Do()
} else {
t.Model.PlaySelected().Do()
}
return
case "F7":
t.IsRecording().Bool().Toggle()
return
case "F8":
t.StopPlaying().Do()
return
case "F9":
t.NoteTracking().Bool().Toggle()
return
case "F12":
t.Panic().Bool().Toggle()
return
case `\`, `<`, `>`:
if e.Modifiers.Contain(key.ModShift) {
t.OctaveNumberInput.Int.Add(1)
} else {
t.OctaveNumberInput.Int.Add(-1)
}
case key.NameTab:
if e.Modifiers.Contain(key.ModShift) {
switch {
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.InstrumentEditor.enlargeBtn.Bool.Value() {
t.InstrumentEditor.unitEditor.sliderList.Focus()
} else {
t.TrackEditor.scrollTable.Focus()
}
default:
t.InstrumentEditor.Focus()
}
} else {
switch {
case t.OrderEditor.scrollTable.Focused():
t.TrackEditor.scrollTable.Focus()
case t.TrackEditor.scrollTable.Focused():
t.InstrumentEditor.Focus()
case t.InstrumentEditor.Focused():
t.InstrumentEditor.unitEditor.sliderList.Focus()
default:
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
t.InstrumentEditor.Focus()
} else {
t.OrderEditor.scrollTable.Focus()
}
}
t.OrderEditor.scrollTable.Focus()
}
}
t.JammingPressed(e)
} else { // e.State == key.Release
t.JammingReleased(e)
default:
if action[:4] == "Note" {
val, err := strconv.Atoi(string(action[4:]))
if err != nil {
break
}
t.JammingPressed(e, val-12)
}
}
}
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.Int.Value(), val)
instr := t.InstrumentEditor.instrumentDragList.TrackerList.Selected()
t.KeyPlaying[e.Name] = t.InstrNoteOn(instr, n)
return n
}
func (t *Tracker) JammingPressed(e key.Event, val int) byte {
if _, ok := t.KeyPlaying[e.Name]; !ok {
n := noteAsValue(t.OctaveNumberInput.Int.Value(), val)
instr := t.InstrumentEditor.instrumentDragList.TrackerList.Selected()
t.KeyPlaying[e.Name] = t.InstrNoteOn(instr, n)
return n
}
return 0
}

View File

@ -6,6 +6,7 @@ import (
"strconv"
"strings"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/layout"
"gioui.org/op"
@ -59,12 +60,16 @@ type NoteEditor struct {
NoteOffBtn *ActionClickable
EffectBtn *BoolClickable
scrollTable *ScrollTable
tag struct{}
scrollTable *ScrollTable
tag struct{}
eventFilters []event.Filter
deleteTrackHint string
addTrackHint string
}
func NewNoteEditor(model *tracker.Model) *NoteEditor {
return &NoteEditor{
ret := &NoteEditor{
TrackVoices: NewNumberInput(model.TrackVoices().Int()),
NewTrackBtn: NewActionClickable(model.AddTrack()),
DeleteTrackBtn: NewActionClickable(model.DeleteTrack()),
@ -80,50 +85,20 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
model.NoteRows().List(),
),
}
for k, a := range keyBindingMap {
if len(a) < 4 || a[:4] != "Note" {
continue
}
ret.eventFilters = append(ret.eventFilters, key.Filter{Focus: ret.scrollTable, Name: k.Name})
}
ret.deleteTrackHint = makeHint("Delete\ntrack", "\n(%s)", "DeleteTrack")
ret.addTrackHint = makeHint("Add\ntrack", "\n(%s)", "AddTrack")
return ret
}
func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions {
for {
e, ok := gtx.Event(
key.Filter{Focus: te.scrollTable, Name: "A"},
key.Filter{Focus: te.scrollTable, Name: "B"},
key.Filter{Focus: te.scrollTable, Name: "C"},
key.Filter{Focus: te.scrollTable, Name: "D"},
key.Filter{Focus: te.scrollTable, Name: "E"},
key.Filter{Focus: te.scrollTable, Name: "F"},
key.Filter{Focus: te.scrollTable, Name: "G"},
key.Filter{Focus: te.scrollTable, Name: "H"},
key.Filter{Focus: te.scrollTable, Name: "I"},
key.Filter{Focus: te.scrollTable, Name: "J"},
key.Filter{Focus: te.scrollTable, Name: "K"},
key.Filter{Focus: te.scrollTable, Name: "L"},
key.Filter{Focus: te.scrollTable, Name: "M"},
key.Filter{Focus: te.scrollTable, Name: "N"},
key.Filter{Focus: te.scrollTable, Name: "O"},
key.Filter{Focus: te.scrollTable, Name: "P"},
key.Filter{Focus: te.scrollTable, Name: "Q"},
key.Filter{Focus: te.scrollTable, Name: "R"},
key.Filter{Focus: te.scrollTable, Name: "S"},
key.Filter{Focus: te.scrollTable, Name: "T"},
key.Filter{Focus: te.scrollTable, Name: "U"},
key.Filter{Focus: te.scrollTable, Name: "V"},
key.Filter{Focus: te.scrollTable, Name: "W"},
key.Filter{Focus: te.scrollTable, Name: "X"},
key.Filter{Focus: te.scrollTable, Name: "Y"},
key.Filter{Focus: te.scrollTable, Name: "Z"},
key.Filter{Focus: te.scrollTable, Name: "0"},
key.Filter{Focus: te.scrollTable, Name: "1"},
key.Filter{Focus: te.scrollTable, Name: "2"},
key.Filter{Focus: te.scrollTable, Name: "3"},
key.Filter{Focus: te.scrollTable, Name: "4"},
key.Filter{Focus: te.scrollTable, Name: "5"},
key.Filter{Focus: te.scrollTable, Name: "6"},
key.Filter{Focus: te.scrollTable, Name: "7"},
key.Filter{Focus: te.scrollTable, Name: "8"},
key.Filter{Focus: te.scrollTable, Name: "9"},
key.Filter{Focus: te.scrollTable, Name: ","},
key.Filter{Focus: te.scrollTable, Name: "."},
)
e, ok := gtx.Event(te.eventFilters...)
if !ok {
break
}
@ -162,8 +137,8 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
addOctaveBtnStyle := ActionButton(gtx, t.Theme, te.AddOctaveBtn, "+12")
subtractOctaveBtnStyle := ActionButton(gtx, t.Theme, te.SubtractOctaveBtn, "-12")
noteOffBtnStyle := ActionButton(gtx, t.Theme, te.NoteOffBtn, "Note Off")
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, "Delete track\n(Ctrl+Shift+T)")
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, "Add track\n(Ctrl+T)")
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
in := layout.UniformInset(unit.Dp(1))
voiceUpDown := func(gtx C) D {
numStyle := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
@ -347,7 +322,11 @@ func (te *NoteEditor) command(gtx C, t *Tracker, e key.Event) {
goto validNote
}
} else {
if e.Name == "A" || e.Name == "1" {
action, ok := keyBindingMap[e]
if !ok {
return
}
if action == "NoteOff" {
t.Model.Notes().Table().Fill(0)
if step := t.Model.Step().Value(); step > 0 {
te.scrollTable.Table.MoveCursor(0, step)
@ -356,8 +335,12 @@ func (te *NoteEditor) command(gtx C, t *Tracker, e key.Event) {
te.scrollTable.EnsureCursorVisible()
return
}
if val, ok := noteMap[e.Name]; ok {
n = noteAsValue(t.OctaveNumberInput.Int.Value(), val)
if action[:4] == "Note" {
val, err := strconv.Atoi(string(action[4:]))
if err != nil {
return
}
n = noteAsValue(t.OctaveNumberInput.Int.Value(), val-12)
t.Model.Notes().Table().Fill(int(n))
goto validNote
}

View File

@ -25,6 +25,7 @@ type ScrollTable struct {
focused bool
requestFocus bool
cursorMoved bool
eventFilters []event.Filter
}
type ScrollTableStyle struct {
@ -40,11 +41,33 @@ type ScrollTableStyle struct {
}
func NewScrollTable(table tracker.Table, vertList, horizList tracker.List) *ScrollTable {
return &ScrollTable{
ret := &ScrollTable{
Table: table,
ColTitleList: NewDragList(vertList, layout.Horizontal),
RowTitleList: NewDragList(horizList, layout.Vertical),
}
ret.eventFilters = []event.Filter{
key.FocusFilter{Target: ret},
transfer.TargetFilter{Target: ret, Type: "application/text"},
pointer.Filter{Target: ret, Kinds: pointer.Press},
key.Filter{Focus: ret, Name: key.NameLeftArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: ret, Name: key.NameUpArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: ret, Name: key.NameRightArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: ret, Name: key.NameDownArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: ret, Name: key.NamePageUp, Optional: key.ModShift},
key.Filter{Focus: ret, Name: key.NamePageDown, Optional: key.ModShift},
key.Filter{Focus: ret, Name: key.NameHome, Optional: key.ModShift},
key.Filter{Focus: ret, Name: key.NameEnd, Optional: key.ModShift},
key.Filter{Focus: ret, Name: key.NameDeleteBackward},
key.Filter{Focus: ret, Name: key.NameDeleteForward},
}
for k, a := range keyBindingMap {
switch a {
case "Copy", "Paste", "Cut", "Increase", "Decrease":
ret.eventFilters = append(ret.eventFilters, key.Filter{Focus: ret, Name: k.Name, Required: k.Modifiers})
}
}
return ret
}
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 {
@ -107,26 +130,7 @@ func (s ScrollTableStyle) Layout(gtx C) D {
func (s *ScrollTableStyle) handleEvents(gtx layout.Context, p image.Point) {
for {
e, ok := gtx.Event(
key.FocusFilter{Target: s.ScrollTable},
transfer.TargetFilter{Target: s.ScrollTable, Type: "application/text"},
pointer.Filter{Target: s.ScrollTable, Kinds: pointer.Press},
key.Filter{Focus: s.ScrollTable, Name: key.NameLeftArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: s.ScrollTable, Name: key.NameUpArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: s.ScrollTable, Name: key.NameRightArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: s.ScrollTable, Name: key.NameDownArrow, Optional: key.ModShift | key.ModCtrl | key.ModAlt},
key.Filter{Focus: s.ScrollTable, Name: key.NamePageUp, Optional: key.ModShift},
key.Filter{Focus: s.ScrollTable, Name: key.NamePageDown, Optional: key.ModShift},
key.Filter{Focus: s.ScrollTable, Name: key.NameHome, Optional: key.ModShift},
key.Filter{Focus: s.ScrollTable, Name: key.NameEnd, Optional: key.ModShift},
key.Filter{Focus: s.ScrollTable, Name: key.NameDeleteBackward},
key.Filter{Focus: s.ScrollTable, Name: key.NameDeleteForward},
key.Filter{Focus: s.ScrollTable, Name: "C", Required: key.ModShortcut},
key.Filter{Focus: s.ScrollTable, Name: "V", Required: key.ModShortcut},
key.Filter{Focus: s.ScrollTable, Name: "X", Required: key.ModShortcut},
key.Filter{Focus: s.ScrollTable, Name: "+"},
key.Filter{Focus: s.ScrollTable, Name: "-"},
)
e, ok := gtx.Event(s.ScrollTable.eventFilters...)
if !ok {
break
}
@ -240,23 +244,6 @@ func (s *ScrollTable) command(gtx C, e key.Event) {
stepY = 1e6
}
switch e.Name {
case "X", "C":
if e.Modifiers.Contain(key.ModShortcut) {
contents, ok := s.Table.Copy()
if !ok {
return
}
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))})
if e.Name == "X" {
s.Table.Clear()
}
return
}
case "V":
if e.Modifiers.Contain(key.ModShortcut) {
gtx.Execute(clipboard.ReadCmd{Tag: s})
}
return
case key.NameDeleteBackward, key.NameDeleteForward:
s.Table.Clear()
return
@ -280,12 +267,29 @@ func (s *ScrollTable) command(gtx C, e key.Event) {
s.Table.SetCursorX(0)
case key.NameEnd:
s.Table.SetCursorX(s.Table.Width() - 1)
case "+":
s.Table.Add(1)
return
case "-":
s.Table.Add(-1)
return
default:
a := keyBindingMap[e]
switch a {
case "Copy", "Cut":
contents, ok := s.Table.Copy()
if !ok {
return
}
gtx.Execute(clipboard.WriteCmd{Type: "application/text", Data: io.NopCloser(bytes.NewReader(contents))})
if a == "Cut" {
s.Table.Clear()
}
return
case "Paste":
gtx.Execute(clipboard.ReadCmd{Tag: s})
return
case "Increase":
s.Table.Add(1)
return
case "Decrease":
s.Table.Add(-1)
return
}
}
if !e.Modifiers.Contain(key.ModShift) {
s.Table.SetCursor2(s.Table.Cursor())

View File

@ -40,6 +40,14 @@ type SongPanel struct {
// Edit menu items
editMenuItems []MenuItem
// Hints
rewindHint string
playHint, stopHint string
recordHint, stopRecordHint string
followOnHint, followOffHint string
panicHint string
loopOffHint, loopOnHint string
}
func NewSongPanel(model *tracker.Model) *SongPanel {
@ -59,25 +67,33 @@ func NewSongPanel(model *tracker.Model) *SongPanel {
RewindBtn: NewActionClickable(model.PlayFromSongStart()),
}
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()},
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: keyActionMap["NewSong"], Doer: model.NewSong()},
{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: keyActionMap["OpenSong"], Doer: model.OpenSong()},
{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: keyActionMap["SaveSong"], Doer: model.SaveSong()},
{IconBytes: icons.ContentSave, Text: "Save Song As...", ShortcutText: keyActionMap["SaveSongAs"], Doer: model.SaveSongAs()},
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav...", ShortcutText: keyActionMap["ExportWav"], Doer: model.Export()},
}
if canQuit {
ret.fileMenuItems = append(ret.fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit", Doer: model.Quit()})
ret.fileMenuItems = append(ret.fileMenuItems, MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit", ShortcutText: keyActionMap["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()},
{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: keyActionMap["Undo"], Doer: model.Undo()},
{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: keyActionMap["Redo"], Doer: model.Redo()},
{IconBytes: icons.ImageCrop, Text: "Remove unused data", ShortcutText: keyActionMap["RemoveUnused"], Doer: model.RemoveUnused()},
}
ret.rewindHint = makeHint("Rewind", "\n(%s)", "PlaySongStartUnfollow")
ret.playHint = makeHint("Play", " (%s)", "PlayCurrentPosUnfollow")
ret.stopHint = makeHint("Stop", " (%s)", "StopPlaying")
ret.panicHint = makeHint("Panic", " (%s)", "PanicToggle")
ret.recordHint = makeHint("Record", " (%s)", "RecordingToggle")
ret.stopRecordHint = makeHint("Stop", " (%s)", "RecordingToggle")
ret.followOnHint = makeHint("Follow on", " (%s)", "FollowToggle")
ret.followOffHint = makeHint("Follow off", " (%s)", "FollowToggle")
ret.loopOffHint = makeHint("Loop off", " (%s)", "LoopToggle")
ret.loopOnHint = makeHint("Loop on", " (%s)", "LoopToggle")
return ret
}
const shortcutKey = "Ctrl+"
func (s *SongPanel) Layout(gtx C, t *Tracker) D {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {
@ -104,12 +120,12 @@ func (t *SongPanel) layoutSongOptions(gtx C, tr *Tracker) D {
in := layout.UniformInset(unit.Dp(1))
panicBtnStyle := ToggleButton(gtx, tr.Theme, t.PanicBtn, "Panic (F12)")
rewindBtnStyle := ActionIcon(gtx, tr.Theme, t.RewindBtn, icons.AVFastRewind, "Rewind\n(Ctrl+F5)")
playBtnStyle := ToggleIcon(gtx, tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F5 / Space)", "Stop (F8)")
recordBtnStyle := ToggleIcon(gtx, tr.Theme, t.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, "Record (F7)", "Stop (F7)")
noteTrackBtnStyle := ToggleIcon(gtx, tr.Theme, t.NoteTracking, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, "Follow\nOff", "Follow\nOn")
loopBtnStyle := ToggleIcon(gtx, tr.Theme, t.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, "Loop\nOff\n(Ctrl+L)", "Loop\nOn\n(Ctrl+L)")
panicBtnStyle := ToggleButton(gtx, tr.Theme, t.PanicBtn, t.panicHint)
rewindBtnStyle := ActionIcon(gtx, tr.Theme, t.RewindBtn, icons.AVFastRewind, t.rewindHint)
playBtnStyle := ToggleIcon(gtx, tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, t.playHint, t.stopHint)
recordBtnStyle := ToggleIcon(gtx, tr.Theme, t.RecordBtn, icons.AVFiberManualRecord, icons.AVFiberSmartRecord, t.recordHint, t.stopRecordHint)
noteTrackBtnStyle := ToggleIcon(gtx, tr.Theme, t.NoteTracking, icons.ActionSpeakerNotesOff, icons.ActionSpeakerNotes, t.followOffHint, t.followOnHint)
loopBtnStyle := ToggleIcon(gtx, tr.Theme, t.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, t.loopOffHint, t.loopOnHint)
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx C) D {

View File

@ -33,6 +33,10 @@ type UnitEditor struct {
DisableUnitBtn *BoolClickable
SelectTypeBtn *widget.Clickable
caser cases.Caser
copyHint string
disableUnitHint string
enableUnitHint string
}
func NewUnitEditor(m *tracker.Model) *UnitEditor {
@ -46,6 +50,9 @@ func NewUnitEditor(m *tracker.Model) *UnitEditor {
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
}
ret.caser = cases.Title(language.English)
ret.copyHint = makeHint("Copy unit", " (%s)", "Copy")
ret.disableUnitHint = makeHint("Disable unit", " (%s)", "UnitDisabledToggle")
ret.enableUnitHint = makeHint("Enable unit", " (%s)", "UnitDisabledToggle")
return ret
}
@ -122,9 +129,9 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
t.Alerts().Add("Unit copied to clipboard", tracker.Info)
}
}
copyUnitBtnStyle := TipIcon(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, "Copy unit (Ctrl+C)")
copyUnitBtnStyle := TipIcon(t.Theme, pe.CopyUnitBtn, icons.ContentContentCopy, pe.copyHint)
deleteUnitBtnStyle := ActionIcon(gtx, t.Theme, pe.DeleteUnitBtn, icons.ActionDelete, "Delete unit (Ctrl+Backspace)")
disableUnitBtnStyle := ToggleIcon(gtx, t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, "Disable unit (Ctrl-D)", "Enable unit (Ctrl-D)")
disableUnitBtnStyle := ToggleIcon(gtx, t.Theme, pe.DisableUnitBtn, icons.AVVolumeUp, icons.AVVolumeOff, pe.disableUnitHint, pe.enableUnitHint)
text := t.Units().SelectedType()
if text == "" {
text = "Choose unit type"