#pragma once #include "app_core/document_route.h" #include "foundation/result.h" #include #include #include #include #include #include namespace pp::app { enum class ProjectOpenDecision { open_now, prompt_discard_unsaved, }; enum class CloseRequestDecision { close_now, show_unsaved_prompt, wait_for_existing_prompt, }; enum class DocumentSaveIntent { save, save_as, save_version, save_dirty_version, }; enum class DocumentSaveDecision { no_op, show_save_dialog, save_existing, save_version, }; enum class DocumentWorkflowDecision { unavailable, continue_now, prompt_save_before_continue, }; enum class DocumentFileWriteDecision { save_now, prompt_overwrite, }; enum class DocumentOpenPlanAction { open_project_now, prompt_discard_unsaved_project, prompt_import_abr, prompt_import_ppbr, }; class DocumentOpenServices { public: virtual ~DocumentOpenServices() = default; virtual void prompt_import_abr(const DocumentOpenRoute& route) = 0; virtual void prompt_import_ppbr(const DocumentOpenRoute& route) = 0; virtual void open_project_now(const DocumentOpenRoute& route) = 0; virtual void prompt_discard_unsaved_project(const DocumentOpenRoute& route) = 0; }; struct DocumentFileTarget { std::string name; std::string directory; std::string path; }; struct DocumentVersionTarget { std::string name; std::string path; }; struct DocumentFileSavePlan { DocumentFileTarget target; DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now; }; struct NewDocumentPlan { DocumentFileTarget target; int resolution = 0; DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now; }; [[nodiscard]] constexpr ProjectOpenDecision plan_project_open(bool has_unsaved_changes) noexcept { return has_unsaved_changes ? ProjectOpenDecision::prompt_discard_unsaved : ProjectOpenDecision::open_now; } [[nodiscard]] constexpr DocumentOpenPlanAction plan_document_open( DocumentOpenKind kind, bool has_unsaved_changes) noexcept { switch (kind) { case DocumentOpenKind::import_abr: return DocumentOpenPlanAction::prompt_import_abr; case DocumentOpenKind::import_ppbr: return DocumentOpenPlanAction::prompt_import_ppbr; case DocumentOpenKind::open_project: return has_unsaved_changes ? DocumentOpenPlanAction::prompt_discard_unsaved_project : DocumentOpenPlanAction::open_project_now; } return DocumentOpenPlanAction::open_project_now; } [[nodiscard]] inline pp::foundation::Status execute_document_open_plan( DocumentOpenPlanAction action, const DocumentOpenRoute& route, DocumentOpenServices& services) { switch (action) { case DocumentOpenPlanAction::open_project_now: if (route.kind != DocumentOpenKind::open_project) { return pp::foundation::Status::invalid_argument("open-project action requires a project route"); } services.open_project_now(route); return pp::foundation::Status::success(); case DocumentOpenPlanAction::prompt_discard_unsaved_project: if (route.kind != DocumentOpenKind::open_project) { return pp::foundation::Status::invalid_argument("discard prompt requires a project route"); } services.prompt_discard_unsaved_project(route); return pp::foundation::Status::success(); case DocumentOpenPlanAction::prompt_import_abr: if (route.kind != DocumentOpenKind::import_abr) { return pp::foundation::Status::invalid_argument("ABR import prompt requires an ABR route"); } services.prompt_import_abr(route); return pp::foundation::Status::success(); case DocumentOpenPlanAction::prompt_import_ppbr: if (route.kind != DocumentOpenKind::import_ppbr) { return pp::foundation::Status::invalid_argument("PPBR import prompt requires a PPBR route"); } services.prompt_import_ppbr(route); return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("unknown document open action"); } [[nodiscard]] constexpr CloseRequestDecision plan_close_request( bool has_unsaved_changes, bool close_prompt_already_open) noexcept { if (!has_unsaved_changes) { return CloseRequestDecision::close_now; } return close_prompt_already_open ? CloseRequestDecision::wait_for_existing_prompt : CloseRequestDecision::show_unsaved_prompt; } [[nodiscard]] constexpr DocumentSaveDecision plan_document_save( bool is_new_document, bool has_unsaved_changes, DocumentSaveIntent intent) noexcept { switch (intent) { case DocumentSaveIntent::save: if (is_new_document) { return DocumentSaveDecision::show_save_dialog; } return has_unsaved_changes ? DocumentSaveDecision::save_existing : DocumentSaveDecision::no_op; case DocumentSaveIntent::save_as: return DocumentSaveDecision::show_save_dialog; case DocumentSaveIntent::save_version: return is_new_document ? DocumentSaveDecision::show_save_dialog : DocumentSaveDecision::save_version; case DocumentSaveIntent::save_dirty_version: if (is_new_document) { return DocumentSaveDecision::show_save_dialog; } return has_unsaved_changes ? DocumentSaveDecision::save_version : DocumentSaveDecision::no_op; } return DocumentSaveDecision::no_op; } [[nodiscard]] constexpr DocumentWorkflowDecision plan_document_workflow( bool has_canvas, bool has_unsaved_changes) noexcept { if (!has_canvas) { return DocumentWorkflowDecision::unavailable; } return has_unsaved_changes ? DocumentWorkflowDecision::prompt_save_before_continue : DocumentWorkflowDecision::continue_now; } [[nodiscard]] inline pp::foundation::Result make_document_file_target( std::string_view work_directory, std::string_view document_name) { if (document_name.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document name must not be empty")); } DocumentFileTarget target; target.name = std::string(document_name); target.directory = std::string(work_directory); target.path.reserve(target.directory.size() + target.name.size() + 5); target.path += target.directory; target.path += "/"; target.path += target.name; target.path += ".ppi"; return pp::foundation::Result::success(std::move(target)); } [[nodiscard]] constexpr DocumentFileWriteDecision plan_document_file_write( bool target_exists) noexcept { return target_exists ? DocumentFileWriteDecision::prompt_overwrite : DocumentFileWriteDecision::save_now; } template [[nodiscard]] pp::foundation::Result plan_document_file_save( std::string_view work_directory, std::string_view document_name, ExistsPredicate&& exists) { auto target = make_document_file_target(work_directory, document_name); if (!target) { return pp::foundation::Result::failure(target.status()); } DocumentFileSavePlan plan; plan.target = std::move(target.value()); plan.write_decision = plan_document_file_write(exists(plan.target.path)); return pp::foundation::Result::success(std::move(plan)); } [[nodiscard]] constexpr pp::foundation::Result document_resolution_from_index(int index) noexcept { constexpr std::array resolutions{ 512, 1024, 1536, 2048, 4096, 8192 }; if (index < 0 || static_cast(index) >= resolutions.size()) { return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("document resolution index is out of range")); } return pp::foundation::Result::success(resolutions[static_cast(index)]); } template [[nodiscard]] pp::foundation::Result plan_new_document( std::string_view work_directory, std::string_view document_name, int resolution_index, ExistsPredicate&& exists) { const auto resolution = document_resolution_from_index(resolution_index); if (!resolution) { return pp::foundation::Result::failure(resolution.status()); } auto save_plan = plan_document_file_save( work_directory, document_name, std::forward(exists)); if (!save_plan) { return pp::foundation::Result::failure(save_plan.status()); } NewDocumentPlan plan; plan.target = std::move(save_plan.value().target); plan.resolution = resolution.value(); plan.write_decision = save_plan.value().write_decision; return pp::foundation::Result::success(std::move(plan)); } [[nodiscard]] inline bool has_legacy_two_character_version_suffix(std::string_view document_name) noexcept { const auto dot = document_name.rfind('.'); if (dot == std::string_view::npos || dot + 3 != document_name.size()) { return false; } const auto is_word = [](char ch) noexcept { return std::isalnum(static_cast(ch)) != 0 || ch == '_'; }; return is_word(document_name[dot + 1]) && is_word(document_name[dot + 2]); } [[nodiscard]] inline int legacy_version_number(std::string_view suffix) noexcept { int value = 0; for (const char ch : suffix) { if (ch < '0' || ch > '9') { break; } value = value * 10 + (ch - '0'); } return value; } [[nodiscard]] inline std::string make_legacy_version_name(std::string_view base_name, int version) { char suffix[4] {}; std::snprintf(suffix, sizeof(suffix), ".%02d", version); std::string name; name.reserve(base_name.size() + 3); name += base_name; name += suffix; return name; } template [[nodiscard]] pp::foundation::Result find_next_document_version_target( std::string_view directory, std::string_view document_name, ExistsPredicate&& exists) { if (directory.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("directory must not be empty")); } if (document_name.empty()) { return pp::foundation::Result::failure( pp::foundation::Status::invalid_argument("document name must not be empty")); } int current = 0; std::string_view base = document_name; if (has_legacy_two_character_version_suffix(document_name)) { const auto dot = document_name.rfind('.'); base = document_name.substr(0, dot); current = legacy_version_number(document_name.substr(dot + 1)); } for (int version = current + 1; version < 99; ++version) { DocumentVersionTarget target; target.name = make_legacy_version_name(base, version); target.path.reserve(directory.size() + target.name.size() + 5); target.path += directory; target.path += "/"; target.path += target.name; target.path += ".ppi"; if (!exists(target.path)) { return pp::foundation::Result::success(std::move(target)); } } return pp::foundation::Result::failure( pp::foundation::Status::out_of_range("no available document version target")); } }