#pragma once #include "app_core/canvas_tool_ui.h" #include "app_core/document_session.h" #include "app_core/history_ui.h" #include "foundation/result.h" namespace pp::app { enum class CanvasHotkeyEvent { key_down, key_up, touch_tap, }; enum class CanvasHotkeyKey { other, android_back, alt, e, s, tab, z, bracket_left, bracket_right, }; enum class CanvasHotkeyAction { none, select_tool, history, save_document, toggle_ui, adjust_brush_size, show_cursor, }; struct CanvasHotkeyState { bool ctrl_down = false; bool shift_down = false; bool mouse_focused = false; int undo_count = 0; int redo_count = 0; int touch_finger_count = 0; }; struct CanvasHotkeyPlan { CanvasHotkeyAction action = CanvasHotkeyAction::none; CanvasHotkeyEvent event = CanvasHotkeyEvent::key_up; CanvasHotkeyKey key = CanvasHotkeyKey::other; CanvasToolPlan tool; HistoryUiPlan history; DocumentSaveIntent save_intent = DocumentSaveIntent::save; float brush_size_delta = 0.0F; bool no_op = true; }; class CanvasHotkeyServices { public: virtual ~CanvasHotkeyServices() = default; virtual pp::foundation::Status execute_tool(const CanvasToolPlan& plan) = 0; virtual pp::foundation::Status execute_history(const HistoryUiPlan& plan) = 0; virtual void save_document(DocumentSaveIntent intent) = 0; virtual void toggle_ui() = 0; virtual void adjust_brush_size(float delta) = 0; virtual void show_cursor() = 0; }; [[nodiscard]] inline pp::foundation::Status validate_canvas_hotkey_state( const CanvasHotkeyState& state) noexcept { if (state.undo_count < 0) { return pp::foundation::Status::out_of_range("undo action count must not be negative"); } if (state.redo_count < 0) { return pp::foundation::Status::out_of_range("redo action count must not be negative"); } if (state.touch_finger_count < 0) { return pp::foundation::Status::out_of_range("touch finger count must not be negative"); } return pp::foundation::Status::success(); } [[nodiscard]] inline pp::foundation::Result plan_canvas_hotkey( CanvasHotkeyEvent event, CanvasHotkeyKey key, const CanvasHotkeyState& state) { const auto state_status = validate_canvas_hotkey_state(state); if (!state_status.ok()) { return pp::foundation::Result::failure(state_status); } CanvasHotkeyPlan plan; plan.event = event; plan.key = key; if (event == CanvasHotkeyEvent::touch_tap) { if (state.touch_finger_count == 2) { auto history = plan_history_undo(state.undo_count); if (!history) { return pp::foundation::Result::failure(history.status()); } plan.action = CanvasHotkeyAction::history; plan.history = history.value(); plan.no_op = plan.history.no_op; } return pp::foundation::Result::success(plan); } if (event == CanvasHotkeyEvent::key_down) { switch (key) { case CanvasHotkeyKey::e: plan.action = CanvasHotkeyAction::select_tool; plan.tool = plan_canvas_tool_select(CanvasToolMode::erase); plan.no_op = false; break; case CanvasHotkeyKey::android_back: { auto history = plan_history_undo(state.undo_count); if (!history) { return pp::foundation::Result::failure(history.status()); } plan.action = CanvasHotkeyAction::history; plan.history = history.value(); plan.no_op = plan.history.no_op; break; } case CanvasHotkeyKey::alt: if (state.mouse_focused) { plan.action = CanvasHotkeyAction::show_cursor; plan.no_op = false; } break; default: break; } return pp::foundation::Result::success(plan); } switch (key) { case CanvasHotkeyKey::e: plan.action = CanvasHotkeyAction::select_tool; plan.tool = plan_canvas_tool_select(CanvasToolMode::draw); plan.no_op = false; break; case CanvasHotkeyKey::tab: plan.action = CanvasHotkeyAction::toggle_ui; plan.no_op = false; break; case CanvasHotkeyKey::z: if (state.ctrl_down) { auto history = state.shift_down ? plan_history_redo(state.redo_count) : plan_history_undo(state.undo_count); if (!history) { return pp::foundation::Result::failure(history.status()); } plan.action = CanvasHotkeyAction::history; plan.history = history.value(); plan.no_op = plan.history.no_op; } break; case CanvasHotkeyKey::s: if (state.ctrl_down) { plan.action = CanvasHotkeyAction::save_document; plan.save_intent = state.shift_down ? DocumentSaveIntent::save_dirty_version : DocumentSaveIntent::save; plan.no_op = false; } break; case CanvasHotkeyKey::bracket_left: plan.action = CanvasHotkeyAction::adjust_brush_size; plan.brush_size_delta = -0.05F; plan.no_op = false; break; case CanvasHotkeyKey::bracket_right: plan.action = CanvasHotkeyAction::adjust_brush_size; plan.brush_size_delta = 0.05F; plan.no_op = false; break; default: break; } return pp::foundation::Result::success(plan); } [[nodiscard]] inline pp::foundation::Status execute_canvas_hotkey_plan( const CanvasHotkeyPlan& plan, CanvasHotkeyServices& services) { if (plan.no_op || plan.action == CanvasHotkeyAction::none) { return pp::foundation::Status::success(); } switch (plan.action) { case CanvasHotkeyAction::select_tool: return services.execute_tool(plan.tool); case CanvasHotkeyAction::history: return services.execute_history(plan.history); case CanvasHotkeyAction::save_document: services.save_document(plan.save_intent); return pp::foundation::Status::success(); case CanvasHotkeyAction::toggle_ui: services.toggle_ui(); return pp::foundation::Status::success(); case CanvasHotkeyAction::adjust_brush_size: if (plan.brush_size_delta == 0.0F) { return pp::foundation::Status::invalid_argument("brush-size hotkey plan must include a delta"); } services.adjust_brush_size(plan.brush_size_delta); return pp::foundation::Status::success(); case CanvasHotkeyAction::show_cursor: services.show_cursor(); return pp::foundation::Status::success(); case CanvasHotkeyAction::none: return pp::foundation::Status::success(); } return pp::foundation::Status::invalid_argument("unknown canvas hotkey action"); } } // namespace pp::app