568 lines
18 KiB
C++
568 lines
18 KiB
C++
#pragma once
|
|
|
|
#include "app_core/app_dialog.h"
|
|
#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,
|
|
};
|
|
|
|
enum class DocumentSessionPromptKind {
|
|
close_unsaved_document,
|
|
save_before_workflow_continue,
|
|
new_document_overwrite,
|
|
document_file_overwrite,
|
|
document_save_error,
|
|
};
|
|
|
|
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;
|
|
};
|
|
|
|
class CloseRequestServices {
|
|
public:
|
|
virtual ~CloseRequestServices() = default;
|
|
|
|
virtual void request_close_now() = 0;
|
|
virtual void show_unsaved_close_prompt() = 0;
|
|
};
|
|
|
|
class DocumentSaveServices {
|
|
public:
|
|
virtual ~DocumentSaveServices() = default;
|
|
|
|
virtual void show_save_dialog() = 0;
|
|
virtual void save_existing_document() = 0;
|
|
virtual void save_document_version() = 0;
|
|
};
|
|
|
|
class DocumentWorkflowServices {
|
|
public:
|
|
virtual ~DocumentWorkflowServices() = default;
|
|
|
|
virtual void continue_workflow_now() = 0;
|
|
virtual void prompt_save_before_continue() = 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;
|
|
};
|
|
|
|
class DocumentFileSaveServices {
|
|
public:
|
|
virtual ~DocumentFileSaveServices() = default;
|
|
|
|
virtual void save_document_file(const DocumentFileSavePlan& plan) = 0;
|
|
virtual void prompt_overwrite_document_file(const DocumentFileSavePlan& plan) = 0;
|
|
};
|
|
|
|
class DocumentVersionSaveServices {
|
|
public:
|
|
virtual ~DocumentVersionSaveServices() = default;
|
|
|
|
virtual void save_document_version(const DocumentVersionTarget& target) = 0;
|
|
};
|
|
|
|
struct NewDocumentPlan {
|
|
DocumentFileTarget target;
|
|
int resolution = 0;
|
|
DocumentFileWriteDecision write_decision = DocumentFileWriteDecision::save_now;
|
|
};
|
|
|
|
class NewDocumentServices {
|
|
public:
|
|
virtual ~NewDocumentServices() = default;
|
|
|
|
virtual void create_new_document(const NewDocumentPlan& plan) = 0;
|
|
virtual void prompt_overwrite_new_document(const NewDocumentPlan& plan) = 0;
|
|
};
|
|
|
|
[[nodiscard]] inline AppMessageDialogPlan plan_document_session_prompt(
|
|
DocumentSessionPromptKind kind,
|
|
std::string_view document_name = {})
|
|
{
|
|
switch (kind) {
|
|
case DocumentSessionPromptKind::close_unsaved_document:
|
|
return plan_app_message_dialog(
|
|
"Unsaved document",
|
|
"Do you want to close without saving?",
|
|
true,
|
|
"Yes",
|
|
"No");
|
|
case DocumentSessionPromptKind::save_before_workflow_continue:
|
|
return plan_app_message_dialog(
|
|
"Unsaved document",
|
|
"Would you like to save this document before closing?",
|
|
true,
|
|
"Yes",
|
|
"No");
|
|
case DocumentSessionPromptKind::new_document_overwrite:
|
|
return plan_app_message_dialog(
|
|
"Warning",
|
|
"A document with this name already exists, continue?",
|
|
true);
|
|
case DocumentSessionPromptKind::document_file_overwrite:
|
|
{
|
|
std::string message = "Are you sure you want to overwrite ";
|
|
message += document_name;
|
|
message += "?";
|
|
return plan_app_message_dialog("Warning", message, true);
|
|
}
|
|
case DocumentSessionPromptKind::document_save_error:
|
|
return plan_app_message_dialog(
|
|
"Saving Error",
|
|
"There was a problem saving the document",
|
|
false);
|
|
}
|
|
|
|
return plan_app_message_dialog("Warning", "", false);
|
|
}
|
|
|
|
[[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]] inline pp::foundation::Status execute_close_request_decision(
|
|
CloseRequestDecision decision,
|
|
CloseRequestServices& services)
|
|
{
|
|
switch (decision) {
|
|
case CloseRequestDecision::close_now:
|
|
services.request_close_now();
|
|
return pp::foundation::Status::success();
|
|
case CloseRequestDecision::show_unsaved_prompt:
|
|
services.show_unsaved_close_prompt();
|
|
return pp::foundation::Status::success();
|
|
case CloseRequestDecision::wait_for_existing_prompt:
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
return pp::foundation::Status::invalid_argument("unknown close request decision");
|
|
}
|
|
|
|
[[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]] inline pp::foundation::Status execute_document_save_decision(
|
|
DocumentSaveDecision decision,
|
|
DocumentSaveServices& services)
|
|
{
|
|
switch (decision) {
|
|
case DocumentSaveDecision::no_op:
|
|
return pp::foundation::Status::success();
|
|
case DocumentSaveDecision::show_save_dialog:
|
|
services.show_save_dialog();
|
|
return pp::foundation::Status::success();
|
|
case DocumentSaveDecision::save_existing:
|
|
services.save_existing_document();
|
|
return pp::foundation::Status::success();
|
|
case DocumentSaveDecision::save_version:
|
|
services.save_document_version();
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
return pp::foundation::Status::invalid_argument("unknown document save decision");
|
|
}
|
|
|
|
[[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::Status execute_document_workflow_decision(
|
|
DocumentWorkflowDecision decision,
|
|
DocumentWorkflowServices& services)
|
|
{
|
|
switch (decision) {
|
|
case DocumentWorkflowDecision::unavailable:
|
|
return pp::foundation::Status::success();
|
|
case DocumentWorkflowDecision::continue_now:
|
|
services.continue_workflow_now();
|
|
return pp::foundation::Status::success();
|
|
case DocumentWorkflowDecision::prompt_save_before_continue:
|
|
services.prompt_save_before_continue();
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
return pp::foundation::Status::invalid_argument("unknown document workflow decision");
|
|
}
|
|
|
|
[[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]] inline pp::foundation::Status execute_document_file_save_plan(
|
|
const DocumentFileSavePlan& plan,
|
|
DocumentFileSaveServices& services)
|
|
{
|
|
switch (plan.write_decision) {
|
|
case DocumentFileWriteDecision::save_now:
|
|
services.save_document_file(plan);
|
|
return pp::foundation::Status::success();
|
|
case DocumentFileWriteDecision::prompt_overwrite:
|
|
services.prompt_overwrite_document_file(plan);
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
return pp::foundation::Status::invalid_argument("unknown document file save write decision");
|
|
}
|
|
|
|
[[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 pp::foundation::Status execute_new_document_plan(
|
|
const NewDocumentPlan& plan,
|
|
NewDocumentServices& services)
|
|
{
|
|
switch (plan.write_decision) {
|
|
case DocumentFileWriteDecision::save_now:
|
|
services.create_new_document(plan);
|
|
return pp::foundation::Status::success();
|
|
case DocumentFileWriteDecision::prompt_overwrite:
|
|
services.prompt_overwrite_new_document(plan);
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
return pp::foundation::Status::invalid_argument("unknown new document write decision");
|
|
}
|
|
|
|
[[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"));
|
|
}
|
|
|
|
[[nodiscard]] inline pp::foundation::Status execute_document_version_save(
|
|
const DocumentVersionTarget& target,
|
|
DocumentVersionSaveServices& services)
|
|
{
|
|
if (target.name.empty() || target.path.empty()) {
|
|
return pp::foundation::Status::invalid_argument("document version target requires a name and path");
|
|
}
|
|
|
|
services.save_document_version(target);
|
|
return pp::foundation::Status::success();
|
|
}
|
|
|
|
}
|