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"
22#include "imgui/imgui.h"
23#include "imgui/misc/cpp/imgui_stdlib.h"
24
25namespace yaze::editor {
26namespace {
27
28constexpr const char* kCategory = "Planning";
29constexpr const char* kMilestoneStatuses[] = {"planned", "active", "done"};
30constexpr const char* kItemStatuses[] = {"implemented", "prototype", "decided",
31 "proposed", "open"};
32constexpr const char* kDecisionStatuses[] = {"open", "decided", "dropped"};
33constexpr const char* kStepKinds[] = {"story", "errand", "dungeon",
34 "shrine", "boss", "optional"};
35
37 public:
38 PlanningPanel(std::string id, std::string name, const char* icon,
39 int priority, std::function<void()> draw)
40 : id_(std::move(id)),
41 name_(std::move(name)),
42 icon_(icon),
43 priority_(priority),
44 draw_(std::move(draw)) {}
45
46 std::string GetId() const override { return id_; }
47 std::string GetDisplayName() const override { return name_; }
48 std::string GetIcon() const override { return icon_; }
49 std::string GetEditorCategory() const override { return kCategory; }
51 return WindowPresentationPolicy::CoreWorkspace();
52 }
53 int GetPriority() const override { return priority_; }
54 bool IsVisibleByDefault() const override { return true; }
55 float GetPreferredWidth() const override { return 640.0f; }
56 void Draw(bool* /*p_open*/) override {
57 if (draw_) {
58 draw_();
59 }
60 }
61
62 private:
63 std::string id_;
64 std::string name_;
65 std::string icon_;
67 std::function<void()> draw_;
68};
69
70template <size_t N>
71bool StatusCombo(const char* label, std::string* value,
72 const char* const (&options)[N]) {
73 bool changed = false;
74 if (ImGui::BeginCombo(label, value->empty() ? "-" : value->c_str())) {
75 for (const char* option : options) {
76 const bool selected = *value == option;
77 if (ImGui::Selectable(option, selected)) {
78 *value = option;
79 changed = true;
80 }
81 }
82 ImGui::EndCombo();
83 }
84 return changed;
85}
86
87ImVec4 StatusColor(const std::string& status) {
88 if (status == "implemented" || status == "done" || status == "decided") {
89 return gui::GetSuccessColor();
90 }
91 if (status == "prototype" || status == "active") {
92 return gui::GetInfoColor();
93 }
94 if (status == "open") {
95 return gui::GetWarningColor();
96 }
97 return gui::GetDisabledColor();
98}
99
100std::string Today() {
101 return absl::FormatTime("%Y-%m-%d", absl::Now(), absl::LocalTimeZone());
102}
103
104std::string DaysUntil(const std::string& date) {
105 absl::Time due;
106 std::string error;
107 if (date.empty() ||
108 !absl::ParseTime("%Y-%m-%d", date, absl::LocalTimeZone(), &due, &error)) {
109 return "";
110 }
111 const int64_t days = absl::ToInt64Hours(due - absl::Now()) / 24;
112 if (days < 0) {
113 return absl::StrFormat("%d days past", -days);
114 }
115 return absl::StrFormat("%d days left", days);
116}
117
118std::string JoinTargets(const std::vector<std::string>& targets) {
119 return absl::StrJoin(targets, ", ");
120}
121
122std::vector<std::string> SplitTargets(const std::string& text) {
123 std::vector<std::string> out;
124 for (absl::string_view part :
125 absl::StrSplit(text, ',', absl::SkipWhitespace())) {
126 out.emplace_back(absl::StripAsciiWhitespace(part));
127 }
128 return out;
129}
130
131std::string StepLabel(const core::PlanRouteStep& step) {
132 return step.code.empty() ? step.title : step.code;
133}
134
135} // namespace
136
139 return;
140 }
141 auto* window_manager = dependencies_.window_manager;
142 window_manager->RegisterWindowContent(std::make_unique<PlanningPanel>(
143 "planning.overview", "Plan Overview", ICON_MD_DASHBOARD, 10,
144 [this]() { DrawOverview(); }));
145 window_manager->RegisterWindowContent(std::make_unique<PlanningPanel>(
146 "planning.route", "Route", ICON_MD_ROUTE, 20, [this]() { DrawRoute(); }));
147 panels_registered_ = true;
148}
149
152 return absl::OkStatus();
153}
154
155absl::Status PlanningEditor::Save() {
156 if (!plan_ || !dirty_ || plan_path_.empty()) {
157 return absl::OkStatus();
158 }
159 absl::Status status = plan_->SaveToFile(plan_path_);
160 if (!status.ok()) {
161 save_error_ = std::string(status.message());
162 return status;
163 }
164 save_error_.clear();
165 dirty_ = false;
166 return absl::OkStatus();
167}
168
170 const std::string path = project() ? project()->PlanningFilePath() : "";
171 if (path == plan_path_) {
172 return;
173 }
174 plan_path_ = path;
175 plan_.reset();
176 load_error_.clear();
177 dirty_ = false;
178 selected_step_ = -1;
179 selected_item_ = -1;
180 if (!plan_path_.empty()) {
181 (void)Reload();
182 }
183}
184
187 if (!loaded.ok()) {
188 load_error_ = std::string(loaded.status().message());
189 plan_.reset();
190 return loaded.status();
191 }
192 plan_ = std::move(*loaded);
193 load_error_.clear();
194 dirty_ = false;
195 if (selected_step_ >= static_cast<int>(plan_->route_steps.size())) {
196 selected_step_ = -1;
197 }
198 if (selected_item_ >= static_cast<int>(plan_->route_items.size())) {
199 selected_item_ = -1;
200 }
201 return absl::OkStatus();
202}
203
205 if (!project() || project()->filepath.empty()) {
206 ImGui::TextWrapped(
207 "Open a .yaze project to plan it. The plan is a JSON file named by "
208 "the project's [planning] section.");
209 return;
210 }
211 if (!load_error_.empty()) {
213 if (ImGui::Button(ICON_MD_REFRESH " Retry")) {
214 (void)Reload();
215 }
216 ImGui::Separator();
217 }
218 ImGui::TextWrapped(
219 "This project has no plan file. Create one, or set [planning] file= in "
220 "the .yaze file (absolute, or relative to the project).");
221 if (new_plan_path_.empty()) {
222 const std::filesystem::path project_path(project()->filepath);
223 new_plan_path_ = (project_path.parent_path() /
224 (project_path.stem().string() + ".plan.json"))
225 .string();
226 }
227 ImGui::SetNextItemWidth(-1);
228 ImGui::InputText("##new_plan_path", &new_plan_path_);
229 if (ImGui::Button(ICON_MD_ADD " Create plan and link it to the project")) {
231 absl::Status status = plan.SaveToFile(new_plan_path_);
232 if (status.ok()) {
234 status = project()->Save();
235 }
236 if (!status.ok()) {
237 load_error_ = std::string(status.message());
238 }
239 }
240}
241
244 if (!plan_) {
245 DrawNoPlan();
246 return false;
247 }
248 ImGui::TextDisabled("%s", plan_path_.c_str());
249 ImGui::SameLine();
250 ImGui::BeginDisabled(!dirty_);
251 if (ImGui::SmallButton(ICON_MD_SAVE " Save plan")) {
252 absl::Status status = Save();
255 status.ok() ? "Plan saved" : std::string(status.message()),
256 status.ok() ? ToastType::kSuccess : ToastType::kError);
257 }
258 }
259 ImGui::EndDisabled();
260 ImGui::SameLine();
261 if (ImGui::SmallButton(ICON_MD_REFRESH " Reload")) {
262 (void)Reload();
263 }
264 if (dirty_) {
265 ImGui::SameLine();
267 }
268 if (!save_error_.empty()) {
270 }
271 return plan_.has_value();
272}
273
275 if (!DrawPlanHeader()) {
276 return;
277 }
278 if (!plan_->title.empty()) {
279 ImGui::TextUnformatted(plan_->title.c_str());
280 }
281 if (!plan_->about.empty()) {
282 ImGui::TextWrapped("%s", plan_->about.c_str());
283 }
284 ImGui::Separator();
285 DrawGoal();
290}
291
293 auto& goal = plan_->goal;
294 ImGui::SeparatorText(ICON_MD_FLAG " Goal");
295 ImGui::SetNextItemWidth(-1);
296 if (ImGui::InputTextWithHint("##goal_title", "What this push delivers",
297 &goal.title)) {
298 MarkDirty();
299 }
300 ImGui::SetNextItemWidth(120);
301 if (ImGui::InputTextWithHint("##goal_due", "YYYY-MM-DD", &goal.due)) {
302 MarkDirty();
303 }
304 const std::string left = DaysUntil(goal.due);
305 if (!left.empty()) {
306 ImGui::SameLine();
307 ImGui::TextDisabled("%s", left.c_str());
308 }
309 if (ImGui::InputTextMultiline("##goal_detail", &goal.detail,
310 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
311 MarkDirty();
312 }
313}
314
316 ImGui::SeparatorText(ICON_MD_TIMELINE " Milestones");
317 const core::PlanMilestone* current = plan_->CurrentMilestone(Today());
318 if (current) {
319 gui::ColoredTextF(gui::GetInfoColor(), "Now: %s", current->title.c_str());
320 if (!current->detail.empty()) {
321 ImGui::TextWrapped("%s", current->detail.c_str());
322 }
323 }
324 if (!ImGui::TreeNode("##milestones", "All milestones (%zu)",
325 plan_->milestones.size())) {
326 return;
327 }
328 if (ImGui::BeginTable("##milestone_table", 4,
329 ImGuiTableFlags_RowBg | ImGuiTableFlags_BordersInnerH |
330 ImGuiTableFlags_SizingStretchProp)) {
331 ImGui::TableSetupColumn("Milestone", ImGuiTableColumnFlags_WidthStretch);
332 ImGui::TableSetupColumn("Start", ImGuiTableColumnFlags_WidthFixed, 90);
333 ImGui::TableSetupColumn("End", ImGuiTableColumnFlags_WidthFixed, 90);
334 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 110);
335 ImGui::TableHeadersRow();
336 for (size_t i = 0; i < plan_->milestones.size(); ++i) {
337 auto& m = plan_->milestones[i];
338 ImGui::PushID(static_cast<int>(i));
339 ImGui::TableNextRow();
340 ImGui::TableNextColumn();
341 ImGui::SetNextItemWidth(-1);
342 if (ImGui::InputText("##title", &m.title)) {
343 MarkDirty();
344 }
345 ImGui::TableNextColumn();
346 ImGui::SetNextItemWidth(-1);
347 if (ImGui::InputText("##start", &m.start)) {
348 MarkDirty();
349 }
350 ImGui::TableNextColumn();
351 ImGui::SetNextItemWidth(-1);
352 if (ImGui::InputText("##end", &m.end)) {
353 MarkDirty();
354 }
355 ImGui::TableNextColumn();
356 ImGui::SetNextItemWidth(-1);
357 if (StatusCombo("##status", &m.status, kMilestoneStatuses)) {
358 MarkDirty();
359 }
360 ImGui::PopID();
361 }
362 ImGui::EndTable();
363 }
364 if (ImGui::SmallButton(ICON_MD_ADD " Milestone")) {
366 m.id = absl::StrFormat("m%zu", plan_->milestones.size() + 1);
367 m.title = "New milestone";
368 m.status = "planned";
369 plan_->milestones.push_back(std::move(m));
370 MarkDirty();
371 }
372 ImGui::TreePop();
373}
374
376 ImGui::SeparatorText(ICON_MD_ROUTE " Route");
377 if (plan_->route_steps.empty()) {
378 ImGui::TextDisabled("No route steps yet. Add them in the Route window.");
379 return;
380 }
381 ImGui::BeginChild("##route_strip",
382 ImVec2(0, ImGui::GetFrameHeightWithSpacing() * 1.6f), false,
383 ImGuiWindowFlags_HorizontalScrollbar);
384 for (size_t i = 0; i < plan_->route_steps.size(); ++i) {
385 const auto& step = plan_->route_steps[i];
386 if (i > 0) {
387 ImGui::SameLine(0, 2);
388 ImGui::TextDisabled(ICON_MD_CHEVRON_RIGHT);
389 ImGui::SameLine(0, 2);
390 }
391 ImGui::PushID(static_cast<int>(i));
392 const bool selected = selected_step_ == static_cast<int>(i);
393 if (selected) {
394 ImGui::PushStyleColor(ImGuiCol_Button,
395 ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
396 }
397 if (ImGui::Button(StepLabel(step).c_str())) {
398 selected_step_ = static_cast<int>(i);
399 selected_item_ = -1;
400 }
401 if (selected) {
402 ImGui::PopStyleColor();
403 }
404 if (ImGui::IsItemHovered()) {
405 ImGui::SetTooltip("%s\n%s", step.title.c_str(), step.note.c_str());
406 }
407 ImGui::PopID();
408 }
409 ImGui::EndChild();
410 if (selected_step_ >= 0 &&
411 selected_step_ < static_cast<int>(plan_->route_steps.size())) {
412 const auto& step = plan_->route_steps[selected_step_];
413 ImGui::Text("%s", step.title.c_str());
414 if (!step.note.empty()) {
415 ImGui::TextWrapped("%s", step.note.c_str());
416 }
417 DrawTargets(step.targets, "strip");
418 for (const auto* item : plan_->ItemsAtStep(step.id)) {
419 ImGui::BulletText("%s", item->name.c_str());
420 ImGui::SameLine();
421 gui::ColoredText(item->status.c_str(), StatusColor(item->status));
422 }
423 }
424}
425
427 const auto open = plan_->OpenDecisions();
428 ImGui::SeparatorText(
429 absl::StrFormat(ICON_MD_CHECKLIST " Decisions (%zu open)", open.size())
430 .c_str());
431 for (size_t i = 0; i < plan_->decisions.size(); ++i) {
432 auto& d = plan_->decisions[i];
433 ImGui::PushID(static_cast<int>(i));
434 const bool is_open = d.status.empty() || d.status == "open";
435 std::string header = d.priority > 0
436 ? absl::StrFormat("%d. %s", d.priority, d.title)
437 : d.title;
438 if (!is_open) {
439 header += absl::StrFormat(" (%s)", d.status);
440 }
441 ImGui::PushStyleColor(ImGuiCol_Text,
442 is_open ? ImGui::GetStyleColorVec4(ImGuiCol_Text)
444 const bool expanded = ImGui::TreeNode("##decision", "%s", header.c_str());
445 ImGui::PopStyleColor();
446 if (expanded) {
447 if (!d.recommendation.empty()) {
448 ImGui::TextWrapped("Recommendation: %s", d.recommendation.c_str());
449 }
450 ImGui::SetNextItemWidth(120);
451 if (StatusCombo("Status", &d.status, kDecisionStatuses)) {
452 MarkDirty();
453 }
454 if (ImGui::InputTextMultiline(
455 "##answer", &d.answer,
456 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
457 MarkDirty();
458 }
459 DrawTargets(d.targets, "decision");
460 for (const auto& ref : d.refs) {
461 ImGui::TextDisabled("%s", ref.c_str());
462 }
463 ImGui::TreePop();
464 }
465 ImGui::PopID();
466 }
467 if (ImGui::SmallButton(ICON_MD_ADD " Decision")) {
469 d.id = absl::StrFormat("decision-%zu", plan_->decisions.size() + 1);
470 d.title = "New decision";
471 d.status = "open";
472 plan_->decisions.push_back(std::move(d));
473 MarkDirty();
474 }
475}
476
478 if (plan_->references.empty()) {
479 return;
480 }
481 ImGui::SeparatorText(ICON_MD_EVENT_NOTE " References");
482 for (size_t i = 0; i < plan_->references.size(); ++i) {
483 const auto& r = plan_->references[i];
484 ImGui::PushID(static_cast<int>(i));
485 ImGui::BulletText("%s", r.label.c_str());
486 if (!r.author.empty() || !r.date.empty()) {
487 ImGui::SameLine();
488 ImGui::TextDisabled("%s %s", r.author.c_str(), r.date.c_str());
489 }
490 if (!r.path.empty()) {
491 ImGui::SameLine();
492 if (ImGui::SmallButton(ICON_MD_CONTENT_COPY)) {
493 ImGui::SetClipboardText(r.path.c_str());
494 }
495 if (ImGui::IsItemHovered()) {
496 ImGui::SetTooltip("Copy path: %s", r.path.c_str());
497 }
498 }
499 ImGui::PopID();
500 }
501}
502
504 if (!DrawPlanHeader()) {
505 return;
506 }
507 if (ImGui::BeginTable(
508 "##route_layout", 2,
509 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
510 ImGui::TableSetupColumn("Steps", ImGuiTableColumnFlags_WidthStretch, 0.45f);
511 ImGui::TableSetupColumn("Detail", ImGuiTableColumnFlags_WidthStretch,
512 0.55f);
513 ImGui::TableNextRow();
514 ImGui::TableNextColumn();
515 DrawStepList();
516 ImGui::TableNextColumn();
518 ImGui::EndTable();
519 }
520 ImGui::Separator();
521 DrawItems();
522}
523
525 auto& steps = plan_->route_steps;
526 ImGui::BeginChild("##step_list", ImVec2(0, 320));
527 std::string last_group;
528 int move_from = -1;
529 int move_to = -1;
530 for (size_t i = 0; i < steps.size(); ++i) {
531 const auto& step = steps[i];
532 if (!step.group.empty() && step.group != last_group) {
533 ImGui::SeparatorText(step.group.c_str());
534 last_group = step.group;
535 }
536 ImGui::PushID(static_cast<int>(i));
537 if (ImGui::Selectable(absl::StrFormat("%zu. %s %s", i + 1, StepLabel(step),
538 step.code.empty() ? "" : step.title)
539 .c_str(),
540 selected_step_ == static_cast<int>(i))) {
541 selected_step_ = static_cast<int>(i);
542 }
543 if (ImGui::BeginDragDropSource()) {
544 const int index = static_cast<int>(i);
545 ImGui::SetDragDropPayload("PLAN_STEP", &index, sizeof(index));
546 ImGui::TextUnformatted(StepLabel(step).c_str());
547 ImGui::EndDragDropSource();
548 }
549 if (ImGui::BeginDragDropTarget()) {
550 if (const ImGuiPayload* payload =
551 ImGui::AcceptDragDropPayload("PLAN_STEP")) {
552 move_from = *static_cast<const int*>(payload->Data);
553 move_to = static_cast<int>(i);
554 }
555 ImGui::EndDragDropTarget();
556 }
557 ImGui::PopID();
558 }
559 ImGui::EndChild();
560 if (move_from >= 0 && move_to >= 0 && move_from != move_to) {
561 auto moved = std::move(steps[move_from]);
562 steps.erase(steps.begin() + move_from);
563 steps.insert(steps.begin() + move_to, std::move(moved));
564 selected_step_ = move_to;
565 MarkDirty();
566 }
567 if (ImGui::SmallButton(ICON_MD_ADD " Step")) {
569 step.id = absl::StrFormat("step-%zu", steps.size() + 1);
570 step.title = "New step";
571 step.kind = "story";
572 const size_t at = selected_step_ >= 0 ? selected_step_ + 1 : steps.size();
573 steps.insert(steps.begin() + at, std::move(step));
574 selected_step_ = static_cast<int>(at);
575 MarkDirty();
576 }
577 ImGui::SameLine();
578 ImGui::TextDisabled("Drag steps to reorder");
579}
580
582 auto& steps = plan_->route_steps;
583 if (selected_step_ < 0 || selected_step_ >= static_cast<int>(steps.size())) {
584 ImGui::TextDisabled("Select a step");
585 return;
586 }
587 auto& step = steps[selected_step_];
588 ImGui::PushID("step_detail");
589 const std::string old_id = step.id;
590 if (ImGui::InputText("Id", &step.id)) {
591 for (auto& item : plan_->route_items) {
592 if (item.step == old_id) {
593 item.step = step.id;
594 }
595 for (auto& need : item.needs) {
596 if (need.step == old_id) {
597 need.step = step.id;
598 }
599 }
600 }
601 MarkDirty();
602 }
603 if (ImGui::InputText("Code", &step.code)) {
604 MarkDirty();
605 }
606 if (ImGui::InputText("Title", &step.title)) {
607 MarkDirty();
608 }
609 if (ImGui::InputText("Group", &step.group)) {
610 MarkDirty();
611 }
612 if (StatusCombo("Kind", &step.kind, kStepKinds)) {
613 MarkDirty();
614 }
615 std::string targets = JoinTargets(step.targets);
616 if (ImGui::InputTextWithHint("Targets", "room:0x0C, map:0x4B, area:D1",
617 &targets)) {
618 step.targets = SplitTargets(targets);
619 MarkDirty();
620 }
621 DrawTargets(step.targets, "detail");
622 if (ImGui::InputTextMultiline("##note", &step.note,
623 ImVec2(-1, ImGui::GetTextLineHeight() * 4))) {
624 MarkDirty();
625 }
626 if (ImGui::SmallButton(ICON_MD_DELETE " Remove step")) {
627 steps.erase(steps.begin() + selected_step_);
629 std::min<int>(selected_step_, static_cast<int>(steps.size()) - 1);
630 MarkDirty();
631 }
632 ImGui::PopID();
633}
634
636 auto& items = plan_->route_items;
637 const std::string step_id =
638 selected_step_ >= 0 &&
639 selected_step_ < static_cast<int>(plan_->route_steps.size())
640 ? plan_->route_steps[selected_step_].id
641 : "";
642 ImGui::Text("Items (%zu)", items.size());
643 ImGui::SameLine();
644 ImGui::Checkbox("Selected step only", &items_at_step_only_);
645 ImGui::SameLine();
646 if (ImGui::SmallButton(ICON_MD_ADD " Item")) {
648 item.id = absl::StrFormat("item-%zu", items.size() + 1);
649 item.name = "New item";
650 item.step = step_id;
651 item.status = "proposed";
652 items.push_back(std::move(item));
653 selected_item_ = static_cast<int>(items.size()) - 1;
654 MarkDirty();
655 }
656 if (ImGui::BeginTable("##items", 5,
657 ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY |
658 ImGuiTableFlags_BordersInnerH,
659 ImVec2(0, 220))) {
660 ImGui::TableSetupScrollFreeze(0, 1);
661 ImGui::TableSetupColumn("Item", ImGuiTableColumnFlags_WidthStretch);
662 ImGui::TableSetupColumn("Step", ImGuiTableColumnFlags_WidthFixed, 80);
663 ImGui::TableSetupColumn("Category", ImGuiTableColumnFlags_WidthFixed, 80);
664 ImGui::TableSetupColumn("Status", ImGuiTableColumnFlags_WidthFixed, 90);
665 ImGui::TableSetupColumn("Where", ImGuiTableColumnFlags_WidthStretch);
666 ImGui::TableHeadersRow();
667 for (size_t i = 0; i < items.size(); ++i) {
668 const auto& item = items[i];
669 if (items_at_step_only_ && !step_id.empty() && item.step != step_id) {
670 continue;
671 }
672 ImGui::PushID(static_cast<int>(i));
673 ImGui::TableNextRow();
674 ImGui::TableNextColumn();
675 if (ImGui::Selectable(item.name.c_str(),
676 selected_item_ == static_cast<int>(i),
677 ImGuiSelectableFlags_SpanAllColumns)) {
678 selected_item_ = static_cast<int>(i);
679 }
680 ImGui::TableNextColumn();
681 const int step_index = plan_->FindStep(item.step);
682 ImGui::TextUnformatted(
683 step_index >= 0 ? StepLabel(plan_->route_steps[step_index]).c_str()
684 : item.step.c_str());
685 ImGui::TableNextColumn();
686 ImGui::TextUnformatted(item.category.c_str());
687 ImGui::TableNextColumn();
688 gui::ColoredText(item.status.c_str(), StatusColor(item.status));
689 ImGui::TableNextColumn();
690 ImGui::TextUnformatted(item.where.c_str());
691 ImGui::PopID();
692 }
693 ImGui::EndTable();
694 }
695
696 if (selected_item_ < 0 || selected_item_ >= static_cast<int>(items.size())) {
697 return;
698 }
699 auto& item = items[selected_item_];
700 ImGui::SeparatorText(item.name.c_str());
701 ImGui::PushID("item_detail");
702 if (ImGui::InputText("Name", &item.name)) {
703 MarkDirty();
704 }
705 if (ImGui::BeginCombo("Step", item.step.c_str())) {
706 for (const auto& step : plan_->route_steps) {
707 if (ImGui::Selectable(
708 absl::StrFormat("%s %s", StepLabel(step), step.title).c_str(),
709 item.step == step.id)) {
710 item.step = step.id;
711 MarkDirty();
712 }
713 }
714 ImGui::EndCombo();
715 }
716 if (ImGui::InputText("Category", &item.category)) {
717 MarkDirty();
718 }
719 if (StatusCombo("Status", &item.status, kItemStatuses)) {
720 MarkDirty();
721 }
722 if (ImGui::Checkbox("Required", &item.required)) {
723 MarkDirty();
724 }
725 if (ImGui::InputText("Where", &item.where)) {
726 MarkDirty();
727 }
728 if (ImGui::InputTextMultiline("##item_note", &item.note,
729 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
730 MarkDirty();
731 }
732 for (const auto& need : item.needs) {
733 const int step_index = plan_->FindStep(need.step);
734 ImGui::BulletText("Needed at %s: %s",
735 step_index >= 0
736 ? StepLabel(plan_->route_steps[step_index]).c_str()
737 : need.step.c_str(),
738 need.what.c_str());
739 }
740 if (ImGui::SmallButton(ICON_MD_DELETE " Remove item")) {
741 items.erase(items.begin() + selected_item_);
742 selected_item_ = -1;
743 MarkDirty();
744 }
745 ImGui::PopID();
746}
747
748void PlanningEditor::DrawTargets(const std::vector<std::string>& targets,
749 const char* id) {
750 if (targets.empty()) {
751 return;
752 }
753 ImGui::PushID(id);
754 for (size_t i = 0; i < targets.size(); ++i) {
755 if (i > 0) {
756 ImGui::SameLine();
757 }
758 ImGui::PushID(static_cast<int>(i));
759 const core::PlanTarget target = core::PlanTarget::Parse(targets[i]);
760 if (target.NumericId() >= 0) {
761 if (ImGui::SmallButton(targets[i].c_str())) {
762 OpenTarget(targets[i]);
763 }
764 } else {
765 ImGui::TextDisabled("%s", targets[i].c_str());
766 }
767 ImGui::PopID();
768 }
769 ImGui::PopID();
770}
771
772void PlanningEditor::OpenTarget(const std::string& text) {
773 const core::PlanTarget target = core::PlanTarget::Parse(text);
774 const int id = target.NumericId();
776 if (!bus || id < 0) {
777 return;
778 }
779 if (target.kind == "room") {
781 } else if (target.kind == "map") {
783 }
784}
785
786} // namespace yaze::editor
static ProjectPlan CreateEmpty(std::string title)
static absl::StatusOr< ProjectPlan > LoadFromFile(const std::string &path)
project::YazeProject * project() const
Definition editor.h:358
EditorDependencies dependencies_
Definition editor.h:370
absl::Status Save() override
const std::optional< core::ProjectPlan > & plan() const
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
::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)
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)
PlanningSettings planning
Definition project.h:350
std::string PlanningFilePath() const
Definition project.cc:2102
std::string GetRelativePath(const std::string &absolute_path) const
Definition project.cc:2008