yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
project_file_editor.cc
Go to the documentation of this file.
2#include "util/i18n/tr.h"
3
4#include <filesystem>
5#include <fstream>
6#include <sstream>
7
8#include "absl/strings/match.h"
9#include "absl/strings/str_format.h"
10#include "absl/strings/str_split.h"
12#include "app/gui/core/icons.h"
14#include "core/project.h"
15#include "imgui/imgui.h"
16#include "util/file_util.h"
17#include "util/macro.h"
18#include "yaze_config.h"
19
20#ifdef __EMSCRIPTEN__
22#endif
23namespace yaze {
24namespace editor {
25
26namespace {
27
28#ifndef __EMSCRIPTEN__
29bool PathsReferToSameProjectFile(const std::string& left,
30 const std::string& right) {
31 if (left.empty() || right.empty()) {
32 return false;
33 }
34
35 std::error_code equivalent_error;
36 if (std::filesystem::equivalent(left, right, equivalent_error)) {
37 return true;
38 }
39
40 std::error_code left_error;
41 std::error_code right_error;
42 const auto left_path =
43 std::filesystem::absolute(left, left_error).lexically_normal();
44 const auto right_path =
45 std::filesystem::absolute(right, right_error).lexically_normal();
46 return !left_error && !right_error && left_path == right_path;
47}
48#endif
49
50#ifdef __EMSCRIPTEN__
51std::string ProjectStorageKey(const project::YazeProject* project,
52 const std::string& filepath) {
53 if (project != nullptr && project->filepath == filepath) {
54 return project->MakeStorageKey("project");
55 }
56 std::string key = std::filesystem::path(filepath).stem().string();
57 return key.empty() ? "project" : key;
58}
59#endif
60
61} // namespace
62
68
70 if (!active_)
71 return;
72
73 ImGui::SetNextWindowSize(ImVec2(900, 700), ImGuiCond_FirstUseEver);
74 if (!ImGui::Begin(absl::StrFormat("%s Project Editor###ProjectFileEditor",
76 .c_str(),
77 &active_)) {
78 ImGui::End();
79 return;
80 }
81
82 // Toolbar
83 if (ImGui::BeginTable("ProjectEditorToolbar", 10,
84 ImGuiTableFlags_SizingFixedFit)) {
85 ImGui::TableNextColumn();
86 if (ImGui::Button(absl::StrFormat("%s New", ICON_MD_NOTE_ADD).c_str())) {
87 auto status = NewFile();
88 if (!status.ok() && toast_manager_) {
89 toast_manager_->Show(std::string(status.message()), ToastType::kError);
90 }
91 }
92
93 ImGui::TableNextColumn();
94 if (ImGui::Button(
95 absl::StrFormat("%s Open", ICON_MD_FOLDER_OPEN).c_str())) {
97 if (!file.empty()) {
98 auto status = LoadFile(file);
99 if (!status.ok() && toast_manager_) {
101 std::string(status.message().data(), status.message().size()),
103 }
104 }
105 }
106
107 ImGui::TableNextColumn();
108 bool can_save = !filepath_.empty() && IsModified();
109 if (!can_save)
110 ImGui::BeginDisabled();
111 if (ImGui::Button(absl::StrFormat("%s Save", ICON_MD_SAVE).c_str())) {
112 auto status = SaveFile();
113 if (status.ok() && toast_manager_) {
114 toast_manager_->Show("Project file saved", ToastType::kSuccess);
115 } else if (!status.ok() && toast_manager_) {
117 std::string(status.message().data(), status.message().size()),
119 }
120 }
121 if (!can_save)
122 ImGui::EndDisabled();
123
124 ImGui::TableNextColumn();
125 if (ImGui::Button(absl::StrFormat("%s Save As", ICON_MD_SAVE_AS).c_str())) {
127 filepath_.empty() ? "project" : filepath_, "yaze");
128 if (!file.empty()) {
129 auto status = SaveFileAs(file);
130 if (status.ok() && toast_manager_) {
131 toast_manager_->Show("Project file saved", ToastType::kSuccess);
132 } else if (!status.ok() && toast_manager_) {
134 std::string(status.message().data(), status.message().size()),
136 }
137 }
138 }
139
140 ImGui::TableNextColumn();
141 ImGui::Text("|");
142
143 ImGui::TableNextColumn();
144 // Import ZScream Labels button
145 if (ImGui::Button(
146 absl::StrFormat("%s Import Labels", ICON_MD_LABEL).c_str())) {
147 auto status = ImportLabelsFromZScream();
148 if (status.ok() && toast_manager_) {
149 toast_manager_->Show("Labels imported successfully",
151 } else if (!status.ok() && toast_manager_) {
153 std::string(status.message().data(), status.message().size()),
155 }
156 }
157 if (ImGui::IsItemHovered()) {
158 ImGui::SetTooltip(tr("Import labels from ZScream DefaultNames.txt"));
159 }
160
161 ImGui::TableNextColumn();
162 if (ImGui::Button(
163 absl::StrFormat("%s Validate", ICON_MD_CHECK_CIRCLE).c_str())) {
165 show_validation_ = true;
166 }
167
168 ImGui::TableNextColumn();
169 ImGui::Checkbox(tr("Show Validation"), &show_validation_);
170
171 ImGui::TableNextColumn();
172 if (!filepath_.empty()) {
173 ImGui::TextDisabled("%s", filepath_.c_str());
174 } else {
175 ImGui::TextDisabled(tr("No file loaded"));
176 }
177
178 ImGui::EndTable();
179 }
180
181 ImGui::Separator();
182
183 // Validation errors panel
184 if (show_validation_ && !validation_errors_.empty()) {
185 gui::StyledChild errors_child("ValidationErrors", ImVec2(0, 100),
186 {.bg = ImVec4(0.3f, 0.2f, 0.2f, 0.5f)}, true);
187 if (errors_child) {
188 ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f),
189 tr("%s Validation Errors:"), ICON_MD_ERROR);
190 for (const auto& error : validation_errors_) {
191 ImGui::BulletText("%s", error.c_str());
192 }
193 }
194 }
195
196 // Main editor
197 ImVec2 editor_size = ImGui::GetContentRegionAvail();
198 text_editor_.Render("##ProjectEditor", editor_size);
200 modified_ = true;
201 }
202
203 ImGui::End();
204}
205
217
219 project::YazeProject* project) {
220 project_ = project;
221 filepath_ = state.filepath;
224 active_ = state.active;
225 modified_ = state.modified;
228}
229
231 project_ = project;
232 filepath_.clear();
234 initialized_ = false;
235 active_ = false;
236 modified_ = false;
237 show_validation_ = true;
238 validation_errors_.clear();
239}
240
241absl::Status ProjectFileEditor::LoadFile(const std::string& filepath) {
243#ifdef __EMSCRIPTEN__
244 const std::string key = ProjectStorageKey(project_, filepath);
245 auto storage_or = platform::WasmStorage::LoadProject(key);
246 if (storage_or.ok()) {
247 text_editor_.SetText(storage_or.value());
249 initialized_ = true;
250 modified_ = false;
252 return absl::OkStatus();
253 }
254#endif
255
256 std::ifstream file(filepath);
257 if (!file.is_open()) {
258 return absl::InvalidArgumentError(
259 absl::StrFormat("Cannot open file: %s", filepath));
260 }
261
262 std::stringstream buffer;
263 buffer << file.rdbuf();
264 file.close();
265
266 text_editor_.SetText(buffer.str());
268 initialized_ = true;
269 modified_ = false;
270
272
273 return absl::OkStatus();
274}
275
277 if (filepath_.empty()) {
278 return absl::InvalidArgumentError("No file path specified");
279 }
280
281 return SaveFileAs(filepath_);
282}
283
284absl::Status ProjectFileEditor::SaveFileAs(const std::string& filepath) {
285 // Ensure .yaze extension
286 std::string final_path = filepath;
287 if (!absl::EndsWith(final_path, ".yaze")) {
288 final_path += ".yaze";
289 }
290 const std::string contents = GetDocumentText();
291
293 RETURN_IF_ERROR(save_guard_callback_(final_path, contents));
294 }
295
296#ifdef __EMSCRIPTEN__
297 const std::string key = ProjectStorageKey(project_, final_path);
298 const bool replace_existing =
299 !filepath_.empty() && ProjectStorageKey(project_, filepath_) == key;
300 auto storage_status =
301 platform::WasmStorage::SaveProject(key, contents, replace_existing);
302 if (!storage_status.ok()) {
303 return storage_status;
304 }
306 RETURN_IF_ERROR(save_complete_callback_(final_path, contents));
307 }
308 filepath_ = final_path;
309 initialized_ = true;
310 modified_ = false;
311 auto& recent_mgr = project::RecentFilesManager::GetInstance();
312 recent_mgr.AddFile(filepath_);
313 recent_mgr.Save();
314 return absl::OkStatus();
315#else
316 const bool replace_existing =
317 PathsReferToSameProjectFile(filepath_, final_path);
319 replace_existing));
320
322 RETURN_IF_ERROR(save_complete_callback_(final_path, contents));
323 }
324
325 filepath_ = final_path;
326 initialized_ = true;
327 modified_ = false;
328
329 // Add to recent files
330 auto& recent_mgr = project::RecentFilesManager::GetInstance();
331 recent_mgr.AddFile(filepath_);
332 recent_mgr.Save();
333
334 return absl::OkStatus();
335#endif
336}
337
339 if (initialized_ && modified_) {
340 return absl::FailedPreconditionError(
341 "Project-file draft has unsaved changes; use Save or Save As first");
342 }
343 return absl::OkStatus();
344}
345
348 // Create a template project file
349 const std::string template_content = absl::StrFormat(
350 R"(# yaze Project File
351# Format Version: 2.0
352
353[project]
354name=New Project
355project_id=
356description=
357author=
358license=
359version=1.0
360created_date=
361last_modified=
362yaze_version=%s
363created_by=YAZE
364tags=
365
366[agent_settings]
367ai_provider=auto
368ai_model=
369ollama_host=http://localhost:11434
370use_custom_prompt=false
371
372[files]
373rom_filename=
374rom_backup_folder=backups
375code_folder=asm
376assets_folder=assets
377patches_folder=patches
378labels_filename=labels.txt
379symbols_filename=symbols.txt
380output_folder=build
381custom_objects_folder=
382# Optional: ASM integration (e.g. Oracle-of-Secrets hack_manifest.json)
383hack_manifest_file=
384additional_roms=
385
386[rom]
387role=dev
388expected_hash=
389write_policy=warn
390
391[feature_flags]
392# REMOVED: kLogInstructions - DisassemblyViewer is always active
393kSaveOverworldMaps=true
394kSaveOverworldEntrances=true
395kSaveOverworldExits=true
396kSaveOverworldItems=true
397kSaveOverworldProperties=true
398kSaveDungeonMaps=true
399kSaveDungeonObjects=true
400kSaveDungeonSprites=true
401kSaveDungeonRoomHeaders=true
402kSaveDungeonTorches=true
403kSaveDungeonPits=true
404kSaveDungeonBlocks=true
405kSaveDungeonCollision=true
406kSaveDungeonChests=true
407kSaveDungeonPotItems=true
408kSaveDungeonPalettes=true
409kSaveGraphicsSheet=true
410kSaveAllPalettes=true
411kSaveGfxGroups=true
412kSaveMessages=true
413kLoadCustomOverworld=false
414
415[workspace]
416font_global_scale=1.0
417autosave_enabled=true
418autosave_interval_secs=300
419backup_on_save=true
420backup_retention_count=20
421backup_keep_daily=true
422backup_keep_daily_days=14
423theme=dark
424
425[rom_addresses]
426# expanded_message_start=0x178000
427# expanded_message_end=0x17FFFF
428# expanded_music_hook=0x008919
429# expanded_music_main=0x1A9EF5
430# expanded_music_aux=0x1ACCA7
431# overworld_messages_expanded=0x1417F8
432# overworld_map16_expanded=0x1E8000
433# overworld_map32_tr_expanded=0x020000
434# overworld_map32_bl_expanded=0x1F0000
435# overworld_map32_br_expanded=0x1F8000
436# overworld_entrance_map_expanded=0x0DB55F
437# overworld_entrance_pos_expanded=0x0DB35F
438# overworld_entrance_id_expanded=0x0DB75F
439# overworld_entrance_flag_expanded=0x0DB895
440# overworld_ptr_marker_expanded=0x1423FF
441# overworld_ptr_magic_expanded=0xEA
442# overworld_gfx_ptr1=0x004F80
443# overworld_gfx_ptr2=0x00505F
444# overworld_gfx_ptr3=0x00513E
445
446[custom_objects]
447# object_0x31=track_LR.bin,track_UD.bin,track_corner_TL.bin
448# object_0x32=furnace.bin,firewood.bin,ice_chair.bin
449
450[build]
451build_script=
452output_folder=build
453build_target=
454asm_entry_point=asm/main.asm
455asm_sources=asm
456build_number=0
457last_build_hash=
458
459[music]
460persist_custom_music=true
461storage_key=
462last_saved_at=
463)",
465
466 text_editor_.SetText(template_content);
467 filepath_.clear();
468 initialized_ = true;
469 modified_ = true;
470 validation_errors_.clear();
471 return absl::OkStatus();
472}
473
475 // TODO: Implement custom syntax highlighting for INI format
476 // For now, use C language definition which provides some basic highlighting
477}
478
480 validation_errors_.clear();
481
482 std::string content = GetDocumentText();
483 std::vector<std::string> lines = absl::StrSplit(content, '\n');
484
485 std::string current_section;
486 int line_num = 0;
487
488 for (const auto& line : lines) {
489 line_num++;
490 std::string trimmed = std::string(absl::StripAsciiWhitespace(line));
491
492 // Skip empty lines and comments
493 if (trimmed.empty() || trimmed[0] == '#')
494 continue;
495
496 // Check for section headers
497 if (trimmed[0] == '[' && trimmed[trimmed.size() - 1] == ']') {
498 current_section = trimmed.substr(1, trimmed.size() - 2);
499
500 // Validate known sections
501 if (current_section != "project" && current_section != "files" &&
502 current_section != "feature_flags" &&
503 current_section != "workspace" &&
504 current_section != "workspace_settings" && current_section != "rom" &&
505 current_section != "rom_addresses" &&
506 current_section != "custom_objects" &&
507 current_section != "dungeon_overlay" && current_section != "build" &&
508 current_section != "agent_settings" && current_section != "music" &&
509 current_section != "keybindings" &&
510 current_section != "editor_visibility") {
511 validation_errors_.push_back(absl::StrFormat(
512 "Line %d: Unknown section [%s]", line_num, current_section));
513 }
514 continue;
515 }
516
517 // Check for key=value pairs
518 size_t equals_pos = trimmed.find('=');
519 if (equals_pos == std::string::npos) {
520 validation_errors_.push_back(absl::StrFormat(
521 "Line %d: Invalid format, expected key=value", line_num));
522 continue;
523 }
524 }
525
527 toast_manager_->Show("Project file validation passed", ToastType::kSuccess);
528 }
529}
530
531std::string ProjectFileEditor::GetDocumentText() const {
532 std::string text = text_editor_.GetText();
533 // TextEditor emits one synthetic trailing newline for its final line.
534 // Exclude that sentinel so capture/restore and load/save are byte-stable.
535 if (!text.empty() && text.back() == '\n') {
536 text.pop_back();
537 }
538 return text;
539}
540
542 if (validation_errors_.empty())
543 return;
544
545 ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), tr("Validation Errors:"));
546 for (const auto& error : validation_errors_) {
547 ImGui::BulletText("%s", error.c_str());
548 }
549}
550
552#ifdef __EMSCRIPTEN__
553 return absl::UnimplementedError(
554 "File-based label import is not supported in the web build");
555#else
556 if (!project_) {
557 return absl::FailedPreconditionError(
558 "No project loaded. Open a project first.");
559 }
560
561 // Show file dialog for DefaultNames.txt
563 if (file.empty()) {
564 return absl::CancelledError("No file selected");
565 }
566
567 // Read the file contents
568 std::ifstream input_file(file);
569 if (!input_file.is_open()) {
570 return absl::InvalidArgumentError(
571 absl::StrFormat("Cannot open file: %s", file));
572 }
573
574 std::stringstream buffer;
575 buffer << input_file.rdbuf();
576 input_file.close();
577
578 // Import using the project's method
579 auto status = project_->ImportLabelsFromZScreamContent(buffer.str());
580 if (!status.ok()) {
581 return status;
582 }
583
584 // Save the project to persist the imported labels
585 return project_->Save();
586#endif
587}
588
589} // namespace editor
590} // namespace yaze
void SetShowWhitespaces(bool aValue)
bool IsTextChanged() const
std::string GetText() const
void Render(const char *aTitle, const ImVec2 &aSize=ImVec2(), bool aBorder=false)
void SetText(const std::string &aText)
void SetTabSize(int aValue)
void SetLanguageDefinition(const LanguageDefinition &aLanguageDef)
absl::Status SaveFileAs(const std::string &filepath)
Save to a new file path.
absl::Status LoadFile(const std::string &filepath)
Load a project file into the editor.
ProjectFileEditorState CaptureState() const
bool IsModified() const
Get whether the file has unsaved changes.
void RestoreState(const ProjectFileEditorState &state, project::YazeProject *project)
std::vector< std::string > validation_errors_
absl::Status SaveFile()
Save the current editor contents to disk.
absl::Status ImportLabelsFromZScream()
Import labels from a ZScream DefaultNames.txt file.
SaveCompleteCallback save_complete_callback_
absl::Status NewFile()
Create a new empty project file.
void ResetForProject(project::YazeProject *project)
const std::string & filepath() const
Get the current filepath.
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f)
RAII guard for ImGui child windows with optional styling.
static RecentFilesManager & GetInstance()
Definition project.h:441
static std::string ShowSaveFileDialog(const std::string &default_name="", const std::string &default_extension="")
ShowSaveFileDialog opens a save file dialog and returns the selected filepath. Uses global feature fl...
static std::string ShowOpenFileDialog()
ShowOpenFileDialog opens a file dialog and returns the selected filepath. Uses global feature flag to...
#define YAZE_VERSION_STRING
#define ICON_MD_FOLDER_OPEN
Definition icons.h:813
#define ICON_MD_NOTE_ADD
Definition icons.h:1330
#define ICON_MD_SAVE_AS
Definition icons.h:1646
#define ICON_MD_LABEL
Definition icons.h:1053
#define ICON_MD_ERROR
Definition icons.h:686
#define ICON_MD_CHECK_CIRCLE
Definition icons.h:400
#define ICON_MD_DESCRIPTION
Definition icons.h:539
#define ICON_MD_SAVE
Definition icons.h:1644
bool PathsReferToSameProjectFile(const std::string &left, const std::string &right)
const char * tr(const char *source)
Definition translator.cc:50
absl::Status WriteProjectFileAtomically(absl::string_view target_path, absl::string_view contents, bool replace_existing)
Definition project.cc:289
#define RETURN_IF_ERROR(expr)
Definition snes.cc:22
static const LanguageDefinition & C()
std::vector< std::string > validation_errors
Modern project structure with comprehensive settings consolidation.
Definition project.h:172
std::string MakeStorageKey(absl::string_view suffix) const
Definition project.cc:638
absl::Status ImportLabelsFromZScreamContent(const std::string &content)
Import labels from ZScream format content directly.
Definition project.cc:2474