526 lines
14 KiB
Go
526 lines
14 KiB
Go
package assets
|
|
|
|
import (
|
|
favs "aego/editor/core/favorites"
|
|
"aego/editor/core/refs"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"aego/asset"
|
|
"aego/core/ids"
|
|
"aego/data"
|
|
"aego/editor/core/assetops"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/document"
|
|
"aego/schema"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
func (p *Panel) menus() {
|
|
ui := p.f.UI
|
|
if widgets.ContextMenu(ui, p.assetMenuID) {
|
|
p.assetItems()
|
|
widgets.EndContextMenu(ui)
|
|
}
|
|
if widgets.ContextMenu(ui, p.dirMenuID) {
|
|
p.dirItems(p.menuDir)
|
|
widgets.EndContextMenu(ui)
|
|
}
|
|
}
|
|
|
|
func (p *Panel) copyText(s string) {
|
|
if c := p.d.Files.Clipboard(); c != nil {
|
|
c.SetText(s)
|
|
}
|
|
}
|
|
|
|
func (p *Panel) assetItems() {
|
|
ui := p.f.UI
|
|
a := p.find(p.active)
|
|
if a == nil || !p.sel[a.ID] {
|
|
return
|
|
}
|
|
single := len(p.sel) == 1
|
|
reg := p.d.Commands
|
|
if widgets.MenuItemIcon(ui, "Open", "", "file.open", single && openable(a.Kind), false) {
|
|
p.open(a)
|
|
}
|
|
widgets.Separator(ui)
|
|
if widgets.MenuItemIcon(ui, "Copy", reg.Shortcut(CommandCopy), "edit.copy", true, false) {
|
|
p.report(p.CopySelected())
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Cut", reg.Shortcut(CommandCut), "edit.cut", true, false) {
|
|
p.report(p.CutSelected())
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Paste", reg.Shortcut(CommandPaste), "edit.paste", p.d.Files.CanPaste(), false) {
|
|
p.report(p.Paste(path.Dir(a.Path)))
|
|
}
|
|
widgets.Separator(ui)
|
|
if widgets.MenuItemIcon(ui, "Rename", reg.Shortcut(CommandRename), "edit.rename", single, false) {
|
|
p.StartRename()
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Batch Rename…", "", "edit.batch_rename", len(p.sel) > 1 && p.d.BatchRename != nil, false) {
|
|
p.d.BatchRename(p.Selection())
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Duplicate", reg.Shortcut(CommandDuplicate), "edit.duplicate", true, false) {
|
|
p.report(p.DuplicateSelected())
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Delete", reg.Shortcut(CommandDelete), "edit.delete", true, false) {
|
|
p.DeleteSelected()
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Reimport", "", "ui.reset", true, false) {
|
|
p.report(p.ReimportSelected())
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Find References", reg.Shortcut(CommandFindReferences), "panel.references", single && p.d.ShowReferences != nil, false) {
|
|
p.d.ShowReferences(a.ID, nil)
|
|
}
|
|
if p.d.Favorites != nil && single {
|
|
fav := favs.Item{Kind: favs.Asset, Asset: a.ID, Label: path.Base(a.Path)}
|
|
label := "Add to Favorites"
|
|
if p.d.Favorites.Has(fav) {
|
|
label = "Remove from Favorites"
|
|
}
|
|
if widgets.MenuItemIcon(ui, label, "", "ui.star", true, false) {
|
|
_, err := p.d.Favorites.Toggle(fav)
|
|
p.report(err)
|
|
}
|
|
}
|
|
if single {
|
|
id, rel, name := a.ID, a.Path, a.Name
|
|
switch a.Kind {
|
|
case asset.ArtScene:
|
|
widgets.Separator(ui)
|
|
startup := p.d.Project != nil && p.d.Project.MainScene == id
|
|
if widgets.MenuItemIcon(ui, "Set as Startup Scene", "", "play.start", !startup, startup) {
|
|
p.d.Files.SetStartup(id, rel)
|
|
}
|
|
case asset.ArtPrefab:
|
|
widgets.Separator(ui)
|
|
if widgets.MenuItemIcon(ui, "Instantiate in Scene", "", "file.prefab", p.d.Docs.Doc() != nil && p.Instantiate != nil, false) {
|
|
p.Instantiate(id, path.Base(name))
|
|
}
|
|
if p.d.Prefabs != nil {
|
|
if widgets.MenuItemIcon(ui, "Create Variant", "", "prefab.variant", true, false) {
|
|
p.report(p.d.Prefabs.CreateVariant(id))
|
|
p.invalidate()
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Find Instances", "", "ui.search", true, false) {
|
|
p.d.Prefabs.FindInstances(id)
|
|
}
|
|
}
|
|
case asset.ArtScript:
|
|
widgets.Separator(ui)
|
|
nodes := p.d.Selection.Selected()
|
|
if widgets.MenuItemIcon(ui, "Attach to Selected", "", "comp.script", len(nodes) > 0, false) {
|
|
p.d.Scenes.AttachScript(nodes, id)
|
|
}
|
|
}
|
|
}
|
|
widgets.Separator(ui)
|
|
if p.d.Compare != nil && single && diffable(a.Path) {
|
|
if widgets.MenuItemIcon(ui, "Compare with HEAD", "", "panel.diff", true, false) {
|
|
p.d.Compare(a.Path)
|
|
}
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Reveal in File Manager", "", "file.reveal", single, false) {
|
|
p.report(reveal(p.d.Pipeline.Abs(a.Path), false))
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Copy Path", "", "edit.copy_path", true, false) {
|
|
var paths []string
|
|
for _, id := range p.Selection() {
|
|
if rel, ok := p.d.Pipeline.PathOf(id); ok {
|
|
paths = append(paths, rel)
|
|
}
|
|
}
|
|
p.copyText(strings.Join(paths, "\n"))
|
|
}
|
|
}
|
|
|
|
func (p *Panel) dirItems(dir string) {
|
|
ui := p.f.UI
|
|
reg := p.d.Commands
|
|
if widgets.SubMenuIcon(ui, "New", "ui.plus", 200) {
|
|
if widgets.MenuItemIcon(ui, "Folder", "", "file.folder", true, false) {
|
|
p.newFolder(dir)
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Scene…", "", "file.scene", p.d.Files.CanNewScene(), false) {
|
|
p.newDoc(dir, "New scene", document.SceneExt, p.d.Files.NewScene)
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Prefab…", "", "file.prefab", p.d.Files.CanNewPrefab(), false) {
|
|
p.newDoc(dir, "New prefab", assetops.PrefabExt, p.d.Files.NewPrefab)
|
|
}
|
|
widgets.Separator(ui)
|
|
if widgets.MenuItemIcon(ui, "Script…", "", "file.script", true, false) {
|
|
p.newScript(dir)
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Script Package…", "", "file.package", true, false) {
|
|
p.newPackage(dir)
|
|
}
|
|
p.dataItems(dir)
|
|
widgets.EndSubMenu(ui)
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Paste", reg.Shortcut(CommandPaste), "edit.paste", p.d.Files.CanPaste(), false) {
|
|
p.report(p.Paste(dir))
|
|
}
|
|
widgets.Separator(ui)
|
|
top := path.Dir(dir) == "."
|
|
if widgets.MenuItemIcon(ui, "Rename", "", "edit.rename", !top, false) {
|
|
p.expanded[path.Dir(dir)] = true
|
|
p.renamingDir, p.renameDirStart = dir, true
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Delete…", "", "edit.delete", !top, false) {
|
|
p.deleteDir(dir)
|
|
}
|
|
widgets.Separator(ui)
|
|
if widgets.MenuItemIcon(ui, "Reveal in File Manager", "", "file.reveal", true, false) {
|
|
p.report(reveal(p.d.Pipeline.Abs(dir), true))
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Copy Path", "", "edit.copy_path", true, false) {
|
|
p.copyText(dir)
|
|
}
|
|
}
|
|
|
|
func (p *Panel) dataTypes() []*schema.TypeDescriptor {
|
|
if p.d.Schema == nil {
|
|
return nil
|
|
}
|
|
var out []*schema.TypeDescriptor
|
|
p.d.Schema.Each(func(td *schema.TypeDescriptor) {
|
|
if td.Kind == schema.TypeAsset && !td.Hidden {
|
|
out = append(out, td)
|
|
}
|
|
})
|
|
return out
|
|
}
|
|
|
|
func typeShort(full string) string {
|
|
return full[strings.LastIndexByte(full, '.')+1:]
|
|
}
|
|
|
|
func (p *Panel) newData(dir string, td *schema.TypeDescriptor, table bool) {
|
|
ext, title := ".aedata", "New "+typeShort(td.FullName)+" data"
|
|
if table {
|
|
ext, title = ".aetable", "New "+typeShort(td.FullName)+" table"
|
|
}
|
|
p.newDoc(dir, title, ext, func(rel string) error {
|
|
var b []byte
|
|
if table {
|
|
b = data.WriteTable(&data.TableDoc{Type: td.FullName})
|
|
} else {
|
|
b = data.WriteDoc(&data.Doc{Type: td.FullName})
|
|
}
|
|
_, err := p.d.Files.CreateFile(rel, b)
|
|
return err
|
|
})
|
|
}
|
|
|
|
func (p *Panel) dataItems(dir string) {
|
|
types := p.dataTypes()
|
|
if len(types) == 0 {
|
|
return
|
|
}
|
|
ui := p.f.UI
|
|
widgets.Separator(ui)
|
|
if widgets.SubMenuIcon(ui, "Data Asset", "file.data", 220) {
|
|
for _, td := range types {
|
|
if widgets.MenuItemIcon(ui, typeShort(td.FullName)+"…", "", "file.data", true, false) {
|
|
p.newData(dir, td, false)
|
|
}
|
|
}
|
|
widgets.EndSubMenu(ui)
|
|
}
|
|
if widgets.SubMenuIcon(ui, "Data Table", "file.table", 220) {
|
|
for _, td := range types {
|
|
if widgets.MenuItemIcon(ui, typeShort(td.FullName)+"…", "", "file.table", true, false) {
|
|
p.newData(dir, td, true)
|
|
}
|
|
}
|
|
widgets.EndSubMenu(ui)
|
|
}
|
|
}
|
|
|
|
func (p *Panel) created(rel string) {
|
|
p.invalidate()
|
|
p.refresh()
|
|
p.folder = path.Dir(rel)
|
|
p.search = ""
|
|
p.expanded[path.Dir(p.folder)] = true
|
|
if id, ok := p.d.Pipeline.Lookup(rel); ok {
|
|
p.selectOnly(id)
|
|
p.scrollTo = id
|
|
}
|
|
}
|
|
|
|
func (p *Panel) newDoc(dir, title, ext string, create func(string) error) {
|
|
p.d.Notices.Ask(context.Question{
|
|
Title: title,
|
|
Fields: []string{"Name"},
|
|
Values: []string{assetops.Stem(p.d.Files.UniqueFile(dir, "untitled", ext))},
|
|
Buttons: []string{"Create", "Cancel"},
|
|
Submit: func(v []string) {
|
|
name := strings.TrimSpace(v[0])
|
|
if !assetops.ValidName(name) {
|
|
p.d.Notices.Notify(context.LevelError, name+" is not a valid file name")
|
|
return
|
|
}
|
|
rel := path.Join(dir, name+ext)
|
|
if p.d.Files.Exists(rel) {
|
|
p.d.Notices.Notify(context.LevelError, rel+" already exists")
|
|
return
|
|
}
|
|
if err := create(rel); err != nil {
|
|
p.d.Notices.Notify(context.LevelError, err.Error())
|
|
return
|
|
}
|
|
p.created(rel)
|
|
},
|
|
})
|
|
}
|
|
|
|
func (p *Panel) createScript(dir, name string) {
|
|
rel, err := p.d.Files.CreateScript(dir, strings.TrimSpace(name))
|
|
if err != nil {
|
|
p.d.Notices.Notify(context.LevelError, err.Error())
|
|
return
|
|
}
|
|
p.created(rel)
|
|
if p.d.Files.ScriptEditor() {
|
|
p.d.Nav.Open(context.At(rel, 3, 1))
|
|
}
|
|
}
|
|
|
|
func (p *Panel) newScript(dir string) {
|
|
if rel, ok := assetops.ScriptsRel(dir); ok && rel != "" {
|
|
p.d.Notices.Ask(context.Question{
|
|
Title: "New script in package " + p.d.Files.PackageOf(dir),
|
|
Fields: []string{"Script name"},
|
|
Values: []string{"NewScript"},
|
|
Buttons: []string{"Create", "Cancel"},
|
|
Submit: func(v []string) { p.createScript(dir, v[0]) },
|
|
})
|
|
return
|
|
}
|
|
p.d.Notices.Ask(context.Question{
|
|
Title: "New script",
|
|
Message: "Scripts live in a package folder under scripts/",
|
|
Fields: []string{"Package", "Script name"},
|
|
Values: []string{"", "NewScript"},
|
|
Buttons: []string{"Create", "Cancel"},
|
|
Submit: func(v []string) {
|
|
pkg := strings.TrimSpace(v[0])
|
|
if !assetops.ValidIdent(pkg) {
|
|
p.d.Notices.Notify(context.LevelError, pkg+" is not a valid package name")
|
|
return
|
|
}
|
|
p.createScript(path.Join("scripts", pkg), v[1])
|
|
},
|
|
})
|
|
}
|
|
|
|
func (p *Panel) newPackage(dir string) {
|
|
base := "scripts"
|
|
if _, ok := assetops.ScriptsRel(dir); ok {
|
|
base = dir
|
|
}
|
|
p.d.Notices.Ask(context.Question{
|
|
Title: "New script package",
|
|
Message: "Creates " + base + "/<package>/ with its first script",
|
|
Fields: []string{"Package", "First script"},
|
|
Values: []string{"", "NewScript"},
|
|
Buttons: []string{"Create", "Cancel"},
|
|
Submit: func(v []string) {
|
|
pkg := strings.TrimSpace(v[0])
|
|
if !assetops.ValidIdent(pkg) {
|
|
p.d.Notices.Notify(context.LevelError, pkg+" is not a valid package name")
|
|
return
|
|
}
|
|
pkgDir := path.Join(base, pkg)
|
|
if p.d.Files.Exists(pkgDir) {
|
|
p.d.Notices.Notify(context.LevelError, pkgDir+" already exists")
|
|
return
|
|
}
|
|
if err := p.d.Pipeline.Mkdir(pkgDir); err != nil {
|
|
p.d.Notices.Notify(context.LevelError, err.Error())
|
|
return
|
|
}
|
|
p.expanded[base] = true
|
|
p.createScript(pkgDir, v[1])
|
|
},
|
|
})
|
|
}
|
|
|
|
func (p *Panel) deleteDir(dir string) {
|
|
var gone []ids.AssetID
|
|
for i := range p.items {
|
|
if assetops.Under(p.items[i].Path, dir) {
|
|
gone = append(gone, p.items[i].ID)
|
|
}
|
|
}
|
|
msg := "Delete the empty folder " + dir + "?"
|
|
if n := len(gone); n > 0 {
|
|
msg = "Delete " + dir + " and the " + strconv.Itoa(n) + " file(s) inside it? Scenes that reference them show them as missing."
|
|
}
|
|
p.d.Notices.Ask(context.Question{
|
|
Title: "Delete folder",
|
|
Message: msg,
|
|
Buttons: []string{"Delete", "Cancel"},
|
|
Then: func(choice int) {
|
|
if choice != 0 {
|
|
return
|
|
}
|
|
err := p.d.Files.DeleteDir(dir)
|
|
for _, id := range gone {
|
|
p.d.Thumbs.Forget(id)
|
|
delete(p.sel, id)
|
|
}
|
|
if p.folder == dir || assetops.Under(p.folder, dir) {
|
|
p.folder = path.Dir(dir)
|
|
}
|
|
if err != nil {
|
|
p.d.Notices.Notify(context.LevelError, err.Error())
|
|
}
|
|
p.invalidate()
|
|
},
|
|
})
|
|
}
|
|
|
|
func (p *Panel) DeleteSelected() {
|
|
list := p.Selection()
|
|
if len(list) == 0 {
|
|
return
|
|
}
|
|
if p.d.Refs != nil && p.d.ShowReferences != nil {
|
|
gone := map[ids.AssetID]bool{}
|
|
for _, id := range list {
|
|
gone[id] = true
|
|
}
|
|
var users []refs.Ref
|
|
var first ids.AssetID
|
|
for _, id := range list {
|
|
for _, r := range p.d.Refs.Referrers(id) {
|
|
if r.Project || !gone[r.Asset] {
|
|
users = append(users, r)
|
|
if first.IsZero() {
|
|
first = id
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if len(users) > 0 {
|
|
p.askUsed(list, users, first)
|
|
return
|
|
}
|
|
}
|
|
title, msg := "Delete asset", ""
|
|
if len(list) == 1 {
|
|
rel, _ := p.d.Pipeline.PathOf(list[0])
|
|
msg = "Delete " + rel + "?"
|
|
} else {
|
|
title, msg = "Delete assets", "Delete "+strconv.Itoa(len(list))+" assets?"
|
|
}
|
|
p.d.Notices.Ask(context.Question{
|
|
Title: title,
|
|
Message: msg + " Nothing in the project references them.",
|
|
Buttons: []string{"Delete", "Cancel"},
|
|
Then: func(choice int) {
|
|
if choice == 0 {
|
|
p.DeleteNow(list)
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
func (p *Panel) DuplicateSelected() error {
|
|
var made []ids.AssetID
|
|
var first error
|
|
scripts := false
|
|
for _, id := range p.Selection() {
|
|
dup, err := p.d.Files.Duplicate(id)
|
|
if err != nil {
|
|
if first == nil {
|
|
first = err
|
|
}
|
|
continue
|
|
}
|
|
made = append(made, dup)
|
|
if rel, ok := p.d.Pipeline.PathOf(dup); ok && path.Ext(rel) == document.ScriptExt {
|
|
scripts = true
|
|
}
|
|
}
|
|
if len(made) > 0 {
|
|
clear(p.sel)
|
|
for _, id := range made {
|
|
p.sel[id] = true
|
|
}
|
|
p.active, p.anchor = made[len(made)-1], made[0]
|
|
}
|
|
if scripts {
|
|
p.d.Notices.Notify(context.LevelWarn, "Rename the script types in the copy, a package cannot declare a script twice")
|
|
}
|
|
p.invalidate()
|
|
return first
|
|
}
|
|
|
|
func (p *Panel) ReimportSelected() error {
|
|
if p.d.Pipeline == nil {
|
|
return nil
|
|
}
|
|
var first error
|
|
for _, id := range p.Selection() {
|
|
err := p.d.Files.Reimport(id)
|
|
if err != nil && first == nil {
|
|
first = err
|
|
}
|
|
}
|
|
if first == nil && len(p.sel) > 0 {
|
|
p.d.Notices.Notify(context.LevelInfo, "Reimported")
|
|
}
|
|
return first
|
|
}
|
|
|
|
func (p *Panel) askUsed(list []ids.AssetID, users []refs.Ref, first ids.AssetID) {
|
|
var names []string
|
|
for i, r := range users {
|
|
if i == 8 {
|
|
names = append(names, "and "+strconv.Itoa(len(users)-i)+" more")
|
|
break
|
|
}
|
|
names = append(names, r.Path)
|
|
}
|
|
p.d.Notices.Ask(context.Question{
|
|
Title: "Assets in use",
|
|
Message: "Still referenced by:\n" + strings.Join(names, "\n") + "\n\nDeleting them leaves these references missing.",
|
|
Buttons: []string{"Show References", "Delete Anyway", "Cancel"},
|
|
Then: func(choice int) {
|
|
switch choice {
|
|
case 0:
|
|
p.d.ShowReferences(first, list)
|
|
case 1:
|
|
p.DeleteNow(list)
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
func (p *Panel) DeleteNow(list []ids.AssetID) {
|
|
err := p.d.Files.Delete(list)
|
|
for _, id := range list {
|
|
p.d.Thumbs.Forget(id)
|
|
delete(p.sel, id)
|
|
}
|
|
p.active = ids.AssetID{}
|
|
if err != nil {
|
|
p.d.Notices.Notify(context.LevelError, err.Error())
|
|
}
|
|
p.invalidate()
|
|
}
|
|
|
|
func diffable(rel string) bool {
|
|
switch path.Ext(rel) {
|
|
case ".aescene", ".aeprefab", ".aedata", ".aetable", ".aepreset":
|
|
return true
|
|
}
|
|
return false
|
|
}
|