Files
panopainter/src/app_core/document_session.h

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"));
}
}