369 lines
12 KiB
C++
369 lines
12 KiB
C++
#pragma once
|
|
|
|
#include "app_core/document_route.h"
|
|
#include "foundation/result.h"
|
|
|
|
#include <array>
|
|
#include <cctype>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <utility>
|
|
|
|
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<DocumentFileTarget> make_document_file_target(
|
|
std::string_view work_directory,
|
|
std::string_view document_name)
|
|
{
|
|
if (document_name.empty()) {
|
|
return pp::foundation::Result<DocumentFileTarget>::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<DocumentFileTarget>::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 <typename ExistsPredicate>
|
|
[[nodiscard]] pp::foundation::Result<DocumentFileSavePlan> 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<DocumentFileSavePlan>::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<DocumentFileSavePlan>::success(std::move(plan));
|
|
}
|
|
|
|
[[nodiscard]] constexpr pp::foundation::Result<int> document_resolution_from_index(int index) noexcept
|
|
{
|
|
constexpr std::array<int, 6> resolutions{ 512, 1024, 1536, 2048, 4096, 8192 };
|
|
if (index < 0 || static_cast<std::size_t>(index) >= resolutions.size()) {
|
|
return pp::foundation::Result<int>::failure(
|
|
pp::foundation::Status::out_of_range("document resolution index is out of range"));
|
|
}
|
|
|
|
return pp::foundation::Result<int>::success(resolutions[static_cast<std::size_t>(index)]);
|
|
}
|
|
|
|
template <typename ExistsPredicate>
|
|
[[nodiscard]] pp::foundation::Result<NewDocumentPlan> 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<NewDocumentPlan>::failure(resolution.status());
|
|
}
|
|
|
|
auto save_plan = plan_document_file_save(
|
|
work_directory,
|
|
document_name,
|
|
std::forward<ExistsPredicate>(exists));
|
|
if (!save_plan) {
|
|
return pp::foundation::Result<NewDocumentPlan>::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<NewDocumentPlan>::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<unsigned char>(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 <typename ExistsPredicate>
|
|
[[nodiscard]] pp::foundation::Result<DocumentVersionTarget> find_next_document_version_target(
|
|
std::string_view directory,
|
|
std::string_view document_name,
|
|
ExistsPredicate&& exists)
|
|
{
|
|
if (directory.empty()) {
|
|
return pp::foundation::Result<DocumentVersionTarget>::failure(
|
|
pp::foundation::Status::invalid_argument("directory must not be empty"));
|
|
}
|
|
|
|
if (document_name.empty()) {
|
|
return pp::foundation::Result<DocumentVersionTarget>::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<DocumentVersionTarget>::success(std::move(target));
|
|
}
|
|
}
|
|
|
|
return pp::foundation::Result<DocumentVersionTarget>::failure(
|
|
pp::foundation::Status::out_of_range("no available document version target"));
|
|
}
|
|
|
|
}
|