Files
aego/editor/shell/project.go
sanspie 5dd9cc91bb editor: refine agent, scene controls and curves
Improve mentions and the default Design workspace, clarify scene lighting controls, and polish editor workflows for 0.12.5-dev.
2026-11-05 20:14:53 +03:00

274 lines
8.3 KiB
Go

package shell
import (
"errors"
"net/url"
"path/filepath"
"runtime"
"strconv"
"strings"
"aego/editor/core/command"
"aego/editor/core/context"
"aego/editor/core/sysopen"
"aego/editor/core/workspace"
"aego/editor/shell/launcher"
"aego/engine"
"aego/engine/i18n"
"aego/platform"
"aego/project"
)
const (
CommandNewProject = "file.new_project"
CommandOpenProject = "file.open_project"
CommandRecentPrefix = "file.recent."
CommandCloseProject = "file.close_project"
CommandRunTests = "play.run_tests"
CommandOpenLogs = "help.open_logs"
CommandCrashReports = "help.crash_reports"
CommandReportBug = "help.report_bug"
CommandInstallGo = "help.install_go"
recentCommands = 5
issuesURL = "https://git.akarpov.ru/sanspie/aego/issues/new"
repositoryURL = "https://git.akarpov.ru/sanspie/aego"
developerURL = "https://about.akarpov.ru"
telegramURL = "https://t.me/sanspie"
goDownloadURL = "https://go.dev/dl/"
tabRecent = "recent"
tabNew = "new"
)
var errNoRelaunch = errors.New("switching projects is not available in this editor")
func (a *App) registerProject() error {
canSwitch := func() bool { return a.opt.Relaunch != nil }
list := []command.Desc{
{ID: CommandNewProject, Name: "New Project…", Category: "File", Order: 50, Enabled: canSwitch,
Run: func() error { return a.switchTo("", tabNew) }},
{ID: CommandOpenProject, Name: "Open Project…", Category: "File", Order: 60, Enabled: canSwitch, Run: a.openProject},
{ID: CommandCloseProject, Name: "Close Project", Category: "File", Order: 890, Enabled: canSwitch,
Run: func() error { return a.switchTo("", tabRecent) }},
{ID: CommandOpenLogs, Name: "Open Logs Folder", Category: "Help", Order: 200,
Enabled: func() bool { return a.opt.LogsDir != "" },
Run: func() error { return sysopen.Open(a.opt.LogsDir) }},
{ID: CommandCrashReports, Name: "Crash Reports", Category: "Help", Order: 210,
Run: func() error {
a.ws.Show(a.crashesPanel.ID())
return nil
}},
{ID: CommandReportBug, Name: "Report a Bug…", Category: "Help", Order: 220,
Run: func() error { return sysopen.Open(a.bugURL()) }},
}
for i := 0; i < recentCommands; i++ {
name := "Open Recent " + strconv.Itoa(i+1)
n := i + 1
list = append(list, command.Desc{
ID: CommandRecentPrefix + strconv.Itoa(i+1), Name: name, Category: "File", Order: 70 + i,
Label: func() string {
a.recentList()
if i < len(a.recentLabels) {
return a.recentLabels[i]
}
return i18n.Tr("editor.shell.open_recent_n", "Open Recent {n}", i18n.Int("n", int64(n)))
},
Enabled: func() bool {
_, ok := a.recentAt(i)
return ok && canSwitch()
},
Run: func() error {
if e, ok := a.recentAt(i); ok {
return a.switchTo(e.Path, "")
}
return nil
},
})
}
if a.opt.RunTests != nil {
list = append(list, command.Desc{ID: CommandRunTests, Name: "Run Script Tests", Category: "Play", Order: 400,
Enabled: func() bool { return !a.testsRunning }, Run: a.runTests})
}
if a.opt.NativeNote != "" {
list = append(list, command.Desc{ID: CommandInstallGo, Name: "Install Go…", Category: "Help", Order: 230,
Run: func() error { return sysopen.Open(goDownloadURL) }})
}
for _, d := range list {
if err := a.ctx.Commands.Register(d); err != nil {
return err
}
}
return nil
}
func recentName(e launcher.Entry) string {
if e.Name != "" {
return e.Name
}
return filepath.Base(e.Path)
}
func (a *App) recentList() []launcher.Entry {
if a.opt.Recent == "" {
return nil
}
now := a.eng.Platform().Now()
if a.recentLoaded && now < a.recentNext {
return a.recent
}
list, _ := launcher.LoadRecent(a.opt.Recent)
out := list[:0]
for _, e := range list {
if !e.Missing && e.Path != a.opt.Project.Root {
out = append(out, e)
}
}
a.recent, a.recentNext, a.recentLoaded = out, now+2, true
a.recentLabels = a.recentLabels[:0]
for _, e := range out {
a.recentLabels = append(a.recentLabels, i18n.Tr("editor.shell.open_recent_item", "Open {name}", i18n.Str("name", recentName(e))))
}
return a.recent
}
func (a *App) recentAt(i int) (launcher.Entry, bool) {
list := a.recentList()
if i < 0 || i >= len(list) {
return launcher.Entry{}, false
}
return list[i], true
}
func (a *App) leave(target, tab string) {
if err := a.opt.Relaunch(target, tab); err != nil {
a.ctx.Notify(context.LevelError, i18n.Note("editor.shell.cannot_start_the_editor_again", "cannot start the editor again: {error}", i18n.Str("error", err.Error())))
return
}
a.quitting = true
a.eng.Quit()
}
func (a *App) switchTo(target, tab string) error {
if a.opt.Relaunch == nil {
return errNoRelaunch
}
if !a.ctx.Docs.Dirty() {
a.leave(target, tab)
return nil
}
a.ctx.Ask(context.Question{
Title: i18n.Tr("editor.shell.unsaved_changes", "Unsaved changes"),
Message: i18n.Tr("editor.shell.save_the_changed_scenes_and", "Save the changed scenes and scripts before leaving this project?"),
Buttons: []string{i18n.Tr("editor.shell.save_and_continue", "Save and Continue"), i18n.Tr("editor.shell.discard", "Discard"), i18n.Tr("editor.shell.cancel", "Cancel")},
Then: func(choice int) {
switch choice {
case 0:
if err := a.saveAll(); err != nil {
a.ctx.Notify(context.LevelError, err.Error())
return
}
a.leave(target, tab)
case 1:
a.leave(target, tab)
}
},
})
return nil
}
func (a *App) openProject() error {
open := func(path string) {
root, ok := launcher.ProjectRoot(strings.TrimSpace(path))
if !ok {
a.ctx.Notify(context.LevelError, i18n.Note("editor.shell.no_filename_in_path", "No {filename} in {path}", i18n.Str("filename", project.FileName), i18n.Str("path", path)))
return
}
if err := a.switchTo(root, ""); err != nil {
a.ctx.Notify(context.LevelError, err.Error())
}
}
if pick := sysopen.Picker(a.eng.Window(), platform.DialogFolder, "", ""); pick != nil {
pick(filepath.Dir(a.opt.Project.Root), open)
return nil
}
a.ctx.Ask(context.Question{
Title: i18n.Tr("editor.shell.open_project", "Open project"),
Fields: []string{i18n.Tr("editor.shell.folder", "Folder")},
Values: []string{filepath.Dir(a.opt.Project.Root)},
Buttons: []string{i18n.Tr("editor.shell.open", "Open"), i18n.Tr("editor.shell.cancel", "Cancel")},
Submit: func(v []string) { open(v[0]) },
})
return nil
}
func (a *App) dropProject() bool {
if len(a.drops) != 1 {
return false
}
root, ok := launcher.ProjectRoot(a.drops[0])
if !ok {
return false
}
if root != a.opt.Project.Root {
if err := a.switchTo(root, ""); err != nil {
a.ctx.Notify(context.LevelError, err.Error())
}
}
return true
}
func (a *App) pickFolder(start string, done func(string)) {
if pick := sysopen.Picker(a.eng.Window(), platform.DialogFolder, "", ""); pick != nil {
pick(start, done)
}
}
func (a *App) bugURL() string {
body := "Aego " + engine.Version + ", " + runtime.GOOS + "/" + runtime.GOARCH + ", GPU: " + a.eng.Caps().Renderer +
"\n\nWhat happened:\n\nWhat you expected:\n\nSteps to reproduce:\n\nThe editor log is in Help → Open Logs Folder.\n"
return issuesURL + "?body=" + url.QueryEscape(body)
}
func (a *App) openSupportURL(target string) {
if err := sysopen.Open(target); err != nil {
a.ctx.Notify(context.LevelError, err.Error())
}
}
func (a *App) runTests() error {
a.testsRunning = true
a.ctx.Notify(context.LevelInfo, i18n.Note("editor.shell.running_script_tests", "Running script tests…"))
return a.opt.RunTests(a.testsDone)
}
func (a *App) testsDone(out string, failed bool) {
a.testsRunning = false
for _, line := range strings.Split(strings.TrimRight(out, "\n"), "\n") {
switch {
case line == "":
case strings.Contains(line, "FAIL") || strings.Contains(line, "error"):
a.scriptLog.Error(line)
default:
a.scriptLog.Info(line)
}
}
a.ws.Show(workspace.Console)
if failed {
a.ctx.Notify(context.LevelError, i18n.Note("editor.shell.script_tests_failed_see_console", "Script tests failed, see Console and Problems"))
return
}
a.ctx.Notify(context.LevelInfo, i18n.Note("editor.shell.script_tests_passed", "Script tests passed"))
}
func (a *App) nativeHint() string {
if a.opt.NativeNote == "" {
return ""
}
return i18n.Tr("editor.shell.go_not_found_native_code", "Go not found — native code is not built (Help → Install Go)")
}
func (a *App) OpenAgentProject(root string) {
if err := a.switchTo(root, ""); err != nil {
a.ctx.Notify(context.LevelWarn, err.Error())
}
}