754 lines
19 KiB
Go
754 lines
19 KiB
Go
package hierarchy
|
|
|
|
import (
|
|
"aego/editor/core/favorites"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"aego/asset"
|
|
"aego/core/ids"
|
|
"aego/core/mathx"
|
|
"aego/editor/core/clipboard"
|
|
"aego/editor/core/command"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/document"
|
|
"aego/editor/core/history/commands"
|
|
"aego/editor/core/prefabs"
|
|
"aego/editor/core/services"
|
|
"aego/editor/core/templates"
|
|
"aego/editor/core/workspace"
|
|
"aego/platform/input"
|
|
"aego/scene"
|
|
"aego/schema"
|
|
"aego/ui/core"
|
|
"aego/ui/dnd"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
const (
|
|
zoneBefore = iota
|
|
zoneInto
|
|
zoneAfter
|
|
)
|
|
|
|
type row struct {
|
|
h scene.NodeHandle
|
|
id ids.NodeID
|
|
depth int
|
|
has bool
|
|
last bool
|
|
lines uint64
|
|
}
|
|
|
|
type Deps struct {
|
|
Templates *templates.Registry
|
|
Docs services.Documents
|
|
Selection services.Selection
|
|
Commands *command.Registry
|
|
Scenes services.SceneOps
|
|
Notices services.Notices
|
|
Prefabs services.Prefabs
|
|
Tools *context.SceneSettings
|
|
}
|
|
|
|
type Panel struct {
|
|
d Deps
|
|
rows []row
|
|
scroll float32
|
|
filter string
|
|
anchor ids.NodeID
|
|
pendingSingle ids.NodeID
|
|
renaming ids.NodeID
|
|
renameStart bool
|
|
renameText string
|
|
reveal ids.NodeID
|
|
lastRev uint64
|
|
menuNode ids.NodeID
|
|
menuID core.ID
|
|
sceneMenuID core.ID
|
|
doc document.ID
|
|
rank map[schema.TypeID]int
|
|
rankReg *schema.Registry
|
|
}
|
|
|
|
func New(d Deps) *Panel {
|
|
return &Panel{d: d}
|
|
}
|
|
|
|
func (p *Panel) ID() string {
|
|
return workspace.Hierarchy
|
|
}
|
|
|
|
func (p *Panel) Title() string {
|
|
return "Hierarchy"
|
|
}
|
|
|
|
func (p *Panel) Scope() string {
|
|
return command.ScopeHierarchy
|
|
}
|
|
|
|
func (p *Panel) StartRename(id ids.NodeID) {
|
|
p.renaming = id
|
|
p.renameStart = true
|
|
p.reveal = id
|
|
p.filter = ""
|
|
}
|
|
|
|
func (p *Panel) sync() {
|
|
p.lastRev = p.d.Selection.SelectionState().Revision()
|
|
}
|
|
|
|
func (p *Panel) report(err error) {
|
|
if err != nil {
|
|
p.d.Notices.Notify(context.LevelError, err.Error())
|
|
}
|
|
}
|
|
|
|
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
|
|
ui := f.UI
|
|
th := &ui.Theme
|
|
d := p.d.Docs.Doc()
|
|
if d == nil {
|
|
widgets.LabelDim(ui, "No scene open")
|
|
return
|
|
}
|
|
if d.ID != p.doc {
|
|
p.doc = d.ID
|
|
p.scroll = 0
|
|
p.anchor = ids.NodeID{}
|
|
p.renaming = ids.NodeID{}
|
|
}
|
|
p.menuID = ui.ID("menu")
|
|
p.sceneMenuID = ui.ID("scenemenu")
|
|
p.track(d)
|
|
if p.renameStart {
|
|
p.expandTo(d, p.renaming)
|
|
}
|
|
|
|
bar := ui.Cursor().Next(0)
|
|
if widgets.SearchField(ui, ui.ID("search"), mathx.Rect{X: bar.X, Y: bar.Y, W: bar.W - bar.H - 4, H: bar.H}, &p.filter) {
|
|
p.scroll = 0
|
|
}
|
|
addR := mathx.Rect{X: bar.X + bar.W - bar.H, Y: bar.Y, W: bar.H, H: bar.H}
|
|
addID := ui.ID("add")
|
|
if ui.Pressed(addID, addR) {
|
|
if ui.PopupOpen(addID) {
|
|
ui.ClosePopup(addID)
|
|
} else {
|
|
ui.CloseAllPopups()
|
|
ui.OpenPopup(addID, mathx.Vec2{X: addR.X, Y: addR.Y + addR.H})
|
|
}
|
|
}
|
|
ui.Rect(addR, ui.WidgetColor(addID))
|
|
ui.TextIn(addR, "+", th.Text, core.AlignCenter)
|
|
if ui.BeginPopup(addID, 200) {
|
|
p.createItems(d, p.activeNode(d))
|
|
ui.EndPopup()
|
|
}
|
|
hdr := ui.Cursor().Next(0)
|
|
p.drawHeader(f, d, hdr)
|
|
|
|
top := hdr.Y + hdr.H + th.Spacing
|
|
list := mathx.Rect{X: r.X, Y: top, W: r.W, H: max(0, r.Y+r.H-top)}
|
|
p.build(d)
|
|
if !p.reveal.IsZero() {
|
|
for i := range p.rows {
|
|
if p.rows[i].id == p.reveal {
|
|
widgets.EnsureVisible(&p.scroll, i, th.RowHeight, list.H)
|
|
break
|
|
}
|
|
}
|
|
p.reveal = ids.NodeID{}
|
|
}
|
|
widgets.VirtualList(ui, ui.ID("list"), list, len(p.rows), th.RowHeight, &p.scroll, func(i int, rr mathx.Rect) {
|
|
p.drawRow(f, d, i, rr)
|
|
})
|
|
if f.DnD.Active() {
|
|
p.pendingSingle = ids.NodeID{}
|
|
}
|
|
end := max(list.Y+float32(len(p.rows))*th.RowHeight-p.scroll, list.Y)
|
|
if end < list.Y+list.H {
|
|
p.emptyArea(f, d, mathx.Rect{X: list.X, Y: end, W: list.W, H: list.Y + list.H - end})
|
|
}
|
|
if widgets.ContextMenu(ui, p.menuID) {
|
|
if widgets.SubMenuIcon(ui, "Create", "ui.plus", 220) {
|
|
p.createItems(d, p.menuNode)
|
|
widgets.EndSubMenu(ui)
|
|
}
|
|
widgets.Separator(ui)
|
|
p.prefabItems(f, d)
|
|
command.MenuItem(f.UI, p.d.Commands, favorites.CommandNode, "")
|
|
command.MenuItem(f.UI, p.d.Commands, command.EditRename, "Rename")
|
|
command.MenuItem(f.UI, p.d.Commands, "edit.batch_rename", "Batch Rename…")
|
|
command.MenuItem(f.UI, p.d.Commands, command.EditDuplicate, "Duplicate")
|
|
command.MenuItem(f.UI, p.d.Commands, command.EditCopy, "Copy")
|
|
command.MenuItem(f.UI, p.d.Commands, command.EditCut, "Cut")
|
|
command.MenuItem(f.UI, p.d.Commands, command.EditPaste, "Paste")
|
|
widgets.Separator(ui)
|
|
command.MenuItem(f.UI, p.d.Commands, command.EditDelete, "Delete")
|
|
widgets.EndContextMenu(ui)
|
|
}
|
|
p.sceneMenu(f, d)
|
|
}
|
|
|
|
func (p *Panel) drawHeader(f *services.Frame, d *document.SceneDocument, r mathx.Rect) {
|
|
ui := f.UI
|
|
th := &ui.Theme
|
|
id := ui.ID("scene")
|
|
ui.Rect(r, th.Header)
|
|
name := d.Caption()
|
|
if !d.OnDisk {
|
|
name += " (unsaved)"
|
|
}
|
|
x := r.X + th.Padding
|
|
if d.IsPrefab() {
|
|
sz := r.H - 8
|
|
widgets.IconAt(ui, mathx.Rect{X: x, Y: r.Y + 4, W: sz, H: sz}, "file.prefab", th.Accent)
|
|
x += sz + 6
|
|
}
|
|
ui.PushClip(r)
|
|
ui.Text(name, mathx.Vec2{X: x, Y: ui.TextY(r)}, th.Text)
|
|
ui.PopClip()
|
|
if ui.Pressed(id, r) && ui.DoubleClicked(id) && !d.IsPrefab() {
|
|
p.d.Scenes.RenameScene(d)
|
|
}
|
|
if ui.RightClicked(id, r) {
|
|
widgets.OpenContextMenu(ui, p.sceneMenuID)
|
|
}
|
|
widgets.Tooltip(ui, id, d.Path)
|
|
}
|
|
|
|
func (p *Panel) sceneMenu(f *services.Frame, d *document.SceneDocument) {
|
|
ui := f.UI
|
|
if !widgets.ContextMenu(ui, p.sceneMenuID) {
|
|
return
|
|
}
|
|
command.MenuItem(f.UI, p.d.Commands, command.FileSave, "Save Scene")
|
|
if widgets.MenuItemIcon(ui, "Save Scene As…", p.d.Commands.Shortcut(command.FileSaveAs), "file.save", true, false) {
|
|
p.d.Scenes.SaveSceneAs(d)
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Rename Scene…", "", "edit.rename", true, false) {
|
|
p.d.Scenes.RenameScene(d)
|
|
}
|
|
widgets.EndContextMenu(ui)
|
|
}
|
|
|
|
func (p *Panel) activeNode(d *document.SceneDocument) ids.NodeID {
|
|
if it, ok := p.d.Selection.SelectionState().Active(); ok && it.Doc == d.ID {
|
|
return it.Node
|
|
}
|
|
return ids.NodeID{}
|
|
}
|
|
|
|
func (p *Panel) track(d *document.SceneDocument) {
|
|
sel := p.d.Selection.SelectionState()
|
|
rev := sel.Revision()
|
|
if rev == p.lastRev {
|
|
return
|
|
}
|
|
p.lastRev = rev
|
|
if it, ok := sel.Active(); ok && it.Doc == d.ID {
|
|
p.expandTo(d, it.Node)
|
|
p.reveal = it.Node
|
|
}
|
|
}
|
|
|
|
func (p *Panel) expandTo(d *document.SceneDocument, id ids.NodeID) {
|
|
s := d.Scene()
|
|
h := s.Handle(id)
|
|
if h.IsZero() {
|
|
return
|
|
}
|
|
root := s.Root()
|
|
for q := s.Parent(h); !q.IsZero() && q != root; q = s.Parent(q) {
|
|
d.View.Expanded[s.ID(q)] = true
|
|
}
|
|
}
|
|
|
|
func (p *Panel) build(d *document.SceneDocument) {
|
|
s := d.Scene()
|
|
p.rows = p.rows[:0]
|
|
if p.filter != "" {
|
|
q := strings.ToLower(p.filter)
|
|
root := s.Root()
|
|
for _, h := range s.PreOrder() {
|
|
if h == root || !s.IsAlive(h) {
|
|
continue
|
|
}
|
|
if strings.Contains(strings.ToLower(s.Name(h)), q) {
|
|
p.rows = append(p.rows, row{h: h, id: s.ID(h)})
|
|
}
|
|
}
|
|
return
|
|
}
|
|
p.walk(s, d.View.Expanded, s.Root(), 0, 0)
|
|
}
|
|
|
|
func (p *Panel) walk(s *scene.Scene, expanded map[ids.NodeID]bool, h scene.NodeHandle, depth int, lines uint64) {
|
|
for c := nextAlive(s, s.FirstChild(h)); !c.IsZero(); {
|
|
next := nextAlive(s, s.NextSibling(c))
|
|
id := s.ID(c)
|
|
has := s.ChildCount(c) > 0
|
|
last := next.IsZero()
|
|
p.rows = append(p.rows, row{h: c, id: id, depth: depth, has: has, last: last, lines: lines})
|
|
if has && expanded[id] {
|
|
sub := lines
|
|
if !last && depth > 0 && depth <= 64 {
|
|
sub |= 1 << (depth - 1)
|
|
}
|
|
p.walk(s, expanded, c, depth+1, sub)
|
|
}
|
|
c = next
|
|
}
|
|
}
|
|
|
|
func nextAlive(s *scene.Scene, c scene.NodeHandle) scene.NodeHandle {
|
|
for !c.IsZero() && !s.IsAlive(c) {
|
|
c = s.NextSibling(c)
|
|
}
|
|
return c
|
|
}
|
|
|
|
func (p *Panel) indexOf(id ids.NodeID) int {
|
|
for i := range p.rows {
|
|
if p.rows[i].id == id {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func rowID(id ids.NodeID) core.ID {
|
|
return core.IDSeq(core.ID(id.Hi), int(uint32(id.Lo)))
|
|
}
|
|
|
|
func (p *Panel) nodeIcon(s *scene.Scene, h scene.NodeHandle) (string, bool) {
|
|
reg := s.Registry()
|
|
if p.rankReg != reg {
|
|
p.rankReg, p.rank = reg, map[schema.TypeID]int{}
|
|
for i, td := range reg.Components() {
|
|
p.rank[td.ID] = i
|
|
}
|
|
}
|
|
icon, prio, rank, any := "", 0, 0, false
|
|
s.ComponentsOf(h, func(t schema.TypeID) {
|
|
td, ok := reg.ByID(t)
|
|
if !ok || td.Hidden {
|
|
return
|
|
}
|
|
any = true
|
|
r := p.rank[t]
|
|
if td.NodeIcon > 0 && td.Icon != "" && (td.NodeIcon > prio || td.NodeIcon == prio && r < rank) {
|
|
icon, prio, rank = td.Icon, td.NodeIcon, r
|
|
}
|
|
})
|
|
if icon == "" && any {
|
|
icon = "cat.node"
|
|
}
|
|
return icon, !any
|
|
}
|
|
|
|
func (p *Panel) drawRow(f *services.Frame, d *document.SceneDocument, i int, rr mathx.Rect) {
|
|
ui := f.UI
|
|
s := d.Scene()
|
|
rw := p.rows[i]
|
|
selected := p.d.Selection.IsSelected(rw.id)
|
|
if p.renaming == rw.id {
|
|
p.drawRename(f, d, rw, rr, selected)
|
|
return
|
|
}
|
|
label := s.Name(rw.h)
|
|
if p.filter != "" {
|
|
label = s.Path(rw.h)
|
|
}
|
|
icon, empty := p.nodeIcon(s, rw.h)
|
|
id := rowID(rw.id)
|
|
ev := widgets.TreeRowRect(ui, id, rr, widgets.TreeRow{
|
|
Depth: rw.depth,
|
|
Label: label,
|
|
HasChildren: rw.has,
|
|
Open: d.View.Expanded[rw.id],
|
|
Selected: selected,
|
|
Focused: f.Focused,
|
|
Dim: !s.EffectiveEnabled(rw.h),
|
|
Icon: icon,
|
|
Marker: empty,
|
|
Guides: p.filter == "",
|
|
Last: rw.last,
|
|
Lines: rw.lines,
|
|
})
|
|
p.decorate(f, d, rw, rr)
|
|
if ev.Toggled {
|
|
d.View.Expanded[rw.id] = !d.View.Expanded[rw.id]
|
|
}
|
|
if ev.Pressed {
|
|
p.press(f, i)
|
|
}
|
|
if ev.DoubleClicked {
|
|
p.focus(rw.id)
|
|
}
|
|
if ev.Released && p.pendingSingle == rw.id {
|
|
p.d.Selection.Select(rw.id)
|
|
p.pendingSingle = ids.NodeID{}
|
|
p.sync()
|
|
}
|
|
if ev.RightClicked {
|
|
if !selected {
|
|
p.d.Selection.Select(rw.id)
|
|
p.sync()
|
|
}
|
|
p.menuNode = rw.id
|
|
widgets.OpenContextMenu(ui, p.menuID)
|
|
}
|
|
if ui.Active() == id {
|
|
f.DnD.Source(ui, id, p.payload(d))
|
|
}
|
|
p.dropRow(f, d, rw, rr)
|
|
}
|
|
|
|
func (p *Panel) decorate(f *services.Frame, d *document.SceneDocument, rw row, rr mathx.Rect) {
|
|
if p.d.Prefabs == nil {
|
|
return
|
|
}
|
|
st := p.d.Prefabs.Row(d, rw.id)
|
|
ui := f.UI
|
|
th := &ui.Theme
|
|
icon, col := "", th.Accent
|
|
switch st.Status {
|
|
case prefabs.StatusRoot:
|
|
icon = "file.prefab"
|
|
case prefabs.StatusNested:
|
|
icon, col = "file.prefab", th.TextDim
|
|
case prefabs.StatusAdded:
|
|
icon = "ui.plus"
|
|
case prefabs.StatusMissing:
|
|
icon, col = "status.warning", th.Error
|
|
}
|
|
if st.Orphans {
|
|
icon, col = "status.warning", th.Warn
|
|
}
|
|
if st.Status == prefabs.StatusRoot || st.Status == prefabs.StatusNested || st.Status == prefabs.StatusMissing {
|
|
ui.Rect(mathx.Rect{X: rr.X, Y: rr.Y + 2, W: 2, H: rr.H - 4}, col)
|
|
}
|
|
if icon == "" {
|
|
return
|
|
}
|
|
sz := rr.H - 6
|
|
widgets.IconAt(ui, mathx.Rect{X: rr.X + rr.W - sz - 4, Y: rr.Y + 3, W: sz, H: sz}, icon, col)
|
|
}
|
|
|
|
func (p *Panel) prefabItems(f *services.Frame, d *document.SceneDocument) {
|
|
pf := p.d.Prefabs
|
|
if pf == nil || p.menuNode.IsZero() {
|
|
return
|
|
}
|
|
ui := f.UI
|
|
info, ok := pf.Info(d, p.menuNode)
|
|
if !ok {
|
|
if widgets.MenuItemIcon(ui, "Create Prefab", "", "file.prefab", true, false) {
|
|
p.report(pf.CreateFrom(d, p.d.Selection.Selected(), ""))
|
|
}
|
|
widgets.Separator(ui)
|
|
return
|
|
}
|
|
if widgets.SubMenuIcon(ui, "Prefab", "file.prefab", 260) {
|
|
live := !info.Missing
|
|
if widgets.MenuItemIcon(ui, "Open Prefab", "", "file.open", live, false) {
|
|
p.report(pf.Open(info.Prefab))
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Select Prefab Asset", "", "panel.assets", true, false) {
|
|
pf.SelectAsset(info.Prefab)
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Find Instances", "", "ui.search", true, false) {
|
|
pf.FindInstances(info.Prefab)
|
|
}
|
|
widgets.Separator(ui)
|
|
touched := live && info.Touched(p.menuNode)
|
|
for i, t := range pf.Targets(d, p.menuNode) {
|
|
ui.PushIDInt(i)
|
|
if widgets.MenuItemIcon(ui, "Apply Node to "+t.Name, "", "prefab.apply", touched, false) {
|
|
p.report(pf.Apply(d, prefabs.Change{Node: p.menuNode, Level: prefabs.LevelNode, Nearest: t.Nearest}))
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Apply All to "+t.Name, "", "prefab.apply", live, false) {
|
|
p.report(pf.Apply(d, prefabs.Change{Node: p.menuNode, Level: prefabs.LevelAll, Nearest: t.Nearest}))
|
|
}
|
|
ui.PopID()
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Revert Node", "", "prefab.revert", touched, false) {
|
|
p.report(pf.Revert(d, prefabs.Change{Node: p.menuNode, Level: prefabs.LevelNode}))
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Revert All", "", "prefab.revert", live, false) {
|
|
p.report(pf.Revert(d, prefabs.Change{Node: p.menuNode, Level: prefabs.LevelAll}))
|
|
}
|
|
widgets.Separator(ui)
|
|
if widgets.MenuItemIcon(ui, "Unpack", "", "prefab.unpack", true, false) {
|
|
p.report(pf.Unpack(d, p.menuNode, false))
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Unpack Completely", "", "prefab.unpack", true, false) {
|
|
p.report(pf.Unpack(d, p.menuNode, true))
|
|
}
|
|
if widgets.MenuItemIcon(ui, "Create Variant", "", "prefab.variant", live, false) {
|
|
p.report(pf.CreateVariant(info.Prefab))
|
|
}
|
|
widgets.EndSubMenu(ui)
|
|
}
|
|
widgets.Separator(ui)
|
|
}
|
|
|
|
func (p *Panel) focus(id ids.NodeID) {
|
|
p.d.Selection.Select(id)
|
|
p.sync()
|
|
p.pendingSingle = ids.NodeID{}
|
|
if p.d.Tools.Tool == context.ToolSelect {
|
|
p.d.Tools.Tool = context.ToolMove
|
|
}
|
|
p.d.Commands.Run(command.SceneFrame)
|
|
}
|
|
|
|
func (p *Panel) press(f *services.Frame, i int) {
|
|
rw := p.rows[i]
|
|
mods := f.UI.Mods()
|
|
switch {
|
|
case mods.Primary():
|
|
p.d.Selection.ToggleSelection(rw.id)
|
|
p.anchor = rw.id
|
|
case mods.Has(input.ModShift) && p.indexOf(p.anchor) >= 0:
|
|
a := p.indexOf(p.anchor)
|
|
lo, hi := min(a, i), max(a, i)
|
|
span := make([]ids.NodeID, 0, hi-lo+1)
|
|
for k := lo; k <= hi; k++ {
|
|
span = append(span, p.rows[k].id)
|
|
}
|
|
p.d.Selection.Select(span...)
|
|
default:
|
|
if p.d.Selection.IsSelected(rw.id) && p.d.Selection.SelectionState().Len() > 1 {
|
|
p.pendingSingle = rw.id
|
|
} else {
|
|
p.d.Selection.Select(rw.id)
|
|
}
|
|
p.anchor = rw.id
|
|
}
|
|
p.sync()
|
|
}
|
|
|
|
func (p *Panel) payload(d *document.SceneDocument) dnd.Payload {
|
|
nodes := append([]ids.NodeID(nil), p.d.Selection.Selected()...)
|
|
label := strconv.Itoa(len(nodes)) + " nodes"
|
|
if len(nodes) == 1 {
|
|
if h := d.Scene().Handle(nodes[0]); !h.IsZero() {
|
|
label = d.Scene().Name(h)
|
|
}
|
|
}
|
|
return dnd.Payload{Kind: context.KindNodes, Label: label, Data: context.NodesPayload{Doc: d.ID, Nodes: nodes}}
|
|
}
|
|
|
|
func contains(list []ids.NodeID, id ids.NodeID) bool {
|
|
for _, x := range list {
|
|
if x == id {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *Panel) accepts(d *document.SceneDocument, pl dnd.Payload, target ids.NodeID) bool {
|
|
switch pl.Kind {
|
|
case context.KindNodes:
|
|
np, ok := pl.Data.(context.NodesPayload)
|
|
return ok && np.Doc == d.ID && !contains(np.Nodes, target)
|
|
case context.KindAsset:
|
|
ap, ok := pl.Data.(context.AssetPayload)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if ap.Kind == asset.ArtScript {
|
|
s := d.Scene()
|
|
return target != s.ID(s.Root())
|
|
}
|
|
return p.d.Templates != nil && p.d.Templates.Accepts(ap.Kind, nil)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func zoneOf(y float32, r mathx.Rect) int {
|
|
t := (y - r.Y) / r.H
|
|
switch {
|
|
case t < 0.25:
|
|
return zoneBefore
|
|
case t > 0.75:
|
|
return zoneAfter
|
|
}
|
|
return zoneInto
|
|
}
|
|
|
|
func (p *Panel) dropRow(f *services.Frame, d *document.SceneDocument, rw row, rr mathx.Rect) {
|
|
ui := f.UI
|
|
dn := f.DnD
|
|
if !dn.Hovering(ui, rr) {
|
|
return
|
|
}
|
|
pl, _ := dn.Payload()
|
|
if !p.accepts(d, pl, rw.id) {
|
|
dn.Target(ui, rr, "reject")
|
|
return
|
|
}
|
|
zone := zoneOf(ui.Mouse().Y, rr)
|
|
if zone != zoneInto {
|
|
y := rr.Y
|
|
if zone == zoneAfter {
|
|
y = rr.Y + rr.H - 2
|
|
}
|
|
ui.Rect(mathx.Rect{X: rr.X, Y: y, W: rr.W, H: 2}, ui.Theme.Accent)
|
|
}
|
|
if got, ok := dn.Target(ui, rr, context.KindNodes, context.KindAsset); ok {
|
|
p.drop(d, got, rw.id, zone)
|
|
}
|
|
}
|
|
|
|
func (p *Panel) emptyArea(f *services.Frame, d *document.SceneDocument, r mathx.Rect) {
|
|
ui := f.UI
|
|
id := ui.ID("empty")
|
|
if ui.Pressed(id, r) && !ui.Primary() && !ui.Mods().Has(input.ModShift) {
|
|
p.d.Selection.Select()
|
|
p.sync()
|
|
}
|
|
if ui.RightClicked(id, r) {
|
|
p.d.Selection.Select()
|
|
p.sync()
|
|
p.menuNode = ids.NodeID{}
|
|
widgets.OpenContextMenu(ui, p.menuID)
|
|
}
|
|
if !f.DnD.Hovering(ui, r) {
|
|
return
|
|
}
|
|
s := d.Scene()
|
|
rootID := s.ID(s.Root())
|
|
pl, _ := f.DnD.Payload()
|
|
if !p.accepts(d, pl, rootID) {
|
|
f.DnD.Target(ui, r, "reject")
|
|
return
|
|
}
|
|
if got, ok := f.DnD.Target(ui, r, context.KindNodes, context.KindAsset); ok {
|
|
p.drop(d, got, rootID, zoneInto)
|
|
}
|
|
}
|
|
|
|
func (p *Panel) drop(d *document.SceneDocument, pl dnd.Payload, target ids.NodeID, zone int) {
|
|
s := d.Scene()
|
|
switch pl.Kind {
|
|
case context.KindNodes:
|
|
np, _ := pl.Data.(context.NodesPayload)
|
|
p.move(d, np.Nodes, target, zone)
|
|
case context.KindAsset:
|
|
ap, _ := pl.Data.(context.AssetPayload)
|
|
if ap.Kind == asset.ArtScript {
|
|
p.d.Scenes.AttachScript([]ids.NodeID{target}, ap.ID)
|
|
return
|
|
}
|
|
parent, index := target, -1
|
|
if zone != zoneInto {
|
|
th := s.Handle(target)
|
|
parent = s.ID(s.Parent(th))
|
|
index = commands.ChildIndex(s, th)
|
|
if zone == zoneAfter {
|
|
index++
|
|
}
|
|
}
|
|
t, ok := p.d.Templates.ForAsset(ap.Kind, nil, ap.ID, path.Base(ap.Name))
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := p.d.Scenes.Instantiate(t, parent, index); err != nil {
|
|
p.d.Notices.Notify(context.LevelError, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *Panel) move(d *document.SceneDocument, nodes []ids.NodeID, target ids.NodeID, zone int) {
|
|
s := d.Scene()
|
|
th := s.Handle(target)
|
|
if th.IsZero() {
|
|
return
|
|
}
|
|
tops := clipboard.TopLevel(s, nodes)
|
|
if len(tops) == 0 {
|
|
return
|
|
}
|
|
tx := d.History.Begin("Move")
|
|
anchor := th
|
|
for _, n := range tops {
|
|
if n == th {
|
|
continue
|
|
}
|
|
parent, index := target, -1
|
|
switch zone {
|
|
case zoneBefore:
|
|
ph := s.Parent(th)
|
|
idx := commands.ChildIndex(s, th)
|
|
if s.Parent(n) == ph && commands.ChildIndex(s, n) < idx {
|
|
idx--
|
|
}
|
|
parent, index = s.ID(ph), idx
|
|
case zoneAfter:
|
|
ph := s.Parent(anchor)
|
|
idx := commands.ChildIndex(s, anchor) + 1
|
|
if s.Parent(n) == ph && commands.ChildIndex(s, n) < idx {
|
|
idx--
|
|
}
|
|
parent, index = s.ID(ph), idx
|
|
}
|
|
if p.d.Prefabs != nil && parent != s.ID(s.Parent(n)) && p.d.Prefabs.Locked(d, s.ID(n)) {
|
|
tx.Abort()
|
|
p.d.Notices.Notify(context.LevelWarn, s.Name(n)+" comes from a prefab and keeps its parent, unpack the instance to move it")
|
|
return
|
|
}
|
|
cmd := commands.NewMutation("Reparent", commands.EnvFor(s), commands.Move(s.ID(n), parent, index, true))
|
|
if err := tx.Exec(cmd); err != nil {
|
|
tx.Abort()
|
|
p.d.Notices.Notify(context.LevelError, err.Error())
|
|
return
|
|
}
|
|
if zone == zoneAfter {
|
|
anchor = n
|
|
}
|
|
}
|
|
tx.Commit()
|
|
if zone == zoneInto && th != s.Root() {
|
|
d.View.Expanded[target] = true
|
|
}
|
|
}
|
|
|
|
func (p *Panel) drawRename(f *services.Frame, d *document.SceneDocument, rw row, rr mathx.Rect, selected bool) {
|
|
ui := f.UI
|
|
th := &ui.Theme
|
|
if selected {
|
|
ui.Rect(rr, th.Selection)
|
|
}
|
|
fid := ui.ID("rename")
|
|
x := rr.X + float32(rw.depth)*th.Indent + th.Indent + 2
|
|
fr := mathx.Rect{X: x, Y: rr.Y + 1, W: max(40, rr.X+rr.W-x-2), H: rr.H - 2}
|
|
if p.renameStart {
|
|
p.renameStart = false
|
|
p.renameText = d.Scene().Name(rw.h)
|
|
ui.SetFocus(fid)
|
|
ui.EditBuffer(fid, p.renameText).SelectAll()
|
|
}
|
|
if _, committed := widgets.TextFieldRect(ui, fid, fr, &p.renameText, false); committed {
|
|
if err := d.Props.Rename(nil, rw.id, p.renameText); err != nil {
|
|
p.d.Notices.Notify(context.LevelError, err.Error())
|
|
}
|
|
}
|
|
if ui.Focus() != fid {
|
|
p.renaming = ids.NodeID{}
|
|
}
|
|
}
|
|
|
|
func (p *Panel) createItems(d *document.SceneDocument, parent ids.NodeID) {
|
|
p.d.Scenes.CreateMenu(p.d.Templates, parent, func([]ids.NodeID) {
|
|
if !parent.IsZero() {
|
|
d.View.Expanded[parent] = true
|
|
}
|
|
})
|
|
}
|