diff --git a/go.mod b/go.mod index 768a743..fe8903d 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.11 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect - github.com/sqweek/dialog v0.0.0-20200911184034-8a3d98e8211d github.com/stretchr/testify v1.6.1 // indirect golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 gopkg.in/yaml.v2 v2.3.0 diff --git a/go.sum b/go.sum index d128321..41c58cb 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/sprig v2.22.0+incompatible h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60= github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= -github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf h1:FPsprx82rdrX2jiKyS17BH6IrTmUBYqZa/CXT4uvb+I= -github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -27,8 +25,6 @@ github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/I github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sqweek/dialog v0.0.0-20200911184034-8a3d98e8211d h1:Chay1rwJnXxI27H+pzu7P81BKf647un9GOoRPTdXN18= -github.com/sqweek/dialog v0.0.0-20200911184034-8a3d98e8211d/go.mod h1:/qNPSY91qTz/8TgHEMioAUc6q7+3SOybeKczHMXFcXw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/tracker/gioui/filedialog.go b/tracker/gioui/filedialog.go new file mode 100644 index 0000000..919f7fc --- /dev/null +++ b/tracker/gioui/filedialog.go @@ -0,0 +1,282 @@ +package gioui + +import ( + "image" + "io/ioutil" + "os" + "path/filepath" + + "gioui.org/io/pointer" + "gioui.org/layout" + "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: material.IconButton(th, &f.BtnFolderUp, widgetForIcon(icons.NavigationArrowUpward)), + DirEditorStyle: material.Editor(th, &f.Directory, "Directory"), + FileNameStyle: material.Editor(th, &f.FileName, "Filename"), + CancelStyle: material.Button(th, &f.BtnCancel, "Cancel"), + UseAltExtStyle: material.Switch(th, &f.UseAltExt), + } + ret.CancelStyle.Background = transparent + ret.CancelStyle.Color = primaryColor + ret.FolderUpStyle.Inset = layout.UniformInset(unit.Dp(1)) + ret.FolderUpStyle.Color = primaryColor + ret.FolderUpStyle.Background = transparent + 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 += ".json" + default: + n += ".yml" + } + 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) + icon.Color = primaryColor + text = subDirs[index] + } else { + icon = widgetForIcon(icons.EditorInsertDriveFile) + icon.Color = primaryColor + 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 { + return icon.Layout(gtx, unit.Dp(24)) + }), + layout.Rigid(Label(text, labelColor)), + ) + }), + layout.Expanded(func(gtx C) D { + rect := image.Rect(0, 0, gtx.Constraints.Min.X, gtx.Constraints.Min.Y) + pointer.Rect(rect).Add(gtx.Ops) + pointer.InputOp{Tag: &f.dialog.tags[index], + Types: pointer.Press | pointer.Drag | pointer.Release, + }.Add(gtx.Ops) + 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.Px(unit.Dp(6)), gtx.Px(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.Px(unit.Dp(600)), gtx.Px(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.Px(unit.Dp(36)) + return layout.W.Layout(gtx, f.FileNameStyle.Layout) + }), + layout.Rigid(func(gtx C) D { + gtx.Constraints = layout.Exact(image.Pt(gtx.Px(unit.Dp(600)), gtx.Px(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{} +} diff --git a/tracker/gioui/files.go b/tracker/gioui/files.go index af124b7..8ce9428 100644 --- a/tracker/gioui/files.go +++ b/tracker/gioui/files.go @@ -12,39 +12,60 @@ import ( "gioui.org/app" "gopkg.in/yaml.v3" - "github.com/sqweek/dialog" "github.com/vsariola/sointu" ) -func (t *Tracker) LoadSongFile() { - if t.ChangedSinceSave() { +func (t *Tracker) OpenSongFile(forced bool) { + if !forced && t.ChangedSinceSave() { t.ConfirmSongActionType = ConfirmLoad t.ConfirmSongDialog.Visible = true return } - t.loadSong() + if p := t.FilePath(); p != "" { + d, _ := filepath.Split(p) + d = filepath.Clean(d) + t.OpenSongDialog.Directory.SetText(d) + t.OpenSongDialog.FileName.SetText("") + } + t.OpenSongDialog.Visible = true } func (t *Tracker) SaveSongFile() bool { if p := t.FilePath(); p != "" { return t.saveSong(p) } - return t.SaveSongAsFile() + t.SaveSongAsFile() + return false } -func (t *Tracker) SaveSongAsFile() bool { - filename, err := dialog.File().Filter("Sointu YAML song", "yml").Filter("Sointu JSON song", "json").Title("Save song").Save() - if err != nil { - return false +func (t *Tracker) SaveSongAsFile() { + t.SaveSongDialog.Visible = true + if p := t.FilePath(); p != "" { + d, f := filepath.Split(p) + d = filepath.Clean(d) + t.SaveSongDialog.Directory.SetText(d) + t.SaveSongDialog.FileName.SetText(f) } - return t.saveSong(filename) } -func (t *Tracker) loadSong() { - filename, err := dialog.File().Filter("Sointu YAML song", "yml").Filter("Sointu JSON song", "json").Title("Load song").Load() - if err != nil { - return +func (t *Tracker) ExportWav() { + t.ExportWavDialog.Visible = true + if p := t.FilePath(); p != "" { + d, _ := filepath.Split(p) + d = filepath.Clean(d) + t.ExportWavDialog.Directory.SetText(d) } +} + +func (t *Tracker) LoadInstrument() { + t.OpenInstrumentDialog.Visible = true +} + +func (t *Tracker) SaveInstrument() { + t.SaveInstrumentDialog.Visible = true +} + +func (t *Tracker) loadSong(filename string) { bytes, err := ioutil.ReadFile(filename) if err != nil { return @@ -52,10 +73,12 @@ func (t *Tracker) loadSong() { var song sointu.Song if errJSON := json.Unmarshal(bytes, &song); errJSON != nil { if errYaml := yaml.Unmarshal(bytes, &song); errYaml != nil { + t.Alert.Update(fmt.Sprintf("Error unmarshaling a song file: %v / %v", errYaml, errJSON), Error, time.Second*3) return } } if song.Score.Length <= 0 || len(song.Score.Tracks) == 0 || len(song.Patch) == 0 { + t.Alert.Update("The song file is malformed", Error, time.Second*3) return } t.SetSong(song) @@ -69,12 +92,13 @@ func (t *Tracker) saveSong(filename string) bool { var extension = filepath.Ext(filename) var contents []byte var err error - if extension == "json" { + if extension == ".json" { contents, err = json.Marshal(t.Song()) } else { contents, err = yaml.Marshal(t.Song()) } if err != nil { + t.Alert.Update(fmt.Sprintf("Error marshaling a song file: %v", err), Error, time.Second*3) return false } if extension == "" { @@ -87,51 +111,7 @@ func (t *Tracker) saveSong(filename string) bool { return true } -func (t *Tracker) SaveInstrument() bool { - filename, err := dialog.File().Filter("Sointu YAML instrument", "yml").Filter("Sointu JSON song", "json").Title(fmt.Sprintf("Save instrument %v", t.Instrument().Name)).Save() - if err != nil { - return false - } - var extension = filepath.Ext(filename) - var contents []byte - if extension == "json" { - contents, err = json.Marshal(t.Instrument()) - } else { - contents, err = yaml.Marshal(t.Instrument()) - } - if err != nil { - return false - } - if extension == "" { - filename = filename + ".yml" - } - ioutil.WriteFile(filename, contents, 0644) - return true -} - -func (t *Tracker) LoadInstrument() { - filename, err := dialog.File().Filter("Sointu YAML instrument", "yml").Filter("Sointu JSON instrument", "json").Title("Load instrument").Load() - if err != nil { - return - } - bytes, err := ioutil.ReadFile(filename) - if err != nil { - return - } - var instrument sointu.Instrument - if errJSON := json.Unmarshal(bytes, &instrument); errJSON != nil { - if errYaml := yaml.Unmarshal(bytes, &instrument); errYaml != nil { - return - } - } - t.SetInstrument(instrument) -} - -func (t *Tracker) exportWav(pcm16 bool) { - filename, err := dialog.File().Filter(".wav file", "wav").Title("Export .wav").Save() - if err != nil { - return - } +func (t *Tracker) exportWav(filename string, pcm16 bool) { var extension = filepath.Ext(filename) if extension == "" { filename = filename + ".wav" @@ -156,3 +136,43 @@ func (t *Tracker) exportWav(pcm16 bool) { } ioutil.WriteFile(filename, buffer, 0644) } + +func (t *Tracker) saveInstrument(filename string) bool { + var extension = filepath.Ext(filename) + var contents []byte + var err error + if extension == ".json" { + contents, err = json.Marshal(t.Instrument()) + } else { + contents, err = yaml.Marshal(t.Instrument()) + } + if err != nil { + t.Alert.Update(fmt.Sprintf("Error marshaling a ínstrument file: %v", err), Error, time.Second*3) + return false + } + if extension == "" { + filename = filename + ".yml" + } + ioutil.WriteFile(filename, contents, 0644) + return true +} + +func (t *Tracker) loadInstrument(filename string) bool { + bytes, err := ioutil.ReadFile(filename) + if err != nil { + return false + } + var instrument sointu.Instrument + if errJSON := json.Unmarshal(bytes, &instrument); errJSON != nil { + if errYaml := yaml.Unmarshal(bytes, &instrument); errYaml != nil { + t.Alert.Update(fmt.Sprintf("Error unmarshaling an instrument file: %v / %v", errYaml, errJSON), Error, time.Second*3) + return false + } + } + if len(instrument.Units) == 0 { + t.Alert.Update("The instrument file is malformed", Error, time.Second*3) + return false + } + t.SetInstrument(instrument) + return true +} diff --git a/tracker/gioui/keyevent.go b/tracker/gioui/keyevent.go index fa9e349..d81fbfb 100644 --- a/tracker/gioui/keyevent.go +++ b/tracker/gioui/keyevent.go @@ -81,7 +81,12 @@ var unitKeyMap = map[string]string{ // KeyEvent handles incoming key events and returns true if repaint is needed. func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool { if e.State == key.Press { - if t.InstrumentNameEditor.Focused() { + if t.InstrumentNameEditor.Focused() || + t.OpenSongDialog.Visible || + t.SaveSongDialog.Visible || + t.SaveInstrumentDialog.Visible || + t.OpenInstrumentDialog.Visible || + t.ExportWavDialog.Visible { return false } switch e.Name { @@ -111,7 +116,7 @@ func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool { } case "N": if e.Modifiers.Contain(key.ModShortcut) { - t.TryResetSong() + t.NewSong(false) return true } case "S": @@ -121,7 +126,7 @@ func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool { } case "O": if e.Modifiers.Contain(key.ModShortcut) { - t.LoadSongFile() + t.OpenSongFile(false) return true } case "F1": diff --git a/tracker/gioui/layout.go b/tracker/gioui/layout.go index f242b73..8b4b4d6 100644 --- a/tracker/gioui/layout.go +++ b/tracker/gioui/layout.go @@ -1,6 +1,7 @@ package gioui import ( + "fmt" "image" "gioui.org/app" @@ -45,35 +46,68 @@ func (t *Tracker) Layout(gtx layout.Context) { dstyle.AltStyle.Text = "Float32" dstyle.Layout(gtx) for t.WaveTypeDialog.BtnOk.Clicked() { - t.exportWav(true) + t.exportWav(t.wavFilePath, true) t.WaveTypeDialog.Visible = false } for t.WaveTypeDialog.BtnAlt.Clicked() { - t.exportWav(false) + t.exportWav(t.wavFilePath, false) t.WaveTypeDialog.Visible = false } for t.WaveTypeDialog.BtnCancel.Clicked() { t.WaveTypeDialog.Visible = false } + fstyle := OpenFileDialog(t.Theme, t.OpenSongDialog) + fstyle.Title = "Open Song File" + 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" + for ok, file := t.OpenInstrumentDialog.FileSelected(); ok; ok, file = t.OpenInstrumentDialog.FileSelected() { + t.loadInstrument(file) + } + fstyle.Layout(gtx) } func (t *Tracker) confirmedSongAction() { switch t.ConfirmSongActionType { case ConfirmLoad: - t.loadSong() + t.OpenSongFile(true) case ConfirmNew: - t.ResetSong() - t.SetFilePath("") - t.window.Option(app.Title("Sointu Tracker")) - t.ClearUndoHistory() - t.SetChangedSinceSave(false) + t.NewSong(true) case ConfirmQuit: - t.quitted = true + t.Quit(true) } } -func (t *Tracker) TryResetSong() { - if t.ChangedSinceSave() { +func (t *Tracker) NewSong(forced bool) { + if !forced && t.ChangedSinceSave() { t.ConfirmSongActionType = ConfirmNew t.ConfirmSongDialog.Visible = true return @@ -85,16 +119,6 @@ func (t *Tracker) TryResetSong() { t.SetChangedSinceSave(false) } -func (t *Tracker) TryQuit() bool { - if t.ChangedSinceSave() { - t.ConfirmSongActionType = ConfirmQuit - t.ConfirmSongDialog.Visible = true - return false - } - t.quitted = true - return true -} - func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions { return t.BottomHorizontalSplit.Layout(gtx, func(gtx C) D { diff --git a/tracker/gioui/popup.go b/tracker/gioui/popup.go index 4280d73..011e7f2 100644 --- a/tracker/gioui/popup.go +++ b/tracker/gioui/popup.go @@ -1,6 +1,7 @@ package gioui import ( + "image" "image/color" "gioui.org/f32" @@ -43,6 +44,7 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D { if !*s.Visible { return D{} } + for _, ev := range gtx.Events(s.Visible) { e, ok := ev.(pointer.Event) if !ok { @@ -56,9 +58,7 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D { } bg := func(gtx C) D { - pointer.InputOp{Tag: s.Visible, - Types: pointer.Press, - }.Add(gtx.Ops) + pointer.PassOp{Pass: false}.Add(gtx.Ops) rrect := clip.RRect{ Rect: f32.Rectangle{Max: f32.Pt(float32(gtx.Constraints.Min.X), float32(gtx.Constraints.Min.Y))}, SE: float32(gtx.Px(s.SE)), @@ -71,6 +71,20 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D { rrect2.Rect.Max = rrect2.Rect.Max.Add(f32.Pt(float32(gtx.Px(s.ShadowE)), float32(gtx.Px(s.ShadowS)))) paint.FillShape(gtx.Ops, s.ShadowColor, rrect2.Op(gtx.Ops)) paint.FillShape(gtx.Ops, s.SurfaceColor, rrect.Op(gtx.Ops)) + rect := image.Rect(int(rrect2.Rect.Min.X), int(rrect2.Rect.Min.Y), int(rrect2.Rect.Max.X), int(rrect2.Rect.Max.Y)) + state := op.Save(gtx.Ops) + pointer.InputOp{Tag: s.Visible, + Types: pointer.Press, + Grab: true, + }.Add(gtx.Ops) + state.Load() + state = op.Save(gtx.Ops) + pointer.Rect(rect).Add(gtx.Ops) + pointer.InputOp{Tag: dummyTag, + Types: pointer.Press, + Grab: true, + }.Add(gtx.Ops) + state.Load() return D{Size: gtx.Constraints.Min} } macro := op.Record(gtx.Ops) @@ -82,3 +96,5 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D { op.Defer(gtx.Ops, callop) return dims } + +var dummyTag bool diff --git a/tracker/gioui/run.go b/tracker/gioui/run.go index 20f6adb..1fc30f0 100644 --- a/tracker/gioui/run.go +++ b/tracker/gioui/run.go @@ -36,7 +36,7 @@ func (t *Tracker) Run(w *app.Window) error { case e := <-w.Events(): switch e := e.(type) { case system.DestroyEvent: - if !t.TryQuit() { + if !t.Quit(false) { // TODO: uh oh, there's no way of canceling the destroyevent in gioui? so we create a new window just to show the dialog w = app.NewWindow( app.Size(unit.Dp(800), unit.Dp(600)), diff --git a/tracker/gioui/songpanel.go b/tracker/gioui/songpanel.go index 431b44a..5668fcc 100644 --- a/tracker/gioui/songpanel.go +++ b/tracker/gioui/songpanel.go @@ -53,17 +53,17 @@ func (t *Tracker) layoutMenuBar(gtx C) D { for clickedItem, hasClicked := t.Menus[0].Clicked(); hasClicked; { switch clickedItem { case 0: - t.TryResetSong() + t.NewSong(false) case 1: - t.LoadSongFile() + t.OpenSongFile(false) case 2: t.SaveSongFile() case 3: t.SaveSongAsFile() case 4: - t.WaveTypeDialog.Visible = true + t.ExportWav() case 5: - t.TryQuit() + t.Quit(false) } clickedItem, hasClicked = t.Menus[0].Clicked() } diff --git a/tracker/gioui/tracker.go b/tracker/gioui/tracker.go index 3bf3d9d..629293e 100644 --- a/tracker/gioui/tracker.go +++ b/tracker/gioui/tracker.go @@ -73,12 +73,18 @@ type Tracker struct { ConfirmInstrDelete *Dialog ConfirmSongDialog *Dialog WaveTypeDialog *Dialog + OpenSongDialog *FileDialog + SaveSongDialog *FileDialog + OpenInstrumentDialog *FileDialog + SaveInstrumentDialog *FileDialog + ExportWavDialog *FileDialog ConfirmSongActionType int window *app.Window lastVolume tracker.Volume volumeChan chan tracker.Volume + wavFilePath string player *tracker.Player refresh chan struct{} playerCloser chan struct{} @@ -172,9 +178,15 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syn ConfirmInstrDelete: new(Dialog), ConfirmSongDialog: new(Dialog), WaveTypeDialog: new(Dialog), - errorChannel: make(chan error, 32), - window: window, - synthService: synthService, + OpenSongDialog: NewFileDialog(), + SaveSongDialog: NewFileDialog(), + OpenInstrumentDialog: NewFileDialog(), + SaveInstrumentDialog: NewFileDialog(), + + ExportWavDialog: NewFileDialog(), + errorChannel: make(chan error, 32), + window: window, + synthService: synthService, } t.Model = tracker.NewModel() vuBufferObserver := make(chan []float32) @@ -203,3 +215,13 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syn t.ResetSong() return t } + +func (t *Tracker) Quit(forced bool) bool { + if !forced && t.ChangedSinceSave() { + t.ConfirmSongActionType = ConfirmQuit + t.ConfirmSongDialog.Visible = true + return false + } + t.quitted = true + return true +}