mirror of
https://github.com/vsariola/sointu.git
synced 2025-07-14 02:54:37 -04:00
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:
@ -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),
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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":
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
@ -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()},
|
||||
|
@ -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)
|
||||
|
@ -29,6 +29,8 @@ type Model struct {
|
||||
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 {
|
||||
|
Reference in New Issue
Block a user