feat(gioui): implement own file save / load dialogs

Removes the dependency on sqweek/dialogs, which was always very buggy.

Closes #12
This commit is contained in:
vsariola 2021-04-18 19:10:41 +03:00
parent ac95fb65c4
commit 147e8a2513
10 changed files with 464 additions and 100 deletions

1
go.mod
View File

@ -12,7 +12,6 @@ require (
github.com/huandu/xstrings v1.3.2 // indirect github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.11 // indirect github.com/imdario/mergo v0.3.11 // indirect
github.com/mitchellh/copystructure v1.0.0 // 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 github.com/stretchr/testify v1.6.1 // indirect
golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3 golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3
gopkg.in/yaml.v2 v2.3.0 gopkg.in/yaml.v2 v2.3.0

4
go.sum
View File

@ -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/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 h1:z4yfnGrZ7netVz+0EDJ0Wi+5VZCSYp4Z0m2dk6cEM60=
github.com/Masterminds/sprig v2.22.0+incompatible/go.mod h1:y6hNFY5UBTIWBxnzTeuNhlNS5hqE0NB0E6fgfo2Br3o= 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 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/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 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

282
tracker/gioui/filedialog.go Normal file
View File

@ -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{}
}

View File

@ -12,39 +12,60 @@ import (
"gioui.org/app" "gioui.org/app"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"github.com/sqweek/dialog"
"github.com/vsariola/sointu" "github.com/vsariola/sointu"
) )
func (t *Tracker) LoadSongFile() { func (t *Tracker) OpenSongFile(forced bool) {
if t.ChangedSinceSave() { if !forced && t.ChangedSinceSave() {
t.ConfirmSongActionType = ConfirmLoad t.ConfirmSongActionType = ConfirmLoad
t.ConfirmSongDialog.Visible = true t.ConfirmSongDialog.Visible = true
return 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 { func (t *Tracker) SaveSongFile() bool {
if p := t.FilePath(); p != "" { if p := t.FilePath(); p != "" {
return t.saveSong(p) return t.saveSong(p)
} }
return t.SaveSongAsFile() t.SaveSongAsFile()
return false
} }
func (t *Tracker) SaveSongAsFile() bool { func (t *Tracker) SaveSongAsFile() {
filename, err := dialog.File().Filter("Sointu YAML song", "yml").Filter("Sointu JSON song", "json").Title("Save song").Save() t.SaveSongDialog.Visible = true
if err != nil { if p := t.FilePath(); p != "" {
return false 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() { func (t *Tracker) ExportWav() {
filename, err := dialog.File().Filter("Sointu YAML song", "yml").Filter("Sointu JSON song", "json").Title("Load song").Load() t.ExportWavDialog.Visible = true
if err != nil { if p := t.FilePath(); p != "" {
return 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) bytes, err := ioutil.ReadFile(filename)
if err != nil { if err != nil {
return return
@ -52,10 +73,12 @@ func (t *Tracker) loadSong() {
var song sointu.Song var song sointu.Song
if errJSON := json.Unmarshal(bytes, &song); errJSON != nil { if errJSON := json.Unmarshal(bytes, &song); errJSON != nil {
if errYaml := yaml.Unmarshal(bytes, &song); errYaml != 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 return
} }
} }
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 {
t.Alert.Update("The song file is malformed", Error, time.Second*3)
return return
} }
t.SetSong(song) t.SetSong(song)
@ -69,12 +92,13 @@ func (t *Tracker) saveSong(filename string) bool {
var extension = filepath.Ext(filename) var extension = filepath.Ext(filename)
var contents []byte var contents []byte
var err error var err error
if extension == "json" { if extension == ".json" {
contents, err = json.Marshal(t.Song()) contents, err = json.Marshal(t.Song())
} else { } else {
contents, err = yaml.Marshal(t.Song()) contents, err = yaml.Marshal(t.Song())
} }
if err != nil { if err != nil {
t.Alert.Update(fmt.Sprintf("Error marshaling a song file: %v", err), Error, time.Second*3)
return false return false
} }
if extension == "" { if extension == "" {
@ -87,51 +111,7 @@ func (t *Tracker) saveSong(filename string) bool {
return true return true
} }
func (t *Tracker) SaveInstrument() bool { func (t *Tracker) exportWav(filename string, pcm16 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
}
var extension = filepath.Ext(filename) var extension = filepath.Ext(filename)
if extension == "" { if extension == "" {
filename = filename + ".wav" filename = filename + ".wav"
@ -156,3 +136,43 @@ func (t *Tracker) exportWav(pcm16 bool) {
} }
ioutil.WriteFile(filename, buffer, 0644) 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
}

View File

@ -81,7 +81,12 @@ var unitKeyMap = map[string]string{
// 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(w *app.Window, e key.Event) bool { func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool {
if e.State == key.Press { 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 return false
} }
switch e.Name { switch e.Name {
@ -111,7 +116,7 @@ func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool {
} }
case "N": case "N":
if e.Modifiers.Contain(key.ModShortcut) { if e.Modifiers.Contain(key.ModShortcut) {
t.TryResetSong() t.NewSong(false)
return true return true
} }
case "S": case "S":
@ -121,7 +126,7 @@ func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool {
} }
case "O": case "O":
if e.Modifiers.Contain(key.ModShortcut) { if e.Modifiers.Contain(key.ModShortcut) {
t.LoadSongFile() t.OpenSongFile(false)
return true return true
} }
case "F1": case "F1":

View File

@ -1,6 +1,7 @@
package gioui package gioui
import ( import (
"fmt"
"image" "image"
"gioui.org/app" "gioui.org/app"
@ -45,35 +46,68 @@ func (t *Tracker) Layout(gtx layout.Context) {
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(true) t.exportWav(t.wavFilePath, true)
t.WaveTypeDialog.Visible = false t.WaveTypeDialog.Visible = false
} }
for t.WaveTypeDialog.BtnAlt.Clicked() { for t.WaveTypeDialog.BtnAlt.Clicked() {
t.exportWav(false) t.exportWav(t.wavFilePath, 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.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() { func (t *Tracker) confirmedSongAction() {
switch t.ConfirmSongActionType { switch t.ConfirmSongActionType {
case ConfirmLoad: case ConfirmLoad:
t.loadSong() t.OpenSongFile(true)
case ConfirmNew: case ConfirmNew:
t.ResetSong() t.NewSong(true)
t.SetFilePath("")
t.window.Option(app.Title("Sointu Tracker"))
t.ClearUndoHistory()
t.SetChangedSinceSave(false)
case ConfirmQuit: case ConfirmQuit:
t.quitted = true t.Quit(true)
} }
} }
func (t *Tracker) TryResetSong() { func (t *Tracker) NewSong(forced bool) {
if t.ChangedSinceSave() { if !forced && t.ChangedSinceSave() {
t.ConfirmSongActionType = ConfirmNew t.ConfirmSongActionType = ConfirmNew
t.ConfirmSongDialog.Visible = true t.ConfirmSongDialog.Visible = true
return return
@ -85,16 +119,6 @@ func (t *Tracker) TryResetSong() {
t.SetChangedSinceSave(false) 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 { func (t *Tracker) layoutBottom(gtx layout.Context) layout.Dimensions {
return t.BottomHorizontalSplit.Layout(gtx, return t.BottomHorizontalSplit.Layout(gtx,
func(gtx C) D { func(gtx C) D {

View File

@ -1,6 +1,7 @@
package gioui package gioui
import ( import (
"image"
"image/color" "image/color"
"gioui.org/f32" "gioui.org/f32"
@ -43,6 +44,7 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D {
if !*s.Visible { if !*s.Visible {
return D{} return D{}
} }
for _, ev := range gtx.Events(s.Visible) { for _, ev := range gtx.Events(s.Visible) {
e, ok := ev.(pointer.Event) e, ok := ev.(pointer.Event)
if !ok { if !ok {
@ -56,9 +58,7 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D {
} }
bg := func(gtx C) D { bg := func(gtx C) D {
pointer.InputOp{Tag: s.Visible, pointer.PassOp{Pass: false}.Add(gtx.Ops)
Types: pointer.Press,
}.Add(gtx.Ops)
rrect := clip.RRect{ rrect := clip.RRect{
Rect: f32.Rectangle{Max: f32.Pt(float32(gtx.Constraints.Min.X), float32(gtx.Constraints.Min.Y))}, Rect: f32.Rectangle{Max: f32.Pt(float32(gtx.Constraints.Min.X), float32(gtx.Constraints.Min.Y))},
SE: float32(gtx.Px(s.SE)), 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)))) 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.ShadowColor, rrect2.Op(gtx.Ops))
paint.FillShape(gtx.Ops, s.SurfaceColor, rrect.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} return D{Size: gtx.Constraints.Min}
} }
macro := op.Record(gtx.Ops) macro := op.Record(gtx.Ops)
@ -82,3 +96,5 @@ func (s PopupStyle) Layout(gtx C, contents layout.Widget) D {
op.Defer(gtx.Ops, callop) op.Defer(gtx.Ops, callop)
return dims return dims
} }
var dummyTag bool

View File

@ -36,7 +36,7 @@ func (t *Tracker) Run(w *app.Window) error {
case e := <-w.Events(): case e := <-w.Events():
switch e := e.(type) { switch e := e.(type) {
case system.DestroyEvent: 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 // 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( w = app.NewWindow(
app.Size(unit.Dp(800), unit.Dp(600)), app.Size(unit.Dp(800), unit.Dp(600)),

View File

@ -53,17 +53,17 @@ func (t *Tracker) layoutMenuBar(gtx C) D {
for clickedItem, hasClicked := t.Menus[0].Clicked(); hasClicked; { for clickedItem, hasClicked := t.Menus[0].Clicked(); hasClicked; {
switch clickedItem { switch clickedItem {
case 0: case 0:
t.TryResetSong() t.NewSong(false)
case 1: case 1:
t.LoadSongFile() t.OpenSongFile(false)
case 2: case 2:
t.SaveSongFile() t.SaveSongFile()
case 3: case 3:
t.SaveSongAsFile() t.SaveSongAsFile()
case 4: case 4:
t.WaveTypeDialog.Visible = true t.ExportWav()
case 5: case 5:
t.TryQuit() t.Quit(false)
} }
clickedItem, hasClicked = t.Menus[0].Clicked() clickedItem, hasClicked = t.Menus[0].Clicked()
} }

View File

@ -73,12 +73,18 @@ type Tracker struct {
ConfirmInstrDelete *Dialog ConfirmInstrDelete *Dialog
ConfirmSongDialog *Dialog ConfirmSongDialog *Dialog
WaveTypeDialog *Dialog WaveTypeDialog *Dialog
OpenSongDialog *FileDialog
SaveSongDialog *FileDialog
OpenInstrumentDialog *FileDialog
SaveInstrumentDialog *FileDialog
ExportWavDialog *FileDialog
ConfirmSongActionType int ConfirmSongActionType int
window *app.Window window *app.Window
lastVolume tracker.Volume lastVolume tracker.Volume
volumeChan chan tracker.Volume volumeChan chan tracker.Volume
wavFilePath string
player *tracker.Player player *tracker.Player
refresh chan struct{} refresh chan struct{}
playerCloser chan struct{} playerCloser chan struct{}
@ -172,9 +178,15 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syn
ConfirmInstrDelete: new(Dialog), ConfirmInstrDelete: new(Dialog),
ConfirmSongDialog: new(Dialog), ConfirmSongDialog: new(Dialog),
WaveTypeDialog: new(Dialog), WaveTypeDialog: new(Dialog),
errorChannel: make(chan error, 32), OpenSongDialog: NewFileDialog(),
window: window, SaveSongDialog: NewFileDialog(),
synthService: synthService, OpenInstrumentDialog: NewFileDialog(),
SaveInstrumentDialog: NewFileDialog(),
ExportWavDialog: NewFileDialog(),
errorChannel: make(chan error, 32),
window: window,
synthService: synthService,
} }
t.Model = tracker.NewModel() t.Model = tracker.NewModel()
vuBufferObserver := make(chan []float32) vuBufferObserver := make(chan []float32)
@ -203,3 +215,13 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syn
t.ResetSong() t.ResetSong()
return t 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
}