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
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.
194 lines
6.0 KiB
Go
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
|
|
}
|