Files

841 lines
21 KiB
Go

package inspector
import (
"aego/editor/ext/inspect"
"fmt"
"strconv"
"strings"
"aego/asset/pipeline"
"aego/core/ids"
"aego/core/mathx"
"aego/editor/core/clipboard"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/core/dataedit"
"aego/editor/core/document"
"aego/editor/core/history"
"aego/editor/core/history/commands"
"aego/editor/core/prefabs"
"aego/editor/core/presets"
"aego/editor/core/props"
"aego/editor/core/services"
"aego/editor/core/workspace"
"aego/engine/module"
"aego/platform/input"
"aego/scene"
"aego/scene/sceneio"
"aego/scene/scriptcomp"
"aego/schema"
"aego/ui/core"
"aego/ui/widgets"
)
type Deps struct {
Docs services.Documents
Selection services.Selection
Commands *command.Registry
Notices services.Notices
Nav services.Navigation
Problems services.Problems
Prefabs services.Prefabs
Sections *module.Sections
Pipeline *pipeline.Pipeline
Thumbs context.ThumbSource
Play context.PlayBridge
Code func() context.ScriptNav
Clip clipboard.Env
Assets services.AssetOps
Scenes services.SceneOps
Collapsed map[string]bool
Watch func(expr string)
Data *dataedit.Service
Schema *schema.Registry
Presets *presets.Service
}
type Panel struct {
reg *inspect.Registry
d Deps
f *services.Frame
tx *history.Transaction
txDoc document.ID
doc *document.SceneDocument
locked bool
lockDoc document.ID
lockNodes []ids.NodeID
collapsed map[core.ID]bool
scroll float32
content float32
search string
pick string
nodes []ids.NodeID
types []schema.TypeID
labels map[string]string
typeNames map[schema.TypeID][2]string
live map[core.ID]schema.Value
curves map[core.ID]*widgets.CurveState
liveT *scene.Transform2D
euler *props.EulerCache
info *prefabs.Info
focusSearch bool
hoverField fieldRef
hoverType schema.TypeID
menuField fieldRef
rightUsed bool
fieldMenuID core.ID
bgMenuID core.ID
scriptNames []string
scriptSearch string
focusScript bool
dataMenuID core.ID
dataMenu string
dataStep bool
}
const (
Scope = "inspector"
CommandCopy = "inspector.copy"
CommandPaste = "inspector.paste"
CommandWatch = "inspector.watch"
)
type fieldRef struct {
t schema.TypeID
f schema.FieldID
ok bool
}
func New(reg *inspect.Registry, d Deps) *Panel {
return &Panel{
reg: reg,
d: d,
collapsed: map[core.ID]bool{},
labels: map[string]string{},
typeNames: map[schema.TypeID][2]string{},
live: map[core.ID]schema.Value{},
curves: map[core.ID]*widgets.CurveState{},
euler: props.NewEulerCache(),
}
}
func (p *Panel) code() context.ScriptNav {
if p.d.Code == nil {
return nil
}
return p.d.Code()
}
func (p *Panel) gotoScript(typeName, field string) {
c := p.code()
if c == nil {
return
}
if l, ok := c.Declaration(typeName, field); ok {
p.d.Nav.Open(l)
return
}
p.d.Notices.Notify(context.LevelInfo, "The declaration of "+shortScript(typeName)+" is not available until the scripts compile")
}
func (p *Panel) ID() string {
return workspace.Inspector
}
func (p *Panel) Title() string {
return "Inspector"
}
func (p *Panel) Scope() string {
return Scope
}
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
p.f = f
ui := f.UI
p.doc = nil
d := p.target()
p.settle(d)
p.hoverField, p.hoverType, p.rightUsed = fieldRef{}, 0, false
p.fieldMenuID, p.bgMenuID = ui.ID("fieldmenu"), ui.ID("bgmenu")
p.info = nil
if p.d.Data != nil && !p.locked {
if id := p.d.Data.Inspected(); !id.IsZero() {
p.drawData(r, id)
return
}
}
if d == nil {
widgets.LabelDim(ui, "No scene open")
return
}
p.doc = d
s := d.Scene()
p.nodes = p.nodes[:0]
src := p.d.Selection.Selected()
if p.locked {
src = p.lockNodes
}
for _, id := range src {
if h := s.Handle(id); !h.IsZero() && s.IsAlive(h) {
p.nodes = append(p.nodes, id)
}
}
if p.locked && len(p.nodes) == 0 {
p.unlock()
for _, id := range p.d.Selection.Selected() {
if h := s.Handle(id); !h.IsZero() && s.IsAlive(h) {
p.nodes = append(p.nodes, id)
}
}
}
r = p.lockBar(d, r)
if len(p.nodes) == 0 {
widgets.LabelDim(ui, "Nothing selected")
return
}
if p.d.Prefabs != nil && len(p.nodes) == 1 {
if info, ok := p.d.Prefabs.Info(d, p.nodes[0]); ok {
p.info = info
}
}
th := &ui.Theme
if ui.MouseOver(ui.ID("scroll"), r) {
if w := ui.ScrollY(th.RowHeight * 3); w != 0 {
p.scroll -= w
}
}
p.scroll = mathx.Clamp(p.scroll, 0, max(0, p.content-r.H+th.Padding*2))
cur := ui.Cursor()
top := cur.Y
cur.Y -= p.scroll
ui.PushClip(r)
p.header(d)
p.prefabCard(d)
p.transform(d)
p.components(d)
p.scripts(d)
p.addComponent(d)
p.addScript(d)
ui.PopClip()
p.content = cur.Y + p.scroll - top
p.menus(d, r)
p.dropScripts(r)
}
func (p *Panel) menus(d *document.SceneDocument, r mathx.Rect) {
ui := p.f.UI
if !p.rightUsed && ui.MousePressed(input.MouseRight) && ui.Hit(r) && !ui.MouseInOverlay() {
widgets.OpenContextMenu(ui, p.bgMenuID)
}
if widgets.ContextMenu(ui, p.bgMenuID) {
kind := p.d.Clip.Peek()
if widgets.MenuItemIcon(ui, "Paste Component", p.d.Commands.Shortcut(CommandPaste), "edit.paste", kind == clipboard.KindComponent, false) {
p.confirm(d, "Paste Component", func() error { return p.d.Clip.PasteComponent(d, p.nodes) })
}
if widgets.MenuItemIcon(ui, "Paste Components", "", "edit.paste", kind == clipboard.KindComponents, false) {
p.confirm(d, "Paste Components", func() error { return p.d.Clip.PasteComponents(d, p.nodes) })
}
widgets.EndContextMenu(ui)
}
if widgets.ContextMenu(ui, p.fieldMenuID) {
p.fieldMenu(d)
widgets.EndContextMenu(ui)
}
}
func (p *Panel) CanCopy() bool {
return p.hoverField.ok || p.hoverType != 0
}
func (p *Panel) CopyHovered() error {
d := p.cur()
if d == nil || len(p.nodes) == 0 {
return nil
}
if p.hoverField.ok {
if td, fd, ok := p.lookup(d, p.hoverField); ok && !p.copyTexture(d, td, fd) {
p.copyValue(d, td, fd)
}
return nil
}
if p.hoverType != 0 {
return p.d.Clip.CopyComponent(d.Scene(), p.nodes[0], p.hoverType)
}
return nil
}
func (p *Panel) CanWatch() bool {
return p.hoverField.ok && p.d.Watch != nil
}
func (p *Panel) WatchHovered() error {
d := p.cur()
if d == nil || len(p.nodes) == 0 || !p.CanWatch() {
return nil
}
td, fd, ok := p.lookup(d, p.hoverField)
if !ok {
return nil
}
s := d.Scene()
comp := td.FullName[strings.LastIndexByte(td.FullName, '.')+1:]
p.d.Watch(s.Path(s.Handle(p.nodes[0])) + "." + comp + "." + fd.Name)
return nil
}
func (p *Panel) PasteHovered() error {
d := p.cur()
if d == nil || len(p.nodes) == 0 {
return nil
}
if p.hoverField.ok {
if td, fd, ok := p.lookup(d, p.hoverField); ok {
if v, ok := p.d.Clip.ReadValue(fd); ok {
p.confirm(d, "Paste "+p.labelOf(fd), func() error { return d.Props.Set(nil, p.nodes, td.ID, fd.ID, v) })
return nil
}
if done, err := p.pasteAsset(d, td, fd); done {
return err
}
}
}
switch p.d.Clip.Peek() {
case clipboard.KindComponent:
p.confirm(d, "Paste Component", func() error { return p.d.Clip.PasteComponent(d, p.nodes) })
return nil
case clipboard.KindComponents:
p.confirm(d, "Paste Components", func() error { return p.d.Clip.PasteComponents(d, p.nodes) })
return nil
}
p.d.Notices.Notify(context.LevelInfo, "The clipboard does not fit this field")
return nil
}
func (p *Panel) settle(d *document.SceneDocument) {
if p.tx == nil {
return
}
ui := p.f.UI
if d == nil || d.ID != p.txDoc || (!ui.MouseDown(input.MouseLeft) && ui.Focus() == 0) {
p.tx.Commit()
p.tx = nil
}
}
func (p *Panel) apply(e widgets.Edit, label string, fn func(*history.Transaction) error) {
d := p.cur()
if d == nil || d.History == nil {
return
}
h := d.History
if e.Begin && e.Changed && e.End && p.tx == nil && len(p.nodes) > confirmOver {
p.confirm(d, label, func() error {
tx := h.Begin(label)
if err := fn(tx); err != nil {
tx.Abort()
return err
}
tx.Commit()
return nil
})
return
}
if e.Begin && p.tx == nil {
p.tx = h.Begin(label)
p.txDoc = d.ID
}
if e.Changed {
tx, own := p.tx, false
if tx == nil {
tx, own = h.Begin(label), true
}
if err := fn(tx); err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
}
if own {
tx.Commit()
}
}
if e.End && p.tx != nil {
p.tx.Commit()
p.tx = nil
}
}
func (p *Panel) labelOf(fd *schema.FieldDescriptor) string {
if fd.Hints.DisplayName != "" {
return fd.Hints.DisplayName
}
if l, ok := p.labels[fd.Name]; ok {
return l
}
l := inspect.Humanize(fd.Name)
p.labels[fd.Name] = l
return l
}
func triCheck(ui *core.Context, id core.ID, r mathx.Rect, state int) bool {
th := &ui.Theme
clicked := ui.Clicked(id, r)
box := mathx.Rect{X: r.X + 3, Y: r.Y + 3, W: r.W - 6, H: r.H - 6}
switch state {
case 1:
ui.Rect(box, th.Accent)
widgets.CheckMark(ui, box, th.Background)
case 2:
ui.Rect(box, ui.WidgetColor(id))
ui.Outline(box, th.Border)
ui.Rect(mathx.Rect{X: box.X + 3, Y: box.Y + box.H/2 - 1, W: box.W - 6, H: 2}, th.Accent)
default:
ui.Rect(box, ui.WidgetColor(id))
ui.Outline(box, th.Border)
}
return clicked
}
func (p *Panel) header(d *document.SceneDocument) {
ui := p.f.UI
th := &ui.Theme
ui.PushID("node")
defer ui.PopID()
c := p.beginCard("node", "Node", "", "cat.node", "", true)
defer p.endCard(ui, c)
menuID := ui.ID("menu")
if ui.RightClicked(c.hid, c.hdr) {
p.rightUsed = true
widgets.OpenContextMenu(ui, menuID)
} else if c.more {
widgets.OpenContextMenu(ui, menuID)
}
if widgets.ContextMenu(ui, menuID) {
p.nodeMenu(d)
widgets.EndContextMenu(ui)
}
if !c.open {
return
}
s := d.Scene()
first := s.Handle(p.nodes[0])
row := ui.Cursor().Next(0)
state := 0
if s.Enabled(first) {
state = 1
}
for _, id := range p.nodes[1:] {
if s.Enabled(s.Handle(id)) != (state == 1) {
state = 2
break
}
}
if triCheck(ui, ui.ID("enabled"), mathx.Rect{X: row.X, Y: row.Y, W: row.H, H: row.H}, state) {
after := state != 1
p.confirm(d, "Toggle Enabled", func() error { return d.Props.SetEnabled(nil, p.nodes, after) })
}
nr := mathx.Rect{X: row.X + row.H + 4, Y: row.Y, W: row.W - row.H - 4, H: row.H}
if len(p.nodes) == 1 {
name := s.Name(first)
if _, committed := widgets.TextFieldRect(ui, ui.ID("name"), nr, &name, false); committed {
if err := d.Props.Rename(nil, p.nodes[0], name); err != nil {
p.d.Notices.Notify(context.LevelError, err.Error())
}
}
} else {
ui.TextIn(nr, strconv.Itoa(len(p.nodes))+" nodes", th.TextDim, core.AlignLeft)
}
mask := s.Layers(first)
mixed := false
for _, id := range p.nodes[1:] {
if s.Layers(s.Handle(id)) != mask {
mixed = true
break
}
}
if widgets.MaskField(ui, ui.ID("layers"), widgets.Field(ui, "Layers"), &mask, nil, mixed) {
m := mask
p.confirm(d, "Set Layers", func() error { return d.Props.SetLayers(nil, p.nodes, m) })
}
p.spaceRow(d)
}
func (p *Panel) editing(ui *core.Context, key core.ID) bool {
a, f := ui.Active(), ui.Focus()
if a == key || f == key || ui.PopupOpen(key) {
return true
}
for i := 1; i <= 4; i++ {
s := core.IDSeq(key, i)
if a == s || f == s {
return true
}
}
return false
}
func (p *Panel) transform2D(d *document.SceneDocument) {
ui := p.f.UI
ui.PushID("transform")
defer ui.PopID()
card := p.beginCard("transform", "Transform", "", "cat.transform", "Reset", false)
defer p.endCard(ui, card)
if card.action {
p.resetTransform(d)
}
if !card.open {
return
}
posID, rotID, scaleID := ui.ID("pos"), ui.ID("rot"), ui.ID("scale")
t, mixed := d.Props.CommonTransform(p.nodes)
if p.liveT != nil && (p.editing(ui, posID) || p.editing(ui, rotID) || p.editing(ui, scaleID)) {
t, mixed = *p.liveT, [5]bool{}
} else {
p.liveT = nil
}
keep := func(e widgets.Edit, nt scene.Transform2D) {
if e.Begin || e.Changed {
p.liveT = &nt
}
if e.End {
p.liveT = nil
}
}
pos := t.Position
e := widgets.Vec2Field(ui, posID, widgets.Field(ui, "Position"), &pos, widgets.NumberOpts{Speed: 0.05}, mixed[0], mixed[1])
if e.Any() {
x, y := pos.X != t.Position.X, pos.Y != t.Position.Y
nt := t
nt.Position = pos
keep(e, nt)
p.apply(e, "Move", func(tx *history.Transaction) error {
return d.Props.EditTransform(tx, p.nodes, func(tr *scene.Transform2D) {
if x {
tr.Position.X = pos.X
}
if y {
tr.Position.Y = pos.Y
}
})
})
}
deg := float64(mathx.Rad2Deg(t.Rotation))
e = widgets.DragNumber(ui, rotID, widgets.Field(ui, "Rotation"), &deg, widgets.NumberOpts{Speed: 0.5, Prec: 2, Suffix: "°", Mixed: mixed[2]})
if e.Any() {
rad := mathx.Deg2Rad(float32(deg))
nt := t
nt.Rotation = rad
keep(e, nt)
p.apply(e, "Rotate", func(tx *history.Transaction) error {
return d.Props.EditTransform(tx, p.nodes, func(tr *scene.Transform2D) {
tr.Rotation = rad
})
})
}
sc := t.Scale
e = widgets.Vec2Field(ui, scaleID, widgets.Field(ui, "Scale"), &sc, widgets.NumberOpts{Speed: 0.01}, mixed[3], mixed[4])
if e.Any() {
x, y := sc.X != t.Scale.X, sc.Y != t.Scale.Y
nt := t
nt.Scale = sc
keep(e, nt)
p.apply(e, "Scale", func(tx *history.Transaction) error {
return d.Props.EditTransform(tx, p.nodes, func(tr *scene.Transform2D) {
if x {
tr.Scale.X = sc.X
}
if y {
tr.Scale.Y = sc.Y
}
})
})
}
}
func (p *Panel) components(d *document.SceneDocument) {
ui := p.f.UI
th := &ui.Theme
s := d.Scene()
reg := s.Registry()
unknown, hasUnknown := reg.ByName("engine.Unknown")
first := s.Handle(p.nodes[0])
p.types = p.types[:0]
scriptT := schema.HashName(scriptcomp.ScriptName)
s.ComponentsOf(first, func(t schema.TypeID) {
if (hasUnknown && t == unknown.ID) || t == scriptT {
return
}
if td, ok := reg.ByID(t); ok && td.Hidden {
return
}
for _, id := range p.nodes[1:] {
if _, ok := s.GetByType(s.Handle(id), t); !ok {
return
}
}
p.types = append(p.types, t)
})
for _, sec := range groupComponents(p.reg, reg, p.types) {
r := ui.Cursor().Next(ui.LineHeight() + 4)
ui.Text(sec.Category, mathx.Vec2{X: r.X + 2, Y: r.Y + 3}, th.TextDisabled)
for _, t := range sec.Types {
p.component(d, t)
}
}
if hasUnknown && len(p.nodes) == 1 {
if u, ok := scene.Get[sceneio.Unknown](s, first); ok {
for _, c := range u.Components {
widgets.LabelDim(ui, c.TypeName+" (not loaded, kept as is)")
}
}
}
}
func (p *Panel) component(d *document.SceneDocument, t schema.TypeID) {
ui := p.f.UI
td, ok := d.Scene().Registry().ByID(t)
if !ok {
return
}
ui.PushRawID(core.ID(t))
defer ui.PopID()
meta := p.reg.Meta(td)
badge := meta.Badge
if p.info != nil && p.info.ComponentAdded(p.nodes[0], ownerOf(td)) {
badge = "+ added"
}
c := p.beginCard(td.FullName, meta.DisplayName, badge, meta.Icon, "", true)
c.typ = t
if ui.Hot() == c.hid && ui.KeyPressed(input.KeyF1) {
p.d.Nav.Open(context.DocsAt(td.FullName, meta.DisplayName))
}
defer func() { p.endCard(ui, c) }()
menuID := ui.ID("menu")
if ui.RightClicked(c.hid, c.hdr) {
p.rightUsed = true
widgets.OpenContextMenu(ui, menuID)
} else if c.more {
widgets.OpenContextMenu(ui, menuID)
}
if widgets.ContextMenu(ui, menuID) {
p.componentMenu(d, td)
widgets.EndContextMenu(ui)
}
if !c.open {
return
}
def := func() { p.fields(d, td) }
custom, ok := p.reg.Custom(t)
if !ok {
def()
return
}
mark := ui.Mark()
func() {
defer func() {
if rec := recover(); rec != nil {
ui.Restore(mark)
msg := fmt.Sprint(rec)
p.reg.Disable(t, msg)
p.d.Notices.Notify(context.LevelError, "inspector for "+td.FullName+" crashed: "+msg)
}
}()
p.reg.DrawCustom(custom, p.nodes, def)
}()
}
func (p *Panel) remove(d *document.SceneDocument, td *schema.TypeDescriptor) {
p.confirm(d, "Remove "+p.reg.Meta(td).DisplayName, func() error {
env := commands.EnvFor(d.Scene())
tx := d.History.Begin("Remove " + td.FullName)
for _, id := range p.nodes {
if err := tx.Exec(commands.NewMutation("Remove "+td.FullName, env, commands.RemoveComp(id, td))); err != nil {
tx.Abort()
return err
}
}
tx.Commit()
return nil
})
}
func (p *Panel) reset(d *document.SceneDocument, td *schema.TypeDescriptor) {
p.confirm(d, "Reset "+p.reg.Meta(td).DisplayName, func() error {
s := d.Scene()
env := commands.EnvFor(s)
defaults := p.reg.Defaults(td.ID)
tx := d.History.Begin("Reset " + td.FullName)
for _, id := range p.nodes {
if err := tx.Exec(commands.NewMutation("Remove "+td.FullName, env, commands.RemoveComp(id, td))); err != nil {
tx.Abort()
return err
}
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(s, id, td, defaults))); err != nil {
tx.Abort()
return err
}
}
tx.Commit()
return nil
})
}
func (p *Panel) blocked(s *scene.Scene, reg *schema.Registry, td *schema.TypeDescriptor) string {
for _, name := range td.Requires {
req, ok := reg.ByName(name)
if !ok {
return "needs " + name
}
for _, n := range p.nodes {
if _, has := s.GetByType(s.Handle(n), req.ID); !has {
return "needs " + p.reg.Meta(req).DisplayName
}
}
}
if td.SceneUnique && (len(p.nodes) > 1 || inScene(s, td.ID)) {
return "one per scene"
}
return ""
}
func inScene(s *scene.Scene, t schema.TypeID) bool {
for _, h := range s.PreOrder() {
if _, ok := s.GetByType(h, t); ok && s.IsAlive(h) {
return true
}
}
return false
}
func (p *Panel) addComponent(d *document.SceneDocument) {
ui := p.f.UI
th := &ui.Theme
s := d.Scene()
reg := s.Registry()
row := ui.Cursor().Next(th.RowHeight + 4)
id := ui.ID("add")
if ui.Pressed(id, row) {
if ui.PopupOpen(id) {
ui.ClosePopup(id)
} else {
ui.CloseAllPopups()
ui.OpenPopup(id, mathx.Vec2{X: row.X, Y: row.Y + row.H})
p.search = ""
p.focusSearch = true
}
}
ui.Rect(row, ui.WidgetColor(id))
ui.Outline(row, th.AccentDim)
ui.TextIn(row, "Add Component", th.Text, core.AlignCenter)
if !ui.BeginPopup(id, row.W) {
return
}
sid := ui.ID("addsearch")
if p.focusSearch {
p.focusSearch = false
ui.SetFocus(sid)
ui.EditBuffer(sid, p.search).SelectAll()
}
widgets.SearchField(ui, sid, ui.Cursor().Next(0), &p.search)
q := strings.ToLower(strings.TrimSpace(p.search))
unknown, hasUnknown := reg.ByName("engine.Unknown")
var candidates []schema.TypeID
for _, td := range reg.Components() {
if (hasUnknown && td.ID == unknown.ID) || td.FullName == scriptcomp.ScriptName || td.Hidden {
continue
}
if !d.Props.Fits(p.nodes, td.ID) {
continue
}
missing := false
for _, n := range p.nodes {
if _, ok := s.GetByType(s.Handle(n), td.ID); !ok {
missing = true
break
}
}
if !missing {
continue
}
meta := p.reg.Meta(td)
if q != "" && !strings.Contains(strings.ToLower(meta.DisplayName), q) && !strings.Contains(strings.ToLower(td.FullName), q) {
continue
}
candidates = append(candidates, td.ID)
}
var first *schema.TypeDescriptor
for _, sec := range groupComponents(p.reg, reg, candidates) {
widgets.HeaderIcon(ui, categoryIcon(ui, sec.Category), sec.Category, false)
for _, t := range sec.Types {
td, ok := reg.ByID(t)
if !ok {
continue
}
meta := p.reg.Meta(td)
label := meta.DisplayName
if why := p.blocked(s, reg, td); why != "" {
widgets.HeaderIcon(ui, meta.Icon, label+" — "+why, true)
continue
}
if first == nil {
first = td
}
if widgets.SelectableIcon(ui, core.IDSeq(id, int(t)), meta.Icon, label, td == first && q != "") {
p.add(d, td)
ui.ClosePopup(id)
}
}
}
if len(candidates) == 0 {
widgets.LabelDim(ui, "No components match")
}
if first != nil && q != "" && (ui.KeyPressed(input.KeyEnter) || ui.KeyPressed(input.KeyKPEnter)) {
p.add(d, first)
ui.ClosePopup(id)
}
ui.EndPopup()
}
func (p *Panel) add(d *document.SceneDocument, td *schema.TypeDescriptor) {
p.confirm(d, "Add "+p.reg.Meta(td).DisplayName, func() error {
s := d.Scene()
env := commands.WithOnAdd(commands.EnvFor(s), p.d.Clip.OnAdd)
defaults := p.reg.Defaults(td.ID)
tx := d.History.Begin("Add " + td.FullName)
for _, n := range p.nodes {
if _, ok := s.GetByType(s.Handle(n), td.ID); ok {
continue
}
if err := tx.Exec(commands.NewMutation("Add "+td.FullName, env, commands.AddComp(s, n, td, defaults))); err != nil {
tx.Abort()
return err
}
}
tx.Commit()
return nil
})
}
func (p *Panel) nodeMenu(d *document.SceneDocument) {
ui := p.f.UI
if widgets.MenuItemIcon(ui, "Copy All Components", "", "edit.copy", len(p.types) > 0, false) {
if err := p.d.Clip.CopyComponents(d.Scene(), p.nodes[0], p.types); err != nil {
p.report(err)
} else {
p.d.Notices.Notify(context.LevelInfo, "Copied "+strconv.Itoa(len(p.types))+" component(s)")
}
}
can := p.d.Clip.Peek() == clipboard.KindComponents
if widgets.MenuItemIcon(ui, "Paste Components", "", "edit.paste", can, false) {
p.confirm(d, "Paste Components", func() error { return p.d.Clip.PasteComponents(d, p.nodes) })
}
}
func categoryIcon(ui *core.Context, category string) string {
if ic := "cat." + strings.ToLower(category); widgets.HasIcon(ui, ic) {
return ic
}
return "cat.node"
}