yaze 0.3.2
Link to the Past ROM Editor
 
Loading...
Searching...
No Matches
planning_editor.cc
Go to the documentation of this file.
2
3#include <algorithm>
4#include <filesystem>
5#include <memory>
6#include <utility>
7
8#include "absl/strings/str_format.h"
9#include "absl/strings/str_join.h"
10#include "absl/strings/str_split.h"
11#include "absl/strings/strip.h"
12#include "absl/time/clock.h"
13#include "absl/time/time.h"
19#include "app/gui/core/icons.h"
21#include "core/project.h"
23#include "imgui/imgui.h"
24#include "imgui/misc/cpp/imgui_stdlib.h"
25
26namespace yaze::editor {
27namespace {
28
29constexpr const char* kCategory = "Planning";
30constexpr const char* kMilestoneStatuses[] = {"planned", "active", "done"};
31constexpr const char* kItemStatuses[] = {"implemented", "prototype", "decided",
32 "proposed", "open"};
33constexpr const char* kDecisionStatuses[] = {"open", "decided", "dropped"};
34constexpr const char* kStepKinds[] = {"story", "errand", "dungeon",
35 "shrine", "boss", "optional"};
36
38 public:
39 PlanningPanel(std::string id, std::string name, const char* icon,
40 int priority, std::function<void()> draw)
41 : id_(std::move(id)),
42 name_(std::move(name)),
43 icon_(icon),
44 priority_(priority),
45 draw_(std::move(draw)) {}
46
47 std::string GetId() const override { return id_; }
48 std::string GetDisplayName() const override { return name_; }
49 std::string GetIcon() const override { return icon_; }
50 std::string GetEditorCategory() const override { return kCategory; }
52 return WindowPresentationPolicy::CoreWorkspace();
53 }
54 int GetPriority() const override { return priority_; }
55 bool IsVisibleByDefault() const override { return true; }
56 float GetPreferredWidth() const override { return 640.0f; }
57 void Draw(bool* /*p_open*/) override {
58 if (draw_) {
59 draw_();
60 }
61 }
62
63 private:
64 std::string id_;
65 std::string name_;
66 std::string icon_;
68 std::function<void()> draw_;
69};
70
71template <size_t N>
72bool StatusCombo(const char* label, std::string* value,
73 const char* const (&options)[N]) {
74 bool changed = false;
75 if (ImGui::BeginCombo(label, value->empty() ? "-" : value->c_str())) {
76 for (const char* option : options) {
77 const bool selected = *value == option;
78 if (ImGui::Selectable(option, selected)) {
79 *value = option;
80 changed = true;
81 }
82 }
83 ImGui::EndCombo();
84 }
85 return changed;
86}
87
88ImVec4 StatusColor(const std::string& status) {
89 if (status == "implemented" || status == "done" || status == "decided") {
90 return gui::GetSuccessColor();
91 }
92 if (status == "prototype" || status == "active") {
93 return gui::GetInfoColor();
94 }
95 if (status == "open") {
96 return gui::GetWarningColor();
97 }
98 return gui::GetDisabledColor();
99}
100
101std::string Today() {
102 return absl::FormatTime("%Y-%m-%d", absl::Now(), absl::LocalTimeZone());
103}
104
105std::string DaysUntil(const std::string& date) {
106 absl::Time due;
107 std::string error;
108 if (date.empty() ||
109 !absl::ParseTime("%Y-%m-%d", date, absl::LocalTimeZone(), &due, &error)) {
110 return "";
111 }
112 const int64_t days = absl::ToInt64Hours(due - absl::Now()) / 24;
113 if (days < 0) {
114 return absl::StrFormat("%d days past", -days);
115 }
116 return absl::StrFormat("%d days left", days);
117}
118
119std::string JoinTargets(const std::vector<std::string>& targets) {
120 return absl::StrJoin(targets, ", ");
121}
122
123std::vector<std::string> SplitTargets(const std::string& text) {
124 std::vector<std::string> out;
125 for (absl::string_view part :
126 absl::StrSplit(text, ',', absl::SkipWhitespace())) {
127 out.emplace_back(absl::StripAsciiWhitespace(part));
128 }
129 return out;
130}
131
132std::string StepLabel(const core::PlanRouteStep& step) {
133 return step.code.empty() ? step.title : step.code;
134}
135
136std::string JsonString(const core::PlanJson& obj, const char* key,
137 const char* fallback) {
138 auto it = obj.find(key);
139 return it != obj.end() && it->is_string() ? it->get<std::string>()
140 : std::string(fallback);
141}
142
143} // namespace
144
147 return;
148 }
149 auto* window_manager = dependencies_.window_manager;
150 window_manager->RegisterWindowContent(std::make_unique<PlanningPanel>(
151 "planning.overview", "Plan Overview", ICON_MD_DASHBOARD, 10,
152 [this]() { DrawOverview(); }));
153 window_manager->RegisterWindowContent(std::make_unique<PlanningPanel>(
154 "planning.route", "Route", ICON_MD_ROUTE, 20, [this]() { DrawRoute(); }));
155 panels_registered_ = true;
156}
157
159 return SyncWithProject();
160}
161
162absl::Status PlanningEditor::Save() {
163 if (!plan_ || !dirty_ || plan_path_.empty()) {
164 return absl::OkStatus();
165 }
166 if (!project() || project()->filepath != project_path_ ||
167 project()->PlanningFilePath() != plan_path_) {
168 return absl::FailedPreconditionError(
169 "Restore the plan's project before saving its edits");
170 }
171 std::string expected_source = loaded_source_;
172 std::string disk_source;
173 if (core::ProjectPlan::LoadFromFile(plan_path_, &disk_source).ok() &&
174 disk_source != loaded_source_) {
176 disk_source)) {
178 "The plan file changed outside agent status since it was loaded. "
179 "Your edits are kept; copy them, then Reload to see the file.";
180 return absl::AbortedError(save_error_);
181 }
182 expected_source = std::move(disk_source);
183 }
184 absl::Status status = plan_->SaveToFile(plan_path_, expected_source);
185 if (!status.ok()) {
186 save_error_ = std::string(status.message());
187 return status;
188 }
189 save_error_.clear();
190 loaded_source_ = plan_->Serialize();
191 dirty_ = false;
192 return absl::OkStatus();
193}
194
196 const std::string path = project() ? project()->PlanningFilePath() : "";
197 const std::string project_path = project() ? project()->filepath : "";
198 if (path == plan_path_ && project_path == project_path_) {
199 return absl::OkStatus();
200 }
201 if (dirty_) {
202 save_error_ = "Save or discard plan edits before replacing their project";
203 return absl::FailedPreconditionError(save_error_);
204 }
205 plan_path_ = path;
206 project_path_ = project_path;
207 loaded_source_.clear();
208 plan_.reset();
209 load_error_.clear();
210 dirty_ = false;
211 selected_step_ = -1;
212 selected_item_ = -1;
213 if (!plan_path_.empty()) {
214 return Reload();
215 }
216 return absl::OkStatus();
217}
218
219absl::Status PlanningEditor::Reload(bool discard_changes) {
220 if (dirty_ && !discard_changes) {
221 save_error_ = "Save or explicitly discard plan edits before reloading";
222 return absl::FailedPreconditionError(save_error_);
223 }
224 std::string source;
225 auto loaded = core::ProjectPlan::LoadFromFile(plan_path_, &source);
226 if (!loaded.ok()) {
227 load_error_ = std::string(loaded.status().message());
228 return loaded.status();
229 }
230 plan_ = std::move(*loaded);
231 loaded_source_ = std::move(source);
232 load_error_.clear();
233 save_error_.clear();
234 dirty_ = false;
235 if (selected_step_ >= static_cast<int>(plan_->route_steps.size())) {
236 selected_step_ = -1;
237 }
238 if (selected_item_ >= static_cast<int>(plan_->route_items.size())) {
239 selected_item_ = -1;
240 }
241 return absl::OkStatus();
242}
243
245 dirty_ = false;
246 plan_.reset();
247 plan_path_.clear();
248 project_path_.clear();
249 loaded_source_.clear();
250 save_error_.clear();
251 (void)SyncWithProject();
252}
253
255 if (!project() || project()->filepath.empty()) {
256 ImGui::TextWrapped(
257 "Open a .yaze project to plan it. The plan is a JSON file named by "
258 "the project's [planning] section.");
259 return;
260 }
261 if (!load_error_.empty()) {
263 if (ImGui::Button(ICON_MD_REFRESH " Retry")) {
264 (void)Reload();
265 }
266 ImGui::Separator();
267 }
268 ImGui::TextWrapped(
269 load_error_.empty()
270 ? "This project has no plan file. Create one, or set [planning] "
271 "file= in the .yaze file."
272 : "The linked plan could not be loaded. Retry after repairing it, or "
273 "create a plan at a new path. Existing files are never replaced.");
274 if (new_plan_path_.empty()) {
275 const std::filesystem::path project_path(project()->filepath);
276 new_plan_path_ = (project_path.parent_path() /
277 (project_path.stem().string() + ".plan.json"))
278 .string();
279 }
280 ImGui::SetNextItemWidth(-1);
281 ImGui::InputText("##new_plan_path", &new_plan_path_);
282 if (ImGui::Button(ICON_MD_ADD " Create plan and link it to the project")) {
283 absl::Status status =
285 if (!status.ok()) {
286 load_error_ = std::string(status.message());
287 } else {
288 (void)SyncWithProject();
289 }
290 }
291}
292
294 (void)SyncWithProject();
295 if (!plan_) {
296 DrawNoPlan();
297 return false;
298 }
299 ImGui::TextDisabled("%s", plan_path_.c_str());
300 ImGui::SameLine();
301 ImGui::BeginDisabled(!dirty_);
302 if (ImGui::SmallButton(ICON_MD_SAVE " Save plan")) {
303 absl::Status status = Save();
306 status.ok() ? "Plan saved" : std::string(status.message()),
307 status.ok() ? ToastType::kSuccess : ToastType::kError);
308 }
309 }
310 ImGui::EndDisabled();
311 ImGui::SameLine();
312 if (ImGui::SmallButton(ICON_MD_REFRESH " Reload")) {
313 if (dirty_) {
314 ImGui::OpenPopup("Discard plan edits?");
315 } else {
316 (void)Reload();
317 }
318 }
319 if (ImGui::BeginPopupModal("Discard plan edits?", nullptr,
320 ImGuiWindowFlags_AlwaysAutoResize)) {
321 ImGui::TextUnformatted("Reload discards the unsaved plan edits.");
322 if (ImGui::Button("Discard and reload")) {
323 (void)Reload(true);
324 ImGui::CloseCurrentPopup();
325 }
326 ImGui::SameLine();
327 if (ImGui::Button("Keep editing")) {
328 ImGui::CloseCurrentPopup();
329 }
330 ImGui::EndPopup();
331 }
332 if (dirty_) {
333 ImGui::SameLine();
335 }
336 if (!save_error_.empty()) {
338 }
339 return plan_.has_value();
340}
341
343 if (!DrawPlanHeader()) {
344 return;
345 }
346 if (!plan_->title.empty()) {
347 ImGui::TextUnformatted(plan_->title.c_str());
348 }
349 if (!plan_->about.empty()) {
350 ImGui::TextWrapped("%s", plan_->about.c_str());
351 }
352 ImGui::Separator();
353 DrawGoal();
359}
360
362 auto& goal = plan_->goal;
363 ImGui::SeparatorText(ICON_MD_FLAG " Goal");
364 ImGui::SetNextItemWidth(-1);
365 if (ImGui::InputTextWithHint("##goal_title", "What this push delivers",
366 &goal.title)) {
367 MarkDirty();
368 }
369 ImGui::SetNextItemWidth(120);
370 if (ImGui::InputTextWithHint("##goal_due", "YYYY-MM-DD", &goal.due)) {
371 MarkDirty();
372 }
373 const std::string left = DaysUntil(goal.due);
374 if (!left.empty()) {
375 ImGui::SameLine();
376 ImGui::TextDisabled("%s", left.c_str());
377 }
378 if (ImGui::InputTextMultiline("##goal_detail", &goal.detail,
379 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
380 MarkDirty();
381 }
382}
383
385 ImGui::SeparatorText(ICON_MD_TIMELINE " Milestones");
386 const core::PlanMilestone* current = plan_->CurrentMilestone(Today());
387 if (current) {
388 gui::ColoredTextF(gui::GetInfoColor(), "Now: %s", current->title.c_str());
389 if (!current->detail.empty()) {
390 ImGui::TextWrapped("%s", current->detail.c_str());
391 }
392 }
393 if (!ImGui::TreeNode("##milestones", "All milestones (%zu)",
394 plan_->milestones.size())) {
395 return;
396 }
397 if (ImGui::BeginTable("##milestone_table", 4,
398 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerH |
399 ImGuiTableFlags_SizingStretchProp)) {
400 ImGui::TableSetupColumn("Milestone", ImGuiTableColumnFlags_WidthStretch);
401 ImGui::TableSetupColumn("Start", ImGuiTableColumnFlags_WidthFixed, 90);
402 ImGui::TableSetupColumn("End", ImGuiTableColumnFlags_WidthFixed, 90);
403 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110);
404 ImGui::TableHeadersRow();
405 for (size_t i = 0; i < plan_->milestones.size(); ++i) {
406 auto& m = plan_->milestones[i];
407 ImGui::PushID(static_cast<int>(i));
408 ImGui::TableNextRow();
409 ImGui::TableNextColumn();
410 ImGui::SetNextItemWidth(-1);
411 if (ImGui::InputText("##title", &m.title)) {
412 MarkDirty();
413 }
414 ImGui::TableNextColumn();
415 ImGui::SetNextItemWidth(-1);
416 if (ImGui::InputText("##start", &m.start)) {
417 MarkDirty();
418 }
419 ImGui::TableNextColumn();
420 ImGui::SetNextItemWidth(-1);
421 if (ImGui::InputText("##end", &m.end)) {
422 MarkDirty();
423 }
424 ImGui::TableNextColumn();
425 ImGui::SetNextItemWidth(-1);
426 if (StatusCombo("##status", &m.status, kMilestoneStatuses)) {
427 MarkDirty();
428 }
429 ImGui::PopID();
430 }
431 ImGui::EndTable();
432 }
433 if (ImGui::SmallButton(ICON_MD_ADD " Milestone")) {
435 m.id = plan_->NewMilestoneId();
436 m.title = "New milestone";
437 m.status = "planned";
438 plan_->milestones.push_back(std::move(m));
439 MarkDirty();
440 }
441 ImGui::TreePop();
442}
443
445 ImGui::SeparatorText(ICON_MD_ROUTE " Route");
446 if (plan_->route_steps.empty()) {
447 ImGui::TextDisabled("No route steps yet. Add them in the Route window.");
448 return;
449 }
450 ImGui::BeginChild("##route_strip",
451 ImVec2(0, ImGui::GetFrameHeightWithSpacing() * 1.6f), false,
452 ImGuiWindowFlags_HorizontalScrollbar);
453 for (size_t i = 0; i < plan_->route_steps.size(); ++i) {
454 const auto& step = plan_->route_steps[i];
455 if (i > 0) {
456 ImGui::SameLine(0, 2);
457 ImGui::TextDisabled(ICON_MD_CHEVRON_RIGHT);
458 ImGui::SameLine(0, 2);
459 }
460 ImGui::PushID(static_cast<int>(i));
461 const bool selected = selected_step_ == static_cast<int>(i);
462 if (selected) {
463 ImGui::PushStyleColor(ImGuiCol_Button,
464 ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
465 }
466 if (ImGui::Button(StepLabel(step).c_str())) {
467 selected_step_ = static_cast<int>(i);
468 selected_item_ = -1;
469 }
470 if (selected) {
471 ImGui::PopStyleColor();
472 }
473 if (ImGui::IsItemHovered()) {
474 ImGui::SetTooltip("%s\n%s", step.title.c_str(), step.note.c_str());
475 }
476 ImGui::PopID();
477 }
478 ImGui::EndChild();
479 if (selected_step_ >= 0 &&
480 selected_step_ < static_cast<int>(plan_->route_steps.size())) {
481 const auto& step = plan_->route_steps[selected_step_];
482 ImGui::Text("%s", step.title.c_str());
483 if (!step.note.empty()) {
484 ImGui::TextWrapped("%s", step.note.c_str());
485 }
486 DrawTargets(step.targets, "strip");
487 DrawAgentObservation("step:" + step.id);
488 for (const auto* item : plan_->ItemsAtStep(step.id)) {
489 ImGui::BulletText("%s", item->name.c_str());
490 ImGui::SameLine();
491 gui::ColoredText(item->status.c_str(), StatusColor(item->status));
492 }
493 }
494}
495
497 const auto open = plan_->OpenDecisions();
498 ImGui::SeparatorText(
499 absl::StrFormat(ICON_MD_CHECKLIST " Decisions (%zu open)", open.size())
500 .c_str());
501 for (size_t i = 0; i < plan_->decisions.size(); ++i) {
502 auto& d = plan_->decisions[i];
503 ImGui::PushID(static_cast<int>(i));
504 const bool is_open = d.status.empty() || d.status == "open";
505 std::string header = d.priority > 0
506 ? absl::StrFormat("%d. %s", d.priority, d.title)
507 : d.title;
508 if (!is_open) {
509 header += absl::StrFormat(" (%s)", d.status);
510 }
511 ImGui::PushStyleColor(ImGuiCol_Text,
512 is_open ? ImGui::GetStyleColorVec4(ImGuiCol_Text)
514 const bool expanded = ImGui::TreeNode("##decision", "%s", header.c_str());
515 ImGui::PopStyleColor();
516 if (expanded) {
517 if (!d.recommendation.empty()) {
518 ImGui::TextWrapped("Recommendation: %s", d.recommendation.c_str());
519 }
520 ImGui::SetNextItemWidth(120);
521 if (StatusCombo("Status", &d.status, kDecisionStatuses)) {
522 MarkDirty();
523 }
524 if (ImGui::InputTextMultiline(
525 "##answer", &d.answer,
526 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
527 MarkDirty();
528 }
529 DrawTargets(d.targets, "decision");
530 for (const auto& ref : d.refs) {
531 ImGui::TextDisabled("%s", ref.c_str());
532 }
533 DrawAgentObservation("decision:" + d.id);
534 ImGui::TreePop();
535 }
536 ImGui::PopID();
537 }
538 if (ImGui::SmallButton(ICON_MD_ADD " Decision")) {
540 d.id = plan_->NewDecisionId();
541 d.title = "New decision";
542 d.status = "open";
543 plan_->decisions.push_back(std::move(d));
544 MarkDirty();
545 }
546}
547
549 if (plan_->references.empty()) {
550 return;
551 }
552 ImGui::SeparatorText(ICON_MD_EVENT_NOTE " References");
553 for (size_t i = 0; i < plan_->references.size(); ++i) {
554 const auto& r = plan_->references[i];
555 ImGui::PushID(static_cast<int>(i));
556 ImGui::BulletText("%s", r.label.c_str());
557 if (!r.author.empty() || !r.date.empty()) {
558 ImGui::SameLine();
559 ImGui::TextDisabled("%s %s", r.author.c_str(), r.date.c_str());
560 }
561 if (!r.path.empty()) {
562 ImGui::SameLine();
563 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY)) {
564 ImGui::SetClipboardText(r.path.c_str());
565 }
566 if (ImGui::IsItemHovered()) {
567 ImGui::SetTooltip("Copy path: %s", r.path.c_str());
568 }
569 }
570 ImGui::PopID();
571 }
572}
573
575 if (!DrawPlanHeader()) {
576 return;
577 }
578 if (ImGui::BeginTable(
579 "##route_layout", 2,
580 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
581 ImGui::TableSetupColumn("Steps", ImGuiTableColumnFlags_WidthStretch, 0.45f);
582 ImGui::TableSetupColumn("Detail", ImGuiTableColumnFlags_WidthStretch,
583 0.55f);
584 ImGui::TableNextRow();
585 ImGui::TableNextColumn();
586 DrawStepList();
587 ImGui::TableNextColumn();
589 ImGui::EndTable();
590 }
591 ImGui::Separator();
592 DrawItems();
593}
594
596 auto& steps = plan_->route_steps;
597 ImGui::BeginChild("##step_list", ImVec2(0, 320));
598 std::string last_group;
599 int move_from = -1;
600 int move_to = -1;
601 for (size_t i = 0; i < steps.size(); ++i) {
602 const auto& step = steps[i];
603 if (!step.group.empty() && step.group != last_group) {
604 ImGui::SeparatorText(step.group.c_str());
605 last_group = step.group;
606 }
607 ImGui::PushID(static_cast<int>(i));
608 if (ImGui::Selectable(absl::StrFormat("%zu. %s %s", i + 1, StepLabel(step),
609 step.code.empty() ? "" : step.title)
610 .c_str(),
611 selected_step_ == static_cast<int>(i))) {
612 selected_step_ = static_cast<int>(i);
613 }
614 if (ImGui::BeginDragDropSource()) {
615 const int index = static_cast<int>(i);
616 ImGui::SetDragDropPayload("PLAN_STEP", &index, sizeof(index));
617 ImGui::TextUnformatted(StepLabel(step).c_str());
618 ImGui::EndDragDropSource();
619 }
620 if (ImGui::BeginDragDropTarget()) {
621 if (const ImGuiPayload* payload =
622 ImGui::AcceptDragDropPayload("PLAN_STEP")) {
623 move_from = *static_cast<const int*>(payload->Data);
624 move_to = static_cast<int>(i);
625 }
626 ImGui::EndDragDropTarget();
627 }
628 ImGui::PopID();
629 }
630 ImGui::EndChild();
631 if (move_from >= 0 && move_to >= 0 && move_from != move_to) {
632 auto moved = std::move(steps[move_from]);
633 steps.erase(steps.begin() + move_from);
634 steps.insert(steps.begin() + move_to, std::move(moved));
635 selected_step_ = move_to;
636 MarkDirty();
637 }
638 if (ImGui::SmallButton(ICON_MD_ADD " Step")) {
640 step.id = plan_->NewStepId();
641 step.title = "New step";
642 step.kind = "story";
643 const size_t at = selected_step_ >= 0 ? selected_step_ + 1 : steps.size();
644 steps.insert(steps.begin() + at, std::move(step));
645 selected_step_ = static_cast<int>(at);
646 MarkDirty();
647 }
648 ImGui::SameLine();
649 ImGui::TextDisabled("Drag steps to reorder");
650}
651
653 auto& steps = plan_->route_steps;
654 if (selected_step_ < 0 || selected_step_ >= static_cast<int>(steps.size())) {
655 ImGui::TextDisabled("Select a step");
656 return;
657 }
658 auto& step = steps[selected_step_];
659 ImGui::PushID("step_detail");
660 std::string edited_id = step.id;
661 if (ImGui::InputText("Id", &edited_id)) {
662 const auto status = plan_->RenameStep(step.id, std::move(edited_id));
663 if (status.ok()) {
664 save_error_.clear();
665 MarkDirty();
666 } else {
667 save_error_ = std::string(status.message());
668 }
669 }
670 if (ImGui::InputText("Code", &step.code)) {
671 MarkDirty();
672 }
673 if (ImGui::InputText("Title", &step.title)) {
674 MarkDirty();
675 }
676 if (ImGui::InputText("Group", &step.group)) {
677 MarkDirty();
678 }
679 if (StatusCombo("Kind", &step.kind, kStepKinds)) {
680 MarkDirty();
681 }
682 std::string targets = JoinTargets(step.targets);
683 if (ImGui::InputTextWithHint("Targets", "room:0x0C, map:0x4B, area:D1",
684 &targets)) {
685 step.targets = SplitTargets(targets);
686 MarkDirty();
687 }
688 DrawTargets(step.targets, "detail");
689 if (ImGui::InputTextMultiline("##note", &step.note,
690 ImVec2(-1, ImGui::GetTextLineHeight() * 4))) {
691 MarkDirty();
692 }
693 DrawAgentObservation("step:" + step.id);
694 if (ImGui::SmallButton(ICON_MD_DELETE " Remove step")) {
695 steps.erase(steps.begin() + selected_step_);
697 std::min<int>(selected_step_, static_cast<int>(steps.size()) - 1);
698 MarkDirty();
699 }
700 ImGui::PopID();
701}
702
704 auto& items = plan_->route_items;
705 const std::string step_id =
706 selected_step_ >= 0 &&
707 selected_step_ < static_cast<int>(plan_->route_steps.size())
708 ? plan_->route_steps[selected_step_].id
709 : "";
710 ImGui::Text("Items (%zu)", items.size());
711 ImGui::SameLine();
712 ImGui::Checkbox("Selected step only", &items_at_step_only_);
713 ImGui::SameLine();
714 if (ImGui::SmallButton(ICON_MD_ADD " Item")) {
716 item.id = plan_->NewItemId();
717 item.name = "New item";
718 item.step = step_id;
719 item.status = "proposed";
720 items.push_back(std::move(item));
721 selected_item_ = static_cast<int>(items.size()) - 1;
722 MarkDirty();
723 }
724 if (ImGui::BeginTable("##items", 5,
725 ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY |
726 ImGuiTableFlags_BordersInnerH,
727 ImVec2(0, 220))) {
728 ImGui::TableSetupScrollFreeze(0, 1);
729 ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
730 ImGui::TableSetupColumn("Step", ImGuiTableColumnFlags_WidthFixed, 80);
731 ImGui::TableSetupColumn("Category", ImGuiTableColumnFlags_WidthFixed, 80);
732 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 90);
733 ImGui::TableSetupColumn("Where", ImGuiTableColumnFlags_WidthStretch);
734 ImGui::TableHeadersRow();
735 for (size_t i = 0; i < items.size(); ++i) {
736 const auto& item = items[i];
737 if (items_at_step_only_ && !step_id.empty() && item.step != step_id) {
738 continue;
739 }
740 ImGui::PushID(static_cast<int>(i));
741 ImGui::TableNextRow();
742 ImGui::TableNextColumn();
743 if (ImGui::Selectable(item.name.c_str(),
744 selected_item_ == static_cast<int>(i),
745 ImGuiSelectableFlags_SpanAllColumns)) {
746 selected_item_ = static_cast<int>(i);
747 }
748 ImGui::TableNextColumn();
749 const int step_index = plan_->FindStep(item.step);
750 ImGui::TextUnformatted(
751 step_index >= 0 ? StepLabel(plan_->route_steps[step_index]).c_str()
752 : item.step.c_str());
753 ImGui::TableNextColumn();
754 ImGui::TextUnformatted(item.category.c_str());
755 ImGui::TableNextColumn();
756 gui::ColoredText(item.status.c_str(), StatusColor(item.status));
757 ImGui::TableNextColumn();
758 ImGui::TextUnformatted(item.where.c_str());
759 ImGui::PopID();
760 }
761 ImGui::EndTable();
762 }
763
764 if (selected_item_ < 0 || selected_item_ >= static_cast<int>(items.size())) {
765 return;
766 }
767 auto& item = items[selected_item_];
768 ImGui::SeparatorText(item.name.c_str());
769 ImGui::PushID("item_detail");
770 if (ImGui::InputText("Name", &item.name)) {
771 MarkDirty();
772 }
773 if (ImGui::BeginCombo("Step", item.step.c_str())) {
774 for (const auto& step : plan_->route_steps) {
775 if (ImGui::Selectable(
776 absl::StrFormat("%s %s", StepLabel(step), step.title).c_str(),
777 item.step == step.id)) {
778 item.step = step.id;
779 MarkDirty();
780 }
781 }
782 ImGui::EndCombo();
783 }
784 if (ImGui::InputText("Category", &item.category)) {
785 MarkDirty();
786 }
787 if (StatusCombo("Status", &item.status, kItemStatuses)) {
788 MarkDirty();
789 }
790 if (ImGui::Checkbox("Required", &item.required)) {
791 MarkDirty();
792 }
793 if (ImGui::InputText("Where", &item.where)) {
794 MarkDirty();
795 }
796 if (ImGui::InputTextMultiline("##item_note", &item.note,
797 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
798 MarkDirty();
799 }
800 DrawAgentObservation("item:" + item.id);
801 for (const auto& need : item.needs) {
802 const int step_index = plan_->FindStep(need.step);
803 ImGui::BulletText("Needed at %s: %s",
804 step_index >= 0
805 ? StepLabel(plan_->route_steps[step_index]).c_str()
806 : need.step.c_str(),
807 need.what.c_str());
808 }
809 if (ImGui::SmallButton(ICON_MD_DELETE " Remove item")) {
810 items.erase(items.begin() + selected_item_);
811 selected_item_ = -1;
812 MarkDirty();
813 }
814 ImGui::PopID();
815}
816
819 if (section == nullptr) {
820 return;
821 }
822 ImGui::SeparatorText(ICON_MD_SMART_TOY " Agent status");
823 const auto observations = section->find("observations");
824 const size_t count =
825 observations != section->end() && observations->is_object()
826 ? observations->size()
827 : 0;
828 ImGui::TextDisabled("%zu observations, updated %s by %s", count,
829 JsonString(*section, "updated_utc", "-").c_str(),
830 JsonString(*section, "agent", "-").c_str());
831 const auto orphans = core::OrphanedPlanAgentObservations(*plan_);
832 if (!orphans.empty()) {
834 "%zu observations name entries no longer in the plan: %s",
835 orphans.size(), absl::StrJoin(orphans, ", ").c_str());
836 }
837 if (count == 0 ||
838 !ImGui::TreeNode("##agent_observations", "Observations (%zu)", count)) {
839 return;
840 }
841 for (auto it = observations->begin(); it != observations->end(); ++it) {
842 ImGui::BulletText("%s", it.key().c_str());
843 const std::string state = JsonString(it.value(), "state", "");
844 if (!state.empty()) {
845 ImGui::SameLine();
846 gui::ColoredText(state.c_str(), gui::GetInfoColor());
847 }
848 }
849 ImGui::TreePop();
850}
851
852void PlanningEditor::DrawAgentObservation(const std::string& key) {
854 if (obs == nullptr) {
855 return;
856 }
857 ImGui::PushID(key.c_str());
858 ImGui::SeparatorText(ICON_MD_SMART_TOY " Agent status");
859 const std::string state = JsonString(*obs, "state", "");
860 if (!state.empty()) {
861 gui::ColoredText(state.c_str(), gui::GetInfoColor());
862 }
863 for (const char* field : {"summary", "next"}) {
864 const std::string text = JsonString(*obs, field, "");
865 if (!text.empty()) {
866 ImGui::TextWrapped("%s", text.c_str());
867 }
868 }
869 if (auto evidence = obs->find("evidence");
870 evidence != obs->end() && evidence->is_array()) {
871 for (const auto& entry : *evidence) {
872 if (entry.is_string()) {
873 ImGui::BulletText("%s", entry.get<std::string>().c_str());
874 }
875 }
876 }
877 if (auto prs = obs->find("prs"); prs != obs->end() && prs->is_array()) {
878 std::vector<std::string> labels;
879 for (const auto& pr : *prs) {
880 labels.push_back(pr.is_string() ? pr.get<std::string>()
881 : "#" + pr.dump());
882 }
883 ImGui::TextDisabled("PRs: %s", absl::StrJoin(labels, ", ").c_str());
884 }
885 ImGui::TextDisabled(
886 "%s %s - read-only; agents update this, your notes stay "
887 "yours",
888 JsonString(*obs, "agent", "").c_str(),
889 JsonString(*obs, "observed_utc", "").c_str());
890 ImGui::PopID();
891}
892
893void PlanningEditor::DrawTargets(const std::vector<std::string>& targets,
894 const char* id) {
895 if (targets.empty()) {
896 return;
897 }
898 ImGui::PushID(id);
899 for (size_t i = 0; i < targets.size(); ++i) {
900 if (i > 0) {
901 ImGui::SameLine();
902 }
903 ImGui::PushID(static_cast<int>(i));
904 const core::PlanTarget target = core::PlanTarget::Parse(targets[i]);
905 if (target.NumericId() >= 0) {
906 if (ImGui::SmallButton(targets[i].c_str())) {
907 OpenTarget(targets[i]);
908 }
909 } else {
910 ImGui::TextDisabled("%s", targets[i].c_str());
911 }
912 ImGui::PopID();
913 }
914 ImGui::PopID();
915}
916
917void PlanningEditor::OpenTarget(const std::string& text) {
918 const core::PlanTarget target = core::PlanTarget::Parse(text);
919 const int id = target.NumericId();
921 if (!bus || id < 0) {
922 return;
923 }
924 if (target.kind == "room") {
926 } else if (target.kind == "map") {
928 }
929}
930
931} // namespace yaze::editor
static absl::Status CreateAndLink(project::YazeProject *project, const std::string &path)
static absl::StatusOr< ProjectPlan > LoadFromFile(const std::string &path, std::string *source=nullptr)
project::YazeProject * project() const
Definition editor.h:370
EditorDependencies dependencies_
Definition editor.h:382
absl::Status Reload(bool discard_changes=false)
absl::Status Save() override
void DrawAgentObservation(const std::string &key)
void DrawTargets(const std::vector< std::string > &targets, const char *id)
std::optional< core::ProjectPlan > plan_
absl::Status Update() override
void OpenTarget(const std::string &target)
void Show(const std::string &message, ToastType type=ToastType::kInfo, float ttl_seconds=3.0f, std::string action_label={}, std::function< void()> on_action=nullptr)
Base interface for all logical window content components.
void RegisterWindowContent(std::unique_ptr< WindowContent > window)
Register a WindowContent instance for central drawing.
std::string GetId() const override
Unique identifier for this panel.
std::string GetIcon() const override
Material Design icon for this panel.
std::string GetDisplayName() const override
Human-readable name shown in menus and title bars.
WindowPresentationPolicy GetPresentationPolicy() const override
How this content is hosted and advertised by workspace chrome.
bool IsVisibleByDefault() const override
Whether this panel should be visible by default.
std::string GetEditorCategory() const override
Editor category this panel belongs to.
PlanningPanel(std::string id, std::string name, const char *icon, int priority, std::function< void()> draw)
int GetPriority() const override
Get display priority for menu ordering.
float GetPreferredWidth() const override
Get preferred width for this panel (optional)
#define ICON_MD_EVENT_NOTE
Definition icons.h:696
#define ICON_MD_CHECKLIST
Definition icons.h:402
#define ICON_MD_REFRESH
Definition icons.h:1572
#define ICON_MD_ROUTE
Definition icons.h:1627
#define ICON_MD_ADD
Definition icons.h:86
#define ICON_MD_FLAG
Definition icons.h:784
#define ICON_MD_DASHBOARD
Definition icons.h:517
#define ICON_MD_SAVE
Definition icons.h:1644
#define ICON_MD_DELETE
Definition icons.h:530
#define ICON_MD_CONTENT_COPY
Definition icons.h:465
#define ICON_MD_CHEVRON_RIGHT
Definition icons.h:406
#define ICON_MD_TIMELINE
Definition icons.h:1981
#define ICON_MD_SMART_TOY
Definition icons.h:1781
const PlanJson * FindPlanAgentStatus(const ProjectPlan &plan)
const PlanJson * FindPlanAgentObservation(const ProjectPlan &plan, std::string_view key)
bool AdoptPlanAgentOnlyChanges(ProjectPlan *draft, std::string_view loaded_source, std::string_view disk_source)
nlohmann::ordered_json PlanJson
std::vector< std::string > OrphanedPlanAgentObservations(const ProjectPlan &plan)
::yaze::EventBus * event_bus()
Get the current EventBus instance.
bool StatusCombo(const char *label, std::string *value, const char *const (&options)[N])
std::vector< std::string > SplitTargets(const std::string &text)
std::string StepLabel(const core::PlanRouteStep &step)
std::string JoinTargets(const std::vector< std::string > &targets)
std::string JsonString(const core::PlanJson &obj, const char *key, const char *fallback)
Editors are the view controllers for the application.
void ColoredText(const char *text, const ImVec4 &color)
ImVec4 GetSuccessColor()
Definition ui_helpers.cc:49
ImVec4 GetDisabledColor()
Definition ui_helpers.cc:74
void ColoredTextF(const ImVec4 &color, const char *fmt,...)
ImVec4 GetErrorColor()
Definition ui_helpers.cc:59
ImVec4 GetWarningColor()
Definition ui_helpers.cc:54
ImVec4 GetModifiedColor()
ImVec4 GetInfoColor()
Definition ui_helpers.cc:64
static PlanTarget Parse(std::string_view text)
WorkspaceWindowManager * window_manager
Definition editor.h:182
static JumpToMapRequestEvent Create(int map, size_t session=0)
static JumpToRoomRequestEvent Create(int room, size_t session=0)
std::string PlanningFilePath() const
Definition project.cc:2760