feat(tracker, gioui): add confirmation dialogs before quit/new/load song

This should avoid accidentally losing all work by destroying window.
This commit is contained in:
vsariola
2021-04-16 22:42:51 +03:00
parent f3cf4a52ce
commit 7893c1d1ed
8 changed files with 181 additions and 23 deletions

View File

@ -10,6 +10,7 @@ import (
type Dialog struct {
Visible bool
BtnAlt widget.Clickable
BtnOk widget.Clickable
BtnCancel widget.Clickable
}
@ -18,6 +19,8 @@ type DialogStyle struct {
dialog *Dialog
Text string
Inset layout.Inset
ShowAlt bool
AltStyle material.ButtonStyle
OkStyle material.ButtonStyle
CancelStyle material.ButtonStyle
}
@ -27,9 +30,11 @@ func ConfirmDialog(th *material.Theme, dialog *Dialog, text string) DialogStyle
dialog: dialog,
Text: text,
Inset: layout.Inset{Top: unit.Dp(12), Bottom: unit.Dp(12), Left: unit.Dp(20), Right: unit.Dp(20)},
AltStyle: material.Button(th, &dialog.BtnAlt, "Alt"),
OkStyle: material.Button(th, &dialog.BtnOk, "Ok"),
CancelStyle: material.Button(th, &dialog.BtnCancel, "Cancel"),
}
ret.AltStyle.Background = primaryColor
ret.OkStyle.Background = primaryColor
ret.CancelStyle.Background = primaryColor
return ret
@ -45,6 +50,13 @@ func (d *DialogStyle) Layout(gtx C) D {
layout.Rigid(Label(d.Text, highEmphasisTextColor)),
layout.Rigid(func(gtx C) D {
gtx.Constraints.Min.X = gtx.Px(unit.Dp(120))
if d.ShowAlt {
return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween}.Layout(gtx,
layout.Rigid(d.OkStyle.Layout),
layout.Rigid(d.AltStyle.Layout),
layout.Rigid(d.CancelStyle.Layout),
)
}
return layout.Flex{Axis: layout.Horizontal, Spacing: layout.SpaceBetween}.Layout(gtx,
layout.Rigid(d.OkStyle.Layout),
layout.Rigid(d.CancelStyle.Layout),

View File

@ -4,9 +4,11 @@ package gioui
import (
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
"gioui.org/app"
"gopkg.in/yaml.v3"
"github.com/sqweek/dialog"
@ -14,6 +16,30 @@ import (
)
func (t *Tracker) LoadSongFile() {
if t.ChangedSinceSave() {
t.ConfirmSongActionType = ConfirmLoad
t.ConfirmSongDialog.Visible = true
return
}
t.loadSong()
}
func (t *Tracker) SaveSongFile() bool {
if p := t.FilePath(); p != "" {
return t.saveSong(p)
}
return t.SaveSongAsFile()
}
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
}
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
@ -29,25 +55,30 @@ func (t *Tracker) LoadSongFile() {
}
}
t.SetSong(song)
t.SetFilePath(filename)
t.window.Option(app.Title(fmt.Sprintf("Sointu Tracker - %v", filename)))
t.ClearUndoHistory()
t.SetChangedSinceSave(false)
}
func (t *Tracker) SaveSongFile() {
filename, err := dialog.File().Filter("Sointu YAML song", "yml").Filter("Sointu JSON song", "json").Title("Save song").Save()
if err != nil {
return
}
func (t *Tracker) saveSong(filename string) bool {
var extension = filepath.Ext(filename)
var contents []byte
var err error
if extension == "json" {
contents, err = json.Marshal(t.Song())
} else {
contents, err = yaml.Marshal(t.Song())
}
if err != nil {
return
return false
}
if extension == "" {
filename = filename + ".yml"
}
ioutil.WriteFile(filename, contents, 0644)
t.SetFilePath(filename)
t.window.Option(app.Title(fmt.Sprintf("Sointu Tracker - %v", filename)))
t.SetChangedSinceSave(false)
return true
}

View File

@ -111,7 +111,7 @@ func (t *Tracker) KeyEvent(w *app.Window, e key.Event) bool {
}
case "N":
if e.Modifiers.Contain(key.ModShortcut) {
t.ResetSong()
t.TryResetSong()
return true
}
case "S":

View File

@ -3,6 +3,7 @@ package gioui
import (
"image"
"gioui.org/app"
"gioui.org/layout"
"gioui.org/op/clip"
"gioui.org/op/paint"
@ -20,6 +21,62 @@ func (t *Tracker) Layout(gtx layout.Context) {
t.Alert.Layout(gtx)
dstyle := ConfirmDialog(t.Theme, t.ConfirmInstrDelete, "Are you sure you want to delete this instrument?")
dstyle.Layout(gtx)
dstyle = ConfirmDialog(t.Theme, t.ConfirmSongDialog, "Do you want to save your changes to the song? Your changes will be lost if you don't save them.")
dstyle.ShowAlt = true
dstyle.OkStyle.Text = "Save"
dstyle.AltStyle.Text = "Don't save"
dstyle.Layout(gtx)
for t.ConfirmSongDialog.BtnOk.Clicked() {
if t.SaveSongFile() {
t.confirmedSongAction()
}
t.ConfirmSongDialog.Visible = false
}
for t.ConfirmSongDialog.BtnAlt.Clicked() {
t.confirmedSongAction()
t.ConfirmSongDialog.Visible = false
}
for t.ConfirmSongDialog.BtnCancel.Clicked() {
t.ConfirmSongDialog.Visible = false
}
}
func (t *Tracker) confirmedSongAction() {
switch t.ConfirmSongActionType {
case ConfirmLoad:
t.loadSong()
case ConfirmNew:
t.ResetSong()
t.SetFilePath("")
t.window.Option(app.Title("Sointu Tracker"))
t.ClearUndoHistory()
t.SetChangedSinceSave(false)
case ConfirmQuit:
t.quitted = true
}
}
func (t *Tracker) TryResetSong() {
if t.ChangedSinceSave() {
t.ConfirmSongActionType = ConfirmNew
t.ConfirmSongDialog.Visible = true
return
}
t.ResetSong()
t.SetFilePath("")
t.window.Option(app.Title("Sointu Tracker"))
t.ClearUndoHistory()
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 {

View File

@ -36,7 +36,13 @@ func (t *Tracker) Run(w *app.Window) error {
case e := <-w.Events():
switch e := e.(type) {
case system.DestroyEvent:
return e.Err
if !t.TryQuit() {
// 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)),
app.Title("Sointu Tracker"),
)
}
case key.Event:
if t.KeyEvent(w, e) {
w.Invalidate()
@ -52,6 +58,9 @@ func (t *Tracker) Run(w *app.Window) error {
e.Frame(gtx.Ops)
}
}
if t.quitted {
return nil
}
}
}
@ -61,7 +70,7 @@ func Main(audioContext sointu.AudioContext, synthService sointu.SynthService, sy
app.Size(unit.Dp(800), unit.Dp(600)),
app.Title("Sointu Tracker"),
)
t := New(audioContext, synthService, syncChannel)
t := New(audioContext, synthService, syncChannel, w)
defer t.Close()
if err := t.Run(w); err != nil {
fmt.Println(err)

View File

@ -53,11 +53,15 @@ func (t *Tracker) layoutMenuBar(gtx C) D {
for clickedItem, hasClicked := t.Menus[0].Clicked(); hasClicked; {
switch clickedItem {
case 0:
t.ResetSong()
t.TryResetSong()
case 1:
t.LoadSongFile()
case 2:
t.SaveSongFile()
case 3:
t.SaveSongAsFile()
case 4:
t.TryQuit()
}
clickedItem, hasClicked = t.Menus[0].Clicked()
}
@ -88,6 +92,8 @@ func (t *Tracker) layoutMenuBar(gtx C) D {
MenuItem{IconBytes: icons.ContentClear, Text: "New Song", ShortcutText: shortcutKey + "N"},
MenuItem{IconBytes: icons.FileFolder, Text: "Open Song", ShortcutText: shortcutKey + "O"},
MenuItem{IconBytes: icons.ContentSave, Text: "Save Song", ShortcutText: shortcutKey + "S"},
MenuItem{IconBytes: icons.ContentSave, Text: "Save Song As..."},
MenuItem{IconBytes: icons.ActionExitToApp, Text: "Quit"},
)),
layout.Rigid(t.layoutMenu("Edit", &t.MenuBar[1], &t.Menus[1], unit.Dp(160),
MenuItem{IconBytes: icons.ContentUndo, Text: "Undo", ShortcutText: shortcutKey + "Z", Disabled: !t.CanUndo()},

View File

@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"gioui.org/app"
"gioui.org/font/gofont"
"gioui.org/layout"
"gioui.org/text"
@ -15,6 +16,12 @@ import (
"gopkg.in/yaml.v3"
)
const (
ConfirmQuit = iota
ConfirmLoad
ConfirmNew
)
type Tracker struct {
Theme *material.Theme
MenuBar []widget.Clickable
@ -62,6 +69,9 @@ type Tracker struct {
PatternOrderList *layout.List
PatternOrderScrollBar *ScrollBar
ConfirmInstrDelete *Dialog
ConfirmSongDialog *Dialog
ConfirmSongActionType int
window *app.Window
lastVolume tracker.Volume
volumeChan chan tracker.Volume
@ -70,6 +80,7 @@ type Tracker struct {
refresh chan struct{}
playerCloser chan struct{}
errorChannel chan error
quitted bool
audioContext sointu.AudioContext
*tracker.Model
@ -105,7 +116,7 @@ func (t *Tracker) Close() {
t.audioContext.Close()
}
func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syncChannel chan<- []float32) *Tracker {
func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syncChannel chan<- []float32, window *app.Window) *Tracker {
t := &Tracker{
Theme: material.NewTheme(gofont.Collection()),
audioContext: audioContext,
@ -153,7 +164,9 @@ func New(audioContext sointu.AudioContext, synthService sointu.SynthService, syn
PatternOrderList: &layout.List{Axis: layout.Vertical},
PatternOrderScrollBar: &ScrollBar{Axis: layout.Vertical},
ConfirmInstrDelete: new(Dialog),
ConfirmSongDialog: new(Dialog),
errorChannel: make(chan error, 32),
window: window,
}
t.Model = tracker.NewModel()
vuBufferObserver := make(chan []float32)

View File

@ -17,18 +17,20 @@ import (
// accidental mutations in the song. But at least the value members are
// protected.
type Model struct {
song sointu.Song
editMode EditMode
selectionCorner SongPoint
cursor SongPoint
lowNibble bool
instrIndex int
unitIndex int
paramIndex int
octave int
noteTracking bool
usedIDs map[int]bool
maxID int
song sointu.Song
editMode EditMode
selectionCorner SongPoint
cursor SongPoint
lowNibble bool
instrIndex int
unitIndex int
paramIndex int
octave int
noteTracking bool
usedIDs map[int]bool
maxID int
filePath string
changedSinceSave bool
prevUndoType string
undoSkipCounter int
@ -76,8 +78,26 @@ func NewModel() *Model {
return ret
}
func (m *Model) FilePath() string {
return m.filePath
}
func (m *Model) SetFilePath(value string) {
m.filePath = value
}
func (m *Model) ChangedSinceSave() bool {
return m.changedSinceSave
}
func (m *Model) SetChangedSinceSave(value bool) {
m.changedSinceSave = value
}
func (m *Model) ResetSong() {
m.SetSong(defaultSong.Copy())
m.filePath = ""
m.changedSinceSave = false
}
func (m *Model) SetSong(song sointu.Song) {
@ -671,6 +691,15 @@ func (m *Model) CanUndo() bool {
return len(m.undoStack) > 0
}
func (m *Model) ClearUndoHistory() {
if len(m.undoStack) > 0 {
m.undoStack = m.undoStack[:0]
}
if len(m.redoStack) > 0 {
m.redoStack = m.redoStack[:0]
}
}
func (m *Model) Redo() {
if !m.CanRedo() {
return
@ -986,6 +1015,7 @@ func (m *Model) saveUndo(undoType string, undoSkipping int) {
m.undoSkipCounter++
return
}
m.changedSinceSave = true
m.prevUndoType = undoType
m.undoSkipCounter = 0
if len(m.undoStack) >= maxUndo {