diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index 53d19f7..f061b5f 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -77,6 +77,13 @@ var ( Color: render.Black, } + // Small font + SmallFont = render.Text{ + Size: 10, + Padding: 4, + Color: render.Black, + } + // Color for draggable doodad. DragColor = render.MustHexColor("#0099FF") @@ -101,6 +108,7 @@ var ( // Button styles, customized in init(). ButtonPrimary = style.DefaultButton + ButtonDanger = style.DefaultButton ) func init() { @@ -109,4 +117,9 @@ func init() { ButtonPrimary.Foreground = render.RGBA(255, 255, 254, 255) ButtonPrimary.HoverBackground = render.RGBA(0, 153, 255, 255) ButtonPrimary.HoverForeground = ButtonPrimary.Foreground + + ButtonDanger.Background = render.RGBA(153, 30, 30, 255) + ButtonDanger.Foreground = render.RGBA(255, 255, 254, 255) + ButtonDanger.HoverBackground = render.RGBA(255, 30, 30, 255) + ButtonDanger.HoverForeground = ButtonPrimary.Foreground } diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index e3af690..68ccf5d 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -50,27 +50,18 @@ type EditorUI struct { paletteEditor *ui.Window layersWindow *ui.Window publishWindow *ui.Window + filesystemWindow *ui.Window // Palette window. Palette *ui.Window PaletteTab *ui.Frame DoodadTab *ui.Frame - // Doodad Palette window variables. - doodadSkip int - doodadRows []*ui.Frame - doodadPager *ui.Frame - doodadButtonSize int - doodadScroller *ui.Frame - // ToolBar window. activeTool string // Draggable Doodad canvas. DraggableActor *DraggableActor - - // Palette variables. - paletteTab string // selected tab, Palette or Doodads } // NewEditorUI initializes the Editor UI. @@ -568,6 +559,10 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { u.SetupPopups(u.d) u.levelSettingsWindow.Show() }) + levelMenu.AddItem("Attached files", func() { + log.Info("Opening the FileSystem window") + u.OpenFileSystemWindow() + }) levelMenu.AddItemAccel("Playtest", "P", func() { u.Scene.Playtest() }) diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index 4958e86..aad8889 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level/publishing" @@ -56,6 +57,14 @@ func (u *EditorUI) OpenPublishWindow() { u.publishWindow.Show() } +// OpenPublishWindow opens the FileSystem window. +func (u *EditorUI) OpenFileSystemWindow() { + u.filesystemWindow.Hide() + u.filesystemWindow = nil + u.SetupPopups(u.d) + u.filesystemWindow.Show() +} + // SetupPopups preloads popup windows like the DoodadDropper. func (u *EditorUI) SetupPopups(d *Doodle) { // Common window configure function. @@ -147,6 +156,54 @@ func (u *EditorUI) SetupPopups(d *Doodle) { configure(u.publishWindow) } + // Level FileSystem Viewer. + if u.filesystemWindow == nil { + scene, _ := d.Scene.(*EditorScene) + + u.filesystemWindow = windows.NewFileSystemWindow(windows.FileSystem{ + Supervisor: u.Supervisor, + Engine: d.Engine, + Level: scene.Level, + + OnDelete: func(filename string) bool { + // Check if it is an embedded doodad. + if strings.HasPrefix(filename, balance.EmbeddedDoodadsBasePath) { + // Check if we have the doodad installed locally. + if _, err := doodads.LoadFile(filepath.Base(filename)); err != nil { + modal.Alert( + "Cannot remove %s:\n\n"+ + "This doodad is still in use by the level and does not\n"+ + "exist on your local device, so can not be deleted.", + filepath.Base(filename), + ).WithTitle("Cannot Remove Custom Doodad") + return false + } + } + + // Can't delete the current wallpaper. + if filepath.Base(filename) == scene.Level.Wallpaper { + modal.Alert( + "This wallpaper is still in use as the level background, so can\n" + + "not be deleted. Change the wallpaper in the Page Settings window\n" + + "to one of the defaults and then you may remove this file from the level.", + ).WithTitle("Cannot Remove Current Wallpaper") + return false + } + + if ok := scene.Level.DeleteFile(filename); !ok { + modal.Alert("Failed to remove file from level data!") + return false + } + + return true + }, + OnCancel: func() { + u.filesystemWindow.Hide() + }, + }) + configure(u.filesystemWindow) + } + // Palette Editor. if u.paletteEditor == nil { scene, _ := d.Scene.(*EditorScene) diff --git a/pkg/level/filesystem.go b/pkg/level/filesystem.go index 13683c7..a79763f 100644 --- a/pkg/level/filesystem.go +++ b/pkg/level/filesystem.go @@ -54,7 +54,7 @@ func (l *Level) DeleteFile(filename string) bool { func (l *Level) ListFiles() []string { var files []string - if l.Files == nil { + if l == nil || l.Files == nil { return files } diff --git a/pkg/windows/filesystem_window.go b/pkg/windows/filesystem_window.go new file mode 100644 index 0000000..580df2b --- /dev/null +++ b/pkg/windows/filesystem_window.go @@ -0,0 +1,260 @@ +package windows + +import ( + "fmt" + "math" + + "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" +) + +// FileSystem window shows the file attachments to the current level. +type FileSystem struct { + // Settings passed in by doodle + Supervisor *ui.Supervisor + Engine render.Engine + Level *level.Level + + OnDelete func(filename string) bool + OnCancel func() + + // Private vars. + includeBuiltins bool // show built-in doodads in checkbox-list. +} + +// NewFileSystemWindow initializes the window. +func NewFileSystemWindow(cfg FileSystem) *ui.Window { + var ( + windowColor = render.RGBA(255, 255, 200, 255) + windowTitleColor = render.RGBA(255, 153, 0, 255) + windowWidth = 380 + windowHeight = 360 + page = 1 + perPage = 6 + pages = 1 + maxPageButtons = 8 + + // columns and sizes to draw the doodad list + btnHeight = 14 + ) + + window := ui.NewWindow("Attached Files") + window.SetButtons(ui.CloseButton) + window.ActiveTitleBackground = windowTitleColor // TODO: not working? + window.InactiveTitleBackground = windowTitleColor.Darken(60) + window.InactiveTitleForeground = render.Grey + window.Configure(ui.Config{ + Width: windowWidth, + Height: windowHeight, + Background: windowColor, + }) + + ///////////// + // 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: "These are the files embedded inside your level data. When\n" + + "a level is Published, it can attach all of its custom doodads,\n" + + "wallpapers or other custom asset so that it easily plays on\n" + + "a different computer.", + Font: balance.UIFont, + }, + { + Text: "File Attachments", + 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, + }) + } + + ///////////// + // Attached files table. + fsFrame := ui.NewFrame("Doodads Frame") + fsFrame.Resize(render.Rect{ + W: windowWidth, + H: btnHeight*perPage + 140, + }) + window.Pack(fsFrame, ui.Pack{ + Side: ui.N, + FillX: true, + }) + + var fileRows = []*ui.Frame{} + + // Get the file attachments. + files := cfg.Level.ListFiles() + for _, file := range files { + file := file + row := ui.NewFrame("Row: " + file) + label := ui.NewLabel(ui.Label{ + Text: file, + Font: balance.UIFont, + }) + row.Pack(label, ui.Pack{ + Side: ui.W, + Padding: 1, + }) + + delBtn := ui.NewButton("Delete: "+file, ui.NewLabel(ui.Label{ + Text: "Delete", + Font: balance.SmallFont, + })) + delBtn.SetStyle(&balance.ButtonDanger) + delBtn.Handle(ui.Click, func(ed ui.EventData) error { + if cfg.OnDelete != nil { + if cfg.OnDelete(file) { + row.Hide() + } + } + return nil + }) + cfg.Supervisor.Add(delBtn) + row.Place(delBtn, ui.Place{ + Right: 4, + }) + + fileRows = append(fileRows, row) + } + + for i, row := range fileRows { + fsFrame.Pack(row, ui.Pack{ + Side: ui.N, + FillX: true, + PadY: 2, + }) + + // Hide if too long for 1st page. + if i >= perPage { + row.Hide() + } + } + + ///////////// + // Buttons at bottom of window + + bottomFrame := ui.NewFrame("Button Frame") + window.Pack(bottomFrame, ui.Pack{ + Side: ui.S, + FillX: true, + }) + + // Pager for the doodads. + pages = int( + math.Ceil( + float64(len(fileRows)) / float64(perPage), + ), + ) + pagerOnChange := func(newPage, perPage int) { + page = newPage + log.Info("Page: %d, %d", page, perPage) + + // Re-evaluate which rows are shown/hidden for the page we're on. + var ( + minRow = (page - 1) * perPage + visible = 0 + ) + for i, row := range fileRows { + if visible >= perPage { + row.Hide() + continue + } + + if i < minRow { + row.Hide() + } else { + row.Show() + visible++ + } + } + } + pager := ui.NewPager(ui.Pager{ + Name: "Files List Pager", + Page: page, + Pages: pages, + PerPage: perPage, + MaxPageButtons: maxPageButtons, + Font: balance.MenuFont, + OnChange: pagerOnChange, + }) + pager.Compute(cfg.Engine) + pager.Supervise(cfg.Supervisor) + bottomFrame.Place(pager, ui.Place{ + Top: 20, + Left: 20, + }) + + frame := ui.NewFrame("Button frame") + buttons := []struct { + label string + primary bool + f func() + }{ + {"Close", false, func() { + if cfg.OnCancel != nil { + cfg.OnCancel() + } + }}, + } + for _, button := range buttons { + button := button + + btn := ui.NewButton(button.label, ui.NewLabel(ui.Label{ + Text: button.label, + Font: balance.MenuFont, + })) + if button.primary { + btn.SetStyle(&balance.ButtonPrimary) + } + + btn.Handle(ui.Click, func(ed ui.EventData) error { + button.f() + return nil + }) + + btn.Compute(cfg.Engine) + cfg.Supervisor.Add(btn) + + frame.Pack(btn, ui.Pack{ + Side: ui.W, + PadX: 4, + Expand: true, + Fill: true, + }) + } + bottomFrame.Pack(frame, ui.Pack{ + Side: ui.E, + Padding: 8, + }) + + return window +} diff --git a/pkg/windows/publish_level.go b/pkg/windows/publish_level.go index 514e162..f39cbf6 100644 --- a/pkg/windows/publish_level.go +++ b/pkg/windows/publish_level.go @@ -123,39 +123,7 @@ func NewPublishWindow(cfg Publish) *ui.Window { PadX: 2, }) - // // Collect all the doodad names in use in this level. - // unique := map[string]interface{}{} - // names := []string{} - // if cfg.Level != nil { - // for _, actor := range cfg.Level.Actors { - // if _, ok := unique[actor.Filename]; ok { - // continue - // } - // unique[actor.Filename] = nil - // names = append(names, actor.Filename) - // } - // } - - // sort.Strings(names) - - // // Identify which of the doodads are built-ins. - // usedBuiltins := []string{} - // builtinMap := map[string]interface{}{} - // usedCustom := []string{} - // if builtins, err := doodads.ListBuiltin(); err == nil { - // for _, filename := range builtins { - // if _, ok := unique[filename]; ok { - // usedBuiltins = append(usedBuiltins, filename) - // builtinMap[filename] = nil - // } - // } - // } - // for _, name := range names { - // if _, ok := builtinMap[name]; ok { - // continue - // } - // usedCustom = append(usedCustom, name) - // } + // Collect the doodads named in this level. usedBuiltins, usedCustom := publishing.GetUsedDoodadNames(cfg.Level) // Helper function to draw the button rows for a set of doodads.