350 lines
8.3 KiB
Go
350 lines
8.3 KiB
Go
package projectsettings
|
|
|
|
import (
|
|
"sort"
|
|
|
|
"aego/asset/pipeline"
|
|
"aego/core/mathx"
|
|
"aego/editor/core/context"
|
|
"aego/editor/core/services"
|
|
"aego/engine/module"
|
|
es "aego/engine/settings"
|
|
"aego/engine/settings/settingsui"
|
|
"aego/format/aetext"
|
|
"aego/project"
|
|
"aego/schema"
|
|
"aego/ui/core"
|
|
"aego/ui/widgets"
|
|
)
|
|
|
|
const (
|
|
narrowPanel = 420
|
|
ID = "project_settings"
|
|
CommandOpen = "file.project_settings"
|
|
)
|
|
|
|
type Deps struct {
|
|
Project *project.Project
|
|
Sections *module.Sections
|
|
Settings *es.Store
|
|
Pages func() []context.SettingsPage
|
|
Pipeline *pipeline.Pipeline
|
|
Notices services.Notices
|
|
OnApply func() string
|
|
}
|
|
|
|
type Panel struct {
|
|
d Deps
|
|
work *project.Project
|
|
drafts map[string]any
|
|
proj map[es.Key]schema.Value
|
|
projDirty bool
|
|
tab map[string]int
|
|
filter string
|
|
ed settingsui.Editor
|
|
page string
|
|
dirty bool
|
|
err string
|
|
scroll float32
|
|
content float32
|
|
bar widgets.ScrollState
|
|
base state
|
|
undo []state
|
|
redo []state
|
|
gesture bool
|
|
touched bool
|
|
flash string
|
|
flashAt float64
|
|
flashGo bool
|
|
}
|
|
|
|
func New(d Deps) *Panel {
|
|
return &Panel{d: d, drafts: map[string]any{}, tab: map[string]int{}}
|
|
}
|
|
|
|
func (p *Panel) ID() string {
|
|
return ID
|
|
}
|
|
|
|
func (p *Panel) Title() string {
|
|
if p.dirty {
|
|
return "Project Settings *"
|
|
}
|
|
return "Project Settings"
|
|
}
|
|
|
|
func (p *Panel) Show(page string) {
|
|
if page != "" {
|
|
p.page = page
|
|
p.scroll = 0
|
|
}
|
|
}
|
|
|
|
func clone(src *project.Project) *project.Project {
|
|
c := *src
|
|
c.Extra = make([]*aetext.Node, len(src.Extra))
|
|
for i, b := range src.Extra {
|
|
c.Extra[i] = b.Clone()
|
|
}
|
|
c.Settings = src.Settings.Clone()
|
|
return &c
|
|
}
|
|
|
|
func (p *Panel) pages() []page {
|
|
out := p.builtin()
|
|
var custom []context.SettingsPage
|
|
if p.d.Pages != nil {
|
|
custom = p.d.Pages()
|
|
}
|
|
withSection := map[string]bool{}
|
|
for _, sec := range p.d.Sections.List() {
|
|
pg := page{id: sec.ID, title: sec.Title, order: sec.Order, section: sec.ID, owner: sec.Owner}
|
|
for _, sp := range custom {
|
|
if sp.Section == sec.ID {
|
|
pg.draw = sp.Draw
|
|
}
|
|
}
|
|
if pg.draw == nil && sec.Type != nil {
|
|
s := sec
|
|
pg.drawUI = func(ui *core.Context, d any) bool { return auto(ui, s, d) }
|
|
}
|
|
if pg.draw != nil || pg.drawUI != nil {
|
|
out = append(out, pg)
|
|
withSection[sec.Owner] = true
|
|
}
|
|
}
|
|
for _, o := range p.projectOwners() {
|
|
if withSection[o.owner] {
|
|
continue
|
|
}
|
|
owner := o.owner
|
|
out = append(out, page{id: "defaults/" + owner, title: o.title, order: 500, owner: owner,
|
|
settings: func(ui *core.Context) bool { return p.drawDefaults(ui, owner) }})
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool { return out[i].order < out[j].order })
|
|
return out
|
|
}
|
|
|
|
func (p *Panel) draft(id string) any {
|
|
if d, ok := p.drafts[id]; ok {
|
|
return d
|
|
}
|
|
d, err := p.d.Sections.Draft(id)
|
|
if err != nil {
|
|
p.err = err.Error()
|
|
return nil
|
|
}
|
|
p.drafts[id] = d
|
|
return d
|
|
}
|
|
|
|
func (p *Panel) reset() {
|
|
p.work = clone(p.d.Project)
|
|
p.drafts = map[string]any{}
|
|
p.proj = map[es.Key]schema.Value{}
|
|
if p.d.Settings != nil {
|
|
p.proj = p.d.Settings.ProjectValues()
|
|
}
|
|
p.dirty, p.projDirty, p.err = false, false, ""
|
|
p.undo, p.redo, p.gesture = nil, nil, false
|
|
p.base = p.capture()
|
|
}
|
|
|
|
func (p *Panel) apply() {
|
|
next := clone(p.work)
|
|
for id, d := range p.drafts {
|
|
n, err := p.d.Sections.Node(id, d)
|
|
if err != nil {
|
|
p.err = err.Error()
|
|
return
|
|
}
|
|
sec, _ := p.d.Sections.Desc(id)
|
|
next.ReplaceBlock(n, sec.Heads()...)
|
|
}
|
|
st := p.d.Settings
|
|
if p.projDirty && st != nil {
|
|
next.Settings = st.ProjectNode(p.proj)
|
|
}
|
|
if err := next.Save(); err != nil {
|
|
p.err = err.Error()
|
|
return
|
|
}
|
|
for id, d := range p.drafts {
|
|
if err := p.d.Sections.Set(id, d); err != nil {
|
|
p.err = err.Error()
|
|
return
|
|
}
|
|
}
|
|
if p.projDirty && st != nil {
|
|
if err := st.SetProject(p.proj); err != nil {
|
|
p.err = err.Error()
|
|
return
|
|
}
|
|
}
|
|
*p.d.Project = *next
|
|
p.reset()
|
|
msg := "Saved project.aego, settings apply on the next Play"
|
|
if p.d.OnApply != nil {
|
|
if note := p.d.OnApply(); note != "" {
|
|
msg = "Saved project.aego, " + note
|
|
}
|
|
}
|
|
p.d.Notices.Notify(context.LevelInfo, msg)
|
|
}
|
|
|
|
func (p *Panel) Draw(f *services.Frame, r mathx.Rect) {
|
|
ui := f.UI
|
|
th := &ui.Theme
|
|
if p.d.Project == nil {
|
|
widgets.LabelDim(ui, "No project")
|
|
return
|
|
}
|
|
if p.work == nil {
|
|
p.reset()
|
|
}
|
|
pages := p.pages()
|
|
if p.page == "" {
|
|
p.page = pages[0].id
|
|
}
|
|
bar := ui.Cursor().Next(0)
|
|
if widgets.ButtonRect(ui, ui.ID("apply"), mathx.Rect{X: bar.X, Y: bar.Y, W: 90, H: bar.H}, "Apply") && p.dirty {
|
|
p.apply()
|
|
}
|
|
if widgets.ButtonRect(ui, ui.ID("revert"), mathx.Rect{X: bar.X + 96, Y: bar.Y, W: 90, H: bar.H}, "Revert") {
|
|
p.reset()
|
|
}
|
|
status := mathx.Rect{X: bar.X + 196, Y: bar.Y, W: max(0, bar.W-196), H: bar.H}
|
|
switch {
|
|
case p.err != "":
|
|
ui.Text(widgets.Elide(ui, p.err, status.W), mathx.Vec2{X: status.X, Y: ui.TextY(status)}, th.Error)
|
|
case p.dirty:
|
|
ui.Text(widgets.Elide(ui, "Unsaved changes", status.W), mathx.Vec2{X: status.X, Y: ui.TextY(status)}, th.Warn)
|
|
}
|
|
top := bar.Y + bar.H + th.Spacing
|
|
sw := th.ScrollbarW
|
|
var body mathx.Rect
|
|
if r.W < narrowPanel {
|
|
sel := 0
|
|
names := make([]string, len(pages))
|
|
for i, pg := range pages {
|
|
names[i] = pg.title
|
|
if pg.id == p.page {
|
|
sel = i
|
|
}
|
|
}
|
|
row := mathx.Rect{X: r.X + th.Padding, Y: top, W: r.W - 2*th.Padding, H: th.RowHeight}
|
|
if widgets.Combo(ui, ui.ID("pages"), row, &sel, names, false) && sel < len(pages) {
|
|
p.page, p.scroll = pages[sel].id, 0
|
|
}
|
|
top += th.RowHeight + th.Spacing
|
|
body = mathx.Rect{X: r.X, Y: top, W: max(0, r.W-sw-2), H: max(0, r.Y+r.H-top)}
|
|
} else {
|
|
left := mathx.Rect{X: r.X, Y: top, W: min(180, max(120, r.W*0.24)), H: max(0, r.Y+r.H-top)}
|
|
p.nav(ui, left, pages)
|
|
body = mathx.Rect{X: left.X + left.W + 8, Y: top, W: max(0, r.X+r.W-left.X-left.W-8-sw-2), H: left.H}
|
|
}
|
|
view := body.H - th.Padding*2
|
|
maxOff := max(0, p.content-view)
|
|
if ui.MouseOver(ui.ID("scroll"), body) {
|
|
if w := ui.ScrollY(th.RowHeight * 3); w != 0 {
|
|
p.scroll -= w
|
|
}
|
|
}
|
|
p.scroll = mathx.Clamp(p.scroll, 0, maxOff)
|
|
ui.PushClip(body)
|
|
ui.PushCursor(core.Cursor{X: body.X + th.Padding, Y: body.Y + th.Padding - p.scroll, W: body.W - 2*th.Padding, RowH: th.RowHeight, Pad: th.Padding, Space: th.Spacing})
|
|
start := ui.Cursor().Y
|
|
if p.flash != "" {
|
|
if p.flashAt == 0 {
|
|
p.flashAt = ui.Time
|
|
}
|
|
ui.Flash, ui.FlashAt, ui.FlashSeen = p.flash, p.flashAt, false
|
|
}
|
|
ui.PushID(p.page)
|
|
changed := false
|
|
for _, pg := range pages {
|
|
if pg.id != p.page {
|
|
continue
|
|
}
|
|
changed = p.drawPage(ui, pg) || changed
|
|
}
|
|
ui.PopID()
|
|
if p.flash != "" {
|
|
if ui.FlashSeen && p.flashGo {
|
|
p.scroll = mathx.Clamp(ui.FlashY-start-view/3, 0, max(0, ui.Cursor().Y-start-view))
|
|
p.flashGo = false
|
|
}
|
|
if ui.Time-p.flashAt > 2 {
|
|
p.flash = ""
|
|
}
|
|
ui.Flash = ""
|
|
}
|
|
p.content = ui.Cursor().Y - start
|
|
ui.PopCursor()
|
|
ui.PopClip()
|
|
track := mathx.Rect{X: body.X + body.W + 2, Y: body.Y, W: sw, H: body.H}
|
|
widgets.ScrollBar(ui, ui.ID("vbar"), track, &p.scroll, p.content, view, &p.bar)
|
|
if changed {
|
|
p.touch()
|
|
}
|
|
p.settle(ui)
|
|
}
|
|
|
|
func (p *Panel) nav(ui *core.Context, r mathx.Rect, pages []page) {
|
|
th := &ui.Theme
|
|
ui.Rect(r, th.PanelAlt)
|
|
ui.PushClip(r)
|
|
y := r.Y + th.Padding
|
|
for i, pg := range pages {
|
|
row := mathx.Rect{X: r.X + 4, Y: y, W: r.W - 8, H: th.RowHeight}
|
|
y += th.RowHeight + 2
|
|
id := ui.IDInt(1000 + i)
|
|
if ui.Clicked(id, row) {
|
|
p.page, p.scroll = pg.id, 0
|
|
}
|
|
sel := pg.id == p.page
|
|
col := th.TextDim
|
|
switch {
|
|
case sel:
|
|
ui.RoundRect(row, th.Radius.Menu, th.SelectionDim)
|
|
ui.Rect(mathx.Rect{X: row.X, Y: row.Y + 3, W: 2, H: row.H - 6}, th.Accent)
|
|
col = th.Text
|
|
case ui.Hot() == id:
|
|
ui.RoundRect(row, th.Radius.Menu, th.WidgetHot)
|
|
col = th.Text
|
|
}
|
|
ui.Text(widgets.Elide(ui, pg.title, row.W-14), mathx.Vec2{X: row.X + 10, Y: ui.TextY(row)}, col)
|
|
if ui.TextSize(pg.title).X > row.W-14 {
|
|
widgets.Tooltip(ui, id, pg.title)
|
|
}
|
|
}
|
|
ui.PopClip()
|
|
}
|
|
|
|
func (p *Panel) drawPage(ui *core.Context, pg page) bool {
|
|
if pg.settings != nil {
|
|
return pg.settings(ui)
|
|
}
|
|
if pg.owner != "" && p.hasDefaults(pg.owner) && p.tabs(ui, pg.id) == 1 {
|
|
return p.drawDefaults(ui, pg.owner)
|
|
}
|
|
switch {
|
|
case pg.project != nil:
|
|
return pg.project(ui, p.work)
|
|
case pg.draw != nil:
|
|
if d := p.draft(pg.section); d != nil {
|
|
return pg.draw(d)
|
|
}
|
|
case pg.drawUI != nil:
|
|
if d := p.draft(pg.section); d != nil {
|
|
return pg.drawUI(ui, d)
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (p *Panel) Focus(page, field string) {
|
|
p.Show(page)
|
|
p.flash, p.flashAt, p.flashGo = field, 0, true
|
|
}
|