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"
22#include "imgui/imgui.h"
23#include "imgui/misc/cpp/imgui_stdlib.h"
30constexpr const char*
kItemStatuses[] = {
"implemented",
"prototype",
"decided",
33constexpr const char*
kStepKinds[] = {
"story",
"errand",
"dungeon",
34 "shrine",
"boss",
"optional"};
39 int priority, std::function<
void()> draw)
41 name_(std::move(name)),
44 draw_(std::move(draw)) {}
46 std::string
GetId()
const override {
return id_; }
48 std::string
GetIcon()
const override {
return icon_; }
51 return WindowPresentationPolicy::CoreWorkspace();
56 void Draw(
bool* )
override {
72 const char*
const (&options)[N]) {
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)) {
88 if (status ==
"implemented" || status ==
"done" || status ==
"decided") {
91 if (status ==
"prototype" || status ==
"active") {
94 if (status ==
"open") {
101 return absl::FormatTime(
"%Y-%m-%d", absl::Now(), absl::LocalTimeZone());
108 !absl::ParseTime(
"%Y-%m-%d", date, absl::LocalTimeZone(), &due, &error)) {
111 const int64_t days = absl::ToInt64Hours(due - absl::Now()) / 24;
113 return absl::StrFormat(
"%d days past", -days);
115 return absl::StrFormat(
"%d days left", days);
119 return absl::StrJoin(targets,
", ");
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));
145 window_manager->RegisterWindowContent(std::make_unique<PlanningPanel>(
152 return absl::OkStatus();
157 return absl::OkStatus();
166 return absl::OkStatus();
188 load_error_ = std::string(loaded.status().message());
190 return loaded.status();
192 plan_ = std::move(*loaded);
201 return absl::OkStatus();
207 "Open a .yaze project to plan it. The plan is a JSON file named by "
208 "the project's [planning] section.");
219 "This project has no plan file. Create one, or set [planning] file= in "
220 "the .yaze file (absolute, or relative to the project).");
222 const std::filesystem::path project_path(
project()->filepath);
224 (project_path.stem().string() +
".plan.json"))
227 ImGui::SetNextItemWidth(-1);
229 if (ImGui::Button(
ICON_MD_ADD " Create plan and link it to the project")) {
248 ImGui::TextDisabled(
"%s",
plan_path_.c_str());
250 ImGui::BeginDisabled(!
dirty_);
252 absl::Status status =
Save();
255 status.ok() ?
"Plan saved" : std::string(status.message()),
259 ImGui::EndDisabled();
271 return plan_.has_value();
278 if (!
plan_->title.empty()) {
279 ImGui::TextUnformatted(
plan_->title.c_str());
281 if (!
plan_->about.empty()) {
282 ImGui::TextWrapped(
"%s",
plan_->about.c_str());
293 auto& goal =
plan_->goal;
295 ImGui::SetNextItemWidth(-1);
296 if (ImGui::InputTextWithHint(
"##goal_title",
"What this push delivers",
300 ImGui::SetNextItemWidth(120);
301 if (ImGui::InputTextWithHint(
"##goal_due",
"YYYY-MM-DD", &goal.due)) {
304 const std::string left = DaysUntil(goal.due);
307 ImGui::TextDisabled(
"%s", left.c_str());
309 if (ImGui::InputTextMultiline(
"##goal_detail", &goal.detail,
310 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
320 if (!current->
detail.empty()) {
321 ImGui::TextWrapped(
"%s", current->
detail.c_str());
324 if (!ImGui::TreeNode(
"##milestones",
"All milestones (%zu)",
325 plan_->milestones.size())) {
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)) {
345 ImGui::TableNextColumn();
346 ImGui::SetNextItemWidth(-1);
347 if (ImGui::InputText(
"##start", &m.start)) {
350 ImGui::TableNextColumn();
351 ImGui::SetNextItemWidth(-1);
352 if (ImGui::InputText(
"##end", &m.end)) {
355 ImGui::TableNextColumn();
356 ImGui::SetNextItemWidth(-1);
357 if (StatusCombo(
"##status", &m.status, kMilestoneStatuses)) {
364 if (ImGui::SmallButton(
ICON_MD_ADD " Milestone")) {
366 m.
id = absl::StrFormat(
"m%zu",
plan_->milestones.size() + 1);
367 m.
title =
"New milestone";
369 plan_->milestones.push_back(std::move(m));
377 if (
plan_->route_steps.empty()) {
378 ImGui::TextDisabled(
"No route steps yet. Add them in the Route window.");
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];
387 ImGui::SameLine(0, 2);
389 ImGui::SameLine(0, 2);
391 ImGui::PushID(
static_cast<int>(i));
394 ImGui::PushStyleColor(ImGuiCol_Button,
395 ImGui::GetStyleColorVec4(ImGuiCol_ButtonActive));
397 if (ImGui::Button(StepLabel(step).c_str())) {
402 ImGui::PopStyleColor();
404 if (ImGui::IsItemHovered()) {
405 ImGui::SetTooltip(
"%s\n%s", step.title.c_str(), step.note.c_str());
413 ImGui::Text(
"%s", step.title.c_str());
414 if (!step.note.empty()) {
415 ImGui::TextWrapped(
"%s", step.note.c_str());
418 for (
const auto* item :
plan_->ItemsAtStep(step.id)) {
419 ImGui::BulletText(
"%s", item->name.c_str());
427 const auto open =
plan_->OpenDecisions();
428 ImGui::SeparatorText(
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)
439 header += absl::StrFormat(
" (%s)", d.status);
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();
447 if (!d.recommendation.empty()) {
448 ImGui::TextWrapped(
"Recommendation: %s", d.recommendation.c_str());
450 ImGui::SetNextItemWidth(120);
451 if (StatusCombo(
"Status", &d.status, kDecisionStatuses)) {
454 if (ImGui::InputTextMultiline(
455 "##answer", &d.answer,
456 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
460 for (
const auto& ref : d.refs) {
461 ImGui::TextDisabled(
"%s", ref.c_str());
469 d.
id = absl::StrFormat(
"decision-%zu",
plan_->decisions.size() + 1);
470 d.
title =
"New decision";
472 plan_->decisions.push_back(std::move(d));
478 if (
plan_->references.empty()) {
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()) {
488 ImGui::TextDisabled(
"%s %s", r.author.c_str(), r.date.c_str());
490 if (!r.path.empty()) {
493 ImGui::SetClipboardText(r.path.c_str());
495 if (ImGui::IsItemHovered()) {
496 ImGui::SetTooltip(
"Copy path: %s", r.path.c_str());
507 if (ImGui::BeginTable(
509 ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV)) {
510 ImGui::TableSetupColumn(
"Steps", ImGuiTableColumnFlags_WidthStretch, 0.45f);
511 ImGui::TableSetupColumn(
"Detail", ImGuiTableColumnFlags_WidthStretch,
513 ImGui::TableNextRow();
514 ImGui::TableNextColumn();
516 ImGui::TableNextColumn();
525 auto& steps =
plan_->route_steps;
526 ImGui::BeginChild(
"##step_list", ImVec2(0, 320));
527 std::string last_group;
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;
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)
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();
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);
555 ImGui::EndDragDropTarget();
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));
569 step.
id = absl::StrFormat(
"step-%zu", steps.size() + 1);
570 step.
title =
"New step";
573 steps.insert(steps.begin() + at, std::move(step));
578 ImGui::TextDisabled(
"Drag steps to reorder");
582 auto& steps =
plan_->route_steps;
583 if (selected_step_ < 0 || selected_step_ >=
static_cast<int>(steps.size())) {
584 ImGui::TextDisabled(
"Select a 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) {
595 for (
auto& need : item.needs) {
596 if (need.step == old_id) {
603 if (ImGui::InputText(
"Code", &step.code)) {
606 if (ImGui::InputText(
"Title", &step.title)) {
609 if (ImGui::InputText(
"Group", &step.group)) {
612 if (StatusCombo(
"Kind", &step.kind, kStepKinds)) {
615 std::string targets = JoinTargets(step.targets);
616 if (ImGui::InputTextWithHint(
"Targets",
"room:0x0C, map:0x4B, area:D1",
618 step.targets = SplitTargets(targets);
622 if (ImGui::InputTextMultiline(
"##note", &step.note,
623 ImVec2(-1, ImGui::GetTextLineHeight() * 4))) {
629 std::min<int>(
selected_step_,
static_cast<int>(steps.size()) - 1);
636 auto& items =
plan_->route_items;
637 const std::string step_id =
639 selected_step_ < static_cast<int>(
plan_->route_steps.size())
642 ImGui::Text(
"Items (%zu)", items.size());
648 item.
id = absl::StrFormat(
"item-%zu", items.size() + 1);
649 item.
name =
"New item";
652 items.push_back(std::move(item));
656 if (ImGui::BeginTable(
"##items", 5,
657 ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY |
658 ImGuiTableFlags_BordersInnerH,
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];
672 ImGui::PushID(
static_cast<int>(i));
673 ImGui::TableNextRow();
674 ImGui::TableNextColumn();
675 if (ImGui::Selectable(item.name.c_str(),
677 ImGuiSelectableFlags_SpanAllColumns)) {
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();
689 ImGui::TableNextColumn();
690 ImGui::TextUnformatted(item.where.c_str());
696 if (selected_item_ < 0 || selected_item_ >=
static_cast<int>(items.size())) {
700 ImGui::SeparatorText(item.name.c_str());
701 ImGui::PushID(
"item_detail");
702 if (ImGui::InputText(
"Name", &item.name)) {
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)) {
716 if (ImGui::InputText(
"Category", &item.category)) {
719 if (StatusCombo(
"Status", &item.status, kItemStatuses)) {
722 if (ImGui::Checkbox(
"Required", &item.required)) {
725 if (ImGui::InputText(
"Where", &item.where)) {
728 if (ImGui::InputTextMultiline(
"##item_note", &item.note,
729 ImVec2(-1, ImGui::GetTextLineHeight() * 3))) {
732 for (
const auto& need : item.needs) {
733 const int step_index =
plan_->FindStep(need.step);
734 ImGui::BulletText(
"Needed at %s: %s",
736 ? StepLabel(
plan_->route_steps[step_index]).c_str()
750 if (targets.empty()) {
754 for (
size_t i = 0; i < targets.size(); ++i) {
758 ImGui::PushID(
static_cast<int>(i));
761 if (ImGui::SmallButton(targets[i].c_str())) {
765 ImGui::TextDisabled(
"%s", targets[i].c_str());
776 if (!bus ||
id < 0) {
779 if (target.
kind ==
"room") {
781 }
else if (target.
kind ==
"map") {
static ProjectPlan CreateEmpty(std::string title)
static absl::StatusOr< ProjectPlan > LoadFromFile(const std::string &path)
project::YazeProject * project() const
EditorDependencies dependencies_
void Initialize() override
absl::Status Save() override
std::string new_plan_path_
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::function< void()> draw_
void Draw(bool *) override
Draw the panel content.
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
#define ICON_MD_CHECKLIST
#define ICON_MD_DASHBOARD
#define ICON_MD_CONTENT_COPY
#define ICON_MD_CHEVRON_RIGHT
::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)
ImVec4 StatusColor(const std::string &status)
constexpr const char * kDecisionStatuses[]
constexpr const char * kStepKinds[]
std::string StepLabel(const core::PlanRouteStep &step)
constexpr const char * kMilestoneStatuses[]
constexpr const char * kItemStatuses[]
std::string JoinTargets(const std::vector< std::string > &targets)
std::string DaysUntil(const std::string &date)
constexpr const char * kCategory
Editors are the view controllers for the application.
void ColoredText(const char *text, const ImVec4 &color)
ImVec4 GetDisabledColor()
void ColoredTextF(const ImVec4 &color, const char *fmt,...)
ImVec4 GetModifiedColor()
static PlanTarget Parse(std::string_view text)
ToastManager * toast_manager
WorkspaceWindowManager * window_manager
static JumpToMapRequestEvent Create(int map, size_t session=0)
static JumpToRoomRequestEvent Create(int room, size_t session=0)
PlanningSettings planning
std::string PlanningFilePath() const
std::string GetRelativePath(const std::string &absolute_path) const