From 0fa1bf8a76c2ba1bf4a9172d8cf93ecce1bfb9f8 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 2 Sep 2021 21:26:55 -0700 Subject: [PATCH] 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! --- dev-assets/doodads/objects/exit-flag.js | 9 +- pkg/balance/theme.go | 7 + pkg/editor_ui.go | 19 +- pkg/editor_ui_menubar.go | 26 +- pkg/editor_ui_popups.go | 22 ++ pkg/windows/add_edit_level.go | 30 +- pkg/windows/doodad_properties.go | 455 ++++++++++++++++++++++++ 7 files changed, 536 insertions(+), 32 deletions(-) create mode 100644 pkg/windows/doodad_properties.go diff --git a/dev-assets/doodads/objects/exit-flag.js b/dev-assets/doodads/objects/exit-flag.js index fe530fe..9e65bf1 100644 --- a/dev-assets/doodads/objects/exit-flag.js +++ b/dev-assets/doodads/objects/exit-flag.js @@ -1,12 +1,17 @@ // Exit Flag. 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) { return; } + // Only care if it's the player. + if (!e.Actor.IsPlayer()) { + return; + } + if (e.InHitbox) { EndLevel(); } diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index 82319d8..57d88ef 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -105,6 +105,13 @@ var ( 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 = render.Text{ Size: 14, diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 91d5509..ca2fcc3 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -43,15 +43,16 @@ type EditorUI struct { PlayButton *ui.Button // Popup windows. - levelSettingsWindow *ui.Window - aboutWindow *ui.Window - doodadWindow *ui.Window - paletteEditor *ui.Window - layersWindow *ui.Window - publishWindow *ui.Window - filesystemWindow *ui.Window - licenseWindow *ui.Window - settingsWindow *ui.Window // lazy loaded + levelSettingsWindow *ui.Window + doodadPropertiesWindow *ui.Window + aboutWindow *ui.Window + doodadWindow *ui.Window + paletteEditor *ui.Window + layersWindow *ui.Window + publishWindow *ui.Window + filesystemWindow *ui.Window + licenseWindow *ui.Window + settingsWindow *ui.Window // lazy loaded // Palette window. Palette *ui.Window diff --git a/pkg/editor_ui_menubar.go b/pkg/editor_ui_menubar.go index e7c50c7..236d5cf 100644 --- a/pkg/editor_ui_menubar.go +++ b/pkg/editor_ui_menubar.go @@ -117,7 +117,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { // Level menu if u.Scene.DrawingType == enum.LevelDrawing { levelMenu := menu.AddMenu("Level") - levelMenu.AddItem("Page settings", func() { + levelMenu.AddItem("Level Properties", func() { log.Info("Opening the window") // 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 if balance.Feature.Zoom { @@ -169,11 +188,6 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { toolMenu.AddItem("Edit Palette", func() { u.OpenPaletteWindow() }) - if u.Scene.DrawingType == enum.DoodadDrawing { - toolMenu.AddItem("Layers", func() { - u.OpenLayersWindow() - }) - } // Draw Tools toolMenu.AddItemAccel("Pencil Tool", "F", func() { diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index 0e39b88..87972bc 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -140,6 +140,28 @@ func (u *EditorUI) SetupPopups(d *Doodle) { 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) if u.publishWindow == nil { scene, _ := d.Scene.(*EditorScene) diff --git a/pkg/windows/add_edit_level.go b/pkg/windows/add_edit_level.go index 42c5b61..67f27a3 100644 --- a/pkg/windows/add_edit_level.go +++ b/pkg/windows/add_edit_level.go @@ -50,7 +50,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { newPageType = config.EditLevel.PageType.String() newWallpaper = config.EditLevel.Wallpaper paletteName = textCurrentPalette - title = "Page Settings" + title = "Level Properties" } window := ui.NewWindow(title) @@ -84,7 +84,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { Font: balance.LabelFont, }) typeFrame.Pack(label1, ui.Pack{ - Side: ui.W, + Side: ui.W, }) type typeObj struct { @@ -102,7 +102,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { Font: ui.MenuFont, }) typeFrame.Pack(typeBtn, ui.Pack{ - Side: ui.W, + Side: ui.W, Expand: true, }) @@ -146,7 +146,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { frame.Pack(wpFrame, ui.Pack{ Side: ui.N, FillX: true, - PadY: 2, + PadY: 2, }) label2 := ui.NewLabel(ui.Label{ @@ -154,7 +154,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { Font: balance.LabelFont, }) wpFrame.Pack(label2, ui.Pack{ - Side: ui.W, + Side: ui.W, PadY: 2, }) @@ -177,7 +177,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { }) wallBtn.AlwaysChange = true wpFrame.Pack(wallBtn, ui.Pack{ - Side: ui.W, + Side: ui.W, Expand: true, }) @@ -249,7 +249,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { frame.Pack(palFrame, ui.Pack{ Side: ui.N, FillX: true, - PadY: 4, + PadY: 4, }) label3 := ui.NewLabel(ui.Label{ @@ -257,7 +257,7 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { Font: balance.LabelFont, }) palFrame.Pack(label3, ui.Pack{ - Side: ui.W, + Side: ui.W, }) palBtn := ui.NewSelectBox("Palette Select", ui.Label{ @@ -266,13 +266,13 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window { palBtn.AlwaysChange = true palFrame.Pack(palBtn, ui.Pack{ - Side: ui.W, + Side: ui.W, Expand: true, }) if config.EditLevel != nil { palBtn.AddItem(paletteName, paletteName, func() {}) - palBtn.AddSeparator(); + palBtn.AddSeparator() } 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 paletteName != textCurrentPalette { modal.Confirm( - "Are you sure you want to change the level palette?\n"+ - "Existing pixels drawn on your level may change, and\n"+ - "if the new palette is smaller, some pixels may be\n"+ - "lost from your level. OK to continue?", + "Are you sure you want to change the level palette?\n" + + "Existing pixels drawn on your level may change, and\n" + + "if the new palette is smaller, some pixels may be\n" + + "lost from your level. OK to continue?", ).WithTitle("Change Level Palette").Then(func() { - config.OnCancel(); + config.OnCancel() }) return nil } diff --git a/pkg/windows/doodad_properties.go b/pkg/windows/doodad_properties.go new file mode 100644 index 0000000..5189517 --- /dev/null +++ b/pkg/windows/doodad_properties.go @@ -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() { + +}