mirror of
https://github.com/vsariola/sointu.git
synced 2025-05-28 03:10:24 -04:00
The behaviour of the user-defined keybindings.yml was changed. It now documents just the changes from the default keybindings. An empty line with no action means "unbind the key".
344 lines
8.2 KiB
Go
344 lines
8.2 KiB
Go
package gioui
|
|
|
|
import (
|
|
_ "embed"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"gioui.org/io/clipboard"
|
|
"gioui.org/io/key"
|
|
"gopkg.in/yaml.v2"
|
|
)
|
|
|
|
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.yml")
|
|
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.yml
|
|
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 := loadDefaultKeyBindings()
|
|
keyBindings = append(keyBindings, loadCustomKeyBindings()...)
|
|
|
|
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
|
|
}
|
|
|
|
keyEvent := key.Event{Name: key.Name(kb.Key), Modifiers: mods, State: key.Press}
|
|
action, ok := keyBindingMap[keyEvent] // if this key has been previously bound, remove it from the hint map
|
|
if ok {
|
|
delete(keyActionMap, KeyAction(action))
|
|
}
|
|
if kb.Action == "" { // unbind
|
|
delete(keyBindingMap, keyEvent)
|
|
} else { // bind
|
|
keyBindingMap[keyEvent] = kb.Action
|
|
// last binding of the some action wins for displaying the hint
|
|
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.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.Follow().Bool().Set(true)
|
|
t.PlayCurrentPos().Do()
|
|
case "PlayCurrentPosUnfollow":
|
|
t.Follow().Bool().Set(false)
|
|
t.PlayCurrentPos().Do()
|
|
case "PlaySongStartFollow":
|
|
t.Follow().Bool().Set(true)
|
|
t.PlaySongStart().Do()
|
|
case "PlaySongStartUnfollow":
|
|
t.Follow().Bool().Set(false)
|
|
t.PlaySongStart().Do()
|
|
case "PlaySelectedFollow":
|
|
t.Follow().Bool().Set(true)
|
|
t.PlaySelected().Do()
|
|
case "PlaySelectedUnfollow":
|
|
t.Follow().Bool().Set(false)
|
|
t.PlaySelected().Do()
|
|
case "PlayLoopFollow":
|
|
t.Follow().Bool().Set(true)
|
|
t.PlayFromLoopStart().Do()
|
|
case "PlayLoopUnfollow":
|
|
t.Follow().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()
|
|
case "SplitTrack":
|
|
t.SplitTrack().Do()
|
|
case "SplitInstrument":
|
|
t.SplitInstrument().Do()
|
|
// Booleans
|
|
case "PanicToggle":
|
|
t.Panic().Bool().Toggle()
|
|
case "RecordingToggle":
|
|
t.IsRecording().Bool().Toggle()
|
|
case "PlayingToggleFollow":
|
|
t.Follow().Bool().Set(true)
|
|
t.Playing().Bool().Toggle()
|
|
case "PlayingToggleUnfollow":
|
|
t.Follow().Bool().Set(false)
|
|
t.Playing().Bool().Toggle()
|
|
case "InstrEnlargedToggle":
|
|
t.InstrEnlarged().Bool().Toggle()
|
|
case "LinkInstrTrackToggle":
|
|
t.LinkInstrTrack().Bool().Toggle()
|
|
case "CommentExpandedToggle":
|
|
t.CommentExpanded().Bool().Toggle()
|
|
case "FollowToggle":
|
|
t.Follow().Bool().Toggle()
|
|
case "UnitDisabledToggle":
|
|
t.UnitDisabled().Bool().Toggle()
|
|
case "LoopToggle":
|
|
t.LoopToggle().Bool().Toggle()
|
|
case "UniquePatternsToggle":
|
|
t.UniquePatterns().Bool().Toggle()
|
|
case "MuteToggle":
|
|
t.Mute().Bool().Toggle()
|
|
case "SoloToggle":
|
|
t.Solo().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()
|
|
case t.InstrumentEditor.Focused():
|
|
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
|
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
|
} else {
|
|
t.TrackEditor.scrollTable.Focus()
|
|
}
|
|
default:
|
|
t.InstrumentEditor.Focus()
|
|
}
|
|
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.OrderEditor.scrollTable.Focus()
|
|
}
|
|
}
|
|
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, 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
|
|
}
|
|
|
|
func (t *Tracker) JammingReleased(e key.Event) bool {
|
|
if noteID, ok := t.KeyPlaying[e.Name]; ok {
|
|
noteID.NoteOff()
|
|
delete(t.KeyPlaying, e.Name)
|
|
return true
|
|
}
|
|
return false
|
|
}
|