Editor: Doodad Properties Window

The Doodad Properties window brings many features that used to be
available only in the `doodad` CLI tool into the Doodad Editor.

* In the Doodad Editor there is a new menubar item: "Doodad" which
  corresponds to the "Level" menu when you're editing a level.
* The "Doodad" menu has two items:
  - "Doodad Properties" (NEW)
  - "Layers" (moved here from the Tools menu)
* The Doodad Properties window lets you edit the Title and Author values
  of the doodad, as well as modify its Tags and manage its Script.
* Its script can be attached (browse for .js file on disk), its existing
  script saved back to disk (dev shell prompt) or deleted altogether
  from the doodad.
* You can create, modify, and delete Tags on the doodad.

Other changes:

* In the Level Editor, the "Level->Page Settings" menu is renamed to
  "Level->Level Properties" to match with "Doodad->Doodad Properties"
  and the pop-up window is retitled accordingly.
* The Exit Flag only exits if the Player touches it - not just any
  mobile doodad!
This commit is contained in:
Noah 2021-09-02 21:26:55 -07:00
parent 0cc1d17f4f
commit 0fa1bf8a76
7 changed files with 536 additions and 32 deletions

View File

@ -1,12 +1,17 @@
// Exit Flag. // Exit Flag.
function main() { function main() {
Self.SetHitbox(22+16, 16, 75-16, 86); Self.SetHitbox(22 + 16, 16, 75 - 16, 86);
Events.OnCollide(function(e) { Events.OnCollide(function (e) {
if (!e.Settled) { if (!e.Settled) {
return; return;
} }
// Only care if it's the player.
if (!e.Actor.IsPlayer()) {
return;
}
if (e.InHitbox) { if (e.InHitbox) {
EndLevel(); EndLevel();
} }

View File

@ -105,6 +105,13 @@ var (
Color: render.Black, Color: render.Black,
} }
LargeLabelFont = render.Text{
Size: 18,
FontFilename: "DejaVuSans-Bold.ttf",
Padding: 4,
Color: render.Black,
}
// SmallMonoFont for cramped spaces like the +/- buttons on Toolbar. // SmallMonoFont for cramped spaces like the +/- buttons on Toolbar.
SmallMonoFont = render.Text{ SmallMonoFont = render.Text{
Size: 14, Size: 14,

View File

@ -44,6 +44,7 @@ type EditorUI struct {
// Popup windows. // Popup windows.
levelSettingsWindow *ui.Window levelSettingsWindow *ui.Window
doodadPropertiesWindow *ui.Window
aboutWindow *ui.Window aboutWindow *ui.Window
doodadWindow *ui.Window doodadWindow *ui.Window
paletteEditor *ui.Window paletteEditor *ui.Window

View File

@ -117,7 +117,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
// Level menu // Level menu
if u.Scene.DrawingType == enum.LevelDrawing { if u.Scene.DrawingType == enum.LevelDrawing {
levelMenu := menu.AddMenu("Level") levelMenu := menu.AddMenu("Level")
levelMenu.AddItem("Page settings", func() { levelMenu.AddItem("Level Properties", func() {
log.Info("Opening the window") log.Info("Opening the window")
// Open the New Level window in edit-settings mode. // Open the New Level window in edit-settings mode.
@ -135,6 +135,25 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
}) })
} }
////////
// Doodad Menu
if u.Scene.DrawingType == enum.DoodadDrawing {
levelMenu := menu.AddMenu("Doodad")
levelMenu.AddItem("Doodad Properties", func() {
log.Info("Opening the window")
// Open the New Level window in edit-settings mode.
u.doodadPropertiesWindow.Hide()
u.doodadPropertiesWindow = nil
u.SetupPopups(u.d)
u.doodadPropertiesWindow.Show()
})
levelMenu.AddItem("Layers", func() {
u.OpenLayersWindow()
})
}
//////// ////////
// View menu // View menu
if balance.Feature.Zoom { if balance.Feature.Zoom {
@ -169,11 +188,6 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
toolMenu.AddItem("Edit Palette", func() { toolMenu.AddItem("Edit Palette", func() {
u.OpenPaletteWindow() u.OpenPaletteWindow()
}) })
if u.Scene.DrawingType == enum.DoodadDrawing {
toolMenu.AddItem("Layers", func() {
u.OpenLayersWindow()
})
}
// Draw Tools // Draw Tools
toolMenu.AddItemAccel("Pencil Tool", "F", func() { toolMenu.AddItemAccel("Pencil Tool", "F", func() {

View File

@ -140,6 +140,28 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
configure(u.levelSettingsWindow) configure(u.levelSettingsWindow)
} }
// Doodad Properties
if u.doodadPropertiesWindow == nil {
scene, _ := d.Scene.(*EditorScene)
cfg := &windows.DoodadProperties{
Supervisor: u.Supervisor,
Engine: d.Engine,
EditDoodad: scene.Doodad,
}
// Rebuild the window. TODO: hacky af.
cfg.OnRefresh = func() {
u.doodadPropertiesWindow.Hide()
u.doodadPropertiesWindow = nil
u.SetupPopups(u.d)
u.doodadPropertiesWindow.Show()
}
u.doodadPropertiesWindow = windows.NewDoodadPropertiesWindow(cfg)
configure(u.doodadPropertiesWindow)
}
// Publish Level (embed doodads) // Publish Level (embed doodads)
if u.publishWindow == nil { if u.publishWindow == nil {
scene, _ := d.Scene.(*EditorScene) scene, _ := d.Scene.(*EditorScene)

View File

@ -50,7 +50,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window {
newPageType = config.EditLevel.PageType.String() newPageType = config.EditLevel.PageType.String()
newWallpaper = config.EditLevel.Wallpaper newWallpaper = config.EditLevel.Wallpaper
paletteName = textCurrentPalette paletteName = textCurrentPalette
title = "Page Settings" title = "Level Properties"
} }
window := ui.NewWindow(title) window := ui.NewWindow(title)
@ -272,7 +272,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window {
if config.EditLevel != nil { if config.EditLevel != nil {
palBtn.AddItem(paletteName, paletteName, func() {}) palBtn.AddItem(paletteName, paletteName, func() {})
palBtn.AddSeparator(); palBtn.AddSeparator()
} }
for _, palName := range level.DefaultPaletteNames { for _, palName := range level.DefaultPaletteNames {
@ -406,12 +406,12 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window {
// If we're editing a level, did we select a new palette? // If we're editing a level, did we select a new palette?
if paletteName != textCurrentPalette { if paletteName != textCurrentPalette {
modal.Confirm( modal.Confirm(
"Are you sure you want to change the level palette?\n"+ "Are you sure you want to change the level palette?\n" +
"Existing pixels drawn on your level may change, and\n"+ "Existing pixels drawn on your level may change, and\n" +
"if the new palette is smaller, some pixels may be\n"+ "if the new palette is smaller, some pixels may be\n" +
"lost from your level. OK to continue?", "lost from your level. OK to continue?",
).WithTitle("Change Level Palette").Then(func() { ).WithTitle("Change Level Palette").Then(func() {
config.OnCancel(); config.OnCancel()
}) })
return nil return nil
} }

View File

@ -0,0 +1,455 @@
package windows
import (
"io/ioutil"
"os"
"path/filepath"
"sort"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/modal"
"git.kirsle.net/apps/doodle/pkg/native"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
)
// DoodadProperties window.
type DoodadProperties struct {
// Settings passed in by doodle
Supervisor *ui.Supervisor
Engine render.Engine
// Configuration options.
EditDoodad *doodads.Doodad
ActiveTab string // specify the tab to open
OnRefresh func() // caller should rebuild the window
// Widgets.
TabFrame *ui.TabFrame
}
// HACKY GLOBAL VARIABLE
var showTagsOnRefreshDoodadPropertiesWindow bool
// NewSettingsWindow initializes the window.
func NewDoodadPropertiesWindow(cfg *DoodadProperties) *ui.Window {
var (
Width = 400
Height = 300
)
window := ui.NewWindow("Doodad Properties")
window.SetButtons(ui.CloseButton)
window.Configure(ui.Config{
Width: Width,
Height: Height,
Background: render.Grey,
})
///////////
// Tab Bar
tabFrame := ui.NewTabFrame("Tab Frame")
tabFrame.SetBackground(render.DarkGrey)
window.Pack(tabFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
cfg.TabFrame = tabFrame
// Make the tabs.
cfg.makeMetaTab(tabFrame, Width, Height)
cfg.makeTagsTab(tabFrame, Width, Height)
if showTagsOnRefreshDoodadPropertiesWindow {
tabFrame.SetTab("Tags")
showTagsOnRefreshDoodadPropertiesWindow = false
}
tabFrame.Supervise(cfg.Supervisor)
return window
}
// DoodadProperties Window "Metadata" Tab
func (c DoodadProperties) makeMetaTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame {
tab := tabFrame.AddTab("Metadata", ui.NewLabel(ui.Label{
Text: "Metadata",
Font: balance.TabFont,
}))
tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46))
if c.EditDoodad == nil {
return tab
}
//////////////
// Draw the editable metadata form.
for _, data := range []struct {
Label string
Variable *string
Update func(string)
}{
{
Label: "Title:",
Variable: &c.EditDoodad.Title,
Update: func(v string) {
c.EditDoodad.Title = v
},
},
{
Label: "Author:",
Variable: &c.EditDoodad.Author,
Update: func(v string) {
c.EditDoodad.Author = v
},
},
} {
data := data
frame := ui.NewFrame("Metadata " + data.Label + " Frame")
tab.Pack(frame, ui.Pack{
Side: ui.N,
PadY: 4,
FillX: true,
})
// The label
label := ui.NewLabel(ui.Label{
Text: data.Label,
Font: balance.MenuFont,
})
label.Configure(ui.Config{
Width: 75,
})
frame.Pack(label, ui.Pack{
Side: ui.W,
})
// The button.
btn := ui.NewButton(data.Label, ui.NewLabel(ui.Label{
TextVariable: data.Variable,
Font: balance.MenuFont,
}))
btn.Handle(ui.Click, func(ed ui.EventData) error {
shmem.Prompt("Enter a new "+data.Label+" ", func(answer string) {
if answer != "" {
data.Update(answer)
}
})
return nil
})
c.Supervisor.Add(btn)
frame.Pack(btn, ui.Pack{
Side: ui.W,
Expand: true,
PadX: 2,
})
}
//////////////////////////////////
// Draw the JavaScript management
scriptHeader := ui.NewLabel(ui.Label{
Text: "Doodad Script",
Font: balance.LargeLabelFont,
})
tab.Pack(scriptHeader, ui.Pack{
Side: ui.N,
FillX: true,
PadY: 8,
})
// Frame for if a script does exist on the doodad.
var (
ifScript *ui.Frame
elseScript *ui.Frame
)
// "If Script" Frame
{
ifScript = ui.NewFrame("If Script")
tab.Pack(ifScript, ui.Pack{
Side: ui.N,
FillX: true,
})
label := ui.NewLabel(ui.Label{
Text: "This Doodad has a script attached.",
Font: balance.MenuFont,
})
ifScript.Pack(label, ui.Pack{
Side: ui.W,
})
// Delete Button
deleteBtn := ui.NewButton("Save", ui.NewLabel(ui.Label{
Text: "Delete",
Font: balance.MenuFont,
}))
deleteBtn.SetStyle(&balance.ButtonDanger)
deleteBtn.Handle(ui.Click, func(ed ui.EventData) error {
modal.Confirm("Are you sure you want to delete this script?").Then(func() {
c.EditDoodad.Script = ""
ifScript.Hide()
elseScript.Show()
})
return nil
})
c.Supervisor.Add(deleteBtn)
ifScript.Pack(deleteBtn, ui.Pack{
Side: ui.E,
PadX: 2,
})
// Save Button
saveBtn := ui.NewButton("Save", ui.NewLabel(ui.Label{
Text: "Save",
Font: balance.MenuFont,
}))
saveBtn.SetStyle(&balance.ButtonPrimary)
saveBtn.Handle(ui.Click, func(ed ui.EventData) error {
shmem.Prompt("Save script as (*.js): ", func(answer string) {
if answer != "" {
cwd, _ := os.Getwd()
err := ioutil.WriteFile(answer, []byte(c.EditDoodad.Script), 0644)
if err != nil {
shmem.Flash(err.Error())
} else {
shmem.Flash("Written to: %s (%d bytes)", filepath.Join(cwd, answer), len(c.EditDoodad.Script))
}
}
})
return nil
})
c.Supervisor.Add(saveBtn)
ifScript.Pack(saveBtn, ui.Pack{
Side: ui.E,
PadX: 2,
})
}
// "Else Script" Frame
{
elseScript = ui.NewFrame("If Script")
tab.Pack(elseScript, ui.Pack{
Side: ui.N,
FillX: true,
})
label := ui.NewLabel(ui.Label{
Text: "There is no script attached to this doodad.",
Font: balance.MenuFont,
})
elseScript.Pack(label, ui.Pack{
Side: ui.W,
})
}
// Browse Script button.
btnBrowse := ui.NewButton("Browse Script", ui.NewLabel(ui.Label{
Text: "Attach a script...",
Font: balance.MenuFont,
}))
btnBrowse.SetStyle(&balance.ButtonPrimary)
btnBrowse.Handle(ui.Click, func(ed ui.EventData) error {
filename, err := native.OpenFile("Choose a .js file", "*.js")
if err != nil {
shmem.Flash("Couldn't show file dialog: %s", err)
return nil
}
data, err := ioutil.ReadFile(filename)
if err != nil {
shmem.Flash("Couldn't read file: %s", err)
return nil
}
c.EditDoodad.Script = string(data)
shmem.Flash("Attached %d-byte script to this doodad.", len(c.EditDoodad.Script))
// Toggle the if/else frames.
ifScript.Show()
elseScript.Hide()
return nil
})
c.Supervisor.Add(btnBrowse)
tab.Pack(btnBrowse, ui.Pack{
Side: ui.N,
Padding: 4,
})
// Show/hide appropriate frames.
if c.EditDoodad.Script == "" {
ifScript.Hide()
elseScript.Show()
} else {
ifScript.Show()
elseScript.Hide()
}
return tab
}
// DoodadProperties Window "Tags" Tab
func (c DoodadProperties) makeTagsTab(tabFrame *ui.TabFrame, Width, Height int) *ui.Frame {
tab := tabFrame.AddTab("Tags", ui.NewLabel(ui.Label{
Text: "Tags",
Font: balance.TabFont,
}))
tab.Resize(render.NewRect(Width-4, Height-tab.Size().H-46))
if c.EditDoodad == nil {
return tab
}
// Draw a table view of the current tags on this doodad.
var (
headers = []string{"Name", "Value", "Del."}
columns = []int{150, 150, 80} // TODO, Width=400
height = 24
row = ui.NewFrame("HeaderRow")
)
tab.Pack(row, ui.Pack{
Side: ui.N,
FillX: true,
})
for i, value := range headers {
cell := ui.NewLabel(ui.Label{
Text: value,
Font: balance.MenuFontBold,
})
cell.Resize(render.NewRect(columns[i], height))
row.Pack(cell, ui.Pack{
Side: ui.W,
})
}
// No tags?
if len(c.EditDoodad.Tags) == 0 {
label := ui.NewLabel(ui.Label{
Text: "There are no tags on this doodad.",
Font: balance.MenuFont,
})
tab.Pack(label, ui.Pack{
Side: ui.N,
FillX: true,
})
} else {
// Draw the rows for each tag.
var sortedTags []string
for name := range c.EditDoodad.Tags {
sortedTags = append(sortedTags, name)
}
sort.Strings(sortedTags)
for _, tagName := range sortedTags {
var (
name = tagName
value = c.EditDoodad.Tags[name]
)
row = ui.NewFrame("Tag Row")
tab.Pack(row, ui.Pack{
Side: ui.N,
FillX: true,
PadY: 2,
})
lblName := ui.NewLabel(ui.Label{
Text: name,
Font: balance.MenuFont,
})
lblName.Resize(render.NewRect(columns[0], height))
btnValue := ui.NewButton("Tag Button", ui.NewLabel(ui.Label{
Text: value,
Font: balance.MenuFont,
}))
btnValue.Resize(render.NewRect(columns[1], height))
btnValue.Handle(ui.Click, func(ed ui.EventData) error {
shmem.Prompt("Enter new value: ", func(answer string) {
if answer == "" {
return
}
c.EditDoodad.Tags[name] = answer
btnValue.SetText(answer)
})
return nil
})
c.Supervisor.Add(btnValue)
btnDelete := ui.NewButton("Delete Button", ui.NewLabel(ui.Label{
Text: "Delete",
Font: balance.MenuFont,
}))
btnDelete.Resize(render.NewRect(columns[2], height))
btnDelete.SetStyle(&balance.ButtonDanger)
btnDelete.Handle(ui.Click, func(ed ui.EventData) error {
modal.Confirm("Delete tag %s?", name).Then(func() {
log.Info("Delete tag: %s", name)
delete(c.EditDoodad.Tags, name)
// Trigger a refresh.
if c.OnRefresh != nil {
showTagsOnRefreshDoodadPropertiesWindow = true
c.OnRefresh()
}
})
return nil
})
c.Supervisor.Add(btnDelete)
// Pack the widgets.
row.Pack(lblName, ui.Pack{
Side: ui.W,
})
row.Pack(btnValue, ui.Pack{
Side: ui.W,
PadX: 4,
})
row.Pack(btnDelete, ui.Pack{
Side: ui.W,
})
}
}
// Add Tag button.
row = ui.NewFrame("Button Frame")
tab.Pack(row, ui.Pack{
Side: ui.N,
FillX: true,
})
btnAdd := ui.NewButton("New Tag", ui.NewLabel(ui.Label{
Text: "Add Tag",
Font: balance.MenuFont,
}))
btnAdd.SetStyle(&balance.ButtonPrimary)
btnAdd.Handle(ui.Click, func(ed ui.EventData) error {
shmem.Prompt("Enter name of the new tag: ", func(answer string) {
if answer == "" {
return
}
log.Info("Adding doodad tag: %s", answer)
c.EditDoodad.Tags[answer] = ""
if c.OnRefresh != nil {
showTagsOnRefreshDoodadPropertiesWindow = true
c.OnRefresh()
}
})
return nil
})
c.Supervisor.Add(btnAdd)
row.Pack(btnAdd, ui.Pack{
Side: ui.E,
})
return tab
}
func (c DoodadProperties) reloadTagFrame() {
}