mirror of
https://github.com/vsariola/sointu.git
synced 2025-11-13 05:12:47 -05:00
refactor(tracker/gioui): use gioui/x/explorer instead of home made file explorer
This commit is contained in:
parent
8c4f7ee61f
commit
3da62179e4
@ -1,279 +0,0 @@
|
|||||||
package gioui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"gioui.org/io/pointer"
|
|
||||||
"gioui.org/layout"
|
|
||||||
"gioui.org/op/clip"
|
|
||||||
"gioui.org/op/paint"
|
|
||||||
"gioui.org/unit"
|
|
||||||
"gioui.org/widget"
|
|
||||||
"gioui.org/widget/material"
|
|
||||||
"golang.org/x/exp/shiny/materialdesign/icons"
|
|
||||||
)
|
|
||||||
|
|
||||||
type FileDialog struct {
|
|
||||||
Visible bool
|
|
||||||
Directory widget.Editor
|
|
||||||
FileList layout.List
|
|
||||||
FileName widget.Editor
|
|
||||||
BtnFolderUp widget.Clickable
|
|
||||||
BtnOk widget.Clickable
|
|
||||||
BtnCancel widget.Clickable
|
|
||||||
UseAltExt widget.Bool
|
|
||||||
ScrollBar ScrollBar
|
|
||||||
selectedFiles []string
|
|
||||||
tags []bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type FileDialogStyle struct {
|
|
||||||
dialog *FileDialog
|
|
||||||
save bool
|
|
||||||
Title string
|
|
||||||
DirEditorStyle material.EditorStyle
|
|
||||||
FileNameStyle material.EditorStyle
|
|
||||||
FolderUpStyle material.IconButtonStyle
|
|
||||||
OkStyle material.ButtonStyle
|
|
||||||
CancelStyle material.ButtonStyle
|
|
||||||
UseAltExtStyle material.SwitchStyle
|
|
||||||
ExtMain string
|
|
||||||
ExtAlt string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewFileDialog() *FileDialog {
|
|
||||||
ret := &FileDialog{
|
|
||||||
Directory: widget.Editor{SingleLine: true, Submit: true},
|
|
||||||
FileName: widget.Editor{SingleLine: true, Submit: true},
|
|
||||||
FileList: layout.List{Axis: layout.Vertical},
|
|
||||||
ScrollBar: ScrollBar{Axis: layout.Vertical},
|
|
||||||
}
|
|
||||||
wd, _ := os.Getwd()
|
|
||||||
ret.Directory.SetText(wd)
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveFileDialog(th *material.Theme, f *FileDialog) FileDialogStyle {
|
|
||||||
ret := commonFileDialog(th, f)
|
|
||||||
ret.save = true
|
|
||||||
ret.Title = "Save As"
|
|
||||||
ret.OkStyle = material.Button(th, &f.BtnOk, "Save")
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func OpenFileDialog(th *material.Theme, f *FileDialog) FileDialogStyle {
|
|
||||||
ret := commonFileDialog(th, f)
|
|
||||||
ret.OkStyle = material.Button(th, &f.BtnOk, "Open")
|
|
||||||
ret.Title = "Open File"
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func commonFileDialog(th *material.Theme, f *FileDialog) FileDialogStyle {
|
|
||||||
ret := FileDialogStyle{
|
|
||||||
dialog: f,
|
|
||||||
FolderUpStyle: IconButton(th, &f.BtnFolderUp, icons.NavigationArrowUpward, true),
|
|
||||||
DirEditorStyle: material.Editor(th, &f.Directory, "Directory"),
|
|
||||||
FileNameStyle: material.Editor(th, &f.FileName, "Filename"),
|
|
||||||
CancelStyle: LowEmphasisButton(th, &f.BtnCancel, "Cancel"),
|
|
||||||
UseAltExtStyle: material.Switch(th, &f.UseAltExt, "Change extension"),
|
|
||||||
}
|
|
||||||
ret.UseAltExtStyle.Color.Enabled = white
|
|
||||||
ret.UseAltExtStyle.Color.Disabled = white
|
|
||||||
ret.ExtMain = ".yml"
|
|
||||||
ret.ExtAlt = ".json"
|
|
||||||
return ret
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *FileDialog) FileSelected() (bool, string) {
|
|
||||||
if len(d.selectedFiles) > 0 {
|
|
||||||
var filePath string
|
|
||||||
filePath, d.selectedFiles = d.selectedFiles[0], d.selectedFiles[1:]
|
|
||||||
return true, filePath
|
|
||||||
}
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *FileDialogStyle) Layout(gtx C) D {
|
|
||||||
if f.dialog.Visible {
|
|
||||||
for f.dialog.BtnCancel.Clicked() {
|
|
||||||
f.dialog.Visible = false
|
|
||||||
}
|
|
||||||
if n := f.dialog.FileName.Text(); len(n) > 0 {
|
|
||||||
for f.dialog.UseAltExt.Changed() {
|
|
||||||
var extension = filepath.Ext(n)
|
|
||||||
n = n[0 : len(n)-len(extension)]
|
|
||||||
switch f.dialog.UseAltExt.Value {
|
|
||||||
case true:
|
|
||||||
n += f.ExtAlt
|
|
||||||
default:
|
|
||||||
n += f.ExtMain
|
|
||||||
}
|
|
||||||
f.dialog.FileName.SetText(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fullFile := filepath.Join(f.dialog.Directory.Text(), f.dialog.FileName.Text())
|
|
||||||
if _, err := os.Stat(fullFile); (f.save || !os.IsNotExist(err)) && f.dialog.FileName.Text() != "" {
|
|
||||||
for f.dialog.BtnOk.Clicked() {
|
|
||||||
f.dialog.selectedFiles = append(f.dialog.selectedFiles, fullFile)
|
|
||||||
f.dialog.Visible = false
|
|
||||||
}
|
|
||||||
f.OkStyle.Color = black
|
|
||||||
f.OkStyle.Background = primaryColor
|
|
||||||
} else {
|
|
||||||
f.OkStyle.Color = mediumEmphasisTextColor
|
|
||||||
f.OkStyle.Background = inactiveSelectionColor
|
|
||||||
}
|
|
||||||
parent := filepath.Dir(f.dialog.Directory.Text())
|
|
||||||
info, err := os.Stat(parent)
|
|
||||||
if err == nil && info.IsDir() && parent != "." {
|
|
||||||
for f.dialog.BtnFolderUp.Clicked() {
|
|
||||||
f.dialog.Directory.SetText(parent)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
f.FolderUpStyle.Color = disabledTextColor
|
|
||||||
}
|
|
||||||
|
|
||||||
var subDirs, files []string
|
|
||||||
dirList, err := ioutil.ReadDir(f.dialog.Directory.Text())
|
|
||||||
if err == nil {
|
|
||||||
for _, file := range dirList {
|
|
||||||
if file.IsDir() {
|
|
||||||
subDirs = append(subDirs, file.Name())
|
|
||||||
} else {
|
|
||||||
if f.dialog.UseAltExt.Value && filepath.Ext(file.Name()) == f.ExtAlt {
|
|
||||||
files = append(files, file.Name())
|
|
||||||
} else if !f.dialog.UseAltExt.Value && filepath.Ext(file.Name()) == f.ExtMain {
|
|
||||||
files = append(files, file.Name())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
listLen := len(subDirs) + len(files)
|
|
||||||
listElement := func(gtx C, index int) D {
|
|
||||||
for len(f.dialog.tags) <= index {
|
|
||||||
f.dialog.tags = append(f.dialog.tags, false)
|
|
||||||
}
|
|
||||||
for _, ev := range gtx.Events(&f.dialog.tags[index]) {
|
|
||||||
e, ok := ev.(pointer.Event)
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch e.Type {
|
|
||||||
case pointer.Press:
|
|
||||||
if index < len(subDirs) {
|
|
||||||
f.dialog.Directory.SetText(filepath.Join(f.dialog.Directory.Text(), subDirs[index]))
|
|
||||||
} else {
|
|
||||||
f.dialog.FileName.SetText(files[index-len(subDirs)])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var icon *widget.Icon
|
|
||||||
var text string
|
|
||||||
if index < len(subDirs) {
|
|
||||||
icon = widgetForIcon(icons.FileFolder)
|
|
||||||
text = subDirs[index]
|
|
||||||
} else {
|
|
||||||
icon = widgetForIcon(icons.EditorInsertDriveFile)
|
|
||||||
text = files[index-len(subDirs)]
|
|
||||||
}
|
|
||||||
labelColor := highEmphasisTextColor
|
|
||||||
if text == f.dialog.FileName.Text() {
|
|
||||||
labelColor = white
|
|
||||||
}
|
|
||||||
return layout.Stack{}.Layout(gtx,
|
|
||||||
layout.Stacked(func(gtx C) D {
|
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
|
||||||
layout.Rigid(func(gtx C) D {
|
|
||||||
p := gtx.Dp(unit.Dp(24))
|
|
||||||
gtx.Constraints.Min = image.Pt(p, p)
|
|
||||||
return icon.Layout(gtx, primaryColor)
|
|
||||||
}),
|
|
||||||
layout.Rigid(Label(text, labelColor)),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
layout.Expanded(func(gtx C) D {
|
|
||||||
rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y)
|
|
||||||
area := clip.Rect(rect).Push(gtx.Ops)
|
|
||||||
pointer.InputOp{Tag: &f.dialog.tags[index],
|
|
||||||
Types: pointer.Press | pointer.Drag | pointer.Release,
|
|
||||||
}.Add(gtx.Ops)
|
|
||||||
area.Pop()
|
|
||||||
return D{}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
paint.Fill(gtx.Ops, dialogBgColor)
|
|
||||||
return layout.Center.Layout(gtx, func(gtx C) D {
|
|
||||||
return Popup(&f.dialog.Visible).Layout(gtx, func(gtx C) D {
|
|
||||||
return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx C) D {
|
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
|
||||||
layout.Rigid(Label(f.Title, white)),
|
|
||||||
layout.Rigid(func(gtx C) D {
|
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
|
||||||
layout.Rigid(f.FolderUpStyle.Layout),
|
|
||||||
layout.Rigid(func(gtx C) D {
|
|
||||||
return D{Size: image.Pt(gtx.Dp(unit.Dp(6)), gtx.Dp(unit.Dp(36)))}
|
|
||||||
}),
|
|
||||||
layout.Rigid(f.DirEditorStyle.Layout))
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx C) D {
|
|
||||||
return layout.Stack{Alignment: layout.NE}.Layout(gtx,
|
|
||||||
layout.Stacked(func(gtx C) D {
|
|
||||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(unit.Dp(600)), gtx.Dp(unit.Dp(400))))
|
|
||||||
if listLen > 0 {
|
|
||||||
return f.dialog.FileList.Layout(gtx, listLen, listElement)
|
|
||||||
} else {
|
|
||||||
return D{Size: gtx.Constraints.Min}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
layout.Expanded(func(gtx C) D {
|
|
||||||
return f.dialog.ScrollBar.Layout(gtx, unit.Dp(10), listLen, &f.dialog.FileList.Position)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx C) D {
|
|
||||||
gtx.Constraints.Min.Y = gtx.Dp(unit.Dp(36))
|
|
||||||
return layout.W.Layout(gtx, f.FileNameStyle.Layout)
|
|
||||||
}),
|
|
||||||
layout.Rigid(func(gtx C) D {
|
|
||||||
gtx.Constraints = layout.Exact(image.Pt(gtx.Dp(unit.Dp(600)), gtx.Dp(unit.Dp(36))))
|
|
||||||
if f.ExtAlt != "" {
|
|
||||||
mainLabelColor := disabledTextColor
|
|
||||||
altLabelColor := disabledTextColor
|
|
||||||
if f.UseAltExtStyle.Switch.Value {
|
|
||||||
altLabelColor = white
|
|
||||||
} else {
|
|
||||||
mainLabelColor = white
|
|
||||||
}
|
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
|
||||||
layout.Rigid(Label(f.ExtMain, mainLabelColor)),
|
|
||||||
layout.Rigid(f.UseAltExtStyle.Layout),
|
|
||||||
layout.Rigid(Label(f.ExtAlt, altLabelColor)),
|
|
||||||
layout.Flexed(1, func(gtx C) D {
|
|
||||||
return D{Size: image.Pt(100, 1)}
|
|
||||||
}),
|
|
||||||
layout.Rigid(f.OkStyle.Layout),
|
|
||||||
layout.Rigid(f.CancelStyle.Layout),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx,
|
|
||||||
layout.Flexed(1, func(gtx C) D {
|
|
||||||
return D{Size: image.Pt(100, 1)}
|
|
||||||
}),
|
|
||||||
layout.Rigid(f.OkStyle.Layout),
|
|
||||||
layout.Rigid(f.CancelStyle.Layout),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return D{}
|
|
||||||
}
|
|
||||||
@ -7,7 +7,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -22,68 +23,76 @@ func (t *Tracker) OpenSongFile(forced bool) {
|
|||||||
t.ConfirmSongDialog.Visible = true
|
t.ConfirmSongDialog.Visible = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if p := t.FilePath(); p != "" {
|
reader, err := t.Explorer.ChooseFile(".yml", ".json")
|
||||||
d, _ := filepath.Split(p)
|
if err != nil {
|
||||||
d = filepath.Clean(d)
|
return
|
||||||
t.OpenSongDialog.Directory.SetText(d)
|
|
||||||
t.OpenSongDialog.FileName.SetText("")
|
|
||||||
}
|
}
|
||||||
t.OpenSongDialog.Visible = true
|
t.loadSong(reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) SaveSongFile() bool {
|
func (t *Tracker) SaveSongFile() bool {
|
||||||
if p := t.FilePath(); p != "" {
|
if p := t.FilePath(); p != "" {
|
||||||
return t.saveSong(p)
|
if f, err := os.Open(p); err == nil {
|
||||||
|
return t.saveSong(f)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
t.SaveSongAsFile()
|
t.SaveSongAsFile()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) SaveSongAsFile() {
|
func (t *Tracker) SaveSongAsFile() {
|
||||||
t.SaveSongDialog.Visible = true
|
p := t.FilePath()
|
||||||
if p := t.FilePath(); p != "" {
|
if p == "" {
|
||||||
d, f := filepath.Split(p)
|
p = "song.yml"
|
||||||
d = filepath.Clean(d)
|
|
||||||
t.SaveSongDialog.Directory.SetText(d)
|
|
||||||
t.SaveSongDialog.FileName.SetText(f)
|
|
||||||
}
|
}
|
||||||
|
writer, err := t.Explorer.CreateFile(p)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.saveSong(writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) ExportWav() {
|
func (t *Tracker) ExportWav(pcm16 bool) {
|
||||||
t.ExportWavDialog.Visible = true
|
filename := "song.wav"
|
||||||
if p := t.FilePath(); p != "" {
|
if p := t.FilePath(); p != "" {
|
||||||
d, _ := filepath.Split(p)
|
filename = p[:len(p)-len(filepath.Ext(p))] + ".wav"
|
||||||
d = filepath.Clean(d)
|
|
||||||
t.ExportWavDialog.Directory.SetText(d)
|
|
||||||
}
|
}
|
||||||
|
writer, err := t.Explorer.CreateFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.exportWav(writer, pcm16)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) LoadInstrument() {
|
func (t *Tracker) LoadInstrument() {
|
||||||
t.OpenInstrumentDialog.Visible = true
|
reader, err := t.Explorer.ChooseFile(".yml", ".json", ".4ki", ".4kp")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.loadInstrument(reader)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) SaveInstrument() {
|
func (t *Tracker) SaveInstrument() {
|
||||||
t.SaveInstrumentDialog.Visible = true
|
writer, err := t.Explorer.CreateFile(t.Instrument().Name + ".yml")
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.saveInstrument(writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) loadSong(filename string) {
|
func (t *Tracker) loadSong(r io.ReadCloser) {
|
||||||
b, err := ioutil.ReadFile(filename)
|
b, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = r.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var song sointu.Song
|
var song sointu.Song
|
||||||
if errJSON := json.Unmarshal(b, &song); errJSON != nil {
|
if errJSON := json.Unmarshal(b, &song); errJSON != nil {
|
||||||
if errYaml := yaml.Unmarshal(b, &song); errYaml != nil {
|
if errYaml := yaml.Unmarshal(b, &song); errYaml != nil {
|
||||||
var err4kp error
|
t.Alert.Update(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error, time.Second*3)
|
||||||
var patch sointu.Patch
|
|
||||||
if patch, err4kp = sointu.Read4klangPatch(bytes.NewReader(b)); err4kp != nil {
|
|
||||||
t.Alert.Update(fmt.Sprintf("Error unmarshaling a song file: %v / %v / %v", errYaml, errJSON, err4kp), Error, time.Second*3)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
song = t.Song()
|
|
||||||
song.Score = t.Song().Score.Copy()
|
|
||||||
song.Patch = patch
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if song.Score.Length <= 0 || len(song.Score.Tracks) == 0 || len(song.Patch) == 0 {
|
if song.Score.Length <= 0 || len(song.Score.Tracks) == 0 || len(song.Patch) == 0 {
|
||||||
@ -91,13 +100,21 @@ func (t *Tracker) loadSong(filename string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
t.SetSong(song)
|
t.SetSong(song)
|
||||||
t.SetFilePath(filename)
|
path := ""
|
||||||
|
if f, ok := r.(*os.File); ok {
|
||||||
|
path = f.Name()
|
||||||
|
}
|
||||||
|
t.SetFilePath(path)
|
||||||
t.ClearUndoHistory()
|
t.ClearUndoHistory()
|
||||||
t.SetChangedSinceSave(false)
|
t.SetChangedSinceSave(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) saveSong(filename string) bool {
|
func (t *Tracker) saveSong(w io.WriteCloser) bool {
|
||||||
var extension = filepath.Ext(filename)
|
path := ""
|
||||||
|
if f, ok := w.(*os.File); ok {
|
||||||
|
path = f.Name()
|
||||||
|
}
|
||||||
|
var extension = filepath.Ext(path)
|
||||||
var contents []byte
|
var contents []byte
|
||||||
var err error
|
var err error
|
||||||
if extension == ".json" {
|
if extension == ".json" {
|
||||||
@ -109,20 +126,14 @@ func (t *Tracker) saveSong(filename string) bool {
|
|||||||
t.Alert.Update(fmt.Sprintf("Error marshaling a song file: %v", err), Error, time.Second*3)
|
t.Alert.Update(fmt.Sprintf("Error marshaling a song file: %v", err), Error, time.Second*3)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if extension == "" {
|
w.Write(contents)
|
||||||
filename = filename + ".yml"
|
w.Close()
|
||||||
}
|
t.SetFilePath(path)
|
||||||
ioutil.WriteFile(filename, contents, 0644)
|
|
||||||
t.SetFilePath(filename)
|
|
||||||
t.SetChangedSinceSave(false)
|
t.SetChangedSinceSave(false)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) exportWav(filename string, pcm16 bool) {
|
func (t *Tracker) exportWav(w io.WriteCloser, pcm16 bool) {
|
||||||
var extension = filepath.Ext(filename)
|
|
||||||
if extension == "" {
|
|
||||||
filename = filename + ".wav"
|
|
||||||
}
|
|
||||||
data, err := sointu.Play(t.synthService, t.Song(), true) // render the song to calculate its length
|
data, err := sointu.Play(t.synthService, t.Song(), true) // render the song to calculate its length
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Alert.Update(fmt.Sprintf("Error rendering the song during export: %v", err), Error, time.Second*3)
|
t.Alert.Update(fmt.Sprintf("Error rendering the song during export: %v", err), Error, time.Second*3)
|
||||||
@ -133,11 +144,16 @@ func (t *Tracker) exportWav(filename string, pcm16 bool) {
|
|||||||
t.Alert.Update(fmt.Sprintf("Error converting to .wav: %v", err), Error, time.Second*3)
|
t.Alert.Update(fmt.Sprintf("Error converting to .wav: %v", err), Error, time.Second*3)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ioutil.WriteFile(filename, buffer, 0644)
|
w.Write(buffer)
|
||||||
|
w.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) saveInstrument(filename string) bool {
|
func (t *Tracker) saveInstrument(w io.WriteCloser) bool {
|
||||||
var extension = filepath.Ext(filename)
|
path := ""
|
||||||
|
if f, ok := w.(*os.File); ok {
|
||||||
|
path = f.Name()
|
||||||
|
}
|
||||||
|
var extension = filepath.Ext(path)
|
||||||
var contents []byte
|
var contents []byte
|
||||||
var err error
|
var err error
|
||||||
if extension == ".json" {
|
if extension == ".json" {
|
||||||
@ -149,30 +165,47 @@ func (t *Tracker) saveInstrument(filename string) bool {
|
|||||||
t.Alert.Update(fmt.Sprintf("Error marshaling a ínstrument file: %v", err), Error, time.Second*3)
|
t.Alert.Update(fmt.Sprintf("Error marshaling a ínstrument file: %v", err), Error, time.Second*3)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if extension == "" {
|
w.Write(contents)
|
||||||
filename = filename + ".yml"
|
w.Close()
|
||||||
}
|
|
||||||
ioutil.WriteFile(filename, contents, 0644)
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) loadInstrument(filename string) bool {
|
func (t *Tracker) loadInstrument(r io.ReadCloser) bool {
|
||||||
b, err := ioutil.ReadFile(filename)
|
b, err := io.ReadAll(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
var instrument sointu.Instrument
|
var instrument sointu.Instrument
|
||||||
if errJSON := json.Unmarshal(b, &instrument); errJSON != nil {
|
var errJSON, errYaml, err4ki, err4kp error
|
||||||
if errYaml := yaml.Unmarshal(b, &instrument); errYaml != nil {
|
var patch sointu.Patch
|
||||||
var err4ki error
|
errJSON = json.Unmarshal(b, &instrument)
|
||||||
if instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b)); err4ki != nil {
|
if errJSON == nil {
|
||||||
t.Alert.Update(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v / %v", errYaml, errJSON, err4ki), Error, time.Second*3)
|
goto success
|
||||||
return false
|
}
|
||||||
}
|
errYaml = yaml.Unmarshal(b, &instrument)
|
||||||
}
|
if errYaml == nil {
|
||||||
|
goto success
|
||||||
|
}
|
||||||
|
patch, err4kp = sointu.Read4klangPatch(bytes.NewReader(b))
|
||||||
|
if err4kp == nil {
|
||||||
|
song := t.Song()
|
||||||
|
song.Score = t.Song().Score.Copy()
|
||||||
|
song.Patch = patch
|
||||||
|
t.SetSong(song)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
instrument, err4ki = sointu.Read4klangInstrument(bytes.NewReader(b))
|
||||||
|
if err4ki == nil {
|
||||||
|
goto success
|
||||||
|
}
|
||||||
|
t.Alert.Update(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v / %v / %v", errYaml, errJSON, err4ki, err4kp), Error, time.Second*3)
|
||||||
|
return false
|
||||||
|
success:
|
||||||
|
if f, ok := r.(*os.File); ok {
|
||||||
|
filename := f.Name()
|
||||||
|
// the 4klang instrument names are junk, replace them with the filename without extension
|
||||||
|
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
|
||||||
}
|
}
|
||||||
// the 4klang instrument names are junk, replace them with the filename without extension
|
|
||||||
instrument.Name = filepath.Base(filename[:len(filename)-len(filepath.Ext(filename))])
|
|
||||||
if len(instrument.Units) == 0 {
|
if len(instrument.Units) == 0 {
|
||||||
t.Alert.Update("The instrument file is malformed", Error, time.Second*3)
|
t.Alert.Update("The instrument file is malformed", Error, time.Second*3)
|
||||||
return false
|
return false
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
package gioui
|
|
||||||
|
|
||||||
func (t *Tracker) LoadSongFile() {
|
|
||||||
// TODO: how to load songs in browser, just disabling it for now
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tracker) SaveSongFile() {
|
|
||||||
// TODO: how to save songs in browser, just disabling it for now
|
|
||||||
}
|
|
||||||
@ -48,13 +48,6 @@ var noteMap = map[string]int{
|
|||||||
// 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, o *op.Ops) {
|
func (t *Tracker) KeyEvent(e key.Event, o *op.Ops) {
|
||||||
if e.State == key.Press {
|
if e.State == key.Press {
|
||||||
if t.OpenSongDialog.Visible ||
|
|
||||||
t.SaveSongDialog.Visible ||
|
|
||||||
t.SaveInstrumentDialog.Visible ||
|
|
||||||
t.OpenInstrumentDialog.Visible ||
|
|
||||||
t.ExportWavDialog.Visible {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch e.Name {
|
switch e.Name {
|
||||||
case "C":
|
case "C":
|
||||||
if e.Modifiers.Contain(key.ModShortcut) {
|
if e.Modifiers.Contain(key.ModShortcut) {
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package gioui
|
package gioui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"image"
|
"image"
|
||||||
|
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
@ -63,55 +62,16 @@ func (t *Tracker) Layout(gtx layout.Context, w *app.Window) {
|
|||||||
dstyle.AltStyle.Text = "Float32"
|
dstyle.AltStyle.Text = "Float32"
|
||||||
dstyle.Layout(gtx)
|
dstyle.Layout(gtx)
|
||||||
for t.WaveTypeDialog.BtnOk.Clicked() {
|
for t.WaveTypeDialog.BtnOk.Clicked() {
|
||||||
t.exportWav(t.wavFilePath, true)
|
t.ExportWav(true)
|
||||||
t.WaveTypeDialog.Visible = false
|
t.WaveTypeDialog.Visible = false
|
||||||
}
|
}
|
||||||
for t.WaveTypeDialog.BtnAlt.Clicked() {
|
for t.WaveTypeDialog.BtnAlt.Clicked() {
|
||||||
t.exportWav(t.wavFilePath, false)
|
t.ExportWav(false)
|
||||||
t.WaveTypeDialog.Visible = false
|
t.WaveTypeDialog.Visible = false
|
||||||
}
|
}
|
||||||
for t.WaveTypeDialog.BtnCancel.Clicked() {
|
for t.WaveTypeDialog.BtnCancel.Clicked() {
|
||||||
t.WaveTypeDialog.Visible = false
|
t.WaveTypeDialog.Visible = false
|
||||||
}
|
}
|
||||||
fstyle := OpenFileDialog(t.Theme, t.OpenSongDialog)
|
|
||||||
fstyle.Title = "Open Song File"
|
|
||||||
fstyle.ExtAlt = ".4kp"
|
|
||||||
fstyle.Layout(gtx)
|
|
||||||
for ok, file := t.OpenSongDialog.FileSelected(); ok; ok, file = t.OpenSongDialog.FileSelected() {
|
|
||||||
t.loadSong(file)
|
|
||||||
}
|
|
||||||
fstyle = SaveFileDialog(t.Theme, t.SaveSongDialog)
|
|
||||||
fstyle.Title = "Save Song As"
|
|
||||||
for ok, file := t.SaveSongDialog.FileSelected(); ok; ok, file = t.SaveSongDialog.FileSelected() {
|
|
||||||
t.saveSong(file)
|
|
||||||
}
|
|
||||||
fstyle.Layout(gtx)
|
|
||||||
exportWavDialogStyle := SaveFileDialog(t.Theme, t.ExportWavDialog)
|
|
||||||
exportWavDialogStyle.Title = "Export Song As Wav"
|
|
||||||
for ok, file := t.ExportWavDialog.FileSelected(); ok; ok, file = t.ExportWavDialog.FileSelected() {
|
|
||||||
t.wavFilePath = file
|
|
||||||
t.WaveTypeDialog.Visible = true
|
|
||||||
}
|
|
||||||
exportWavDialogStyle.ExtMain = ".wav"
|
|
||||||
exportWavDialogStyle.ExtAlt = ""
|
|
||||||
exportWavDialogStyle.Layout(gtx)
|
|
||||||
fstyle = SaveFileDialog(t.Theme, t.SaveInstrumentDialog)
|
|
||||||
fstyle.Title = "Save Instrument As"
|
|
||||||
if t.SaveInstrumentDialog.Visible && t.Instrument().Name != "" {
|
|
||||||
fstyle.Title = fmt.Sprintf("Save Instrument \"%v\" As", t.Instrument().Name)
|
|
||||||
}
|
|
||||||
for ok, file := t.SaveInstrumentDialog.FileSelected(); ok; ok, file = t.SaveInstrumentDialog.FileSelected() {
|
|
||||||
t.saveInstrument(file)
|
|
||||||
t.OpenInstrumentDialog.Directory.SetText(t.SaveInstrumentDialog.Directory.Text())
|
|
||||||
}
|
|
||||||
fstyle.Layout(gtx)
|
|
||||||
fstyle = OpenFileDialog(t.Theme, t.OpenInstrumentDialog)
|
|
||||||
fstyle.Title = "Open Instrument File"
|
|
||||||
fstyle.ExtAlt = ".4ki"
|
|
||||||
for ok, file := t.OpenInstrumentDialog.FileSelected(); ok; ok, file = t.OpenInstrumentDialog.FileSelected() {
|
|
||||||
t.loadInstrument(file)
|
|
||||||
}
|
|
||||||
fstyle.Layout(gtx)
|
|
||||||
if t.ModalDialog != nil {
|
if t.ModalDialog != nil {
|
||||||
t.ModalDialog(gtx)
|
t.ModalDialog(gtx)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,7 +75,7 @@ func (t *Tracker) layoutMenuBar(gtx C) D {
|
|||||||
case 3:
|
case 3:
|
||||||
t.SaveSongAsFile()
|
t.SaveSongAsFile()
|
||||||
case 4:
|
case 4:
|
||||||
t.ExportWav()
|
t.WaveTypeDialog.Visible = true
|
||||||
case 5:
|
case 5:
|
||||||
t.Quit(false)
|
t.Quit(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import (
|
|||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"gioui.org/widget"
|
"gioui.org/widget"
|
||||||
"gioui.org/widget/material"
|
"gioui.org/widget/material"
|
||||||
|
"gioui.org/x/explorer"
|
||||||
"github.com/vsariola/sointu"
|
"github.com/vsariola/sointu"
|
||||||
"github.com/vsariola/sointu/tracker"
|
"github.com/vsariola/sointu/tracker"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@ -47,16 +48,12 @@ type Tracker struct {
|
|||||||
Alert Alert
|
Alert Alert
|
||||||
ConfirmSongDialog *Dialog
|
ConfirmSongDialog *Dialog
|
||||||
WaveTypeDialog *Dialog
|
WaveTypeDialog *Dialog
|
||||||
OpenSongDialog *FileDialog
|
|
||||||
SaveSongDialog *FileDialog
|
|
||||||
OpenInstrumentDialog *FileDialog
|
|
||||||
SaveInstrumentDialog *FileDialog
|
|
||||||
ExportWavDialog *FileDialog
|
|
||||||
ConfirmSongActionType int
|
ConfirmSongActionType int
|
||||||
ModalDialog layout.Widget
|
ModalDialog layout.Widget
|
||||||
InstrumentEditor *InstrumentEditor
|
InstrumentEditor *InstrumentEditor
|
||||||
OrderEditor *OrderEditor
|
OrderEditor *OrderEditor
|
||||||
TrackEditor *TrackEditor
|
TrackEditor *TrackEditor
|
||||||
|
Explorer *explorer.Explorer
|
||||||
|
|
||||||
lastVolume tracker.Volume
|
lastVolume tracker.Volume
|
||||||
|
|
||||||
@ -116,21 +113,16 @@ func NewTracker(model *tracker.Model, synthService sointu.SynthService) *Tracker
|
|||||||
BottomHorizontalSplit: &Split{Ratio: -.6},
|
BottomHorizontalSplit: &Split{Ratio: -.6},
|
||||||
VerticalSplit: &Split{Axis: layout.Vertical},
|
VerticalSplit: &Split{Axis: layout.Vertical},
|
||||||
|
|
||||||
KeyPlaying: make(map[string]tracker.NoteID),
|
KeyPlaying: make(map[string]tracker.NoteID),
|
||||||
ConfirmSongDialog: new(Dialog),
|
ConfirmSongDialog: new(Dialog),
|
||||||
WaveTypeDialog: new(Dialog),
|
WaveTypeDialog: new(Dialog),
|
||||||
OpenSongDialog: NewFileDialog(),
|
InstrumentEditor: NewInstrumentEditor(),
|
||||||
SaveSongDialog: NewFileDialog(),
|
OrderEditor: NewOrderEditor(),
|
||||||
OpenInstrumentDialog: NewFileDialog(),
|
TrackEditor: NewTrackEditor(),
|
||||||
SaveInstrumentDialog: NewFileDialog(),
|
|
||||||
InstrumentEditor: NewInstrumentEditor(),
|
|
||||||
OrderEditor: NewOrderEditor(),
|
|
||||||
TrackEditor: NewTrackEditor(),
|
|
||||||
|
|
||||||
ExportWavDialog: NewFileDialog(),
|
errorChannel: make(chan error, 32),
|
||||||
errorChannel: make(chan error, 32),
|
synthService: synthService,
|
||||||
synthService: synthService,
|
Model: model,
|
||||||
Model: model,
|
|
||||||
}
|
}
|
||||||
t.Theme.Palette.Fg = primaryColor
|
t.Theme.Palette.Fg = primaryColor
|
||||||
t.Theme.Palette.ContrastFg = black
|
t.Theme.Palette.ContrastFg = black
|
||||||
@ -146,6 +138,7 @@ func (t *Tracker) Main() {
|
|||||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||||
app.Title("Sointu Tracker"),
|
app.Title("Sointu Tracker"),
|
||||||
)
|
)
|
||||||
|
t.Explorer = explorer.NewExplorer(w)
|
||||||
var ops op.Ops
|
var ops op.Ops
|
||||||
mainloop:
|
mainloop:
|
||||||
for {
|
for {
|
||||||
@ -189,6 +182,7 @@ mainloop:
|
|||||||
app.Size(unit.Dp(800), unit.Dp(600)),
|
app.Size(unit.Dp(800), unit.Dp(600)),
|
||||||
app.Title("Sointu Tracker"),
|
app.Title("Sointu Tracker"),
|
||||||
)
|
)
|
||||||
|
t.Explorer = explorer.NewExplorer(w)
|
||||||
}
|
}
|
||||||
case system.FrameEvent:
|
case system.FrameEvent:
|
||||||
gtx := layout.NewContext(&ops, e)
|
gtx := layout.NewContext(&ops, e)
|
||||||
|
|||||||
Reference in New Issue
Block a user