Show and configure safe Editor autosave
This commit is contained in:
@@ -67,6 +67,20 @@ resume and single-step process controls; it cannot read or modify game entities.
|
||||
scenes that have never been saved. Restoring an already open document requires its
|
||||
current revision. Recovery refuses to overwrite an externally changed scene file.
|
||||
|
||||
Long-lived headless and graphical Editor sessions also poll the same autosave
|
||||
controller. With `editor.autosave` enabled (the default), a named dirty scene is
|
||||
saved after two seconds idle. `faset_autosave_status {}` reports `enabled` and each
|
||||
open document's `state`, `revision`, `path`, and `error`. States include `saved`,
|
||||
`pending`, `saving`, `conflict`, `failed`, `save_as_required`, and `disabled`.
|
||||
An unnamed scene remains in recovery until you give it an explicit path. An
|
||||
external disk edit produces `conflict` without overwriting that edit. For scripted
|
||||
workflows, use `faset_document_save` with `expected_revision` when you need a
|
||||
definite save boundary instead of waiting for idle autosave. If a conflict occurs,
|
||||
save to a new project-relative path and compare the two files. Change the
|
||||
preference through `faset_project_settings_get` and `faset_project_settings_set`
|
||||
with the returned project revision and `{"editor":{"autosave":false}}`; the
|
||||
new value applies to the current session and persists for the next Editor launch.
|
||||
|
||||
## Single-command CLI
|
||||
|
||||
The CLI is useful for scripts that do not need an MCP session:
|
||||
|
||||
@@ -50,6 +50,19 @@ outside the Editor, ordinary Save reports a conflict instead of overwriting it.
|
||||
Save your in-memory work to a new path and compare the two versions before replacing
|
||||
anything.
|
||||
|
||||
Autosave is enabled by default. After a named scene has been idle for two seconds,
|
||||
the Editor saves its current revision to that scene file. The status bar shows
|
||||
**Pending autosave**, **Saving**, or **Saved**. An unnamed scene shows **Save As
|
||||
required** until you choose its path. The status bar's **Save As...** action opens
|
||||
an editable, new project-relative path; it never overwrites the conflicting file.
|
||||
|
||||
If the disk file changes outside Faset, autosave stops for that revision and shows
|
||||
**Save conflict**. Use **Save As...** to keep both versions, then compare or reload
|
||||
the original. **Save failed** preserves the unsaved scene in the recovery journal;
|
||||
correct the write error or use Save As. Autosave does not clear Undo history or
|
||||
replace the [recovery journal](#recover-unsaved-work). Recovery still protects
|
||||
unnamed scenes and edits before the idle deadline.
|
||||
|
||||
## Navigate and transform
|
||||
|
||||
- **Right drag:** orbit the 3D view; pan in a 2D scene.
|
||||
@@ -119,13 +132,16 @@ for the C++ iteration loop.
|
||||
## Project settings
|
||||
|
||||
Open **File → Project settings** to change the project name, initial **2D / 3D** type,
|
||||
and start-scene path. Save a scene first, then choose it from the saved-scene list or
|
||||
enter its project-relative path. Choose **Save project** to update
|
||||
`project.faset.json` explicitly. A missing start scene is rejected and the dialog
|
||||
stays open.
|
||||
start-scene path, or **Autosave named scenes** preference. Save a scene first, then
|
||||
choose it from the saved-scene list or enter its project-relative path. Choose
|
||||
**Save project** to update `project.faset.json` explicitly. A missing start scene is
|
||||
rejected and the dialog stays open. The Autosave preference applies immediately;
|
||||
when disabled, the status bar says **Autosave off** and you can still use Ctrl+S or
|
||||
Save As. The project file stores it as `editor.autosave`; omitting it means enabled.
|
||||
|
||||
These settings are used when the project opens again; they do not convert the active
|
||||
scene, switch its dimension, restart the Player, or add a scene Undo entry.
|
||||
The project name, default dimension, and start scene are used when the project opens
|
||||
again; they do not convert the active scene, switch its dimension, restart the Player,
|
||||
or add a scene Undo entry.
|
||||
**Cancel** or **Escape** discards this form's draft. If another writer changes the
|
||||
project settings while the dialog is open, Save reports a revision conflict and
|
||||
keeps your draft. **Reload saved** deliberately discards it and reloads the latest
|
||||
|
||||
@@ -186,6 +186,7 @@ struct EditorUI::Impl {
|
||||
bool project_settings_open = false;
|
||||
Json project_settings_state;
|
||||
int project_settings_dimension = 3;
|
||||
bool project_settings_autosave = true;
|
||||
std::string project_settings_error;
|
||||
bool project_switch_enabled = true, project_switch_requested = false,
|
||||
project_switch_warning = false;
|
||||
@@ -601,6 +602,20 @@ struct EditorUI::Impl {
|
||||
schema_status.visible = false;
|
||||
schema_status.enabled = false;
|
||||
schema_status.tooltip = "Choose Build to refresh gameplay fields in Inspector";
|
||||
auto& autosave_status = statusrow.add(Kind::Label, "autosave-status", "Saved");
|
||||
autosave_status.layout.width = 160;
|
||||
autosave_status.enabled = false;
|
||||
auto& autosave_action = button(statusrow, "autosave-save-as", "Save As...", [this] {
|
||||
const auto path = current.value("path", std::string());
|
||||
const auto directory = path.empty() ? std::string("Scenes/")
|
||||
: path.substr(0, path.find_last_of('/') + 1);
|
||||
ui.update_text("save-path",
|
||||
directory + "Recovered-" + new_id().substr(0, 8) + ".scene.json",
|
||||
true);
|
||||
menu = "File";
|
||||
}, 100);
|
||||
autosave_action.visible = false;
|
||||
autosave_action.tooltip = "Save this scene to a new project-relative path";
|
||||
label(statusrow, "renderer-status", "Vulkan", 225);
|
||||
statusrow.find("renderer-status")->enabled = false;
|
||||
build_overlays();
|
||||
@@ -792,12 +807,20 @@ struct EditorUI::Impl {
|
||||
}
|
||||
label(project, "project-settings-start-label", "Start scene (saved project-relative path)");
|
||||
project.add(Kind::TextField, "project-settings-start");
|
||||
auto& autosave = project.add(Kind::Checkbox, "project-settings-autosave",
|
||||
"Autosave named scenes after 2 seconds idle");
|
||||
autosave.layout.height = 30;
|
||||
autosave.checked = true;
|
||||
autosave.on_commit = [this](Widget& widget) {
|
||||
project_settings_autosave = widget.checked;
|
||||
};
|
||||
autosave.tooltip = "Unsaved scenes remain in recovery until you choose Save As";
|
||||
auto& scenes = project.add(Kind::Column, "project-settings-scenes");
|
||||
scenes.layout.height = 104;
|
||||
scenes.layout.scroll = true;
|
||||
scenes.layout.gap = 1;
|
||||
label(project, "project-settings-note",
|
||||
"Applies on next project open. Scene Undo is unchanged.");
|
||||
"Autosave applies now; other settings on next open. Scene Undo is unchanged.");
|
||||
auto& project_actions = project.add(Kind::Row, "project-settings-actions");
|
||||
project_actions.layout.height = 32;
|
||||
button(
|
||||
@@ -1258,6 +1281,42 @@ struct EditorUI::Impl {
|
||||
(current.value("dirty", false) ? " *" : "");
|
||||
ui.find("project-title")->tooltip = project_name + " / " + scene_name;
|
||||
ui.find("status")->text = status;
|
||||
const auto autosave = call("faset_autosave_status");
|
||||
std::string save_state = "saved", save_error;
|
||||
if (!autosave.is_null()) {
|
||||
if (!autosave.value("enabled", true))
|
||||
save_state = "disabled";
|
||||
for (const auto& entry : autosave.at("documents"))
|
||||
if (entry.at("id") == document) {
|
||||
save_state = entry.at("state").get<std::string>();
|
||||
save_error = entry.value("error", std::string());
|
||||
break;
|
||||
}
|
||||
if (!autosave.value("enabled", true) && save_state == "saved")
|
||||
save_state = "disabled";
|
||||
}
|
||||
auto& save_label = *ui.find("autosave-status");
|
||||
save_label.text = save_state == "pending" ? "Pending autosave"
|
||||
: save_state == "saving" ? "Saving"
|
||||
: save_state == "conflict" ? "Save conflict"
|
||||
: save_state == "failed" ? "Save failed"
|
||||
: save_state == "save_as_required" ? "Save As required"
|
||||
: save_state == "disabled" ? "Autosave off"
|
||||
: "Saved";
|
||||
save_label.tooltip = save_state == "conflict"
|
||||
? "Scene file changed outside Faset. Use Save As to keep both "
|
||||
"versions, then compare or reload. " + save_error
|
||||
: save_state == "failed"
|
||||
? "Scene save failed. Your recovery journal remains available. "
|
||||
"Use Save As or correct the error. " + save_error
|
||||
: save_state == "save_as_required"
|
||||
? "Choose Save As; unnamed scenes remain in recovery only"
|
||||
: save_state == "disabled"
|
||||
? "Autosave is off in Project settings; use Save manually"
|
||||
: "Scene autosave status";
|
||||
ui.find("autosave-save-as")->visible =
|
||||
save_state == "conflict" || save_state == "failed" ||
|
||||
save_state == "save_as_required";
|
||||
ui.find("renderer-status")->text =
|
||||
"Vulkan 1.3 | " + std::to_string(resolved.at("entities").size()) + " objects";
|
||||
}
|
||||
@@ -2179,7 +2238,7 @@ struct EditorUI::Impl {
|
||||
refresh_project_settings();
|
||||
const bool modal = palette || !recovery.empty() || simulation_open ||
|
||||
project_switch_warning || project_settings_open;
|
||||
for (const auto* id : {"menubar", "toolbar", "workspace", "bottom_panel"})
|
||||
for (const auto* id : {"menubar", "toolbar", "workspace", "bottom_panel", "statusbar"})
|
||||
ui.find(id)->enabled = !modal;
|
||||
const bool file = menu == "File" || menu == "Faset",
|
||||
edit = menu == "Edit" || menu == "Scene",
|
||||
@@ -2234,6 +2293,8 @@ struct EditorUI::Impl {
|
||||
project_settings_state = result;
|
||||
const auto& settings = result.at("settings");
|
||||
project_settings_dimension = settings.value("dimension", 3);
|
||||
project_settings_autosave = settings.at("editor").at("autosave").get<bool>();
|
||||
ui.find("project-settings-autosave")->checked = project_settings_autosave;
|
||||
ui.update_text("project-settings-name", settings.value("name", std::string()), true);
|
||||
ui.update_text("project-settings-start", settings.value("start_scene", std::string()),
|
||||
true);
|
||||
@@ -2300,7 +2361,9 @@ struct EditorUI::Impl {
|
||||
project_settings_error = "Enter a project name.";
|
||||
return;
|
||||
}
|
||||
Json changes = {{"name", name}, {"dimension", project_settings_dimension}};
|
||||
Json changes = {{"name", name},
|
||||
{"dimension", project_settings_dimension},
|
||||
{"editor", {{"autosave", project_settings_autosave}}}};
|
||||
if (!start.empty() ||
|
||||
!project_settings_state.at("settings").value("start_scene", std::string()).empty())
|
||||
changes["start_scene"] = start;
|
||||
@@ -2314,7 +2377,7 @@ struct EditorUI::Impl {
|
||||
project_settings_state = result;
|
||||
project_settings_open = false;
|
||||
project_settings_error.clear();
|
||||
status = "Project settings saved for the next project open";
|
||||
status = "Project settings saved; autosave preference applies now";
|
||||
}
|
||||
void refresh_project_settings() {
|
||||
auto* panel = ui.find("project-settings-panel");
|
||||
@@ -2325,6 +2388,7 @@ struct EditorUI::Impl {
|
||||
panel->layout.y = std::max(0.f, (logical_height() - panel->layout.height) * .5f);
|
||||
ui.find("project-settings-2d")->selected = project_settings_dimension == 2;
|
||||
ui.find("project-settings-3d")->selected = project_settings_dimension == 3;
|
||||
ui.find("project-settings-autosave")->checked = project_settings_autosave;
|
||||
ui.find("project-settings-error")->text = project_settings_error;
|
||||
for (auto& choice : ui.find("project-settings-scenes")->children)
|
||||
if (choice->kind == Kind::TreeRow)
|
||||
|
||||
+73
-35
@@ -23,6 +23,52 @@ BuildConfig build_config(const SessionConfig& config) {
|
||||
result.cache_root = config.project_root / ".faset/cache";
|
||||
return result;
|
||||
}
|
||||
struct ProjectState {
|
||||
Json value;
|
||||
std::string revision;
|
||||
};
|
||||
ProjectState read_project_state(const std::filesystem::path& project_root) {
|
||||
const auto path = project_root / "project.faset.json";
|
||||
if (std::filesystem::exists(path)) {
|
||||
auto value = read_json(path);
|
||||
// The revision represents persisted content, including whether a
|
||||
// default-valued setting was explicitly written by another client.
|
||||
const auto revision = sha256(value.dump());
|
||||
require(value.is_object() && value.value("format", "") == "faset.project" &&
|
||||
value.value("version", 0) == 1,
|
||||
"project.version", "Unsupported project format or version");
|
||||
require(value.contains("name") && value.at("name").is_string() &&
|
||||
!value.at("name").get<std::string>().empty(),
|
||||
"project.name", "Project name must be a nonempty string");
|
||||
const auto dimension = value.value("dimension", 3);
|
||||
require(dimension == 2 || dimension == 3, "project.dimension",
|
||||
"Project dimension must be 2 or 3");
|
||||
if (value.contains("start_scene")) {
|
||||
require(value.at("start_scene").is_string(), "project.start_scene",
|
||||
"Project start_scene must be a relative path");
|
||||
const auto scene = value.at("start_scene").get<std::string>();
|
||||
if (!scene.empty())
|
||||
project_path(project_root, path_from_utf8(scene));
|
||||
}
|
||||
if (value.contains("editor"))
|
||||
require(value.at("editor").is_object(), "project.editor",
|
||||
"Project editor settings must be an object");
|
||||
else
|
||||
value["editor"] = Json::object();
|
||||
if (value["editor"].contains("autosave"))
|
||||
require(value["editor"]["autosave"].is_boolean(), "project.autosave",
|
||||
"Project autosave setting must be true or false");
|
||||
else
|
||||
value["editor"]["autosave"] = true;
|
||||
return {std::move(value), revision};
|
||||
}
|
||||
Json value = {{"format", "faset.project"},
|
||||
{"version", 1},
|
||||
{"name", path_to_utf8(project_root.filename())},
|
||||
{"dimension", 3},
|
||||
{"editor", {{"autosave", true}}}};
|
||||
return {value, sha256(value.dump())};
|
||||
}
|
||||
Json resolved_or_throw(Commands& commands, const std::string& id) {
|
||||
const auto resolved = commands.resolved_scene(id);
|
||||
if (!resolved.at("conflicts").empty())
|
||||
@@ -56,6 +102,9 @@ Session::Session(SessionConfig config)
|
||||
autosave_([this](const std::string& document, std::uint64_t revision) {
|
||||
return authoring_.save(document, {}, revision);
|
||||
}) {
|
||||
autosave_enabled_ = project().at("editor").at("autosave").get<bool>();
|
||||
autosave_.observe(authoring_.documents(), AutosaveController::Clock::now(),
|
||||
autosave_enabled_);
|
||||
register_commands();
|
||||
plugins_ = std::make_unique<PluginManager>(
|
||||
commands_, [this](std::string message) { log(std::move(message)); });
|
||||
@@ -86,31 +135,7 @@ void Session::log(std::string value) {
|
||||
logs_.erase(logs_.begin(), logs_.begin() + 100);
|
||||
}
|
||||
Json Session::project() const {
|
||||
const auto path = config_.project_root / "project.faset.json";
|
||||
if (std::filesystem::exists(path)) {
|
||||
const auto value = read_json(path);
|
||||
require(value.is_object() && value.value("format", "") == "faset.project" &&
|
||||
value.value("version", 0) == 1,
|
||||
"project.version", "Unsupported project format or version");
|
||||
require(value.contains("name") && value.at("name").is_string() &&
|
||||
!value.at("name").get<std::string>().empty(),
|
||||
"project.name", "Project name must be a nonempty string");
|
||||
const auto dimension = value.value("dimension", 3);
|
||||
require(dimension == 2 || dimension == 3, "project.dimension",
|
||||
"Project dimension must be 2 or 3");
|
||||
if (value.contains("start_scene")) {
|
||||
require(value.at("start_scene").is_string(), "project.start_scene",
|
||||
"Project start_scene must be a relative path");
|
||||
const auto scene = value.at("start_scene").get<std::string>();
|
||||
if (!scene.empty())
|
||||
project_path(config_.project_root, path_from_utf8(scene));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return {{"format", "faset.project"},
|
||||
{"version", 1},
|
||||
{"name", path_to_utf8(config_.project_root.filename())},
|
||||
{"dimension", 3}};
|
||||
return read_project_state(config_.project_root).value;
|
||||
}
|
||||
void Session::scaffold(const std::string& name, int dimension) {
|
||||
builds_.scaffold(name, dimension);
|
||||
@@ -346,23 +371,25 @@ void Session::register_commands() {
|
||||
"faset_project_settings_get", "Read project settings and their content revision.",
|
||||
schema(Json::object()),
|
||||
[&](const Json&) {
|
||||
const auto value = project();
|
||||
return Json{{"settings", value}, {"revision", sha256(value.dump())}};
|
||||
const auto state = read_project_state(config_.project_root);
|
||||
return Json{{"settings", state.value}, {"revision", state.revision}};
|
||||
},
|
||||
true);
|
||||
commands_.add(
|
||||
"faset_project_settings_set",
|
||||
"Save project name, initial scene dimension or start scene with an expected content "
|
||||
"revision. Applies on the next project open; does not change the active scene or its Undo "
|
||||
"history.",
|
||||
"Save project name, initial scene dimension, start scene or editor.autosave with an "
|
||||
"expected content revision. Autosave applies immediately; other settings apply on the "
|
||||
"next project open without changing scene Undo history.",
|
||||
schema({{"revision", text}, {"settings", {{"type", "object"}}}}, {"revision", "settings"}),
|
||||
[&](const Json& args) {
|
||||
auto value = project();
|
||||
require(args.at("revision") == sha256(value.dump()), "revision.conflict",
|
||||
auto state = read_project_state(config_.project_root);
|
||||
auto value = std::move(state.value);
|
||||
require(args.at("revision") == state.revision, "revision.conflict",
|
||||
"Project settings changed; reload them before saving");
|
||||
const auto& changes = args.at("settings");
|
||||
for (const auto& [key, field] : changes.items()) {
|
||||
require(key == "name" || key == "dimension" || key == "start_scene",
|
||||
require(key == "name" || key == "dimension" || key == "start_scene" ||
|
||||
key == "editor",
|
||||
"project.setting", "Unknown editable project setting: " + key);
|
||||
if (key == "name")
|
||||
require(field.is_string() && !field.get<std::string>().empty(), "project.name",
|
||||
@@ -370,7 +397,15 @@ void Session::register_commands() {
|
||||
else if (key == "dimension")
|
||||
require(field.is_number_integer() && (field == 2 || field == 3),
|
||||
"project.dimension", "Initial scene dimension must be 2 or 3");
|
||||
else {
|
||||
else if (key == "editor") {
|
||||
require(field.is_object() && field.size() == 1 &&
|
||||
field.contains("autosave"),
|
||||
"project.setting", "Only editor.autosave can be changed here");
|
||||
require(field.at("autosave").is_boolean(), "project.autosave",
|
||||
"Autosave must be enabled or disabled");
|
||||
value["editor"]["autosave"] = field.at("autosave");
|
||||
continue;
|
||||
} else {
|
||||
require(field.is_string() && !field.get<std::string>().empty(),
|
||||
"project.start_scene", "Choose a saved scene inside the project");
|
||||
const auto file = project_path(config_.project_root,
|
||||
@@ -388,7 +423,10 @@ void Session::register_commands() {
|
||||
if (!value.contains("id"))
|
||||
value["id"] = new_id();
|
||||
atomic_write_json(config_.project_root / "project.faset.json", value);
|
||||
log("Project settings saved; changes apply on next project open");
|
||||
autosave_enabled_ = value.at("editor").at("autosave").get<bool>();
|
||||
autosave_.observe(authoring_.documents(), AutosaveController::Clock::now(),
|
||||
autosave_enabled_);
|
||||
log("Project settings saved; autosave preference applies immediately");
|
||||
return Json{{"settings", value}, {"revision", sha256(value.dump())}};
|
||||
});
|
||||
commands_.add(
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
int test_main(int argc, char** argv) {
|
||||
using namespace faset;
|
||||
if (argc >= 3 && std::string_view(argv[1]) == "--editor-probe") {
|
||||
Json arguments = Json::array();
|
||||
@@ -36,6 +36,8 @@ int main(int argc, char** argv) {
|
||||
require(!session.play_pending() && !session.playing(), "test",
|
||||
"Stop must cancel a pending Play build");
|
||||
const auto initial = commands.call("faset_project_settings_get", Json::object());
|
||||
require(initial.at("settings").at("editor").at("autosave") == true, "test",
|
||||
"Missing autosave setting must read as enabled");
|
||||
auto changed = commands.call("faset_project_settings_set",
|
||||
{{"revision", initial.at("revision")},
|
||||
{"settings",
|
||||
@@ -75,15 +77,57 @@ int main(int argc, char** argv) {
|
||||
atomic_write_json(root / "project.faset.json", external);
|
||||
rejects({{"revision", changed.at("revision")}, {"settings", {{"name", "Race"}}}},
|
||||
"revision.conflict");
|
||||
const auto reloaded = commands.call("faset_project_settings_get", Json::object());
|
||||
auto reloaded = commands.call("faset_project_settings_get", Json::object());
|
||||
auto explicit_default = external;
|
||||
explicit_default["editor"]["autosave"] = true;
|
||||
atomic_write_json(root / "project.faset.json", explicit_default);
|
||||
rejects({{"revision", reloaded.at("revision")},
|
||||
{"settings", {{"name", "Lost external write"}}}},
|
||||
"revision.conflict");
|
||||
reloaded = commands.call("faset_project_settings_get", Json::object());
|
||||
changed =
|
||||
commands.call("faset_project_settings_set", {{"revision", reloaded.at("revision")},
|
||||
{"settings", {{"name", "Preserved"}}}});
|
||||
require(changed.at("settings").at("custom_tool").at("keep") == true, "test",
|
||||
"Saving settings erased unknown project metadata");
|
||||
require(changed.at("settings").at("scripting") == external.at("scripting") &&
|
||||
changed.at("settings").at("editor") == external.at("editor"),
|
||||
changed.at("settings").at("editor").at("script_editor") ==
|
||||
external.at("editor").at("script_editor"),
|
||||
"test", "Project settings erased Lua configuration or external editor command");
|
||||
const auto autosave_off = commands.call(
|
||||
"faset_project_settings_set",
|
||||
{{"revision", changed.at("revision")},
|
||||
{"settings", {{"editor", {{"autosave", false}}}}}});
|
||||
require(autosave_off.at("settings").at("editor").at("autosave") == false &&
|
||||
session.project().at("editor").at("script_editor") ==
|
||||
external.at("editor").at("script_editor"),
|
||||
"test", "Autosave setting must merge without erasing the editor command");
|
||||
session.poll();
|
||||
require(commands.call("faset_autosave_status", Json::object()).at("enabled") == false,
|
||||
"test", "Autosave disable must take effect in the current session");
|
||||
{
|
||||
editor::Session reopened({root, path_from_utf8(FASET_TEST_ENGINE), {}});
|
||||
reopened.poll();
|
||||
require(reopened.commands()
|
||||
.call("faset_autosave_status", Json::object())
|
||||
.at("enabled") == false,
|
||||
"test", "Autosave setting must survive Editor reopen");
|
||||
}
|
||||
rejects({{"revision", changed.at("revision")},
|
||||
{"settings", {{"editor", {{"autosave", true}}}}}},
|
||||
"revision.conflict");
|
||||
rejects({{"revision", autosave_off.at("revision")},
|
||||
{"settings", {{"editor", {{"autosave", "sometimes"}}}}}},
|
||||
"project.autosave");
|
||||
rejects({{"revision", autosave_off.at("revision")},
|
||||
{"settings", {{"editor", {{"script_editor", {"other"}}}}}}},
|
||||
"project.setting");
|
||||
changed = commands.call("faset_project_settings_set",
|
||||
{{"revision", autosave_off.at("revision")},
|
||||
{"settings", {{"editor", {{"autosave", true}}}}}});
|
||||
session.poll();
|
||||
require(commands.call("faset_autosave_status", Json::object()).at("enabled") == true,
|
||||
"test", "Autosave enable must take effect in the current session");
|
||||
const auto setup = commands.call("faset_lua_setup", Json::object());
|
||||
require(setup.at("configuration_created") == true &&
|
||||
setup.at("scripts_configuration_created") == true &&
|
||||
@@ -193,3 +237,12 @@ int main(int argc, char** argv) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
#ifdef _WIN32
|
||||
int wmain(int argc, wchar_t** argv) {
|
||||
return run_utf8_main(argc, argv, test_main);
|
||||
}
|
||||
#else
|
||||
int main(int argc, char** argv) {
|
||||
return test_main(argc, argv);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#include <faset/core/io.hpp>
|
||||
#include <faset/editor/editor_ui.hpp>
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
using namespace faset;
|
||||
namespace {
|
||||
void check(bool value, const std::string& message) {
|
||||
@@ -50,6 +52,7 @@ int main() {
|
||||
{"id", new_id()},
|
||||
{"name", "Original project"},
|
||||
{"dimension", 3},
|
||||
{"editor", {{"script_editor", {"zed", "{file}"}}}},
|
||||
{"custom_metadata", {{"preserve", true}}}});
|
||||
editor::Session session({root, path_from_utf8(FASET_TEST_ENGINE), root});
|
||||
render::Renderer renderer({1280, 800, "Project settings acceptance", true, true});
|
||||
@@ -73,6 +76,13 @@ int main() {
|
||||
open(ui);
|
||||
check(ui.widgets().find("project-settings-name")->text == "Original project",
|
||||
"Opening settings reloads current saved values");
|
||||
check(ui.widgets().find("project-settings-autosave")->checked,
|
||||
"Missing autosave setting appears enabled in Project settings");
|
||||
check(ui.widgets().focus("project-settings-autosave"),
|
||||
"Keyboard focus reaches the Autosave toggle");
|
||||
ui.frame({key("Space")});
|
||||
check(!ui.widgets().find("project-settings-autosave")->checked,
|
||||
"Space toggles Autosave from the keyboard");
|
||||
text(ui, "project-settings-name", "Новый проект");
|
||||
click(ui, "project-settings-2d");
|
||||
click(ui, "project-scene-choice-1");
|
||||
@@ -81,11 +91,25 @@ int main() {
|
||||
click(ui, "project-settings-save");
|
||||
auto saved = read_json(project_file);
|
||||
check(saved["name"] == "Новый проект" && saved["dimension"] == 2 &&
|
||||
saved["start_scene"] == "Scenes/Другая.scene.json",
|
||||
saved["start_scene"] == "Scenes/Другая.scene.json" &&
|
||||
saved["editor"]["autosave"] == false,
|
||||
"Explicit Save project persists typed settings");
|
||||
check(saved["custom_metadata"] == project_before["custom_metadata"] &&
|
||||
saved["id"] == project_before["id"],
|
||||
saved["id"] == project_before["id"] &&
|
||||
saved["editor"]["script_editor"] ==
|
||||
project_before["editor"]["script_editor"],
|
||||
"Project settings preserve unknown metadata and project identity");
|
||||
ui.frame({});
|
||||
check(ui.widgets().find("autosave-status")->text == "Autosave off",
|
||||
"Disabling Autosave is visible in the Editor status bar");
|
||||
open(ui);
|
||||
check(!ui.widgets().find("project-settings-autosave")->checked,
|
||||
"Autosave preference is restored when reopening Project settings");
|
||||
click(ui, "project-settings-autosave");
|
||||
click(ui, "project-settings-save");
|
||||
saved = read_json(project_file);
|
||||
check(saved["editor"]["autosave"] == true,
|
||||
"Autosave can be enabled again through Project settings");
|
||||
check(session.authoring().query(ui.current_document()) == document_before,
|
||||
"Project settings do not change current document, dimension, revision or Undo");
|
||||
open(ui);
|
||||
@@ -118,6 +142,51 @@ int main() {
|
||||
"Save after explicit reload uses new content revision");
|
||||
check(session.authoring().query(ui.current_document()) == document_before,
|
||||
"Project settings remain outside scene Undo throughout conflicts");
|
||||
const auto original_document = ui.current_document();
|
||||
const auto revision =
|
||||
session.authoring().query(original_document).at("revision").get<std::uint64_t>();
|
||||
session.authoring().transact(
|
||||
original_document, revision,
|
||||
Json::array({{{"op", "scene.rename"}, {"name", "Pending autosave"}}}));
|
||||
ui.frame({});
|
||||
check(ui.widgets().find("autosave-status")->text == "Pending autosave",
|
||||
"Status bar shows pending scene save after edit");
|
||||
session.authoring().save(original_document);
|
||||
ui.frame({});
|
||||
check(ui.widgets().find("autosave-status")->text == "Saved",
|
||||
"Status bar returns to Saved after the scene reaches disk");
|
||||
session.authoring().transact(
|
||||
original_document,
|
||||
session.authoring().query(original_document).at("revision"),
|
||||
Json::array({{{"op", "scene.rename"}, {"name", "Conflicting autosave"}}}));
|
||||
ui.frame({});
|
||||
auto disk_scene = read_json(root / "Scenes/Главная.scene.json");
|
||||
disk_scene["name"] = "External scene edit";
|
||||
atomic_write_json(root / "Scenes/Главная.scene.json", disk_scene);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(2100));
|
||||
ui.frame({});
|
||||
check(ui.widgets().find("autosave-status")->text == "Save conflict" &&
|
||||
ui.widgets().find("autosave-status")->tooltip.find("Save As") !=
|
||||
std::string::npos,
|
||||
"Disk conflict remains visible with Save As guidance");
|
||||
renderer.render(ui.snapshot());
|
||||
renderer.capture(root / "autosave-conflict.ppm");
|
||||
check(ui.widgets().focus("autosave-save-as"),
|
||||
"Keyboard focus reaches the recovery Save As action");
|
||||
ui.frame({key("Return")});
|
||||
check(ui.widgets().find("menu-popup")->visible &&
|
||||
ui.widgets().find("save-path")->visible,
|
||||
"Recovery action opens an editable Save As path");
|
||||
ui.frame({key("Escape")});
|
||||
const auto unnamed = session.authoring().create("Unsaved", 2);
|
||||
session.authoring().transact(
|
||||
unnamed.at("id"), unnamed.at("revision"),
|
||||
Json::array({{{"op", "scene.rename"}, {"name", "Needs a path"}}}));
|
||||
ui.select_document(unnamed.at("id"));
|
||||
ui.frame({});
|
||||
check(ui.widgets().find("autosave-status")->text == "Save As required" &&
|
||||
ui.widgets().find("autosave-save-as")->visible,
|
||||
"Unnamed dirty scene is never assigned an implicit path");
|
||||
open(ui);
|
||||
renderer.render(ui.snapshot());
|
||||
renderer.capture(root / "project-settings.ppm");
|
||||
|
||||
Reference in New Issue
Block a user