Files
aego/editor/panels/references/missing.go
sanspie f22242e5aa
ci / release (push) Successful in 5m5s
ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci / checks (push) Successful in 4m41s
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 7m14s
Stage 11g2–11g3: registry texts, CLI and the site in Russian (D348)
The remaining editor strings and undo step names; component, field, enum, setting, diagnostic and Script API texts under schema.*, enum.*, setting.*, diag.* and api.*; the launcher, the in-game Settings View with a Language setting, the CLI and aego schema show --lang. Every documentation page has a .ru.md translation; the site has a full Russian tree with hreflang, a language switch and ru/llms.txt; aego docs check --lang ru. New doc.tutorial and doc.recipe icons. Fix: an animator restored by a rewind prepares its controller before the replayed step.
2026-10-25 15:31:22 +03:00

194 lines
6.0 KiB
Go

package references
import (
"aego/asset"
"aego/engine/i18n"
"path"
"strconv"
"strings"
"time"
"aego/core/ids"
"aego/core/mathx"
"aego/editor/core/context"
"aego/editor/core/refs"
"aego/editor/core/services"
"aego/ui/core"
"aego/ui/widgets"
)
type Missing struct {
d Deps
assets []refs.Missing
nodes []refs.MissingNode
rev uint64
at time.Time
scroll float32
pick string
picking int
}
func NewMissing(d Deps) *Missing {
return &Missing{d: d, picking: -1}
}
func (m *Missing) ID() string {
return MissingID
}
func (m *Missing) Title() string {
return i18n.Tr("editor.missingrefs.title", "Missing References")
}
func (m *Missing) Icon() string {
return "status.warning"
}
func (m *Missing) Refresh() {
m.assets = m.d.Refs.MissingAssets()
m.nodes = m.d.Refs.MissingNodes()
m.rev = m.d.Pipeline.Revision()
m.at = time.Now()
}
func (m *Missing) Count() int {
return len(m.assets) + len(m.nodes)
}
func (m *Missing) Draw(f *services.Frame, r mathx.Rect) {
ui := f.UI
th := &ui.Theme
if m.at.IsZero() || (m.rev != m.d.Pipeline.Revision() && time.Since(m.at) > time.Second) {
m.Refresh()
}
head := ui.Cursor().Next(0)
rw := ui.TextSize(i18n.Tr("editor.references.rescan", "Rescan")).X + 16
ui.Text(i18n.Tr("editor.references.assets_missing_asset_s_nodes", "{assets} missing asset(s), {nodes} missing node reference(s)", i18n.Str("assets", strconv.Itoa(len(m.assets))), i18n.Str("nodes", strconv.Itoa(len(m.nodes)))), mathx.Vec2{X: head.X, Y: ui.TextY(head)}, th.TextDim)
if widgets.ButtonRect(ui, ui.ID("rescan"), mathx.Rect{X: head.X + head.W - rw, Y: head.Y, W: rw, H: head.H}, i18n.Tr("editor.references.rescan", "Rescan")) {
m.Refresh()
}
if m.Count() == 0 {
widgets.LabelDim(ui, i18n.Tr("editor.references.every_reference_resolves", "Every reference resolves"))
return
}
top := ui.Cursor().Y
list := mathx.Rect{X: r.X, Y: top, W: r.W, H: max(0, r.Y+r.H-top)}
rowH := th.RowHeight + 4
total := len(m.assets) + len(m.nodes)
widgets.VirtualList(ui, ui.ID("rows"), list, total, rowH, &m.scroll, func(i int, rr mathx.Rect) {
if i < len(m.assets) {
m.assetRow(ui, i, rr)
} else {
m.nodeRow(ui, i-len(m.assets), rr)
}
})
}
func (m *Missing) buttons(ui *core.Context, rr mathx.Rect, id core.ID, labels ...string) int {
x := rr.X + rr.W
hit := -1
for i := len(labels) - 1; i >= 0; i-- {
w := ui.TextSize(labels[i]).X + 14
x -= w + 4
if widgets.ButtonRect(ui, core.IDSeq(id, i+1), mathx.Rect{X: x, Y: rr.Y + 2, W: w, H: rr.H - 4}, labels[i]) {
hit = i
}
}
return hit
}
func (m *Missing) assetRow(ui *core.Context, i int, rr mathx.Rect) {
th := &ui.Theme
it := m.assets[i]
id := core.IDSeq(ui.ID("asset"), i)
widgets.IconAt(ui, mathx.Rect{X: rr.X + 2, Y: rr.Y + (rr.H-16)/2, W: 16, H: 16}, "status.warning", th.Warn)
text := i18n.Tr("editor.references.label_refs_referrer_s", "{label} · {refs} referrer(s)", i18n.Str("label", it.Label()), i18n.Str("refs", strconv.Itoa(len(it.Refs))))
labels := []string{i18n.Tr("editor.references.show", "Show"), i18n.Tr("editor.references.replace_ellipsis", "Replace…"), i18n.Tr("editor.references.clear", "Clear")}
if !it.Suggest.IsZero() {
labels = append([]string{i18n.Tr("editor.references.use", "Use {name}", i18n.Str("name", path.Base(it.Path)))}, labels...)
}
bw := float32(0)
for _, l := range labels {
bw += ui.TextSize(l).X + 18
}
ui.PushClip(mathx.Rect{X: rr.X, Y: rr.Y, W: max(0, rr.W-bw), H: rr.H})
ui.Text(text, mathx.Vec2{X: rr.X + 22, Y: ui.TextY(rr)}, th.Text)
ui.PopClip()
tip := i18n.Tr("editor.references.referenced_by", "Referenced by:")
for k, ref := range it.Refs {
if k == 8 {
tip += "\n…"
break
}
tip += "\n" + ref.Path
}
ui.MouseOver(id, rr)
widgets.Tooltip(ui, id, tip)
hit := m.buttons(ui, rr, id, labels...)
if hit < 0 {
m.picker(ui, i, rr)
return
}
if !it.Suggest.IsZero() {
if hit == 0 {
runReplace(m.d, it.ID, it.Suggest, i18n.Tr("editor.history.resolve", "Resolve {name}", i18n.Str("name", path.Base(it.Label()))), m.Refresh)
return
}
hit--
}
switch hit {
case 0:
if m.d.Show != nil {
m.d.Show(it.ID)
}
case 1:
m.picking, m.pick = i, ""
ui.CloseAllPopups()
ui.OpenPopup(core.IDSeq(ui.ID("pick"), i), mathx.Vec2{X: rr.X + rr.W/2, Y: rr.Y + rr.H})
case 2:
runReplace(m.d, it.ID, ids.AssetID{}, i18n.Tr("editor.history.clear", "Clear {name}", i18n.Str("name", path.Base(it.Label()))), m.Refresh)
}
m.picker(ui, i, rr)
}
func (m *Missing) picker(ui *core.Context, i int, rr mathx.Rect) {
if m.picking != i {
return
}
it := m.assets[i]
if to, ok := pickAsset(ui, core.IDSeq(ui.ID("pick"), i), m.d.Pipeline, it.Grave.Kind, &m.pick, ids.AssetID{}); ok {
m.picking = -1
runReplace(m.d, it.ID, to, i18n.Tr("editor.history.resolve", "Resolve {name}", i18n.Str("name", path.Base(it.Label()))), m.Refresh)
}
}
func (m *Missing) nodeRow(ui *core.Context, i int, rr mathx.Rect) {
th := &ui.Theme
it := m.nodes[i]
id := core.IDSeq(ui.ID("node"), i)
widgets.IconAt(ui, mathx.Rect{X: rr.X + 2, Y: rr.Y + (rr.H-16)/2, W: 16, H: 16}, "cat.node", th.Warn)
node := strings.TrimPrefix(it.Path, "/")
text := i18n.Tr("editor.references.scene_node_owner_field_missing", "{scene} · {node} · {owner}.{field} → missing node", i18n.Str("scene", it.Scene), i18n.Str("node", path.Base(node)), i18n.Str("owner", it.Owner), i18n.Str("field", it.Field))
widgets.Tooltip(ui, id, node)
labels := []string{i18n.Tr("editor.references.select", "Select"), i18n.Tr("editor.references.clear", "Clear")}
ui.PushClip(mathx.Rect{X: rr.X, Y: rr.Y, W: max(0, rr.W-130), H: rr.H})
ui.Text(text, mathx.Vec2{X: rr.X + 22, Y: ui.TextY(rr)}, th.Text)
ui.PopClip()
switch m.buttons(ui, rr, id, labels...) {
case 0:
openPlace(m.d, refs.Ref{Path: it.Scene, Kind: sceneKind(it.Scene)}, refs.Place{Node: it.Node})
case 1:
if err := m.d.Refs.ClearNode(it); err != nil {
m.d.Notices.Notify(context.LevelError, err.Error())
}
m.Refresh()
}
}
func sceneKind(rel string) asset.ArtifactKind {
if path.Ext(rel) == ".aeprefab" {
return asset.ArtPrefab
}
return asset.ArtScene
}