Refactor Level Publishing + MagicForm
* magicform is a helper package that may eventually be part of the go/ui library, for easily creating structured form layouts. * The Level Publisher UI is the first to utilize magicform. Refactor how level publishing works: * Level data now stores SaveDoodads and SaveBuiltins (bools) and when the level editor saves the file, it will attach custom and/or builtin doodads just before save. * Move the menu item from the File menu to Level->Publish * The Publisher UI just shows the checkboxes to toggle the level settings and a convenient Save button along with descriptive text. * Free versions get the "Register" window popping up if they click the Save Now button from within the publisher window. Note: free versions can still toggle the booleans on/off but their game will not attach any new doodads on save. * Free games which open a level w/ embedded doodads will get a pop-up warning that the doodads aren't available. * If they DON'T turn off the SaveDoodads option, they can still edit and save the level and keep the existing doodads attached. * If they UNCHECK the option and save, all attached doodads are removed from the level.
This commit is contained in:
parent
5ca87c752f
commit
1cc6eee5c8
|
@ -13,6 +13,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/enum"
|
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||||
"git.kirsle.net/apps/doodle/pkg/keybind"
|
"git.kirsle.net/apps/doodle/pkg/keybind"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/level/publishing"
|
||||||
"git.kirsle.net/apps/doodle/pkg/license"
|
"git.kirsle.net/apps/doodle/pkg/license"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
"git.kirsle.net/apps/doodle/pkg/modal"
|
"git.kirsle.net/apps/doodle/pkg/modal"
|
||||||
|
@ -513,6 +514,11 @@ func (s *EditorScene) SaveLevel(filename string) error {
|
||||||
// Clear the modified flag on the level.
|
// Clear the modified flag on the level.
|
||||||
s.UI.Canvas.SetModified(false)
|
s.UI.Canvas.SetModified(false)
|
||||||
|
|
||||||
|
// Attach doodads to the level on save.
|
||||||
|
if err := publishing.Publish(m); err != nil {
|
||||||
|
log.Error("Error publishing level: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
return m.WriteFile(filename)
|
return m.WriteFile(filename)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ package doodle
|
||||||
// The rest of it is controlled in editor_ui.go
|
// The rest of it is controlled in editor_ui.go
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
||||||
"git.kirsle.net/apps/doodle/pkg/enum"
|
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level/giant_screenshot"
|
"git.kirsle.net/apps/doodle/pkg/level/giant_screenshot"
|
||||||
|
@ -25,13 +24,11 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
|
||||||
|
|
||||||
// Save and Save As common menu handler
|
// Save and Save As common menu handler
|
||||||
var (
|
var (
|
||||||
drawingType string
|
saveFunc func(filename string)
|
||||||
saveFunc func(filename string)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
switch u.Scene.DrawingType {
|
switch u.Scene.DrawingType {
|
||||||
case enum.LevelDrawing:
|
case enum.LevelDrawing:
|
||||||
drawingType = "level"
|
|
||||||
saveFunc = func(filename string) {
|
saveFunc = func(filename string) {
|
||||||
if err := u.Scene.SaveLevel(filename); err != nil {
|
if err := u.Scene.SaveLevel(filename); err != nil {
|
||||||
d.FlashError("Error: %s", err)
|
d.FlashError("Error: %s", err)
|
||||||
|
@ -40,7 +37,6 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case enum.DoodadDrawing:
|
case enum.DoodadDrawing:
|
||||||
drawingType = "doodad"
|
|
||||||
saveFunc = func(filename string) {
|
saveFunc = func(filename string) {
|
||||||
if err := u.Scene.SaveDoodad(filename); err != nil {
|
if err := u.Scene.SaveDoodad(filename); err != nil {
|
||||||
d.FlashError("Error: %s", err)
|
d.FlashError("Error: %s", err)
|
||||||
|
@ -71,12 +67,6 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
if balance.Feature.EmbeddableDoodads && drawingType == "level" {
|
|
||||||
fileMenu.AddItem("Publish level", func() {
|
|
||||||
u.OpenPublishWindow()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fileMenu.AddItemAccel("Open...", "Ctrl-O", u.Scene.MenuOpen)
|
fileMenu.AddItemAccel("Open...", "Ctrl-O", u.Scene.MenuOpen)
|
||||||
fileMenu.AddSeparator()
|
fileMenu.AddSeparator()
|
||||||
fileMenu.AddItem("Exit to menu", func() {
|
fileMenu.AddItem("Exit to menu", func() {
|
||||||
|
@ -125,6 +115,9 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
|
||||||
levelMenu.AddItemAccel("Playtest", "P", func() {
|
levelMenu.AddItemAccel("Playtest", "P", func() {
|
||||||
u.Scene.Playtest()
|
u.Scene.Playtest()
|
||||||
})
|
})
|
||||||
|
levelMenu.AddItem("Publish", func() {
|
||||||
|
u.OpenPublishWindow()
|
||||||
|
})
|
||||||
|
|
||||||
levelMenu.AddSeparator()
|
levelMenu.AddSeparator()
|
||||||
levelMenu.AddItem("Giant Screenshot", func() {
|
levelMenu.AddItem("Giant Screenshot", func() {
|
||||||
|
|
|
@ -2,14 +2,12 @@ package doodle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level/publishing"
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/license"
|
"git.kirsle.net/apps/doodle/pkg/license"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
"git.kirsle.net/apps/doodle/pkg/modal"
|
"git.kirsle.net/apps/doodle/pkg/modal"
|
||||||
|
@ -52,8 +50,38 @@ func (u *EditorUI) OpenDoodadDropper() {
|
||||||
|
|
||||||
// OpenPublishWindow opens the Publisher window.
|
// OpenPublishWindow opens the Publisher window.
|
||||||
func (u *EditorUI) OpenPublishWindow() {
|
func (u *EditorUI) OpenPublishWindow() {
|
||||||
|
scene, _ := u.d.Scene.(*EditorScene)
|
||||||
|
|
||||||
|
u.publishWindow = windows.NewPublishWindow(windows.Publish{
|
||||||
|
Supervisor: u.Supervisor,
|
||||||
|
Engine: u.d.Engine,
|
||||||
|
Level: scene.Level,
|
||||||
|
|
||||||
|
OnPublish: func(includeBuiltins bool) {
|
||||||
|
u.d.FlashError("OnPublish Called")
|
||||||
|
// XXX: Paid Version Only.
|
||||||
|
if !license.IsRegistered() {
|
||||||
|
if u.licenseWindow != nil {
|
||||||
|
u.licenseWindow.Show()
|
||||||
|
u.Supervisor.FocusWindow(u.licenseWindow)
|
||||||
|
}
|
||||||
|
u.d.FlashError("Level Publishing is only available in the full version of the game.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: this function just saves the level. SaveDoodads and SaveBuiltins
|
||||||
|
// are toggled in the publish window and the save handler does publishing.
|
||||||
|
u.Scene.SaveLevel(u.Scene.filename)
|
||||||
|
u.d.Flash("Saved level: %s", u.Scene.filename)
|
||||||
|
},
|
||||||
|
OnCancel: func() {
|
||||||
|
u.publishWindow.Hide()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
u.ConfigureWindow(u.d, u.publishWindow)
|
||||||
|
|
||||||
u.publishWindow.Hide()
|
u.publishWindow.Hide()
|
||||||
u.publishWindow = nil
|
// u.publishWindow = nil
|
||||||
u.SetupPopups(u.d)
|
u.SetupPopups(u.d)
|
||||||
u.publishWindow.Show()
|
u.publishWindow.Show()
|
||||||
}
|
}
|
||||||
|
@ -66,23 +94,24 @@ func (u *EditorUI) OpenFileSystemWindow() {
|
||||||
u.filesystemWindow.Show()
|
u.filesystemWindow.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConfigureWindow sets default window config functions, like
|
||||||
|
// centering them on screen.
|
||||||
|
func (u *EditorUI) ConfigureWindow(d *Doodle, window *ui.Window) {
|
||||||
|
var size = window.Size()
|
||||||
|
window.Compute(d.Engine)
|
||||||
|
window.Supervise(u.Supervisor)
|
||||||
|
|
||||||
|
// Center the window.
|
||||||
|
window.MoveTo(render.Point{
|
||||||
|
X: (d.width / 2) - (size.W / 2),
|
||||||
|
Y: (d.height / 2) - (size.H / 2),
|
||||||
|
})
|
||||||
|
|
||||||
|
window.Hide()
|
||||||
|
}
|
||||||
|
|
||||||
// SetupPopups preloads popup windows like the DoodadDropper.
|
// SetupPopups preloads popup windows like the DoodadDropper.
|
||||||
func (u *EditorUI) SetupPopups(d *Doodle) {
|
func (u *EditorUI) SetupPopups(d *Doodle) {
|
||||||
// Common window configure function.
|
|
||||||
var configure = func(window *ui.Window) {
|
|
||||||
var size = window.Size()
|
|
||||||
window.Compute(d.Engine)
|
|
||||||
window.Supervise(u.Supervisor)
|
|
||||||
|
|
||||||
// Center the window.
|
|
||||||
window.MoveTo(render.Point{
|
|
||||||
X: (d.width / 2) - (size.W / 2),
|
|
||||||
Y: (d.height / 2) - (size.H / 2),
|
|
||||||
})
|
|
||||||
|
|
||||||
window.Hide()
|
|
||||||
}
|
|
||||||
|
|
||||||
// License Registration Window.
|
// License Registration Window.
|
||||||
if u.licenseWindow == nil {
|
if u.licenseWindow == nil {
|
||||||
cfg := windows.License{
|
cfg := windows.License{
|
||||||
|
@ -115,7 +144,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
|
||||||
u.doodadWindow.Hide()
|
u.doodadWindow.Hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
configure(u.doodadWindow)
|
u.ConfigureWindow(d, u.doodadWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Page Settings
|
// Page Settings
|
||||||
|
@ -141,7 +170,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
|
||||||
u.levelSettingsWindow.Hide()
|
u.levelSettingsWindow.Hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
configure(u.levelSettingsWindow)
|
u.ConfigureWindow(d, u.levelSettingsWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Doodad Properties
|
// Doodad Properties
|
||||||
|
@ -163,58 +192,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
u.doodadPropertiesWindow = windows.NewDoodadPropertiesWindow(cfg)
|
u.doodadPropertiesWindow = windows.NewDoodadPropertiesWindow(cfg)
|
||||||
configure(u.doodadPropertiesWindow)
|
u.ConfigureWindow(d, u.doodadPropertiesWindow)
|
||||||
}
|
|
||||||
|
|
||||||
// Publish Level (embed doodads)
|
|
||||||
if u.publishWindow == nil {
|
|
||||||
scene, _ := d.Scene.(*EditorScene)
|
|
||||||
|
|
||||||
u.publishWindow = windows.NewPublishWindow(windows.Publish{
|
|
||||||
Supervisor: u.Supervisor,
|
|
||||||
Engine: d.Engine,
|
|
||||||
Level: scene.Level,
|
|
||||||
|
|
||||||
OnPublish: func(includeBuiltins bool) {
|
|
||||||
// XXX: Paid Version Only.
|
|
||||||
if !license.IsRegistered() {
|
|
||||||
if u.licenseWindow != nil {
|
|
||||||
u.licenseWindow.Show()
|
|
||||||
u.Supervisor.FocusWindow(u.licenseWindow)
|
|
||||||
}
|
|
||||||
d.FlashError("Level Publishing is only available in the full version of the game.")
|
|
||||||
// modal.Alert(
|
|
||||||
// "This feature is only available in the full version of the game.",
|
|
||||||
// ).WithTitle("Please register")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Debug("OnPublish: include builtins=%+v", includeBuiltins)
|
|
||||||
cwd, _ := os.Getwd()
|
|
||||||
d.Prompt(fmt.Sprintf("File name (relative to %s)> ", cwd), func(answer string) {
|
|
||||||
if answer == "" {
|
|
||||||
d.FlashError("A file name is required to publish this level.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasSuffix(answer, ".level") {
|
|
||||||
answer += ".level"
|
|
||||||
}
|
|
||||||
|
|
||||||
answer = filepath.Join(cwd, answer)
|
|
||||||
log.Debug("call with includeBuiltins=%+v", includeBuiltins)
|
|
||||||
if _, err := publishing.Publish(scene.Level, answer, includeBuiltins); err != nil {
|
|
||||||
modal.Alert("Error when publishing the level: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
d.Flash("Exported published level to: %s", answer)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
OnCancel: func() {
|
|
||||||
u.publishWindow.Hide()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
configure(u.publishWindow)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Level FileSystem Viewer.
|
// Level FileSystem Viewer.
|
||||||
|
@ -262,7 +240,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
|
||||||
u.filesystemWindow.Hide()
|
u.filesystemWindow.Hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
configure(u.filesystemWindow)
|
u.ConfigureWindow(d, u.filesystemWindow)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Palette Editor.
|
// Palette Editor.
|
||||||
|
@ -315,7 +293,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
|
||||||
u.paletteEditor.Hide()
|
u.paletteEditor.Hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
configure(u.paletteEditor)
|
u.ConfigureWindow(d, u.paletteEditor)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layers window (doodad editor)
|
// Layers window (doodad editor)
|
||||||
|
@ -376,6 +354,6 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
|
||||||
u.layersWindow.Hide()
|
u.layersWindow.Hide()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
configure(u.layersWindow)
|
u.ConfigureWindow(d, u.layersWindow)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,6 +50,18 @@ func (l *Level) DeleteFile(filename string) bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteFiles removes all files beginning with the prefix.
|
||||||
|
func (l *Level) DeleteFiles(prefix string) int {
|
||||||
|
var count int
|
||||||
|
for filename := range l.Files {
|
||||||
|
if strings.HasPrefix(filename, prefix) {
|
||||||
|
delete(l.Files, filename)
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
// ListFiles returns the list of all embedded file names, alphabetically.
|
// ListFiles returns the list of all embedded file names, alphabetically.
|
||||||
func (l *Level) ListFiles() []string {
|
func (l *Level) ListFiles() []string {
|
||||||
var files []string
|
var files []string
|
||||||
|
|
|
@ -9,12 +9,15 @@ levels.
|
||||||
package publishing
|
package publishing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/license"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,32 +27,55 @@ doodads within level files.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Publish writes a published level file, with embedded doodads included.
|
// Publish writes a published level file, with embedded doodads included.
|
||||||
func Publish(lvl *level.Level, filename string, includeBuiltins bool) (*level.Level, error) {
|
func Publish(lvl *level.Level) error {
|
||||||
|
// Not embedding doodads?
|
||||||
|
if !lvl.SaveDoodads {
|
||||||
|
if removed := lvl.DeleteFiles(balance.EmbeddedDoodadsBasePath); removed > 0 {
|
||||||
|
log.Info("Note: removed %d attached doodads because SaveDoodads is false", removed)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Registered games only.
|
||||||
|
if !license.IsRegistered() {
|
||||||
|
return errors.New("only registered versions of the game can attach doodads to levels")
|
||||||
|
}
|
||||||
|
|
||||||
// Get and embed the doodads.
|
// Get and embed the doodads.
|
||||||
builtins, customs := GetUsedDoodadNames(lvl)
|
builtins, customs := GetUsedDoodadNames(lvl)
|
||||||
if includeBuiltins {
|
var names = map[string]interface{}{}
|
||||||
|
if lvl.SaveBuiltins {
|
||||||
log.Debug("including builtins: %+v", builtins)
|
log.Debug("including builtins: %+v", builtins)
|
||||||
customs = append(customs, builtins...)
|
customs = append(customs, builtins...)
|
||||||
}
|
}
|
||||||
for _, filename := range customs {
|
for _, filename := range customs {
|
||||||
log.Debug("Embed filename: %s", filename)
|
log.Debug("Embed filename: %s", filename)
|
||||||
|
names[filename] = nil
|
||||||
|
|
||||||
doodad, err := doodads.LoadFromEmbeddable(filename, lvl)
|
doodad, err := doodads.LoadFromEmbeddable(filename, lvl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't load doodad %s: %s", filename, err)
|
return fmt.Errorf("couldn't load doodad %s: %s", filename, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
bin, err := doodad.Serialize()
|
bin, err := doodad.Serialize()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("couldn't serialize doodad %s: %s", filename, err)
|
return fmt.Errorf("couldn't serialize doodad %s: %s", filename, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Embed it.
|
// Embed it.
|
||||||
lvl.SetFile(balance.EmbeddedDoodadsBasePath+filename, bin)
|
lvl.SetFile(balance.EmbeddedDoodadsBasePath+filename, bin)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("Publish: write file to %s", filename)
|
// Trim any doodads not currently in the level.
|
||||||
err := lvl.WriteFile(filename)
|
for _, filename := range lvl.ListFilesAt(balance.EmbeddedDoodadsBasePath) {
|
||||||
return lvl, err
|
basename := strings.TrimPrefix(filename, balance.EmbeddedDoodadsBasePath)
|
||||||
|
if _, ok := names[basename]; !ok {
|
||||||
|
log.Debug("Remove embedded doodad %s (cleanup)", basename)
|
||||||
|
lvl.DeleteFile(filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUsedDoodadNames returns the lists of doodad filenames in use in a level,
|
// GetUsedDoodadNames returns the lists of doodad filenames in use in a level,
|
||||||
|
|
|
@ -48,6 +48,10 @@ type Level struct {
|
||||||
// Actors keep a list of the doodad instances in this map.
|
// Actors keep a list of the doodad instances in this map.
|
||||||
Actors ActorMap `json:"actors"`
|
Actors ActorMap `json:"actors"`
|
||||||
|
|
||||||
|
// Publishing: attach any custom doodads the map uses on save.
|
||||||
|
SaveDoodads bool `json:"saveDoodads"`
|
||||||
|
SaveBuiltins bool `json:"saveBuiltins"`
|
||||||
|
|
||||||
// Undo history, temporary live data not persisted to the level file.
|
// Undo history, temporary live data not persisted to the level file.
|
||||||
UndoHistory *drawtool.History `json:"-"`
|
UndoHistory *drawtool.History `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
271
pkg/uix/magic-form/magic_form.go
Normal file
271
pkg/uix/magic-form/magic_form.go
Normal file
|
@ -0,0 +1,271 @@
|
||||||
|
// Package magicform helps create simple form layouts with go/ui.
|
||||||
|
package magicform
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
|
"git.kirsle.net/go/render"
|
||||||
|
"git.kirsle.net/go/ui"
|
||||||
|
"git.kirsle.net/go/ui/style"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Type int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Auto Type = iota
|
||||||
|
Text // free, wide Label row
|
||||||
|
Frame // custom frame from the caller
|
||||||
|
Button // Single button with a label
|
||||||
|
Textbox
|
||||||
|
Checkbox
|
||||||
|
Radiobox
|
||||||
|
Selectbox
|
||||||
|
)
|
||||||
|
|
||||||
|
// Form configuration.
|
||||||
|
type Form struct {
|
||||||
|
Supervisor *ui.Supervisor // Required for most useful forms
|
||||||
|
Engine render.Engine
|
||||||
|
|
||||||
|
// For vertical forms.
|
||||||
|
Vertical bool
|
||||||
|
LabelWidth int // size of left frame for labels.
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Field for your form (or form-aligned label sections, etc.)
|
||||||
|
|
||||||
|
The type of Form control to render is inferred based on bound
|
||||||
|
variables and other configuration.
|
||||||
|
*/
|
||||||
|
type Field struct {
|
||||||
|
// Type may be inferred by presence of other params.
|
||||||
|
Type Type
|
||||||
|
|
||||||
|
// Set a text string and font for simple labels or paragraphs.
|
||||||
|
Label string
|
||||||
|
Font render.Text
|
||||||
|
|
||||||
|
// Easy button row: make Buttons an array of Button fields
|
||||||
|
Buttons []Field
|
||||||
|
ButtonStyle *style.Button
|
||||||
|
|
||||||
|
// Easy Paginator. DO NOT SUPERVISE, let the Create do so!
|
||||||
|
Pager *ui.Pager
|
||||||
|
|
||||||
|
// If you send a *ui.Frame to insert, the Type is inferred
|
||||||
|
// to be Frame.
|
||||||
|
Frame *ui.Frame
|
||||||
|
|
||||||
|
// Variable bindings, the type may infer to be:
|
||||||
|
BoolVariable *bool // Checkbox
|
||||||
|
TextVariable *string // Textbox
|
||||||
|
Options []Option // Selectbox
|
||||||
|
|
||||||
|
// Tooltip to add to a form control.
|
||||||
|
// Checkbox only for now.
|
||||||
|
Tooltip ui.Tooltip // config for the tooltip only
|
||||||
|
|
||||||
|
// Handlers you can configure
|
||||||
|
OnSelect func(value interface{}) // Selectbox
|
||||||
|
OnClick func() // Button
|
||||||
|
}
|
||||||
|
|
||||||
|
// Option used in Selectbox or Radiobox fields.
|
||||||
|
type Option struct {
|
||||||
|
Value interface{}
|
||||||
|
Label string
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create the form field and populate it into the given Frame.
|
||||||
|
|
||||||
|
Renders the form vertically.
|
||||||
|
*/
|
||||||
|
func (form Form) Create(into *ui.Frame, fields []Field) {
|
||||||
|
for n, row := range fields {
|
||||||
|
row := row
|
||||||
|
|
||||||
|
if row.Frame != nil {
|
||||||
|
into.Pack(row.Frame, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
FillX: true,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
frame := ui.NewFrame(fmt.Sprintf("Line %d", n))
|
||||||
|
into.Pack(frame, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
FillX: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Pager row?
|
||||||
|
if row.Pager != nil {
|
||||||
|
row.Pager.Compute(form.Engine)
|
||||||
|
form.Supervisor.Add(row.Pager)
|
||||||
|
frame.Pack(row.Pager, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
Expand: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buttons row?
|
||||||
|
if row.Buttons != nil && len(row.Buttons) > 0 {
|
||||||
|
for _, row := range row.Buttons {
|
||||||
|
row := row
|
||||||
|
|
||||||
|
btn := ui.NewButton(row.Label, ui.NewLabel(ui.Label{
|
||||||
|
Text: row.Label,
|
||||||
|
Font: row.Font,
|
||||||
|
}))
|
||||||
|
if row.ButtonStyle != nil {
|
||||||
|
btn.SetStyle(row.ButtonStyle)
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
if row.OnClick != nil {
|
||||||
|
row.OnClick()
|
||||||
|
} else {
|
||||||
|
log.Error("No OnClick handler for button %s", row.Label)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
btn.Compute(form.Engine)
|
||||||
|
form.Supervisor.Add(btn)
|
||||||
|
|
||||||
|
frame.Pack(btn, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
PadX: 4,
|
||||||
|
PadY: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Infer the type of the form field.
|
||||||
|
if row.Type == Auto {
|
||||||
|
row.Type = row.Infer()
|
||||||
|
if row.Type == Auto {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is there a label frame to the left?
|
||||||
|
// - Checkbox gets a full row.
|
||||||
|
if row.Label != "" && row.Type != Checkbox {
|
||||||
|
labFrame := ui.NewFrame("Label Frame")
|
||||||
|
labFrame.Configure(ui.Config{
|
||||||
|
Width: form.LabelWidth,
|
||||||
|
})
|
||||||
|
frame.Pack(labFrame, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Draw the label text into it.
|
||||||
|
label := ui.NewLabel(ui.Label{
|
||||||
|
Text: row.Label,
|
||||||
|
Font: row.Font,
|
||||||
|
})
|
||||||
|
labFrame.Pack(label, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkbox?
|
||||||
|
if row.Type == Checkbox {
|
||||||
|
cb := ui.NewCheckbox("Checkbox", row.BoolVariable, ui.NewLabel(ui.Label{
|
||||||
|
Text: row.Label,
|
||||||
|
Font: row.Font,
|
||||||
|
}))
|
||||||
|
cb.Supervise(form.Supervisor)
|
||||||
|
frame.Pack(cb, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
FillX: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tooltip? TODO - make nicer.
|
||||||
|
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
|
||||||
|
ui.NewTooltip(cb, row.Tooltip)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handlers
|
||||||
|
cb.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
if row.OnClick != nil {
|
||||||
|
row.OnClick()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selectbox? also Radiobox for now.
|
||||||
|
if row.Type == Selectbox || row.Type == Radiobox {
|
||||||
|
btn := ui.NewSelectBox("Select", ui.Label{
|
||||||
|
Font: row.Font,
|
||||||
|
})
|
||||||
|
frame.Pack(btn, ui.Pack{
|
||||||
|
Side: ui.W,
|
||||||
|
FillX: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
if row.Options != nil {
|
||||||
|
for _, option := range row.Options {
|
||||||
|
btn.AddItem(option.Label, option.Value, func() {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
||||||
|
if selection, ok := btn.GetValue(); ok {
|
||||||
|
if row.OnSelect != nil {
|
||||||
|
row.OnSelect(selection.Value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
form.Supervisor.Add(btn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Infer the type if the field was of type Auto.
|
||||||
|
|
||||||
|
Returns the first Type inferred from the field by checking in
|
||||||
|
this order:
|
||||||
|
|
||||||
|
- Frame if the field has a *Frame
|
||||||
|
- Checkbox if there is a *BoolVariable
|
||||||
|
- Selectbox if there are Options
|
||||||
|
- Textbox if there is a *TextVariable
|
||||||
|
- Text if there is a Label
|
||||||
|
|
||||||
|
May return Auto if none of the above and be ignored.
|
||||||
|
*/
|
||||||
|
func (field Field) Infer() Type {
|
||||||
|
if field.Frame != nil {
|
||||||
|
return Frame
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.BoolVariable != nil {
|
||||||
|
return Checkbox
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Options != nil && len(field.Options) > 0 {
|
||||||
|
return Selectbox
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.TextVariable != nil {
|
||||||
|
return Textbox
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.Label != "" {
|
||||||
|
return Text
|
||||||
|
}
|
||||||
|
|
||||||
|
return Auto
|
||||||
|
}
|
|
@ -9,6 +9,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
"git.kirsle.net/apps/doodle/pkg/level/publishing"
|
"git.kirsle.net/apps/doodle/pkg/level/publishing"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
"git.kirsle.net/apps/doodle/pkg/log"
|
||||||
|
magicform "git.kirsle.net/apps/doodle/pkg/uix/magic-form"
|
||||||
"git.kirsle.net/go/render"
|
"git.kirsle.net/go/render"
|
||||||
"git.kirsle.net/go/ui"
|
"git.kirsle.net/go/ui"
|
||||||
)
|
)
|
||||||
|
@ -30,8 +31,8 @@ type Publish struct {
|
||||||
// NewPublishWindow initializes the window.
|
// NewPublishWindow initializes the window.
|
||||||
func NewPublishWindow(cfg Publish) *ui.Window {
|
func NewPublishWindow(cfg Publish) *ui.Window {
|
||||||
var (
|
var (
|
||||||
windowWidth = 400
|
windowWidth = 380
|
||||||
windowHeight = 300
|
windowHeight = 220
|
||||||
page = 1
|
page = 1
|
||||||
perPage = 4
|
perPage = 4
|
||||||
pages = 1
|
pages = 1
|
||||||
|
@ -51,76 +52,12 @@ func NewPublishWindow(cfg Publish) *ui.Window {
|
||||||
Background: render.RGBA(200, 200, 255, 255),
|
Background: render.RGBA(200, 200, 255, 255),
|
||||||
})
|
})
|
||||||
|
|
||||||
/////////////
|
|
||||||
// Intro text
|
|
||||||
|
|
||||||
introFrame := ui.NewFrame("Intro Frame")
|
|
||||||
window.Pack(introFrame, ui.Pack{
|
|
||||||
Side: ui.N,
|
|
||||||
FillX: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
lines := []struct {
|
|
||||||
Text string
|
|
||||||
Font render.Text
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Text: "About",
|
|
||||||
Font: balance.LabelFont,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Text: "Share your level easily! If you are using custom doodads in\n" +
|
|
||||||
"your level, you may attach them directly to your\n" +
|
|
||||||
"level file -- so it can easily run on another computer!",
|
|
||||||
Font: balance.UIFont,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Text: "List of Doodads in Your Level",
|
|
||||||
Font: balance.LabelFont,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
for n, row := range lines {
|
|
||||||
frame := ui.NewFrame(fmt.Sprintf("Intro Line %d", n))
|
|
||||||
introFrame.Pack(frame, ui.Pack{
|
|
||||||
Side: ui.N,
|
|
||||||
FillX: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
label := ui.NewLabel(ui.Label{
|
|
||||||
Text: row.Text,
|
|
||||||
Font: row.Font,
|
|
||||||
})
|
|
||||||
frame.Pack(label, ui.Pack{
|
|
||||||
Side: ui.W,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/////////////
|
/////////////
|
||||||
// Custom Doodads checkbox-list.
|
// Custom Doodads checkbox-list.
|
||||||
doodadFrame := ui.NewFrame("Doodads Frame")
|
doodadFrame := ui.NewFrame("Doodads Frame")
|
||||||
doodadFrame.Resize(render.Rect{
|
doodadFrame.Resize(render.Rect{
|
||||||
W: windowWidth,
|
W: windowWidth,
|
||||||
H: btnHeight*perPage + 100,
|
H: btnHeight*perPage + 40,
|
||||||
})
|
|
||||||
window.Pack(doodadFrame, ui.Pack{
|
|
||||||
Side: ui.N,
|
|
||||||
FillX: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// First, the checkbox to show built-in doodads or not.
|
|
||||||
builtinRow := ui.NewFrame("Show Builtins Frame")
|
|
||||||
doodadFrame.Pack(builtinRow, ui.Pack{
|
|
||||||
Side: ui.N,
|
|
||||||
FillX: true,
|
|
||||||
})
|
|
||||||
builtinCB := ui.NewCheckbox("Show Builtins", &cfg.includeBuiltins, ui.NewLabel(ui.Label{
|
|
||||||
Text: "Attach built-in* doodads too",
|
|
||||||
Font: balance.UIFont,
|
|
||||||
}))
|
|
||||||
builtinCB.Supervise(cfg.Supervisor)
|
|
||||||
builtinRow.Pack(builtinCB, ui.Pack{
|
|
||||||
Side: ui.W,
|
|
||||||
PadX: 2,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Collect the doodads named in this level.
|
// Collect the doodads named in this level.
|
||||||
|
@ -190,15 +127,6 @@ func NewPublishWindow(cfg Publish) *ui.Window {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////
|
|
||||||
// Buttons at bottom of window
|
|
||||||
|
|
||||||
bottomFrame := ui.NewFrame("Button Frame")
|
|
||||||
window.Pack(bottomFrame, ui.Pack{
|
|
||||||
Side: ui.S,
|
|
||||||
FillX: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Pager for the doodads.
|
// Pager for the doodads.
|
||||||
pages = int(
|
pages = int(
|
||||||
math.Ceil(
|
math.Ceil(
|
||||||
|
@ -237,59 +165,95 @@ func NewPublishWindow(cfg Publish) *ui.Window {
|
||||||
Font: balance.MenuFont,
|
Font: balance.MenuFont,
|
||||||
OnChange: pagerOnChange,
|
OnChange: pagerOnChange,
|
||||||
})
|
})
|
||||||
pager.Compute(cfg.Engine)
|
_ = pager
|
||||||
pager.Supervise(cfg.Supervisor)
|
|
||||||
bottomFrame.Place(pager, ui.Place{
|
/////////////
|
||||||
Top: 20,
|
// Intro text
|
||||||
Left: 20,
|
|
||||||
|
introFrame := ui.NewFrame("Intro Frame")
|
||||||
|
window.Pack(introFrame, ui.Pack{
|
||||||
|
Side: ui.N,
|
||||||
|
FillX: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
frame := ui.NewFrame("Button frame")
|
// Render the form, putting it all together.
|
||||||
buttons := []struct {
|
form := magicform.Form{
|
||||||
label string
|
Supervisor: cfg.Supervisor,
|
||||||
primary bool
|
Engine: cfg.Engine,
|
||||||
f func()
|
Vertical: true,
|
||||||
}{
|
LabelWidth: 100,
|
||||||
{"Export Level", true, func() {
|
|
||||||
if cfg.OnPublish != nil {
|
|
||||||
cfg.OnPublish(cfg.includeBuiltins)
|
|
||||||
}
|
|
||||||
}},
|
|
||||||
{"Close", false, func() {
|
|
||||||
if cfg.OnCancel != nil {
|
|
||||||
cfg.OnCancel()
|
|
||||||
}
|
|
||||||
}},
|
|
||||||
}
|
}
|
||||||
for _, button := range buttons {
|
form.Create(introFrame, []magicform.Field{
|
||||||
button := button
|
{
|
||||||
|
Label: "About",
|
||||||
btn := ui.NewButton(button.label, ui.NewLabel(ui.Label{
|
Font: balance.LabelFont,
|
||||||
Text: button.label,
|
},
|
||||||
Font: balance.MenuFont,
|
{
|
||||||
}))
|
Label: "Share your level easily! If you are using custom doodads in\n" +
|
||||||
if button.primary {
|
"your level, you may attach them directly to your level file\n" +
|
||||||
btn.SetStyle(&balance.ButtonPrimary)
|
"so it can easily run on another computer!",
|
||||||
}
|
Font: balance.UIFont,
|
||||||
|
},
|
||||||
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
{
|
||||||
button.f()
|
Label: "Attach custom doodads when I save the level",
|
||||||
return nil
|
Font: balance.UIFont,
|
||||||
})
|
BoolVariable: &cfg.Level.SaveDoodads,
|
||||||
|
},
|
||||||
btn.Compute(cfg.Engine)
|
{
|
||||||
cfg.Supervisor.Add(btn)
|
Label: "Attach built-in doodads too",
|
||||||
|
Font: balance.UIFont.Update(render.Text{
|
||||||
frame.Pack(btn, ui.Pack{
|
Color: render.Red,
|
||||||
Side: ui.W,
|
}),
|
||||||
PadX: 4,
|
BoolVariable: &cfg.Level.SaveBuiltins,
|
||||||
Expand: true,
|
Tooltip: ui.Tooltip{
|
||||||
Fill: true,
|
Edge: ui.Top,
|
||||||
})
|
Text: "If enabled, the attached doodads will override the built-ins\n" +
|
||||||
}
|
"for this level. Bugfixes or updates to the built-ins will not\n" +
|
||||||
bottomFrame.Pack(frame, ui.Pack{
|
"affect your level, either.",
|
||||||
Side: ui.E,
|
},
|
||||||
Padding: 8,
|
},
|
||||||
|
{
|
||||||
|
Label: "The above settings are saved with your level file, and each\n" +
|
||||||
|
"time you save, custom doodads will be re-attached.",
|
||||||
|
Font: balance.UIFont,
|
||||||
|
},
|
||||||
|
// Pager is broken, Supervisor doesn't pick it up, TODO
|
||||||
|
/*{
|
||||||
|
Label: "Doodads currently used on this level:",
|
||||||
|
Font: balance.LabelFont,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Frame: doodadFrame,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "* Built-in doodad",
|
||||||
|
Font: balance.UIFont,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Pager: pager,
|
||||||
|
},*/
|
||||||
|
{
|
||||||
|
Buttons: []magicform.Field{
|
||||||
|
{
|
||||||
|
ButtonStyle: &balance.ButtonPrimary,
|
||||||
|
Label: "Save Level Now",
|
||||||
|
OnClick: func() {
|
||||||
|
if cfg.OnPublish != nil {
|
||||||
|
cfg.OnPublish(cfg.includeBuiltins)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Type: magicform.Button,
|
||||||
|
Label: "Close",
|
||||||
|
OnClick: func() {
|
||||||
|
if cfg.OnCancel != nil {
|
||||||
|
cfg.OnCancel()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return window
|
return window
|
||||||
|
|
Loading…
Reference in New Issue
Block a user