mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-14 11:04:23 -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]
|
||||
### 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
|
||||
|
@ -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)
|
||||
|
||||
|
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
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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 {
|
||||
|
@ -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"
|
||||
|
Reference in New Issue
Block a user