diff options
Diffstat (limited to 'src/platform/gui/osx')
-rw-r--r-- | src/platform/gui/osx/CMakeLists.txt | 15 | ||||
-rw-r--r-- | src/platform/gui/osx/Clipboard.mm | 46 | ||||
-rw-r--r-- | src/platform/gui/osx/ClipboardPrivate.h | 27 | ||||
-rw-r--r-- | src/platform/gui/osx/Cursor.mm | 93 | ||||
-rw-r--r-- | src/platform/gui/osx/CursorPrivate.h | 29 | ||||
-rw-r--r-- | src/platform/gui/osx/InputMethod.mm | 84 | ||||
-rw-r--r-- | src/platform/gui/osx/InputMethodPrivate.h | 64 | ||||
-rw-r--r-- | src/platform/gui/osx/Keyboard.mm | 283 | ||||
-rw-r--r-- | src/platform/gui/osx/KeyboardPrivate.h | 9 | ||||
-rw-r--r-- | src/platform/gui/osx/Menu.mm | 180 | ||||
-rw-r--r-- | src/platform/gui/osx/MenuPrivate.h | 65 | ||||
-rw-r--r-- | src/platform/gui/osx/Resource.cpp | 6 | ||||
-rw-r--r-- | src/platform/gui/osx/UiApplication.mm | 260 | ||||
-rw-r--r-- | src/platform/gui/osx/Window.mm | 800 | ||||
-rw-r--r-- | src/platform/gui/osx/WindowPrivate.h | 118 |
15 files changed, 2079 insertions, 0 deletions
diff --git a/src/platform/gui/osx/CMakeLists.txt b/src/platform/gui/osx/CMakeLists.txt new file mode 100644 index 00000000..5442ad15 --- /dev/null +++ b/src/platform/gui/osx/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(CruPlatformGuiOsx SHARED + Clipboard.mm + Cursor.mm + InputMethod.mm + Keyboard.mm + Menu.mm + Resource.cpp + UiApplication.mm + Window.mm +) + +find_library(APPKIT AppKit REQUIRED) +find_library(UNIFORMTYPEIDENTIFIERS UniformTypeIdentifiers REQUIRED) + +target_link_libraries(CruPlatformGuiOsx PUBLIC CruPlatformGui CruPlatformGraphicsQuartz ${APPKIT} ${UNIFORMTYPEIDENTIFIERS}) diff --git a/src/platform/gui/osx/Clipboard.mm b/src/platform/gui/osx/Clipboard.mm new file mode 100644 index 00000000..068771c8 --- /dev/null +++ b/src/platform/gui/osx/Clipboard.mm @@ -0,0 +1,46 @@ +#include "cru/platform/gui/osx/Clipboard.h" +#include "ClipboardPrivate.h" + +#include "cru/common/log/Logger.h" +#include "cru/platform/osx/Convert.h" + +#include <memory> + +namespace cru::platform::gui::osx { +using cru::platform::osx::Convert; + +OsxClipboard::OsxClipboard(cru::platform::gui::IUiApplication* ui_application, + std::unique_ptr<details::OsxClipboardPrivate> p) + : OsxGuiResource(ui_application), p_(std::move(p)) {} + +OsxClipboard::~OsxClipboard() {} + +String OsxClipboard::GetText() { return p_->GetText(); } + +void OsxClipboard::SetText(String text) { p_->SetText(text); } + +namespace details { +OsxClipboardPrivate::OsxClipboardPrivate(NSPasteboard* pasteboard) : pasteboard_(pasteboard) {} + +OsxClipboardPrivate::~OsxClipboardPrivate() {} + +String OsxClipboardPrivate::GetText() { + auto result = [pasteboard_ readObjectsForClasses:@[ NSString.class ] options:nil]; + if (result == nil) { + CRU_LOG_WARN(u"Failed to get text from clipboard"); + return u""; + } else { + if (result.count == 0) { + return u""; + } else { + return Convert((CFStringRef)result[0]); + } + } +} + +void OsxClipboardPrivate::SetText(String text) { + [pasteboard_ clearContents]; + [pasteboard_ writeObjects:@[ (NSString*)Convert(text) ]]; +} +} +} // namespace cru::platform::gui::osx diff --git a/src/platform/gui/osx/ClipboardPrivate.h b/src/platform/gui/osx/ClipboardPrivate.h new file mode 100644 index 00000000..e00c59dc --- /dev/null +++ b/src/platform/gui/osx/ClipboardPrivate.h @@ -0,0 +1,27 @@ +#pragma once +#include "cru/common/Base.h" +#include "cru/platform/gui/osx/Clipboard.h" + +#include <AppKit/AppKit.h> + +namespace cru::platform::gui::osx { +namespace details { +class OsxClipboardPrivate : public Object { + CRU_DEFINE_CLASS_LOG_TAG(u"OsxClipboardPrivate") + public: + explicit OsxClipboardPrivate(NSPasteboard* pasteboard); + + CRU_DELETE_COPY(OsxClipboardPrivate) + CRU_DELETE_MOVE(OsxClipboardPrivate) + + ~OsxClipboardPrivate(); + + public: + String GetText(); + void SetText(String text); + + private: + NSPasteboard* pasteboard_; +}; +} // namespace details +} // namespace cru::platform::gui::osx diff --git a/src/platform/gui/osx/Cursor.mm b/src/platform/gui/osx/Cursor.mm new file mode 100644 index 00000000..fae1514c --- /dev/null +++ b/src/platform/gui/osx/Cursor.mm @@ -0,0 +1,93 @@ +#include "cru/platform/gui/osx/Cursor.h" +#include "CursorPrivate.h" + +#include "cru/platform/osx/Exception.h" +#include "cru/platform/gui/osx/Resource.h" +#include "cru/platform/gui/Cursor.h" +#include "cru/platform/gui/UiApplication.h" + +#include <memory> + +namespace cru::platform::gui::osx { +namespace details { +OsxCursorPrivate::OsxCursorPrivate(OsxCursor* cursor, SystemCursorType cursor_type) { + cursor_ = cursor; + + switch (cursor_type) { + case SystemCursorType::Arrow: + ns_cursor_ = [NSCursor arrowCursor]; + break; + case SystemCursorType::Hand: + ns_cursor_ = [NSCursor pointingHandCursor]; + break; + case SystemCursorType::IBeam: + ns_cursor_ = [NSCursor IBeamCursor]; + break; + default: + throw Exception(u"Unknown system cursor type."); + } +} + +OsxCursorPrivate::~OsxCursorPrivate() {} +} + +OsxCursor::OsxCursor(IUiApplication* ui_application, SystemCursorType cursor_type) + : OsxGuiResource(ui_application) { + p_ = std::make_unique<details::OsxCursorPrivate>(this, cursor_type); +} + +OsxCursor::~OsxCursor() {} + +namespace details { +class OsxCursorManagerPrivate { + friend OsxCursorManager; + + public: + explicit OsxCursorManagerPrivate(OsxCursorManager* cursor_manager); + + CRU_DELETE_COPY(OsxCursorManagerPrivate) + CRU_DELETE_MOVE(OsxCursorManagerPrivate) + + ~OsxCursorManagerPrivate(); + + private: + OsxCursorManager* cursor_manager_; + + std::shared_ptr<OsxCursor> arrow_cursor_; + std::shared_ptr<OsxCursor> hand_cursor_; + std::shared_ptr<OsxCursor> ibeam_cursor_; +}; + +OsxCursorManagerPrivate::OsxCursorManagerPrivate(OsxCursorManager* cursor_manager) { + cursor_manager_ = cursor_manager; + arrow_cursor_ = + std::make_shared<OsxCursor>(cursor_manager->GetUiApplication(), SystemCursorType::Arrow); + hand_cursor_ = + std::make_shared<OsxCursor>(cursor_manager->GetUiApplication(), SystemCursorType::Hand); + ibeam_cursor_ = + std::make_shared<OsxCursor>(cursor_manager->GetUiApplication(), SystemCursorType::IBeam); +} + +OsxCursorManagerPrivate::~OsxCursorManagerPrivate() {} +} + +OsxCursorManager::OsxCursorManager(IUiApplication* ui_application) + : OsxGuiResource(ui_application) { + p_ = std::make_unique<details::OsxCursorManagerPrivate>(this); +} + +OsxCursorManager::~OsxCursorManager() {} + +std::shared_ptr<ICursor> OsxCursorManager::GetSystemCursor(SystemCursorType type) { + switch (type) { + case SystemCursorType::Arrow: + return p_->arrow_cursor_; + case SystemCursorType::Hand: + return p_->hand_cursor_; + case SystemCursorType::IBeam: + return p_->ibeam_cursor_; + default: + throw Exception(u"Unknown system cursor type."); + } +} +} diff --git a/src/platform/gui/osx/CursorPrivate.h b/src/platform/gui/osx/CursorPrivate.h new file mode 100644 index 00000000..2dcfed8f --- /dev/null +++ b/src/platform/gui/osx/CursorPrivate.h @@ -0,0 +1,29 @@ +#pragma once +#include "cru/platform/gui/osx/Cursor.h" + +#import <AppKit/NSCursor.h> + +namespace cru::platform::gui::osx { +class OsxWindow; + +namespace details { +class OsxWindowPrivate; + +class OsxCursorPrivate { + friend OsxWindow; + friend OsxWindowPrivate; + + public: + OsxCursorPrivate(OsxCursor* cursor, SystemCursorType cursor_type); + + CRU_DELETE_COPY(OsxCursorPrivate) + CRU_DELETE_MOVE(OsxCursorPrivate) + + ~OsxCursorPrivate(); + + private: + OsxCursor* cursor_; + NSCursor* ns_cursor_; +}; +} // namespace details +} // namespace cru::platform::gui::osx diff --git a/src/platform/gui/osx/InputMethod.mm b/src/platform/gui/osx/InputMethod.mm new file mode 100644 index 00000000..50ff80de --- /dev/null +++ b/src/platform/gui/osx/InputMethod.mm @@ -0,0 +1,84 @@ +#include "cru/platform/gui/osx/InputMethod.h" + +#import <AppKit/AppKit.h> +#include "InputMethodPrivate.h" +#include "WindowPrivate.h" +#include "cru/common/log/Logger.h" +#include "cru/platform/osx/Convert.h" +#include "cru/platform/gui/osx/Window.h" + +namespace cru::platform::gui::osx { +namespace details { +OsxInputMethodContextPrivate::OsxInputMethodContextPrivate( + OsxInputMethodContext* input_method_context, OsxWindow* window) { + input_method_context_ = input_method_context; + window_ = window; +} + +OsxInputMethodContextPrivate::~OsxInputMethodContextPrivate() {} + +void OsxInputMethodContextPrivate::RaiseCompositionStartEvent() { + composition_start_event_.Raise(nullptr); +} +void OsxInputMethodContextPrivate::RaiseCompositionEndEvent() { + composition_end_event_.Raise(nullptr); +} +void OsxInputMethodContextPrivate::RaiseCompositionEvent() { composition_event_.Raise(nullptr); } + +void OsxInputMethodContextPrivate::RaiseTextEvent(StringView text) { text_event_.Raise(text); } + +void OsxInputMethodContextPrivate::PerformSel(SEL sel) { + // [window_->p_->GetNSWindow() performSelector:sel]; +} + +void OsxInputMethodContextPrivate::Activate() { is_enabled_ = true; } + +void OsxInputMethodContextPrivate::Deactivate() { + input_method_context_->CompleteComposition(); + is_enabled_ = false; +} +} + +OsxInputMethodContext::OsxInputMethodContext(OsxWindow* window) + : OsxGuiResource(window->GetUiApplication()) { + p_ = std::make_unique<details::OsxInputMethodContextPrivate>(this, window); +} + +OsxInputMethodContext::~OsxInputMethodContext() {} + +void OsxInputMethodContext::EnableIME() { p_->Activate(); } + +void OsxInputMethodContext::DisableIME() { p_->Deactivate(); } + +bool OsxInputMethodContext::ShouldManuallyDrawCompositionText() { return true; } + +void OsxInputMethodContext::CompleteComposition() { + // TODO: Implement this. +} + +void OsxInputMethodContext::CancelComposition() { + [[NSTextInputContext currentInputContext] discardMarkedText]; +} + +CompositionText OsxInputMethodContext::GetCompositionText() { return p_->composition_text_; } + +void OsxInputMethodContext::SetCandidateWindowPosition(const Point& point) { + p_->SetCandidateWindowPosition(point); +} + +IEvent<std::nullptr_t>* OsxInputMethodContext::CompositionStartEvent() { + return &p_->composition_start_event_; +} + +IEvent<std::nullptr_t>* OsxInputMethodContext::CompositionEndEvent() { + return &p_->composition_end_event_; +} + +IEvent<std::nullptr_t>* OsxInputMethodContext::CompositionEvent() { + return &p_->composition_event_; +} + +IEvent<StringView>* OsxInputMethodContext::TextEvent() { return &p_->text_event_; } + +bool OsxInputMethodContext::IsEnabled() { return p_->is_enabled_; } +} diff --git a/src/platform/gui/osx/InputMethodPrivate.h b/src/platform/gui/osx/InputMethodPrivate.h new file mode 100644 index 00000000..ac2d1bf4 --- /dev/null +++ b/src/platform/gui/osx/InputMethodPrivate.h @@ -0,0 +1,64 @@ +#pragma once +#include "cru/platform/gui/osx/InputMethod.h" + +#include <AppKit/AppKit.h> + +namespace cru::platform::gui::osx { +namespace details { +class OsxInputMethodContextPrivate { + friend OsxInputMethodContext; + + public: + OsxInputMethodContextPrivate(OsxInputMethodContext* input_method_context, + OsxWindow* window); + + CRU_DELETE_COPY(OsxInputMethodContextPrivate) + CRU_DELETE_MOVE(OsxInputMethodContextPrivate) + + ~OsxInputMethodContextPrivate(); + + void SetCompositionText(CompositionText composition_text) { + composition_text_ = std::move(composition_text); + } + + void RaiseCompositionStartEvent(); + void RaiseCompositionEndEvent(); + void RaiseCompositionEvent(); + void RaiseTextEvent(StringView text); + + Point GetCandidateWindowPosition() const { return candidate_window_point_; } + void SetCandidateWindowPosition(const Point& p) { + candidate_window_point_ = p; + } + + Range GetSelectionRange() const { return selection_range_; } + void SetSelectionRange(Range selection_range) { + selection_range_ = selection_range; + } + + void PerformSel(SEL sel); + + void Activate(); + void Deactivate(); + + private: + OsxWindow* window_; + + CompositionText composition_text_; + + Range selection_range_; + + OsxInputMethodContext* input_method_context_; + + // On Osx, this is the text lefttop point on screen. + Point candidate_window_point_; + + Event<std::nullptr_t> composition_start_event_; + Event<std::nullptr_t> composition_event_; + Event<std::nullptr_t> composition_end_event_; + Event<StringView> text_event_; + + bool is_enabled_ = false; +}; +} // namespace details +} // namespace cru::platform::gui::osx diff --git a/src/platform/gui/osx/Keyboard.mm b/src/platform/gui/osx/Keyboard.mm new file mode 100644 index 00000000..d4489c96 --- /dev/null +++ b/src/platform/gui/osx/Keyboard.mm @@ -0,0 +1,283 @@ +#include "cru/platform/gui/osx/Keyboard.h" + +#import <AppKit/NSText.h> +#import <Carbon/Carbon.h> +#import "KeyboardPrivate.h" + +namespace cru::platform::gui::osx { +KeyCode KeyCodeFromOsxToCru(unsigned short n) { + switch (n) { +#define CRU_DEFINE_KEYCODE_MAP(osx, cru) \ + case osx: \ + return cru; + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_0, KeyCode::N0) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_1, KeyCode::N1) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_2, KeyCode::N2) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_3, KeyCode::N3) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_4, KeyCode::N4) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_5, KeyCode::N5) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_6, KeyCode::N6) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_7, KeyCode::N7) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_8, KeyCode::N8) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_9, KeyCode::N9) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_A, KeyCode::A) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_B, KeyCode::B) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_C, KeyCode::C) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_D, KeyCode::D) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_E, KeyCode::E) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_F, KeyCode::F) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_G, KeyCode::G) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_H, KeyCode::H) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_I, KeyCode::I) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_J, KeyCode::J) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_K, KeyCode::K) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_L, KeyCode::L) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_M, KeyCode::M) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_N, KeyCode::N) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_O, KeyCode::O) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_P, KeyCode::P) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Q, KeyCode::Q) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_R, KeyCode::R) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_S, KeyCode::S) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_T, KeyCode::T) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_U, KeyCode::U) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_V, KeyCode::V) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_W, KeyCode::W) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_X, KeyCode::X) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Y, KeyCode::Y) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Z, KeyCode::Z) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Comma, KeyCode::Comma) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Period, KeyCode::Period) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Slash, KeyCode::Slash) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Semicolon, KeyCode::Semicolon) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Quote, KeyCode::Quote) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_LeftBracket, KeyCode::LeftSquareBracket) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_RightBracket, KeyCode::RightSquareBracket) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Minus, KeyCode::Minus) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Equal, KeyCode::Equal) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Backslash, KeyCode::BackSlash) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Grave, KeyCode::GraveAccent) + CRU_DEFINE_KEYCODE_MAP(kVK_Escape, KeyCode::Escape) + CRU_DEFINE_KEYCODE_MAP(kVK_Tab, KeyCode::Tab) + CRU_DEFINE_KEYCODE_MAP(kVK_CapsLock, KeyCode::CapsLock) + CRU_DEFINE_KEYCODE_MAP(kVK_Shift, KeyCode::LeftShift) + CRU_DEFINE_KEYCODE_MAP(kVK_RightShift, KeyCode::RightShift) + CRU_DEFINE_KEYCODE_MAP(kVK_Control, KeyCode::LeftCtrl) + CRU_DEFINE_KEYCODE_MAP(kVK_RightControl, KeyCode::RightCtrl) + CRU_DEFINE_KEYCODE_MAP(kVK_Option, KeyCode::LeftAlt) + CRU_DEFINE_KEYCODE_MAP(kVK_RightOption, KeyCode::RightAlt) + CRU_DEFINE_KEYCODE_MAP(kVK_Command, KeyCode::LeftCommand) + CRU_DEFINE_KEYCODE_MAP(kVK_RightCommand, KeyCode::RightCommand) + CRU_DEFINE_KEYCODE_MAP(kVK_Delete, KeyCode::Backspace) + CRU_DEFINE_KEYCODE_MAP(kVK_Return, KeyCode::Return) + CRU_DEFINE_KEYCODE_MAP(kVK_ForwardDelete, KeyCode::Delete) + CRU_DEFINE_KEYCODE_MAP(kVK_Home, KeyCode::Home) + CRU_DEFINE_KEYCODE_MAP(kVK_End, KeyCode::End) + CRU_DEFINE_KEYCODE_MAP(kVK_PageUp, KeyCode::PageUp) + CRU_DEFINE_KEYCODE_MAP(kVK_PageDown, KeyCode::PageDown) + CRU_DEFINE_KEYCODE_MAP(kVK_LeftArrow, KeyCode::Left) + CRU_DEFINE_KEYCODE_MAP(kVK_RightArrow, KeyCode::Right) + CRU_DEFINE_KEYCODE_MAP(kVK_UpArrow, KeyCode::Up) + CRU_DEFINE_KEYCODE_MAP(kVK_DownArrow, KeyCode::Down) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad0, KeyCode::NumPad0) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad1, KeyCode::NumPad1) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad2, KeyCode::NumPad2) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad3, KeyCode::NumPad3) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad4, KeyCode::NumPad4) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad5, KeyCode::NumPad5) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad6, KeyCode::NumPad6) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad7, KeyCode::NumPad7) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad8, KeyCode::NumPad8) + CRU_DEFINE_KEYCODE_MAP(kVK_ANSI_Keypad9, KeyCode::NumPad9) + CRU_DEFINE_KEYCODE_MAP(kVK_Space, KeyCode::Space) + default: + return KeyCode::Unknown; + } + +#undef CRU_DEFINE_KEYCODE_MAP +} + +unsigned short KeyCodeFromCruToOsx(KeyCode k) { + switch (k) { +#define CRU_DEFINE_KEYCODE_MAP(cru, osx) \ + case cru: \ + return osx; + CRU_DEFINE_KEYCODE_MAP(KeyCode::N0, kVK_ANSI_0) + CRU_DEFINE_KEYCODE_MAP(KeyCode::N1, kVK_ANSI_1) + CRU_DEFINE_KEYCODE_MAP(KeyCode::N2, kVK_ANSI_2) + CRU_DEFINE_KEYCODE_MAP(KeyCode::N3, kVK_ANSI_3) + CRU_DEFINE_KEYCODE_MAP(KeyCode::N4, kVK_ANSI_4) + CRU_DEFINE_KEYCODE_MAP(KeyCode::N5, kVK_ANSI_5) + CRU_DEFINE_KEYCODE_MAP(KeyCode::N6, kVK_ANSI_6) + CRU_DEFINE_KEYCODE_MAP(KeyCode::N7, kVK_ANSI_7) + CRU_DEFINE_KEYCODE_MAP(KeyCode::N8, kVK_ANSI_8) + CRU_DEFINE_KEYCODE_MAP(KeyCode::N9, kVK_ANSI_9) + CRU_DEFINE_KEYCODE_MAP(KeyCode::A, kVK_ANSI_A) + CRU_DEFINE_KEYCODE_MAP(KeyCode::B, kVK_ANSI_B) + CRU_DEFINE_KEYCODE_MAP(KeyCode::C, kVK_ANSI_C) + CRU_DEFINE_KEYCODE_MAP(KeyCode::D, kVK_ANSI_D) + CRU_DEFINE_KEYCODE_MAP(KeyCode::E, kVK_ANSI_E) + CRU_DEFINE_KEYCODE_MAP(KeyCode::F, kVK_ANSI_F) + CRU_DEFINE_KEYCODE_MAP(KeyCode::G, kVK_ANSI_G) + CRU_DEFINE_KEYCODE_MAP(KeyCode::H, kVK_ANSI_H) + CRU_DEFINE_KEYCODE_MAP(KeyCode::I, kVK_ANSI_I) + CRU_DEFINE_KEYCODE_MAP(KeyCode::J, kVK_ANSI_J) + CRU_DEFINE_KEYCODE_MAP(KeyCode::K, kVK_ANSI_K) + CRU_DEFINE_KEYCODE_MAP(KeyCode::L, kVK_ANSI_L) + CRU_DEFINE_KEYCODE_MAP(KeyCode::M, kVK_ANSI_M) + CRU_DEFINE_KEYCODE_MAP(KeyCode::N, kVK_ANSI_N) + CRU_DEFINE_KEYCODE_MAP(KeyCode::O, kVK_ANSI_O) + CRU_DEFINE_KEYCODE_MAP(KeyCode::P, kVK_ANSI_P) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Q, kVK_ANSI_Q) + CRU_DEFINE_KEYCODE_MAP(KeyCode::R, kVK_ANSI_R) + CRU_DEFINE_KEYCODE_MAP(KeyCode::S, kVK_ANSI_S) + CRU_DEFINE_KEYCODE_MAP(KeyCode::T, kVK_ANSI_T) + CRU_DEFINE_KEYCODE_MAP(KeyCode::U, kVK_ANSI_U) + CRU_DEFINE_KEYCODE_MAP(KeyCode::V, kVK_ANSI_V) + CRU_DEFINE_KEYCODE_MAP(KeyCode::W, kVK_ANSI_W) + CRU_DEFINE_KEYCODE_MAP(KeyCode::X, kVK_ANSI_X) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Y, kVK_ANSI_Y) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Z, kVK_ANSI_Z) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Comma, kVK_ANSI_Comma) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Period, kVK_ANSI_Period) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Slash, kVK_ANSI_Slash) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Semicolon, kVK_ANSI_Semicolon) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Quote, kVK_ANSI_Quote) + CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftSquareBracket, kVK_ANSI_LeftBracket) + CRU_DEFINE_KEYCODE_MAP(KeyCode::RightSquareBracket, kVK_ANSI_RightBracket) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Minus, kVK_ANSI_Minus) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Equal, kVK_ANSI_Equal) + CRU_DEFINE_KEYCODE_MAP(KeyCode::BackSlash, kVK_ANSI_Backslash) + CRU_DEFINE_KEYCODE_MAP(KeyCode::GraveAccent, kVK_ANSI_Grave) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Escape, kVK_Escape) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Tab, kVK_Tab) + CRU_DEFINE_KEYCODE_MAP(KeyCode::CapsLock, kVK_CapsLock) + CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftShift, kVK_Shift) + CRU_DEFINE_KEYCODE_MAP(KeyCode::RightShift, kVK_RightShift) + CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftCtrl, kVK_Control) + CRU_DEFINE_KEYCODE_MAP(KeyCode::RightCtrl, kVK_RightControl) + CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftAlt, kVK_Option) + CRU_DEFINE_KEYCODE_MAP(KeyCode::RightAlt, kVK_RightOption) + CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftCommand, kVK_Command) + CRU_DEFINE_KEYCODE_MAP(KeyCode::RightCommand, kVK_RightCommand) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Backspace, kVK_Delete) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Return, kVK_Return) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Delete, kVK_ForwardDelete) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Home, kVK_Home) + CRU_DEFINE_KEYCODE_MAP(KeyCode::End, kVK_End) + CRU_DEFINE_KEYCODE_MAP(KeyCode::PageUp, kVK_PageUp) + CRU_DEFINE_KEYCODE_MAP(KeyCode::PageDown, kVK_PageDown) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Left, kVK_LeftArrow) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Right, kVK_RightArrow) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Up, kVK_UpArrow) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Down, kVK_DownArrow) + CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad0, kVK_ANSI_Keypad0) + CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad1, kVK_ANSI_Keypad1) + CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad2, kVK_ANSI_Keypad2) + CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad3, kVK_ANSI_Keypad3) + CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad4, kVK_ANSI_Keypad4) + CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad5, kVK_ANSI_Keypad5) + CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad6, kVK_ANSI_Keypad6) + CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad7, kVK_ANSI_Keypad7) + CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad8, kVK_ANSI_Keypad8) + CRU_DEFINE_KEYCODE_MAP(KeyCode::NumPad9, kVK_ANSI_Keypad9) + CRU_DEFINE_KEYCODE_MAP(KeyCode::Space, kVK_Space) + default: + return 0; + } +#undef CRU_DEFINE_KEYCODE_MAP +} + +NSString* ConvertKeyCodeToKeyEquivalent(KeyCode key_code) { +#define CRU_DEFINE_KEYCODE_MAP(key_code, str) \ + case key_code: \ + return str; + + switch (key_code) { + CRU_DEFINE_KEYCODE_MAP(KeyCode::A, @"a") + CRU_DEFINE_KEYCODE_MAP(KeyCode::B, @"b") + CRU_DEFINE_KEYCODE_MAP(KeyCode::C, @"c") + CRU_DEFINE_KEYCODE_MAP(KeyCode::D, @"d") + CRU_DEFINE_KEYCODE_MAP(KeyCode::E, @"e") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F, @"f") + CRU_DEFINE_KEYCODE_MAP(KeyCode::G, @"g") + CRU_DEFINE_KEYCODE_MAP(KeyCode::H, @"h") + CRU_DEFINE_KEYCODE_MAP(KeyCode::I, @"i") + CRU_DEFINE_KEYCODE_MAP(KeyCode::J, @"j") + CRU_DEFINE_KEYCODE_MAP(KeyCode::K, @"k") + CRU_DEFINE_KEYCODE_MAP(KeyCode::L, @"l") + CRU_DEFINE_KEYCODE_MAP(KeyCode::M, @"m") + CRU_DEFINE_KEYCODE_MAP(KeyCode::N, @"n") + CRU_DEFINE_KEYCODE_MAP(KeyCode::O, @"o") + CRU_DEFINE_KEYCODE_MAP(KeyCode::P, @"p") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Q, @"q") + CRU_DEFINE_KEYCODE_MAP(KeyCode::R, @"r") + CRU_DEFINE_KEYCODE_MAP(KeyCode::S, @"s") + CRU_DEFINE_KEYCODE_MAP(KeyCode::T, @"t") + CRU_DEFINE_KEYCODE_MAP(KeyCode::U, @"u") + CRU_DEFINE_KEYCODE_MAP(KeyCode::V, @"v") + CRU_DEFINE_KEYCODE_MAP(KeyCode::W, @"w") + CRU_DEFINE_KEYCODE_MAP(KeyCode::X, @"x") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Y, @"y") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Z, @"z") + CRU_DEFINE_KEYCODE_MAP(KeyCode::N0, @"0") + CRU_DEFINE_KEYCODE_MAP(KeyCode::N1, @"1") + CRU_DEFINE_KEYCODE_MAP(KeyCode::N2, @"2") + CRU_DEFINE_KEYCODE_MAP(KeyCode::N3, @"3") + CRU_DEFINE_KEYCODE_MAP(KeyCode::N4, @"4") + CRU_DEFINE_KEYCODE_MAP(KeyCode::N5, @"5") + CRU_DEFINE_KEYCODE_MAP(KeyCode::N6, @"6") + CRU_DEFINE_KEYCODE_MAP(KeyCode::N7, @"7") + CRU_DEFINE_KEYCODE_MAP(KeyCode::N8, @"8") + CRU_DEFINE_KEYCODE_MAP(KeyCode::N9, @"9") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F1, @"F1") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F2, @"F2") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F3, @"F3") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F4, @"F4") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F5, @"F5") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F6, @"F6") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F7, @"F7") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F8, @"F8") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F9, @"F9") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F10, @"F10") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F11, @"F11") + CRU_DEFINE_KEYCODE_MAP(KeyCode::F12, @"F12") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Minus, @"-") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Equal, @"=") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Comma, @",") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Period, @".") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Slash, @"/") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Semicolon, @";") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Quote, @"'") + CRU_DEFINE_KEYCODE_MAP(KeyCode::LeftSquareBracket, @"[") + CRU_DEFINE_KEYCODE_MAP(KeyCode::RightSquareBracket, @"]") + CRU_DEFINE_KEYCODE_MAP(KeyCode::BackSlash, @"\\") + CRU_DEFINE_KEYCODE_MAP(KeyCode::GraveAccent, @"`") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Return, @"\n") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Escape, @"\e") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Tab, @"\t") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Backspace, @"\x08") + CRU_DEFINE_KEYCODE_MAP(KeyCode::Delete, @"\x7F") + default: + throw Exception(u"Failed to convert key code to key equivalent string."); + } +#undef CRU_DEFINE_KEYCODE_MAP +} + +NSEventModifierFlags ConvertKeyModifier(KeyModifier k) { + NSEventModifierFlags flags = 0; + if (k & KeyModifiers::shift) { + flags |= NSEventModifierFlagShift; + } + if (k & KeyModifiers::ctrl) { + flags |= NSEventModifierFlagControl; + } + if (k & KeyModifiers::alt) { + flags |= NSEventModifierFlagOption; + } + if (k & KeyModifiers::command) { + flags |= NSEventModifierFlagCommand; + } + return flags; +} +} diff --git a/src/platform/gui/osx/KeyboardPrivate.h b/src/platform/gui/osx/KeyboardPrivate.h new file mode 100644 index 00000000..4bf53cc4 --- /dev/null +++ b/src/platform/gui/osx/KeyboardPrivate.h @@ -0,0 +1,9 @@ +#pragma once +#include "cru/platform/gui/osx/Keyboard.h" + +#import <AppKit/NSEvent.h> + +namespace cru::platform::gui::osx { +NSString* ConvertKeyCodeToKeyEquivalent(KeyCode key_code); +NSEventModifierFlags ConvertKeyModifier(KeyModifier k); +} diff --git a/src/platform/gui/osx/Menu.mm b/src/platform/gui/osx/Menu.mm new file mode 100644 index 00000000..568a5208 --- /dev/null +++ b/src/platform/gui/osx/Menu.mm @@ -0,0 +1,180 @@ +#include "cru/platform/gui/osx/Menu.h" +#import "MenuPrivate.h" + +#include "KeyboardPrivate.h" +#include "cru/common/platform/osx/Convert.h" + +#import <AppKit/NSApplication.h> + +namespace cru::platform::gui::osx { +using platform::osx::Convert; + +namespace { +std::unique_ptr<OsxMenu> application_menu = nullptr; +} + +namespace details { +OsxMenuItemPrivate::OsxMenuItemPrivate(OsxMenuItem* d) { + d_ = d; + sub_menu_ = new OsxMenu(d->GetUiApplication()); + sub_menu_->p_->SetParentItem(d); + handler_ = [[CruOsxMenuItemClickHandler alloc] init:this]; +} + +OsxMenuItemPrivate::~OsxMenuItemPrivate() { delete sub_menu_; } + +void OsxMenuItemPrivate::AttachToNative(NSMenuItem* native_menu_item, bool check_submenu) { + Expects(sub_menu_); + + menu_item_ = native_menu_item; + [native_menu_item setTarget:handler_]; + [native_menu_item setAction:@selector(handleClick)]; + if (check_submenu && [native_menu_item hasSubmenu]) { + sub_menu_->p_->AttachToNative([native_menu_item submenu]); + } +} + +OsxMenuPrivate::OsxMenuPrivate(OsxMenu* d) { d_ = d; } + +OsxMenuPrivate::~OsxMenuPrivate() { + for (auto item : items_) { + delete item; + } +} + +void OsxMenuPrivate::AttachToNative(NSMenu* native_menu) { + menu_ = native_menu; + + auto item_count = [native_menu numberOfItems]; + for (int i = 0; i < item_count; i++) { + auto native_item = [native_menu itemAtIndex:i]; + auto item = new OsxMenuItem(d_->GetUiApplication()); + item->p_->SetParentMenu(d_); + item->p_->AttachToNative(native_item, true); + items_.push_back(item); + } +} +} + +OsxMenuItem::OsxMenuItem(IUiApplication* ui_application) : OsxGuiResource(ui_application) { + p_ = new details::OsxMenuItemPrivate(this); +} + +OsxMenuItem::~OsxMenuItem() { delete p_; } + +String OsxMenuItem::GetTitle() { return Convert((CFStringRef)[p_->menu_item_ title]); } + +void OsxMenuItem::SetTitle(String title) { [p_->menu_item_ setTitle:(NSString*)Convert(title)]; } + +bool OsxMenuItem::IsEnabled() { return [p_->menu_item_ isEnabled]; } + +void OsxMenuItem::SetEnabled(bool enabled) { [p_->menu_item_ setEnabled:enabled]; } + +IMenu* OsxMenuItem::GetParentMenu() { return p_->parent_menu_; } + +IMenu* OsxMenuItem::GetSubmenu() { return p_->sub_menu_; } + +void OsxMenuItem::SetKeyboardShortcut(KeyCode key, KeyModifier modifiers) { + [p_->menu_item_ setKeyEquivalent:ConvertKeyCodeToKeyEquivalent(key)]; + [p_->menu_item_ setKeyEquivalentModifierMask:ConvertKeyModifier(modifiers)]; +} + +void OsxMenuItem::DeleteKeyboardShortcut() { + [p_->menu_item_ setKeyEquivalent:@""]; + [p_->menu_item_ setKeyEquivalentModifierMask:0]; +} + +void OsxMenuItem::SetOnClickHandler(std::function<void()> handler) { + p_->on_click_handler_ = std::move(handler); +} + +OsxMenu* OsxMenu::CreateOrGetApplicationMenu(IUiApplication* ui_application) { + if (application_menu) { + return application_menu.get(); + } + + NSMenu* native_main_menu = [[NSMenu alloc] init]; + [NSApp setMainMenu:native_main_menu]; + [native_main_menu setAutoenablesItems:NO]; + + application_menu.reset(new OsxMenu(ui_application)); + application_menu->p_->AttachToNative(native_main_menu); + + application_menu->CreateItemAt(0); + + return application_menu.get(); +} + +OsxMenu::OsxMenu(IUiApplication* ui_application) : OsxGuiResource(ui_application) { + p_ = new details::OsxMenuPrivate(this); +} + +OsxMenu::~OsxMenu() { delete p_; } + +IMenuItem* OsxMenu::GetItemAt(int index) { + if (index < 0 || index >= p_->items_.size()) { + return nullptr; + } + + return p_->items_[index]; +} + +int OsxMenu::GetItemCount() { return p_->items_.size(); } + +IMenuItem* OsxMenu::CreateItemAt(int index) { + if (index < 0) index = 0; + if (index > p_->items_.size()) index = p_->items_.size(); + + if (p_->parent_item_ && p_->items_.empty()) { + Expects(p_->menu_ == nullptr); + p_->menu_ = [[NSMenu alloc] init]; + [p_->menu_ setAutoenablesItems:NO]; + [p_->parent_item_->p_->GetNative() setSubmenu:p_->menu_]; + } + + auto native_item = [[NSMenuItem alloc] init]; + [p_->menu_ insertItem:native_item atIndex:index]; + + auto item = new OsxMenuItem(GetUiApplication()); + item->p_->SetParentMenu(this); + item->p_->AttachToNative(native_item, false); + p_->items_.insert(p_->items_.begin() + index, item); + + return item; +} + +void OsxMenu::RemoveItemAt(int index) { + if (index < 0 || index >= p_->items_.size()) { + return; + } + + auto item = p_->items_[index]; + [p_->menu_ removeItem:item->p_->GetNative()]; + p_->items_.erase(p_->items_.begin() + index); + + delete item; + + if (p_->items_.empty() && p_->parent_item_) { + Expects(p_->menu_ != nullptr); + [p_->parent_item_->p_->GetNative() setSubmenu:nullptr]; + p_->menu_ = nullptr; + } +} +} + +@implementation CruOsxMenuItemClickHandler { + cru::platform::gui::osx::details::OsxMenuItemPrivate* p_; +} + +- (id)init:(cru::platform::gui::osx::details::OsxMenuItemPrivate*)p { + p_ = p; + return self; +} + +- (void)handleClick { + if (p_->GetOnClickHandler()) { + p_->GetOnClickHandler()(); + } +} + +@end diff --git a/src/platform/gui/osx/MenuPrivate.h b/src/platform/gui/osx/MenuPrivate.h new file mode 100644 index 00000000..cda8216b --- /dev/null +++ b/src/platform/gui/osx/MenuPrivate.h @@ -0,0 +1,65 @@ +#pragma once +#include "cru/platform/gui/osx/Menu.h" + +#import <AppKit/NSMenu.h> +#import <AppKit/NSMenuItem.h> + +@interface CruOsxMenuItemClickHandler : NSObject +- init:(cru::platform::gui::osx::details::OsxMenuItemPrivate*)p; +- (void)handleClick; +@end + +namespace cru::platform::gui::osx { +namespace details { + +class OsxMenuItemPrivate { + friend OsxMenuItem; + + public: + explicit OsxMenuItemPrivate(OsxMenuItem* d); + + CRU_DELETE_COPY(OsxMenuItemPrivate) + CRU_DELETE_MOVE(OsxMenuItemPrivate) + + ~OsxMenuItemPrivate(); + + public: + NSMenuItem* GetNative() { return menu_item_; } + void SetParentMenu(OsxMenu* menu) { parent_menu_ = menu; } + void AttachToNative(NSMenuItem* native_menu_item, bool check_submenu); + + const std::function<void()> GetOnClickHandler() const { return on_click_handler_; } + + private: + OsxMenuItem* d_; + OsxMenu* parent_menu_ = nullptr; + NSMenuItem* menu_item_ = nullptr; + OsxMenu* sub_menu_ = nullptr; + std::function<void()> on_click_handler_; + CruOsxMenuItemClickHandler* handler_; +}; + +class OsxMenuPrivate { + friend OsxMenu; + + public: + explicit OsxMenuPrivate(OsxMenu* d); + + CRU_DELETE_COPY(OsxMenuPrivate) + CRU_DELETE_MOVE(OsxMenuPrivate) + + ~OsxMenuPrivate(); + + public: + void SetParentItem(OsxMenuItem* item) { parent_item_ = item; } + void AttachToNative(NSMenu* native_menu); + + private: + OsxMenu* d_; + OsxMenuItem* parent_item_ = nullptr; + NSMenu* menu_ = nullptr; + std::vector<OsxMenuItem*> items_; +}; +} // namespace details + +} // namespace cru::platform::gui::osx diff --git a/src/platform/gui/osx/Resource.cpp b/src/platform/gui/osx/Resource.cpp new file mode 100644 index 00000000..d33133c7 --- /dev/null +++ b/src/platform/gui/osx/Resource.cpp @@ -0,0 +1,6 @@ +#include "cru/platform/gui/osx/Resource.h" + +namespace cru::platform::gui::osx { +OsxGuiResource::OsxGuiResource(IUiApplication* ui_application) + : ui_application_(ui_application) {} +} // namespace cru::platform::gui::osx diff --git a/src/platform/gui/osx/UiApplication.mm b/src/platform/gui/osx/UiApplication.mm new file mode 100644 index 00000000..ef62af58 --- /dev/null +++ b/src/platform/gui/osx/UiApplication.mm @@ -0,0 +1,260 @@ +#include "cru/platform/gui/osx/UiApplication.h" + +#include "ClipboardPrivate.h" +#include "cru/common/log/Logger.h" +#include "cru/common/platform/osx/Convert.h" +#include "cru/platform/graphics/quartz/Factory.h" +#include "cru/platform/gui/osx/Clipboard.h" +#include "cru/platform/gui/osx/Cursor.h" +#include "cru/platform/gui/osx/Menu.h" +#include "cru/platform/gui/osx/Window.h" +#include "cru/platform/graphics/Factory.h" +#include "cru/platform/gui/Base.h" +#include "cru/platform/gui/UiApplication.h" +#include "cru/platform/gui/Window.h" + +#include <AppKit/NSApplication.h> +#include <Foundation/NSRunLoop.h> +#include <UniformTypeIdentifiers/UTType.h> + +#include <algorithm> +#include <iterator> +#include <memory> +#include <unordered_map> +#include <vector> + +@interface CruAppDelegate : NSObject <NSApplicationDelegate> +- (id)init:(cru::platform::gui::osx::details::OsxUiApplicationPrivate*)p; +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender; +- (void)applicationWillTerminate:(NSNotification*)notification; +@end + +namespace cru::platform::gui::osx { + +using cru::platform::osx::Convert; + +namespace details { +class OsxUiApplicationPrivate { + friend OsxUiApplication; + + public: + explicit OsxUiApplicationPrivate(OsxUiApplication* osx_ui_application) + : osx_ui_application_(osx_ui_application) { + app_delegate_ = [[CruAppDelegate alloc] init:this]; + } + + CRU_DELETE_COPY(OsxUiApplicationPrivate) + CRU_DELETE_MOVE(OsxUiApplicationPrivate) + + ~OsxUiApplicationPrivate() = default; + + void CallQuitHandlers(); + + private: + OsxUiApplication* osx_ui_application_; + CruAppDelegate* app_delegate_; + std::vector<std::function<void()>> quit_handlers_; + bool quit_on_all_window_closed_ = true; + + long long current_timer_id_ = 1; + std::unordered_map<long long, std::function<void()>> next_tick_; + std::unordered_map<long long, NSTimer*> timers_; + + std::vector<OsxWindow*> windows_; + + std::unique_ptr<OsxCursorManager> cursor_manager_; + + std::unique_ptr<OsxClipboard> clipboard_; + + std::unique_ptr<platform::graphics::quartz::QuartzGraphicsFactory> quartz_graphics_factory_; +}; + +void OsxUiApplicationPrivate::CallQuitHandlers() { + for (const auto& handler : quit_handlers_) { + handler(); + } +} +} + +OsxUiApplication::OsxUiApplication() + : OsxGuiResource(this), p_(new details::OsxUiApplicationPrivate(this)) { + [NSApplication sharedApplication]; + + [NSApp setDelegate:p_->app_delegate_]; + p_->quartz_graphics_factory_ = std::make_unique<graphics::quartz::QuartzGraphicsFactory>(); + p_->cursor_manager_ = std::make_unique<OsxCursorManager>(this); + p_->clipboard_ = std::make_unique<OsxClipboard>( + this, std::make_unique<details::OsxClipboardPrivate>([NSPasteboard generalPasteboard])); +} + +OsxUiApplication::~OsxUiApplication() {} + +int OsxUiApplication::Run() { + [NSApp run]; + return 0; +} + +void OsxUiApplication::RequestQuit(int quit_code) { + [NSApp terminate:[NSNumber numberWithInteger:quit_code]]; +} + +void OsxUiApplication::AddOnQuitHandler(std::function<void()> handler) { + p_->quit_handlers_.push_back(std::move(handler)); +} + +bool OsxUiApplication::IsQuitOnAllWindowClosed() { return p_->quit_on_all_window_closed_; } + +void OsxUiApplication::SetQuitOnAllWindowClosed(bool quit_on_all_window_closed) { + p_->quit_on_all_window_closed_ = quit_on_all_window_closed; +} + +long long OsxUiApplication::SetImmediate(std::function<void()> action) { + const long long id = p_->current_timer_id_++; + p_->next_tick_.emplace(id, std::move(action)); + + [[NSRunLoop mainRunLoop] performBlock:^{ + const auto i = p_->next_tick_.find(id); + if (i != p_->next_tick_.cend()) { + i->second(); + } + p_->next_tick_.erase(i); + }]; + + return id; +} + +long long OsxUiApplication::SetTimeout(std::chrono::milliseconds milliseconds, + std::function<void()> action) { + long long id = p_->current_timer_id_++; + p_->timers_.emplace(id, [NSTimer scheduledTimerWithTimeInterval:milliseconds.count() / 1000.0 + repeats:false + block:^(NSTimer* timer) { + action(); + p_->timers_.erase(id); + }]); + + return id; +} + +long long OsxUiApplication::SetInterval(std::chrono::milliseconds milliseconds, + std::function<void()> action) { + long long id = p_->current_timer_id_++; + p_->timers_.emplace(id, [NSTimer scheduledTimerWithTimeInterval:milliseconds.count() / 1000.0 + repeats:true + block:^(NSTimer* timer) { + action(); + }]); + + return id; +} + +void OsxUiApplication::CancelTimer(long long id) { + p_->next_tick_.erase(id); + auto i = p_->timers_.find(id); + if (i != p_->timers_.cend()) { + [i->second invalidate]; + p_->timers_.erase(i); + } +} + +std::vector<INativeWindow*> OsxUiApplication::GetAllWindow() { + std::vector<INativeWindow*> result; + std::transform(p_->windows_.cbegin(), p_->windows_.cend(), std::back_inserter(result), + [](OsxWindow* w) { return static_cast<INativeWindow*>(w); }); + return result; +} + +INativeWindow* OsxUiApplication::CreateWindow() { + auto window = new OsxWindow(this); + p_->windows_.push_back(window); + return window; +} + +ICursorManager* OsxUiApplication::GetCursorManager() { return p_->cursor_manager_.get(); } + +IClipboard* OsxUiApplication::GetClipboard() { return p_->clipboard_.get(); } + +IMenu* OsxUiApplication::GetApplicationMenu() { return OsxMenu::CreateOrGetApplicationMenu(this); } + +graphics::IGraphicsFactory* OsxUiApplication::GetGraphicsFactory() { + return p_->quartz_graphics_factory_.get(); +} + +std::optional<String> OsxUiApplication::ShowSaveDialog(SaveDialogOptions options) { + NSSavePanel* panel = [NSSavePanel savePanel]; + [panel setTitle:(NSString*)Convert(options.title)]; + [panel setPrompt:(NSString*)Convert(options.prompt)]; + [panel setMessage:(NSString*)Convert(options.message)]; + + NSMutableArray* allowed_content_types = [NSMutableArray array]; + + for (const auto& file_type : options.allowed_file_types) { + [allowed_content_types + addObject:[UTType typeWithFilenameExtension:(NSString*)Convert(file_type)]]; + } + + [panel setAllowedContentTypes:allowed_content_types]; + [panel setAllowsOtherFileTypes:options.allow_all_file_types]; + + auto model_result = [panel runModal]; + if (model_result == NSModalResponseOK) { + return Convert((CFStringRef)[[panel URL] path]); + } else { + return std::nullopt; + } +} + +std::optional<std::vector<String>> OsxUiApplication::ShowOpenDialog(OpenDialogOptions options) { + NSOpenPanel* panel = [NSOpenPanel openPanel]; + [panel setTitle:(NSString*)Convert(options.title)]; + [panel setPrompt:(NSString*)Convert(options.prompt)]; + [panel setMessage:(NSString*)Convert(options.message)]; + + NSMutableArray* allowed_content_types = [NSMutableArray array]; + + for (const auto& file_type : options.allowed_file_types) { + [allowed_content_types + addObject:[UTType typeWithFilenameExtension:(NSString*)Convert(file_type)]]; + } + + [panel setAllowedContentTypes:allowed_content_types]; + [panel setAllowsOtherFileTypes:options.allow_all_file_types]; + + [panel setCanChooseFiles:options.can_choose_files]; + [panel setCanChooseDirectories:options.can_choose_directories]; + [panel setAllowsMultipleSelection:options.allow_mulitple_selection]; + + auto model_result = [panel runModal]; + if (model_result == NSModalResponseOK) { + std::vector<String> result; + for (NSURL* url in [panel URLs]) { + result.push_back(Convert((CFStringRef)[url path])); + } + return result; + } else { + return std::nullopt; + } +} + +void OsxUiApplication::UnregisterWindow(OsxWindow* window) { + p_->windows_.erase( + std::remove(p_->windows_.begin(), p_->windows_.end(), static_cast<INativeWindow*>(window)), + p_->windows_.cend()); +} +} + +@implementation CruAppDelegate { + cru::platform::gui::osx::details::OsxUiApplicationPrivate* _p; +} + +- (id)init:(cru::platform::gui::osx::details::OsxUiApplicationPrivate*)p { + _p = p; + return self; +} +- (NSApplicationTerminateReply)applicationShouldTerminate:(NSApplication*)sender { + return NSApplicationTerminateReply::NSTerminateNow; +} +- (void)applicationWillTerminate:(NSNotification*)notification { + _p->CallQuitHandlers(); +} +@end diff --git a/src/platform/gui/osx/Window.mm b/src/platform/gui/osx/Window.mm new file mode 100644 index 00000000..2c55d2dd --- /dev/null +++ b/src/platform/gui/osx/Window.mm @@ -0,0 +1,800 @@ +#include "cru/platform/gui/osx/Window.h" +#include "WindowPrivate.h" + +#include "CursorPrivate.h" +#include "InputMethodPrivate.h" +#include "cru/common/Range.h" +#include "cru/common/log/Logger.h" +#include "cru/platform/osx/Convert.h" +#include "cru/platform/graphics/quartz/Convert.h" +#include "cru/platform/graphics/quartz/Painter.h" +#include "cru/platform/gui/osx/Cursor.h" +#include "cru/platform/gui/osx/InputMethod.h" +#include "cru/platform/gui/osx/Keyboard.h" +#include "cru/platform/gui/osx/Resource.h" +#include "cru/platform/gui/osx/UiApplication.h" +#include "cru/platform/Check.h" +#include "cru/platform/graphics/NullPainter.h" +#include "cru/platform/gui/TimerHelper.h" + +#include <AppKit/AppKit.h> +#include <Foundation/Foundation.h> + +#include <limits> +#include <memory> +#include <unordered_set> + +namespace { +constexpr int key_down_debug = 0; +} + +using cru::platform::osx::Convert; +using cru::platform::graphics::quartz::Convert; + +namespace cru::platform::gui::osx { +namespace { +inline NSWindowStyleMask CalcWindowStyleMask(WindowStyleFlag flag) { + return flag & WindowStyleFlags::NoCaptionAndBorder + ? NSWindowStyleMaskBorderless + : NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; +} +} + +namespace details { +OsxWindowPrivate::OsxWindowPrivate(OsxWindow* osx_window) : osx_window_(osx_window) { + window_delegate_ = [[CruWindowDelegate alloc] init:this]; + + content_rect_ = {100, 100, 400, 200}; + + input_method_context_ = std::make_unique<OsxInputMethodContext>(osx_window); +} + +OsxWindowPrivate::~OsxWindowPrivate() {} + +void OsxWindowPrivate::OnWindowWillClose() { + if (window_) destroy_event_.Raise(nullptr); + window_ = nil; + CGLayerRelease(draw_layer_); + draw_layer_ = nullptr; + + if (osx_window_->GetUiApplication()->IsQuitOnAllWindowClosed()) { + const auto& all_window = osx_window_->GetUiApplication()->GetAllWindow(); + + bool quit = true; + + for (auto window : all_window) { + auto w = CheckPlatform<OsxWindow>(window, osx_window_->GetPlatformId()); + if (w->p_->window_) { + quit = false; + break; + } + } + + if (quit) { + osx_window_->GetUiApplication()->RequestQuit(0); + } + } +} + +void OsxWindowPrivate::OnWindowDidExpose() { osx_window_->RequestRepaint(); } +void OsxWindowPrivate::OnWindowDidUpdate() {} +void OsxWindowPrivate::OnWindowDidMove() { content_rect_ = RetrieveContentRect(); } + +void OsxWindowPrivate::OnWindowDidResize() { + content_rect_ = RetrieveContentRect(); + + auto view = [window_ contentView]; + [view removeTrackingArea:[view trackingAreas][0]]; + auto tracking_area = [[NSTrackingArea alloc] + initWithRect:CGRectMake(0, 0, content_rect_.width, content_rect_.height) + options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways) + owner:view + userInfo:nil]; + [view addTrackingArea:tracking_area]; + + CGLayerRelease(draw_layer_); + draw_layer_ = CreateLayer(Convert(content_rect_.GetSize())); + + resize_event_.Raise(osx_window_->GetClientSize()); + + osx_window_->RequestRepaint(); +} + +void OsxWindowPrivate::OnBecomeKeyWindow() { focus_event_.Raise(FocusChangeType::Gain); } + +void OsxWindowPrivate::OnResignKeyWindow() { focus_event_.Raise(FocusChangeType::Lose); } + +void OsxWindowPrivate::OnMouseEnterLeave(MouseEnterLeaveType type) { + mouse_enter_leave_event_.Raise(type); + if (type == MouseEnterLeaveType::Enter) { + mouse_in_ = true; + UpdateCursor(); + } else { + mouse_in_ = false; + } +} + +void OsxWindowPrivate::OnMouseMove(Point p) { mouse_move_event_.Raise(TransformMousePoint(p)); } + +void OsxWindowPrivate::OnMouseDown(MouseButton button, Point p, KeyModifier key_modifier) { + mouse_down_event_.Raise({button, TransformMousePoint(p), key_modifier}); +} + +void OsxWindowPrivate::OnMouseUp(MouseButton button, Point p, KeyModifier key_modifier) { + mouse_up_event_.Raise({button, TransformMousePoint(p), key_modifier}); +} + +void OsxWindowPrivate::OnMouseWheel(float delta, Point p, KeyModifier key_modifier, + bool horizontal) { + mouse_wheel_event_.Raise({delta, TransformMousePoint(p), key_modifier, horizontal}); +} + +void OsxWindowPrivate::OnKeyDown(KeyCode key, KeyModifier key_modifier) { + key_down_event_.Raise({key, key_modifier}); +} + +void OsxWindowPrivate::OnKeyUp(KeyCode key, KeyModifier key_modifier) { + key_up_event_.Raise({key, key_modifier}); +} + +CGLayerRef OsxWindowPrivate::CreateLayer(const CGSize& size) { + auto s = size; + if (s.width == 0) s.width = 1; + if (s.height == 0) s.height = 1; + + auto draw_layer = CGLayerCreateWithContext(nullptr, s, nullptr); + Ensures(draw_layer); + + return draw_layer; +} + +void OsxWindowPrivate::UpdateCursor() { + auto cursor = cursor_ == nullptr + ? std::dynamic_pointer_cast<OsxCursor>( + osx_window_->GetUiApplication()->GetCursorManager()->GetSystemCursor( + SystemCursorType::Arrow)) + : cursor_; + + [cursor->p_->ns_cursor_ set]; +} + +Point OsxWindowPrivate::TransformMousePoint(const Point& point) { + Point r = point; + r.y = content_rect_.height - r.y; + return r; +} + +void OsxWindowPrivate::CreateWindow() { + Expects(!window_); + + NSWindowStyleMask style_mask = CalcWindowStyleMask(style_flag_); + window_ = [[CruWindow alloc] init:this + contentRect:{0, 0, content_rect_.width, content_rect_.height} + style:style_mask]; + Ensures(window_); + + osx_window_->SetClientRect(content_rect_); + + [window_ setDelegate:window_delegate_]; + + if (parent_) { + auto parent = CheckPlatform<OsxWindow>(parent_, this->osx_window_->GetPlatformId()); + [window_ setParentWindow:parent->p_->window_]; + } + + NSView* content_view = [[CruView alloc] init:this + input_context_p:input_method_context_->p_.get() + frame:Rect(Point{}, content_rect_.GetSize())]; + + [window_ setContentView:content_view]; + + auto title_str = Convert(title_); + [window_ setTitle:(NSString*)title_str]; + CFRelease(title_str); + + draw_layer_ = CreateLayer(Convert(content_rect_.GetSize())); + + create_event_.Raise(nullptr); + + osx_window_->RequestRepaint(); +} + +Size OsxWindowPrivate::GetScreenSize() { + auto screen = window_ ? [window_ screen] : [NSScreen mainScreen]; + auto size = [screen frame].size; + return Convert(size); +} + +Rect OsxWindowPrivate::RetrieveContentRect() { + NSRect rect = [NSWindow contentRectForFrameRect:[window_ frame] + styleMask:CalcWindowStyleMask(style_flag_)]; + rect.origin.y = GetScreenSize().height - rect.origin.y - rect.size.height; + return cru::platform::graphics::quartz::Convert(rect); +} + +} + +OsxWindow::OsxWindow(OsxUiApplication* ui_application) + : OsxGuiResource(ui_application), p_(new details::OsxWindowPrivate(this)) {} + +OsxWindow::~OsxWindow() { + if (p_->window_) { + [p_->window_ close]; + } + dynamic_cast<OsxUiApplication*>(GetUiApplication())->UnregisterWindow(this); +} + +void OsxWindow::Close() { + if (p_->window_) { + [p_->window_ close]; + } +} + +INativeWindow* OsxWindow::GetParent() { return p_->parent_; } + +void OsxWindow::SetParent(INativeWindow* parent) { + auto p = CheckPlatform<OsxWindow>(parent, GetPlatformId()); + + p_->parent_ = parent; + + if (p_->window_) { + [p_->window_ setParentWindow:p->p_->window_]; + } +} + +WindowStyleFlag OsxWindow::GetStyleFlag() { return p_->style_flag_; } + +void OsxWindow::SetStyleFlag(WindowStyleFlag flag) { + p_->style_flag_ = flag; + + if (p_->window_) { + [p_->window_ close]; + } +} + +String OsxWindow::GetTitle() { return p_->title_; } + +void OsxWindow::SetTitle(String title) { + p_->title_ = title; + + if (p_->window_) { + auto str = Convert(title); + [p_->window_ setTitle:(NSString*)str]; + CFRelease(str); + } +} + +WindowVisibilityType OsxWindow::GetVisibility() { + if (!p_->window_) return WindowVisibilityType::Hide; + if ([p_->window_ isMiniaturized]) return WindowVisibilityType::Minimize; + return [p_->window_ isVisible] ? WindowVisibilityType::Show : WindowVisibilityType::Hide; +} + +void OsxWindow::SetVisibility(WindowVisibilityType visibility) { + if (p_->window_) { + if (visibility == WindowVisibilityType::Show) { + [p_->window_ orderFront:nil]; + p_->visibility_change_event_.Raise(WindowVisibilityType::Show); + } else if (visibility == WindowVisibilityType::Hide) { + [p_->window_ orderOut:nil]; + p_->visibility_change_event_.Raise(WindowVisibilityType::Hide); + } else if (visibility == WindowVisibilityType::Minimize) { + [p_->window_ miniaturize:nil]; + } + } else { + if (visibility == WindowVisibilityType::Show) { + p_->CreateWindow(); + [p_->window_ orderFront:nil]; + p_->visibility_change_event_.Raise(WindowVisibilityType::Show); + } + } +} + +Size OsxWindow::GetClientSize() { return p_->content_rect_.GetSize(); } + +void OsxWindow::SetClientSize(const Size& size) { + if (p_->window_) { + auto rect = GetClientRect(); + rect.SetSize(size); + SetClientRect(rect); + } else { + p_->content_rect_.SetSize(size); + } +} + +Rect OsxWindow::GetClientRect() { return p_->content_rect_; } + +void OsxWindow::SetClientRect(const Rect& rect) { + if (p_->window_) { + auto r = Convert(rect); + r.origin.y = p_->GetScreenSize().height - r.origin.y - r.size.height; + r = [NSWindow frameRectForContentRect:r styleMask:CalcWindowStyleMask(p_->style_flag_)]; + [p_->window_ setFrame:r display:false]; + } else { + p_->content_rect_ = rect; + } +} + +Rect OsxWindow::GetWindowRect() { + auto r = Convert(p_->content_rect_); + r.origin.y = p_->GetScreenSize().height - r.origin.y - r.size.height; + r = [NSWindow frameRectForContentRect:r styleMask:CalcWindowStyleMask(p_->style_flag_)]; + r.origin.y = p_->GetScreenSize().height - r.origin.y - r.size.height; + return Convert(r); +} + +void OsxWindow::SetWindowRect(const Rect& rect) { + auto r = Convert(rect); + r.origin.y = p_->GetScreenSize().height - r.origin.y - r.size.height; + r = [NSWindow frameRectForContentRect:r styleMask:CalcWindowStyleMask(p_->style_flag_)]; + r.origin.y = p_->GetScreenSize().height - r.origin.y - r.size.height; + SetClientRect(Convert(r)); +} + +void OsxWindow::RequestRepaint() { + if (!p_->draw_timer_) { + p_->draw_timer_ = GetUiApplication()->SetImmediate([this] { + p_->paint_event_.Raise(nullptr); + p_->draw_timer_.Release(); + }); + } +} + +std::unique_ptr<graphics::IPainter> OsxWindow::BeginPaint() { + if (!p_->window_) { + return std::make_unique<graphics::NullPainter>(); + } + + CGContextRef cg_context = CGLayerGetContext(p_->draw_layer_); + + return std::make_unique<cru::platform::graphics::quartz::QuartzCGContextPainter>( + GetUiApplication()->GetGraphicsFactory(), cg_context, false, GetClientSize(), + [this](graphics::quartz::QuartzCGContextPainter*) { + [[p_->window_ contentView] setNeedsDisplay:YES]; + }); +} + +bool OsxWindow::RequestFocus() { + if (!p_->window_) return false; + [p_->window_ makeKeyWindow]; + return true; +} + +Point OsxWindow::GetMousePosition() { + auto p = [p_->window_ mouseLocationOutsideOfEventStream]; + return Point(p.x, p.y); +} + +bool OsxWindow::CaptureMouse() { return true; } + +bool OsxWindow::ReleaseMouse() { return true; } + +void OsxWindow::SetCursor(std::shared_ptr<ICursor> cursor) { + p_->cursor_ = CheckPlatform<OsxCursor>(cursor, GetPlatformId()); + p_->UpdateCursor(); +} + +void OsxWindow::SetToForeground() { + if (!p_->window_) return; + [p_->window_ makeMainWindow]; + [p_->window_ orderFrontRegardless]; +} + +IEvent<std::nullptr_t>* OsxWindow::CreateEvent() { return &p_->create_event_; } +IEvent<std::nullptr_t>* OsxWindow::DestroyEvent() { return &p_->destroy_event_; } +IEvent<std::nullptr_t>* OsxWindow::PaintEvent() { return &p_->paint_event_; } +IEvent<WindowVisibilityType>* OsxWindow::VisibilityChangeEvent() { + return &p_->visibility_change_event_; +} +IEvent<Size>* OsxWindow::ResizeEvent() { return &p_->resize_event_; } +IEvent<FocusChangeType>* OsxWindow::FocusEvent() { return &p_->focus_event_; } +IEvent<MouseEnterLeaveType>* OsxWindow::MouseEnterLeaveEvent() { + return &p_->mouse_enter_leave_event_; +} +IEvent<Point>* OsxWindow::MouseMoveEvent() { return &p_->mouse_move_event_; } +IEvent<NativeMouseButtonEventArgs>* OsxWindow::MouseDownEvent() { return &p_->mouse_down_event_; } +IEvent<NativeMouseButtonEventArgs>* OsxWindow::MouseUpEvent() { return &p_->mouse_up_event_; } +IEvent<NativeMouseWheelEventArgs>* OsxWindow::MouseWheelEvent() { return &p_->mouse_wheel_event_; } +IEvent<NativeKeyEventArgs>* OsxWindow::KeyDownEvent() { return &p_->key_down_event_; } +IEvent<NativeKeyEventArgs>* OsxWindow::KeyUpEvent() { return &p_->key_up_event_; } + +IInputMethodContext* OsxWindow::GetInputMethodContext() { return p_->input_method_context_.get(); } +} + +namespace { +cru::platform::gui::KeyModifier GetKeyModifier(NSEvent* event) { + cru::platform::gui::KeyModifier key_modifier; + if (event.modifierFlags & NSEventModifierFlagControl) + key_modifier |= cru::platform::gui::KeyModifiers::ctrl; + if (event.modifierFlags & NSEventModifierFlagOption) + key_modifier |= cru::platform::gui::KeyModifiers::alt; + if (event.modifierFlags & NSEventModifierFlagShift) + key_modifier |= cru::platform::gui::KeyModifiers::shift; + if (event.modifierFlags & NSEventModifierFlagCommand) + key_modifier |= cru::platform::gui::KeyModifiers::command; + return key_modifier; +} +} + +@implementation CruWindow { + cru::platform::gui::osx::details::OsxWindowPrivate* _p; +} + +- (instancetype)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p + contentRect:(NSRect)contentRect + style:(NSWindowStyleMask)style { + [super initWithContentRect:contentRect + styleMask:style + backing:NSBackingStoreBuffered + defer:false]; + _p = p; + + [self setAcceptsMouseMovedEvents:YES]; + + return self; +} + +- (BOOL)canBecomeMainWindow { + return YES; +} + +- (BOOL)canBecomeKeyWindow { + return YES; +} +@end + +@implementation CruView { + cru::platform::gui::osx::details::OsxWindowPrivate* _p; + cru::platform::gui::osx::details::OsxInputMethodContextPrivate* _input_context_p; + NSMutableAttributedString* _input_context_text; +} + +- (instancetype)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p + input_context_p: + (cru::platform::gui::osx::details::OsxInputMethodContextPrivate*)input_context_p + frame:(cru::platform::Rect)frame { + [super initWithFrame:cru::platform::graphics::quartz::Convert(frame)]; + _p = p; + _input_context_p = input_context_p; + + auto tracking_area = [[NSTrackingArea alloc] + initWithRect:Convert(frame) + options:(NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveAlways) + owner:self + userInfo:nil]; + [self addTrackingArea:tracking_area]; + + return self; +} + +- (void)drawRect:(NSRect)dirtyRect { + auto cg_context = [[NSGraphicsContext currentContext] CGContext]; + auto layer = _p->GetDrawLayer(); + Ensures(layer); + CGContextDrawLayerAtPoint(cg_context, CGPointMake(0, 0), layer); +} + +- (BOOL)acceptsFirstResponder { + return YES; +} + +- (BOOL)canBecomeKeyView { + return YES; +} + +- (void)mouseMoved:(NSEvent*)event { + _p->OnMouseMove(cru::platform::Point(event.locationInWindow.x, event.locationInWindow.y)); +} + +- (void)mouseDragged:(NSEvent*)event { + _p->OnMouseMove(cru::platform::Point(event.locationInWindow.x, event.locationInWindow.y)); +} + +- (void)rightMouseDragged:(NSEvent*)event { + _p->OnMouseMove(cru::platform::Point(event.locationInWindow.x, event.locationInWindow.y)); +} + +- (void)mouseEntered:(NSEvent*)event { + _p->OnMouseEnterLeave(cru::platform::gui::MouseEnterLeaveType::Enter); +} + +- (void)mouseExited:(NSEvent*)event { + _p->OnMouseEnterLeave(cru::platform::gui::MouseEnterLeaveType::Leave); +} + +- (void)mouseDown:(NSEvent*)event { + [[self window] makeKeyWindow]; + + auto key_modifier = GetKeyModifier(event); + cru::platform::Point p(event.locationInWindow.x, event.locationInWindow.y); + + _p->OnMouseDown(cru::platform::gui::mouse_buttons::left, p, key_modifier); +} + +- (void)mouseUp:(NSEvent*)event { + auto key_modifier = GetKeyModifier(event); + cru::platform::Point p(event.locationInWindow.x, event.locationInWindow.y); + + _p->OnMouseUp(cru::platform::gui::mouse_buttons::left, p, key_modifier); +} + +- (void)rightMouseDown:(NSEvent*)event { + auto key_modifier = GetKeyModifier(event); + cru::platform::Point p(event.locationInWindow.x, event.locationInWindow.y); + + _p->OnMouseDown(cru::platform::gui::mouse_buttons::right, p, key_modifier); +} + +- (void)rightMouseUp:(NSEvent*)event { + auto key_modifier = GetKeyModifier(event); + cru::platform::Point p(event.locationInWindow.x, event.locationInWindow.y); + + _p->OnMouseUp(cru::platform::gui::mouse_buttons::right, p, key_modifier); +} + +- (void)scrollWheel:(NSEvent*)event { + auto key_modifier = GetKeyModifier(event); + cru::platform::Point p(event.locationInWindow.x, event.locationInWindow.y); + + if (event.scrollingDeltaY) { + _p->OnMouseWheel(static_cast<float>(event.scrollingDeltaY), p, key_modifier, false); + } + + if (event.scrollingDeltaX) { + _p->OnMouseWheel(static_cast<float>(event.scrollingDeltaX), p, key_modifier, true); + } +} + +namespace { +using cru::platform::gui::KeyCode; +const std::unordered_set<KeyCode> input_context_handle_codes{ + KeyCode::A, + KeyCode::B, + KeyCode::C, + KeyCode::D, + KeyCode::E, + KeyCode::F, + KeyCode::G, + KeyCode::H, + KeyCode::I, + KeyCode::J, + KeyCode::K, + KeyCode::L, + KeyCode::M, + KeyCode::N, + KeyCode::O, + KeyCode::P, + KeyCode::Q, + KeyCode::R, + KeyCode::S, + KeyCode::T, + KeyCode::U, + KeyCode::V, + KeyCode::W, + KeyCode::X, + KeyCode::Y, + KeyCode::Z, + KeyCode::N0, + KeyCode::N1, + KeyCode::N2, + KeyCode::N3, + KeyCode::N4, + KeyCode::N5, + KeyCode::N6, + KeyCode::N7, + KeyCode::N8, + KeyCode::N9, + KeyCode::Comma, + KeyCode::Period, + KeyCode::Slash, + KeyCode::Semicolon, + KeyCode::Quote, + KeyCode::LeftSquareBracket, + KeyCode::RightSquareBracket, + KeyCode::BackSlash, + KeyCode::Minus, + KeyCode::Equal, + KeyCode::GraveAccent, +}; +} + +const std::unordered_set<KeyCode> input_context_handle_codes_when_has_text{ + KeyCode::Backspace, KeyCode::Space, KeyCode::Return, KeyCode::Left, + KeyCode::Right, KeyCode::Up, KeyCode::Down}; + +- (void)keyDown:(NSEvent*)event { + auto key_modifier = GetKeyModifier(event); + + bool handled = false; + + auto input_context = dynamic_cast<cru::platform::gui::osx::OsxInputMethodContext*>( + _p->GetWindow()->GetInputMethodContext()); + Ensures(input_context); + + auto c = cru::platform::gui::osx::KeyCodeFromOsxToCru(event.keyCode); + + if (input_context->IsEnabled()) { + if (input_context_handle_codes.count(c) && + !(key_modifier & ~cru::platform::gui::KeyModifiers::shift)) { + handled = [[self inputContext] handleEvent:event]; + } else if (input_context_handle_codes_when_has_text.count(c) && !key_modifier) { + if (!input_context->GetCompositionText().text.empty()) { + handled = [[self inputContext] handleEvent:event]; + } else { + if (c == KeyCode::Return) { + _input_context_p->RaiseTextEvent(u"\n"); + handled = true; + } else if (c == KeyCode::Space) { + _input_context_p->RaiseTextEvent(u" "); + handled = true; + } + } + } + } + + if (!handled) { + _p->OnKeyDown(c, key_modifier); + } +} + +- (void)keyUp:(NSEvent*)event { + // cru::CRU_LOG_DEBUG(u"CruView", u"Recieved key up."); + + auto key_modifier = GetKeyModifier(event); + auto c = cru::platform::gui::osx::KeyCodeFromOsxToCru(event.keyCode); + + _p->OnKeyUp(c, key_modifier); +} + +- (BOOL)hasMarkedText { + return _input_context_text != nil; +} + +- (NSRange)markedRange { + return _input_context_text == nil ? NSRange{NSNotFound, 0} + : NSRange{0, [_input_context_text length]}; +} + +- (NSRange)selectedRange { + return NSMakeRange(_input_context_p->GetSelectionRange().position, + _input_context_p->GetSelectionRange().count); +} + +- (void)setMarkedText:(id)string + selectedRange:(NSRange)selectedRange + replacementRange:(NSRange)replacementRange { + CFStringRef s; + if ([string isKindOfClass:[NSString class]]) { + s = (CFStringRef)string; + } else { + auto as = (CFAttributedStringRef)string; + s = CFAttributedStringGetString(as); + } + + auto ss = Convert(s); + + // cru::CRU_LOG_DEBUG(u"CruView", + // u"Received setMarkedText string: {}, selected range: ({}, {}), " + // u"replacement range: ({}, {}).", + // ss, selectedRange.location, selectedRange.length, replacementRange.location, + // replacementRange.length); + + if (_input_context_text == nil) { + _input_context_text = [[NSMutableAttributedString alloc] init]; + _input_context_p->RaiseCompositionStartEvent(); + } + + if (replacementRange.location == NSNotFound) replacementRange.location = 0; + + [_input_context_text + replaceCharactersInRange:NSMakeRange(0, [_input_context_text length]) + withAttributedString:[[NSAttributedString alloc] initWithString:(NSString*)s]]; + + cru::platform::gui::CompositionText composition_text; + composition_text.text = Convert((CFStringRef)[_input_context_text string]); + composition_text.selection.position = ss.IndexFromCodePointToCodeUnit(selectedRange.location); + composition_text.selection.count = + ss.IndexFromCodePointToCodeUnit(selectedRange.location + selectedRange.length) - + composition_text.selection.position; + _input_context_p->SetCompositionText(composition_text); + _input_context_p->RaiseCompositionEvent(); +} + +- (void)unmarkText { + _input_context_text = nil; + _input_context_p->RaiseCompositionEndEvent(); +} + +- (NSArray<NSAttributedStringKey>*)validAttributesForMarkedText { + return @[ + (NSString*)kCTUnderlineColorAttributeName, (NSString*)kCTUnderlineStyleAttributeName, + (NSString*)kCTForegroundColorAttributeName, (NSString*)kCTBackgroundColorAttributeName + ]; +} + +- (NSAttributedString*)attributedSubstringForProposedRange:(NSRange)range + actualRange:(NSRangePointer)actualRange { + cru::Range r(range.location, range.length); + + r = r.CoerceInto(0, [_input_context_text length]); + + return [_input_context_text attributedSubstringFromRange:NSMakeRange(r.position, r.count)]; +} + +- (void)insertText:(id)string replacementRange:(NSRange)replacementRange { + CFStringRef s; + if ([string isKindOfClass:[NSString class]]) { + s = (CFStringRef)string; + } else { + auto as = (CFAttributedStringRef)string; + s = CFAttributedStringGetString(as); + } + + _input_context_text = nil; + _input_context_p->SetCompositionText(cru::platform::gui::CompositionText()); + cru::String ss = Convert(s); + + // cru::CRU_LOG_DEBUG(u"CruView", u"Finish composition: {}, replacement range: ({}, {})", ss, + // replacementRange.location, replacementRange.length); + + _input_context_p->RaiseCompositionEvent(); + _input_context_p->RaiseCompositionEndEvent(); + _input_context_p->RaiseTextEvent(ss); +} + +- (NSUInteger)characterIndexForPoint:(NSPoint)point { + return NSNotFound; +} + +- (NSRect)firstRectForCharacterRange:(NSRange)range actualRange:(NSRangePointer)actualRange { + NSRect result; + result.origin.x = _input_context_p->GetCandidateWindowPosition().x; + result.origin.y = _input_context_p->GetCandidateWindowPosition().y; + result.size.height = 16; + result.size.width = 0; + return result; +} + +- (void)doCommandBySelector:(SEL)selector { + _input_context_p->PerformSel(selector); +} +@end + +@implementation CruWindowDelegate { + cru::platform::gui::osx::details::OsxWindowPrivate* _p; +} + +- (id)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p { + _p = p; + return self; +} + +- (void)windowWillClose:(NSNotification*)notification { + _p->OnWindowWillClose(); +} + +- (void)windowDidExpose:(NSNotification*)notification { + _p->OnWindowDidExpose(); +} + +- (void)windowDidUpdate:(NSNotification*)notification { + _p->OnWindowDidUpdate(); +} + +- (void)windowDidMove:(NSNotification*)notification { + _p->OnWindowDidMove(); +} + +- (void)windowDidResize:(NSNotification*)notification { + _p->OnWindowDidResize(); +} + +- (void)windowDidBecomeKey:(NSNotification*)notification { + _p->OnBecomeKeyWindow(); +} + +- (void)windowDidResignKey:(NSNotification*)notification { + _p->OnResignKeyWindow(); +} +@end diff --git a/src/platform/gui/osx/WindowPrivate.h b/src/platform/gui/osx/WindowPrivate.h new file mode 100644 index 00000000..49cc0154 --- /dev/null +++ b/src/platform/gui/osx/WindowPrivate.h @@ -0,0 +1,118 @@ +#pragma once +#include "cru/platform/gui/osx/Window.h" + +#include "cru/common/Event.h" +#include "cru/platform/gui/osx/Cursor.h" +#include "cru/platform/gui/TimerHelper.h" +#include "cru/platform/gui/Window.h" + +#import <AppKit/AppKit.h> + +@interface CruWindowDelegate : NSObject <NSWindowDelegate> +- (id)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p; +@end + +@interface CruWindow : NSWindow +- (instancetype)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p + contentRect:(NSRect)contentRect + style:(NSWindowStyleMask)style; +@end + +@interface CruView : NSView <NSTextInputClient> +- (instancetype)init:(cru::platform::gui::osx::details::OsxWindowPrivate*)p + input_context_p: + (cru::platform::gui::osx::details::OsxInputMethodContextPrivate*)input_context_p + frame:(cru::platform::Rect)frame; +@end + +namespace cru::platform::gui::osx { + +namespace details { +class OsxInputMethodContextPrivate; + +class OsxWindowPrivate { + friend OsxWindow; + friend OsxInputMethodContextPrivate; + + public: + explicit OsxWindowPrivate(OsxWindow* osx_window); + + CRU_DELETE_COPY(OsxWindowPrivate) + CRU_DELETE_MOVE(OsxWindowPrivate) + + ~OsxWindowPrivate(); + + public: + void OnMouseEnterLeave(MouseEnterLeaveType type); + void OnMouseMove(Point p); + void OnMouseDown(MouseButton button, Point p, KeyModifier key_modifier); + void OnMouseUp(MouseButton button, Point p, KeyModifier key_modifier); + void OnMouseWheel(float delta, Point p, KeyModifier key_modifier, bool horizontal); + void OnKeyDown(KeyCode key, KeyModifier key_modifier); + void OnKeyUp(KeyCode key, KeyModifier key_modifier); + + void OnWindowWillClose(); + void OnWindowDidExpose(); + void OnWindowDidUpdate(); + void OnWindowDidMove(); + void OnWindowDidResize(); + void OnBecomeKeyWindow(); + void OnResignKeyWindow(); + + CGLayerRef GetDrawLayer() { return draw_layer_; } + + OsxWindow* GetWindow() { return osx_window_; } + NSWindow* GetNSWindow() { return window_; } + + private: + Size GetScreenSize(); + + void CreateWindow(); + + void UpdateCursor(); + + Point TransformMousePoint(const Point& point); + + CGLayerRef CreateLayer(const CGSize& size); + + Rect RetrieveContentRect(); + + private: + OsxWindow* osx_window_; + + INativeWindow* parent_ = nullptr; + WindowStyleFlag style_flag_ = WindowStyleFlag{}; + + String title_; + + Rect content_rect_; + + NSWindow* window_ = nil; + CruWindowDelegate* window_delegate_ = nil; + + CGLayerRef draw_layer_ = nullptr; + + bool mouse_in_ = false; + + std::shared_ptr<OsxCursor> cursor_ = nullptr; + + std::unique_ptr<OsxInputMethodContext> input_method_context_; + + TimerAutoCanceler draw_timer_; + + Event<std::nullptr_t> create_event_; + Event<std::nullptr_t> destroy_event_; + Event<std::nullptr_t> paint_event_; + Event<WindowVisibilityType> visibility_change_event_; + Event<Size> resize_event_; + Event<FocusChangeType> focus_event_; + Event<MouseEnterLeaveType> mouse_enter_leave_event_; + Event<Point> mouse_move_event_; + Event<NativeMouseButtonEventArgs> mouse_down_event_; + Event<NativeMouseButtonEventArgs> mouse_up_event_; + Event<NativeMouseWheelEventArgs> mouse_wheel_event_; + Event<NativeKeyEventArgs> key_down_event_; + Event<NativeKeyEventArgs> key_up_event_; +}; +} // namespace details +} // namespace cru::platform::gui::osx |