mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-18 21:14:31 -04:00
feat(tracker): make keybindings user configurable
Closes #94, closes #151.
This commit is contained in:
parent
5c51932f60
commit
a6bb5c2afc
@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
### Added
|
### 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 /
|
- A small number above the instrument name identifies the MIDI channel /
|
||||||
instrument number, with numbering starting from 1 ([#154][i154])
|
instrument number, with numbering starting from 1 ([#154][i154])
|
||||||
- The filter unit frequency parameter is displayed in Hz, corresponding roughly
|
- 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
|
[0.1.0]: https://github.com/vsariola/sointu/compare/4klang-3.11...v0.1.0
|
||||||
[i65]: https://github.com/vsariola/sointu/issues/65
|
[i65]: https://github.com/vsariola/sointu/issues/65
|
||||||
[i68]: https://github.com/vsariola/sointu/issues/68
|
[i68]: https://github.com/vsariola/sointu/issues/68
|
||||||
|
[i94]: https://github.com/vsariola/sointu/issues/94
|
||||||
[i112]: https://github.com/vsariola/sointu/issues/112
|
[i112]: https://github.com/vsariola/sointu/issues/112
|
||||||
[i116]: https://github.com/vsariola/sointu/issues/116
|
[i116]: https://github.com/vsariola/sointu/issues/116
|
||||||
[i120]: https://github.com/vsariola/sointu/issues/120
|
[i120]: https://github.com/vsariola/sointu/issues/120
|
||||||
|
@ -51,6 +51,13 @@ type InstrumentEditor struct {
|
|||||||
commentKeyFilters []event.Filter
|
commentKeyFilters []event.Filter
|
||||||
searchkeyFilters []event.Filter
|
searchkeyFilters []event.Filter
|
||||||
nameKeyFilters []event.Filter
|
nameKeyFilters []event.Filter
|
||||||
|
|
||||||
|
enlargeHint, shrinkHint string
|
||||||
|
addInstrumentHint string
|
||||||
|
octaveHint string
|
||||||
|
expandCommentHint string
|
||||||
|
collapseCommentHint string
|
||||||
|
deleteInstrumentHint string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewInstrumentEditor(model *tracker.Model) *InstrumentEditor {
|
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)})
|
ret.presetMenuItems = append(ret.presetMenuItems, MenuItem{Text: name, IconBytes: icons.ImageAudiotrack, Doer: model.LoadPreset(index)})
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
for k := range noteMap {
|
for k, a := range keyBindingMap {
|
||||||
ret.commentKeyFilters = append(ret.commentKeyFilters, key.Filter{Name: k, Focus: ret.commentEditor})
|
if len(a) < 4 || a[:4] != "Note" {
|
||||||
ret.searchkeyFilters = append(ret.searchkeyFilters, key.Filter{Name: k, Focus: ret.searchEditor})
|
continue
|
||||||
ret.nameKeyFilters = append(ret.nameKeyFilters, key.Filter{Name: k, Focus: ret.nameEditor})
|
}
|
||||||
|
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.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})
|
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.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.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.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
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,16 +126,16 @@ func (ie *InstrumentEditor) childFocused(gtx C) bool {
|
|||||||
|
|
||||||
func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
|
func (ie *InstrumentEditor) Layout(gtx C, t *Tracker) D {
|
||||||
ie.wasFocused = ie.Focused() || ie.childFocused(gtx)
|
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 {
|
octave := func(gtx C) D {
|
||||||
in := layout.UniformInset(unit.Dp(1))
|
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)
|
dims := in.Layout(gtx, numStyle.Layout)
|
||||||
return dims
|
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,
|
ret := layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
return layout.Flex{}.Layout(
|
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 {
|
func (ie *InstrumentEditor) layoutInstrumentHeader(gtx C, t *Tracker) D {
|
||||||
header := func(gtx C) 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")
|
presetMenuBtnStyle := TipIcon(t.Theme, ie.presetMenuBtn, icons.NavigationMenu, "Load preset")
|
||||||
copyInstrumentBtnStyle := TipIcon(t.Theme, ie.copyInstrumentBtn, icons.ContentContentCopy, "Copy instrument")
|
copyInstrumentBtnStyle := TipIcon(t.Theme, ie.copyInstrumentBtn, icons.ContentContentCopy, "Copy instrument")
|
||||||
saveInstrumentBtnStyle := TipIcon(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, "Save instrument")
|
saveInstrumentBtnStyle := TipIcon(t.Theme, ie.saveInstrumentBtn, icons.ContentSave, "Save instrument")
|
||||||
loadInstrumentBtnStyle := TipIcon(t.Theme, ie.loadInstrumentBtn, icons.FileFolderOpen, "Load 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)
|
m := PopupMenu(&ie.presetMenu, t.Theme.Shaper)
|
||||||
|
|
||||||
|
78
tracker/gioui/keybindings.yaml
Normal file
78
tracker/gioui/keybindings.yaml
Normal 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"}
|
@ -1,211 +1,314 @@
|
|||||||
package gioui
|
package gioui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"gioui.org/io/clipboard"
|
"gioui.org/io/clipboard"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
var noteMap = map[key.Name]int{
|
type (
|
||||||
"Z": -12,
|
KeyAction string
|
||||||
"S": -11,
|
|
||||||
"X": -10,
|
KeyBinding struct {
|
||||||
"D": -9,
|
Key string
|
||||||
"C": -8,
|
Shortcut, Ctrl, Command, Shift, Alt, Super bool
|
||||||
"V": -7,
|
Action string
|
||||||
"G": -6,
|
}
|
||||||
"B": -5,
|
)
|
||||||
"H": -4,
|
|
||||||
"N": -3,
|
var keyBindingMap = map[key.Event]string{}
|
||||||
"J": -2,
|
var keyActionMap = map[KeyAction]string{} // holds an informative string of the first key bound to an action
|
||||||
"M": -1,
|
|
||||||
",": 0,
|
func loadCustomKeyBindings() []KeyBinding {
|
||||||
"L": 1,
|
var keyBindings []KeyBinding
|
||||||
".": 2,
|
configDir, err := os.UserConfigDir()
|
||||||
"Q": 0,
|
if err != nil {
|
||||||
"2": 1,
|
return nil
|
||||||
"W": 2,
|
}
|
||||||
"3": 3,
|
path := filepath.Join(configDir, "sointu", "keybindings.yaml")
|
||||||
"E": 4,
|
bytes, err := os.ReadFile(path)
|
||||||
"R": 5,
|
if err != nil {
|
||||||
"5": 6,
|
return nil
|
||||||
"T": 7,
|
}
|
||||||
"6": 8,
|
err = yaml.Unmarshal(bytes, &keyBindings)
|
||||||
"Y": 9,
|
if err != nil {
|
||||||
"7": 10,
|
return nil
|
||||||
"U": 11,
|
}
|
||||||
"I": 12,
|
if len(keyBindings) == 0 {
|
||||||
"9": 13,
|
return nil
|
||||||
"O": 14,
|
}
|
||||||
"0": 15,
|
return keyBindings
|
||||||
"P": 16,
|
}
|
||||||
|
|
||||||
|
//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.
|
// KeyEvent handles incoming key events and returns true if repaint is needed.
|
||||||
func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
func (t *Tracker) KeyEvent(e key.Event, gtx C) {
|
||||||
if e.State == key.Press {
|
if e.State == key.Release {
|
||||||
switch e.Name {
|
t.JammingReleased(e)
|
||||||
case "V":
|
return
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
}
|
||||||
gtx.Execute(clipboard.ReadCmd{Tag: t})
|
action, ok := keyBindingMap[e]
|
||||||
return
|
if !ok {
|
||||||
}
|
return
|
||||||
case "Z":
|
}
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
switch action {
|
||||||
t.Model.Undo().Do()
|
// Actions
|
||||||
return
|
case "AddTrack":
|
||||||
}
|
t.AddTrack().Do()
|
||||||
case "Y":
|
case "DeleteTrack":
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
t.DeleteTrack().Do()
|
||||||
t.Model.Redo().Do()
|
case "AddInstrument":
|
||||||
return
|
t.AddInstrument().Do()
|
||||||
}
|
case "DeleteInstrument":
|
||||||
case "D":
|
t.DeleteInstrument().Do()
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
case "AddUnitAfter":
|
||||||
t.Model.UnitDisabled().Bool().Toggle()
|
t.AddUnit(false).Do()
|
||||||
return
|
case "AddUnitBefore":
|
||||||
}
|
t.AddUnit(true).Do()
|
||||||
case "L":
|
case "DeleteUnit":
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
t.DeleteUnit().Do()
|
||||||
t.Model.LoopToggle().Bool().Toggle()
|
case "ClearUnit":
|
||||||
return
|
t.ClearUnit().Do()
|
||||||
}
|
case "Undo":
|
||||||
case "N":
|
t.Undo().Do()
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
case "Redo":
|
||||||
t.NewSong().Do()
|
t.Redo().Do()
|
||||||
return
|
case "AddSemitone":
|
||||||
}
|
t.AddSemitone().Do()
|
||||||
case "S":
|
case "SubtractSemitone":
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
t.SubtractSemitone().Do()
|
||||||
t.SaveSong().Do()
|
case "AddOctave":
|
||||||
return
|
t.AddOctave().Do()
|
||||||
}
|
case "SubtractOctave":
|
||||||
case "O":
|
t.SubtractOctave().Do()
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
case "EditNoteOff":
|
||||||
t.OpenSong().Do()
|
t.EditNoteOff().Do()
|
||||||
return
|
case "RemoveUnused":
|
||||||
}
|
t.RemoveUnused().Do()
|
||||||
case "I":
|
case "PlayCurrentPosFollow":
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
t.NoteTracking().Bool().Set(true)
|
||||||
if e.Modifiers.Contain(key.ModShift) {
|
t.PlayFromCurrentPosition().Do()
|
||||||
t.DeleteInstrument().Do()
|
case "PlayCurrentPosUnfollow":
|
||||||
} else {
|
t.NoteTracking().Bool().Set(false)
|
||||||
t.AddInstrument().Do()
|
t.PlayFromCurrentPosition().Do()
|
||||||
}
|
case "PlaySongStartFollow":
|
||||||
return
|
t.NoteTracking().Bool().Set(true)
|
||||||
}
|
t.PlayFromSongStart().Do()
|
||||||
case "T":
|
case "PlaySongStartUnfollow":
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
t.NoteTracking().Bool().Set(false)
|
||||||
if e.Modifiers.Contain(key.ModShift) {
|
t.PlayFromSongStart().Do()
|
||||||
t.DeleteTrack().Do()
|
case "PlaySelectedFollow":
|
||||||
} else {
|
t.NoteTracking().Bool().Set(true)
|
||||||
t.AddTrack().Do()
|
t.PlaySelected().Do()
|
||||||
}
|
case "PlaySelectedUnfollow":
|
||||||
return
|
t.NoteTracking().Bool().Set(false)
|
||||||
}
|
t.PlaySelected().Do()
|
||||||
case "E":
|
case "PlayLoopFollow":
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
t.NoteTracking().Bool().Set(true)
|
||||||
t.InstrEnlarged().Bool().Toggle()
|
t.PlayFromLoopStart().Do()
|
||||||
return
|
case "PlayLoopUnfollow":
|
||||||
}
|
t.NoteTracking().Bool().Set(false)
|
||||||
case "W":
|
t.PlayFromLoopStart().Do()
|
||||||
if e.Modifiers.Contain(key.ModShortcut) && canQuit {
|
case "StopPlaying":
|
||||||
t.Quit().Do()
|
t.StopPlaying().Do()
|
||||||
return
|
case "AddOrderRowBefore":
|
||||||
}
|
t.AddOrderRow(true).Do()
|
||||||
case "F1":
|
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()
|
t.OrderEditor.scrollTable.Focus()
|
||||||
return
|
case t.InstrumentEditor.Focused():
|
||||||
case "F2":
|
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
||||||
t.TrackEditor.scrollTable.Focus()
|
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
||||||
return
|
} else {
|
||||||
case "F3":
|
t.TrackEditor.scrollTable.Focus()
|
||||||
|
}
|
||||||
|
default:
|
||||||
t.InstrumentEditor.Focus()
|
t.InstrumentEditor.Focus()
|
||||||
return
|
}
|
||||||
case "Space":
|
case "FocusNext":
|
||||||
t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift))
|
switch {
|
||||||
t.Playing().Bool().Toggle()
|
case t.OrderEditor.scrollTable.Focused():
|
||||||
return
|
t.TrackEditor.scrollTable.Focus()
|
||||||
case "F5":
|
case t.TrackEditor.scrollTable.Focused():
|
||||||
t.NoteTracking().Bool().Set(e.Modifiers.Contain(key.ModShift))
|
t.InstrumentEditor.Focus()
|
||||||
if e.Modifiers.Contain(key.ModCtrl) {
|
case t.InstrumentEditor.Focused():
|
||||||
t.Model.PlayFromSongStart().Do()
|
t.InstrumentEditor.unitEditor.sliderList.Focus()
|
||||||
|
default:
|
||||||
|
if t.InstrumentEditor.enlargeBtn.Bool.Value() {
|
||||||
|
t.InstrumentEditor.Focus()
|
||||||
} else {
|
} else {
|
||||||
t.Model.PlayFromCurrentPosition().Do()
|
t.OrderEditor.scrollTable.Focus()
|
||||||
}
|
|
||||||
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.JammingPressed(e)
|
default:
|
||||||
} else { // e.State == key.Release
|
if action[:4] == "Note" {
|
||||||
t.JammingReleased(e)
|
val, err := strconv.Atoi(string(action[4:]))
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
t.JammingPressed(e, val-12)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) JammingPressed(e key.Event) byte {
|
func (t *Tracker) JammingPressed(e key.Event, val int) byte {
|
||||||
if val, ok := noteMap[e.Name]; ok {
|
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
||||||
if _, ok := t.KeyPlaying[e.Name]; !ok {
|
n := noteAsValue(t.OctaveNumberInput.Int.Value(), val)
|
||||||
n := noteAsValue(t.OctaveNumberInput.Int.Value(), val)
|
instr := t.InstrumentEditor.instrumentDragList.TrackerList.Selected()
|
||||||
instr := t.InstrumentEditor.instrumentDragList.TrackerList.Selected()
|
t.KeyPlaying[e.Name] = t.InstrNoteOn(instr, n)
|
||||||
t.KeyPlaying[e.Name] = t.InstrNoteOn(instr, n)
|
return n
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
@ -59,12 +60,16 @@ type NoteEditor struct {
|
|||||||
NoteOffBtn *ActionClickable
|
NoteOffBtn *ActionClickable
|
||||||
EffectBtn *BoolClickable
|
EffectBtn *BoolClickable
|
||||||
|
|
||||||
scrollTable *ScrollTable
|
scrollTable *ScrollTable
|
||||||
tag struct{}
|
tag struct{}
|
||||||
|
eventFilters []event.Filter
|
||||||
|
|
||||||
|
deleteTrackHint string
|
||||||
|
addTrackHint string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
||||||
return &NoteEditor{
|
ret := &NoteEditor{
|
||||||
TrackVoices: NewNumberInput(model.TrackVoices().Int()),
|
TrackVoices: NewNumberInput(model.TrackVoices().Int()),
|
||||||
NewTrackBtn: NewActionClickable(model.AddTrack()),
|
NewTrackBtn: NewActionClickable(model.AddTrack()),
|
||||||
DeleteTrackBtn: NewActionClickable(model.DeleteTrack()),
|
DeleteTrackBtn: NewActionClickable(model.DeleteTrack()),
|
||||||
@ -80,50 +85,20 @@ func NewNoteEditor(model *tracker.Model) *NoteEditor {
|
|||||||
model.NoteRows().List(),
|
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 {
|
func (te *NoteEditor) Layout(gtx layout.Context, t *Tracker) layout.Dimensions {
|
||||||
for {
|
for {
|
||||||
e, ok := gtx.Event(
|
e, ok := gtx.Event(te.eventFilters...)
|
||||||
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: "."},
|
|
||||||
)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -162,8 +137,8 @@ func (te *NoteEditor) layoutButtons(gtx C, t *Tracker) D {
|
|||||||
addOctaveBtnStyle := ActionButton(gtx, t.Theme, te.AddOctaveBtn, "+12")
|
addOctaveBtnStyle := ActionButton(gtx, t.Theme, te.AddOctaveBtn, "+12")
|
||||||
subtractOctaveBtnStyle := ActionButton(gtx, t.Theme, te.SubtractOctaveBtn, "-12")
|
subtractOctaveBtnStyle := ActionButton(gtx, t.Theme, te.SubtractOctaveBtn, "-12")
|
||||||
noteOffBtnStyle := ActionButton(gtx, t.Theme, te.NoteOffBtn, "Note Off")
|
noteOffBtnStyle := ActionButton(gtx, t.Theme, te.NoteOffBtn, "Note Off")
|
||||||
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, "Delete track\n(Ctrl+Shift+T)")
|
deleteTrackBtnStyle := ActionIcon(gtx, t.Theme, te.DeleteTrackBtn, icons.ActionDelete, te.deleteTrackHint)
|
||||||
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, "Add track\n(Ctrl+T)")
|
newTrackBtnStyle := ActionIcon(gtx, t.Theme, te.NewTrackBtn, icons.ContentAdd, te.addTrackHint)
|
||||||
in := layout.UniformInset(unit.Dp(1))
|
in := layout.UniformInset(unit.Dp(1))
|
||||||
voiceUpDown := func(gtx C) D {
|
voiceUpDown := func(gtx C) D {
|
||||||
numStyle := NumericUpDown(t.Theme, te.TrackVoices, "Number of voices for this track")
|
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
|
goto validNote
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if e.Name == "A" || e.Name == "1" {
|
action, ok := keyBindingMap[e]
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if action == "NoteOff" {
|
||||||
t.Model.Notes().Table().Fill(0)
|
t.Model.Notes().Table().Fill(0)
|
||||||
if step := t.Model.Step().Value(); step > 0 {
|
if step := t.Model.Step().Value(); step > 0 {
|
||||||
te.scrollTable.Table.MoveCursor(0, step)
|
te.scrollTable.Table.MoveCursor(0, step)
|
||||||
@ -356,8 +335,12 @@ func (te *NoteEditor) command(gtx C, t *Tracker, e key.Event) {
|
|||||||
te.scrollTable.EnsureCursorVisible()
|
te.scrollTable.EnsureCursorVisible()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if val, ok := noteMap[e.Name]; ok {
|
if action[:4] == "Note" {
|
||||||
n = noteAsValue(t.OctaveNumberInput.Int.Value(), val)
|
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))
|
t.Model.Notes().Table().Fill(int(n))
|
||||||
goto validNote
|
goto validNote
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ type ScrollTable struct {
|
|||||||
focused bool
|
focused bool
|
||||||
requestFocus bool
|
requestFocus bool
|
||||||
cursorMoved bool
|
cursorMoved bool
|
||||||
|
eventFilters []event.Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
type ScrollTableStyle struct {
|
type ScrollTableStyle struct {
|
||||||
@ -40,11 +41,33 @@ type ScrollTableStyle struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewScrollTable(table tracker.Table, vertList, horizList tracker.List) *ScrollTable {
|
func NewScrollTable(table tracker.Table, vertList, horizList tracker.List) *ScrollTable {
|
||||||
return &ScrollTable{
|
ret := &ScrollTable{
|
||||||
Table: table,
|
Table: table,
|
||||||
ColTitleList: NewDragList(vertList, layout.Horizontal),
|
ColTitleList: NewDragList(vertList, layout.Horizontal),
|
||||||
RowTitleList: NewDragList(horizList, layout.Vertical),
|
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 {
|
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) {
|
func (s *ScrollTableStyle) handleEvents(gtx layout.Context, p image.Point) {
|
||||||
for {
|
for {
|
||||||
e, ok := gtx.Event(
|
e, ok := gtx.Event(s.ScrollTable.eventFilters...)
|
||||||
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: "-"},
|
|
||||||
)
|
|
||||||
if !ok {
|
if !ok {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -240,23 +244,6 @@ func (s *ScrollTable) command(gtx C, e key.Event) {
|
|||||||
stepY = 1e6
|
stepY = 1e6
|
||||||
}
|
}
|
||||||
switch e.Name {
|
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:
|
case key.NameDeleteBackward, key.NameDeleteForward:
|
||||||
s.Table.Clear()
|
s.Table.Clear()
|
||||||
return
|
return
|
||||||
@ -280,12 +267,29 @@ func (s *ScrollTable) command(gtx C, e key.Event) {
|
|||||||
s.Table.SetCursorX(0)
|
s.Table.SetCursorX(0)
|
||||||
case key.NameEnd:
|
case key.NameEnd:
|
||||||
s.Table.SetCursorX(s.Table.Width() - 1)
|
s.Table.SetCursorX(s.Table.Width() - 1)
|
||||||
case "+":
|
default:
|
||||||
s.Table.Add(1)
|
a := keyBindingMap[e]
|
||||||
return
|
switch a {
|
||||||
case "-":
|
case "Copy", "Cut":
|
||||||
s.Table.Add(-1)
|
contents, ok := s.Table.Copy()
|
||||||
return
|
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) {
|
if !e.Modifiers.Contain(key.ModShift) {
|
||||||
s.Table.SetCursor2(s.Table.Cursor())
|
s.Table.SetCursor2(s.Table.Cursor())
|
||||||
|
@ -40,6 +40,14 @@ type SongPanel struct {
|
|||||||
|
|
||||||
// Edit menu items
|
// Edit menu items
|
||||||
editMenuItems []MenuItem
|
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 {
|
func NewSongPanel(model *tracker.Model) *SongPanel {
|
||||||
@ -59,25 +67,33 @@ func NewSongPanel(model *tracker.Model) *SongPanel {
|
|||||||
RewindBtn: NewActionClickable(model.PlayFromSongStart()),
|
RewindBtn: NewActionClickable(model.PlayFromSongStart()),
|
||||||
}
|
}
|
||||||
ret.fileMenuItems = []MenuItem{
|
ret.fileMenuItems = []MenuItem{
|
||||||
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N", Doer: model.NewSong()},
|
{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: keyActionMap["NewSong"], Doer: model.NewSong()},
|
||||||
{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O", Doer: model.OpenSong()},
|
{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: keyActionMap["OpenSong"], Doer: model.OpenSong()},
|
||||||
{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S", Doer: model.SaveSong()},
|
{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: keyActionMap["SaveSong"], Doer: model.SaveSong()},
|
||||||
{IconBytes: icons.ContentSave, Text: "Save Song As...", Doer: model.SaveSongAs()},
|
{IconBytes: icons.ContentSave, Text: "Save Song As...", ShortcutText: keyActionMap["SaveSongAs"], Doer: model.SaveSongAs()},
|
||||||
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav...", Doer: model.Export()},
|
{IconBytes: icons.ImageAudiotrack, Text: "Export Wav...", ShortcutText: keyActionMap["ExportWav"], Doer: model.Export()},
|
||||||
}
|
}
|
||||||
if canQuit {
|
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{
|
ret.editMenuItems = []MenuItem{
|
||||||
{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Doer: model.Undo()},
|
{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: keyActionMap["Undo"], Doer: model.Undo()},
|
||||||
{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: shortcutKey + "Y", Doer: model.Redo()},
|
{IconBytes: icons.ContentRedo, Text: "Redo", ShortcutText: keyActionMap["Redo"], Doer: model.Redo()},
|
||||||
{IconBytes: icons.ImageCrop, Text: "Remove unused data", Doer: model.RemoveUnused()},
|
{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
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
const shortcutKey = "Ctrl+"
|
|
||||||
|
|
||||||
func (s *SongPanel) Layout(gtx C, t *Tracker) D {
|
func (s *SongPanel) Layout(gtx C, t *Tracker) D {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D {
|
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))
|
in := layout.UniformInset(unit.Dp(1))
|
||||||
|
|
||||||
panicBtnStyle := ToggleButton(gtx, tr.Theme, t.PanicBtn, "Panic (F12)")
|
panicBtnStyle := ToggleButton(gtx, tr.Theme, t.PanicBtn, t.panicHint)
|
||||||
rewindBtnStyle := ActionIcon(gtx, tr.Theme, t.RewindBtn, icons.AVFastRewind, "Rewind\n(Ctrl+F5)")
|
rewindBtnStyle := ActionIcon(gtx, tr.Theme, t.RewindBtn, icons.AVFastRewind, t.rewindHint)
|
||||||
playBtnStyle := ToggleIcon(gtx, tr.Theme, t.PlayingBtn, icons.AVPlayArrow, icons.AVStop, "Play (F5 / Space)", "Stop (F8)")
|
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, "Record (F7)", "Stop (F7)")
|
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, "Follow\nOff", "Follow\nOn")
|
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, "Loop\nOff\n(Ctrl+L)", "Loop\nOn\n(Ctrl+L)")
|
loopBtnStyle := ToggleIcon(gtx, tr.Theme, t.LoopBtn, icons.NavigationArrowForward, icons.AVLoop, t.loopOffHint, t.loopOnHint)
|
||||||
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx C) D {
|
layout.Rigid(func(gtx C) D {
|
||||||
|
@ -33,6 +33,10 @@ type UnitEditor struct {
|
|||||||
DisableUnitBtn *BoolClickable
|
DisableUnitBtn *BoolClickable
|
||||||
SelectTypeBtn *widget.Clickable
|
SelectTypeBtn *widget.Clickable
|
||||||
caser cases.Caser
|
caser cases.Caser
|
||||||
|
|
||||||
|
copyHint string
|
||||||
|
disableUnitHint string
|
||||||
|
enableUnitHint string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUnitEditor(m *tracker.Model) *UnitEditor {
|
func NewUnitEditor(m *tracker.Model) *UnitEditor {
|
||||||
@ -46,6 +50,9 @@ func NewUnitEditor(m *tracker.Model) *UnitEditor {
|
|||||||
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
|
searchList: NewDragList(m.SearchResults().List(), layout.Vertical),
|
||||||
}
|
}
|
||||||
ret.caser = cases.Title(language.English)
|
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
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,9 +129,9 @@ func (pe *UnitEditor) layoutFooter(gtx C, t *Tracker) D {
|
|||||||
t.Alerts().Add("Unit copied to clipboard", tracker.Info)
|
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)")
|
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()
|
text := t.Units().SelectedType()
|
||||||
if text == "" {
|
if text == "" {
|
||||||
text = "Choose unit type"
|
text = "Choose unit type"
|
||||||
|
Reference in New Issue
Block a user