diff --git a/Aerofoil/GpSystemServices_Win32.cpp b/Aerofoil/GpSystemServices_Win32.cpp index ec40190..928749a 100644 --- a/Aerofoil/GpSystemServices_Win32.cpp +++ b/Aerofoil/GpSystemServices_Win32.cpp @@ -1,14 +1,117 @@ #include "GpSystemServices_Win32.h" #include "GpMutex_Win32.h" #include "GpThreadEvent_Win32.h" +#include "GpWindows.h" + +#include "IGpClipboardContents.h" + +#include "UTF16.h" +#include "UTF8.h" #include +#include #pragma push_macro("CreateMutex") #ifdef CreateMutex #undef CreateMutex #endif +extern GpWindowsGlobals g_gpWindowsGlobals; + +namespace GpSystemServices_Win32_Private +{ + class RefCountedClipboard + { + public: + RefCountedClipboard(); + + protected: + virtual ~RefCountedClipboard(); + + void AddRef(); + void DecRef(); + + unsigned int m_refCount; + }; + + class TextClipboard : public RefCountedClipboard, public IGpClipboardContentsText + { + public: + TextClipboard(const uint8_t *utf8Text, size_t utf8Size); + ~TextClipboard() override; + + GpClipboardContentsType_t GetContentsType() const override; + void Destroy() override; + IGpClipboardContents *Clone() const override; + const uint8_t *GetBytes() const override; + size_t GetSize() const override; + + private: + std::vector m_utf8Text; + }; + + + RefCountedClipboard::RefCountedClipboard() + : m_refCount(1) + { + } + + RefCountedClipboard::~RefCountedClipboard() + { + } + + void RefCountedClipboard::AddRef() + { + m_refCount++; + } + + void RefCountedClipboard::DecRef() + { + unsigned int rc = --m_refCount; + if (rc == 0) + delete this; + } + + TextClipboard::TextClipboard(const uint8_t *utf8Text, size_t utf8Size) + { + m_utf8Text.resize(utf8Size); + if (utf8Size > 0) + memcpy(&m_utf8Text[0], utf8Text, utf8Size); + } + + TextClipboard::~TextClipboard() + { + } + + GpClipboardContentsType_t TextClipboard::GetContentsType() const + { + return GpClipboardContentsTypes::kText; + } + + void TextClipboard::Destroy() + { + this->DecRef(); + } + + IGpClipboardContents *TextClipboard::Clone() const + { + const_cast(this)->AddRef(); + return const_cast(this); + } + + const uint8_t *TextClipboard::GetBytes() const + { + if (m_utf8Text.size() == 0) + return nullptr; + return &m_utf8Text[0]; + } + + size_t TextClipboard::GetSize() const + { + return m_utf8Text.size(); + } +} + struct GpSystemServices_Win32_ThreadStartParams { GpSystemServices_Win32::ThreadFunc_t m_threadFunc; @@ -34,6 +137,10 @@ GpSystemServices_Win32::GpSystemServices_Win32() { } +GpSystemServices_Win32::~GpSystemServices_Win32() +{ +} + int64_t GpSystemServices_Win32::GetTime() const { SYSTEMTIME epochStart; @@ -171,6 +278,89 @@ bool GpSystemServices_Win32::AreFontResourcesSeekable() const return true; } +IGpClipboardContents *GpSystemServices_Win32::GetClipboardContents() const +{ + IGpClipboardContents *cbObject = nullptr; + + if (IsClipboardFormatAvailable(CF_UNICODETEXT)) + { + if (OpenClipboard(g_gpWindowsGlobals.m_hwnd)) + { + HGLOBAL textHandle = GetClipboardData(CF_UNICODETEXT); + if (textHandle) + { + const wchar_t *str = static_cast(GlobalLock(textHandle)); + if (str) + { + if (str[0] == 0) + cbObject = new GpSystemServices_Win32_Private::TextClipboard(nullptr, 0); + else + { + int bytesRequired = WideCharToMultiByte(CP_UTF8, 0, str, -1, nullptr, 0, nullptr, nullptr); + + if (bytesRequired > 0) + { + std::vector decodedText; + decodedText.resize(bytesRequired); + WideCharToMultiByte(CP_UTF8, 0, str, -1, &decodedText[0], bytesRequired, nullptr, nullptr); + + cbObject = new GpSystemServices_Win32_Private::TextClipboard(reinterpret_cast(&decodedText[0]), decodedText.size() - 1); + } + } + + GlobalUnlock(textHandle); + } + } + CloseClipboard(); + } + } + + return cbObject; +} + +void GpSystemServices_Win32::SetClipboardContents(IGpClipboardContents *contents) +{ + if (!contents) + return; + + if (contents->GetContentsType() == GpClipboardContentsTypes::kText) + { + IGpClipboardContentsText *textContents = static_cast(contents); + + if (OpenClipboard(g_gpWindowsGlobals.m_hwnd)) + { + if (EmptyClipboard()) + { + int wcharsRequired = MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast(textContents->GetBytes()), textContents->GetSize(), nullptr, 0); + + std::vector wideChars; + + if (wcharsRequired) + { + wideChars.resize(wcharsRequired + 1); + MultiByteToWideChar(CP_UTF8, 0, reinterpret_cast(textContents->GetBytes()), textContents->GetSize(), &wideChars[0], wcharsRequired); + } + else + wideChars.resize(1); + + wideChars[wideChars.size() - 1] = static_cast(0); + + HGLOBAL textObject = GlobalAlloc(GMEM_MOVEABLE, wideChars.size() * sizeof(wchar_t)); + if (textObject) + { + wchar_t *buffer = static_cast(GlobalLock(textObject)); + memcpy(buffer, &wideChars[0], wideChars.size() * sizeof(wchar_t)); + GlobalUnlock(textObject); + + SetClipboardData(CF_UNICODETEXT, textObject); + } + } + + CloseClipboard(); + } + } +} + void GpSystemServices_Win32::SetTouchscreenSimulation(bool isTouchscreenSimulation) { diff --git a/Aerofoil/GpSystemServices_Win32.h b/Aerofoil/GpSystemServices_Win32.h index dbcb5bc..2ec7595 100644 --- a/Aerofoil/GpSystemServices_Win32.h +++ b/Aerofoil/GpSystemServices_Win32.h @@ -19,6 +19,7 @@ class GpSystemServices_Win32 final : public IGpSystemServices { public: GpSystemServices_Win32(); + ~GpSystemServices_Win32(); int64_t GetTime() const override; void GetLocalDateTime(unsigned int &year, unsigned int &month, unsigned int &day, unsigned int &hour, unsigned int &minute, unsigned int &second) const override; @@ -36,6 +37,8 @@ public: void SetTextInputEnabled(bool isEnabled) override; bool IsTextInputEnabled() const override; bool AreFontResourcesSeekable() const override; + IGpClipboardContents *GetClipboardContents() const override; + void SetClipboardContents(IGpClipboardContents *contents) override; void SetTouchscreenSimulation(bool isTouchscreenSimulation); diff --git a/AerofoilAndroid/app/jni/main/GpSystemServices_Android.cpp b/AerofoilAndroid/app/jni/main/GpSystemServices_Android.cpp index 5e16b4b..4e3791c 100644 --- a/AerofoilAndroid/app/jni/main/GpSystemServices_Android.cpp +++ b/AerofoilAndroid/app/jni/main/GpSystemServices_Android.cpp @@ -290,6 +290,15 @@ bool GpSystemServices_Android::AreFontResourcesSeekable() const return false; } +IGpClipboardContents *GpSystemServices_Android::GetClipboardContents() const +{ + return nullptr; +} + +void GpSystemServices_Android::SetClipboardContents(IGpClipboardContents *contents) +{ +} + GpSystemServices_Android *GpSystemServices_Android::GetInstance() { return &ms_instance; diff --git a/AerofoilAndroid/app/jni/main/GpSystemServices_Android.h b/AerofoilAndroid/app/jni/main/GpSystemServices_Android.h index 93b44c5..63b29f5 100644 --- a/AerofoilAndroid/app/jni/main/GpSystemServices_Android.h +++ b/AerofoilAndroid/app/jni/main/GpSystemServices_Android.h @@ -24,6 +24,8 @@ public: void SetTextInputEnabled(bool isEnabled) override; bool IsTextInputEnabled() const override; bool AreFontResourcesSeekable() const override; + IGpClipboardContents *GetClipboardContents() const override; + void SetClipboardContents(IGpClipboardContents *contents) override; void FlushTextInputEnabled(); diff --git a/GpCommon/GpClipboardContentsType.h b/GpCommon/GpClipboardContentsType.h new file mode 100644 index 0000000..454927f --- /dev/null +++ b/GpCommon/GpClipboardContentsType.h @@ -0,0 +1,11 @@ +#pragma once + +namespace GpClipboardContentsTypes +{ + enum GpClipboardContentsType + { + kText, + }; +} + +typedef GpClipboardContentsTypes::GpClipboardContentsType GpClipboardContentsType_t; diff --git a/GpCommon/IGpClipboardContents.h b/GpCommon/IGpClipboardContents.h new file mode 100644 index 0000000..8bfd1ba --- /dev/null +++ b/GpCommon/IGpClipboardContents.h @@ -0,0 +1,16 @@ +#include "GpClipboardContentsType.h" + +#include + +struct IGpClipboardContents +{ + virtual GpClipboardContentsType_t GetContentsType() const = 0; + virtual void Destroy() = 0; + virtual IGpClipboardContents *Clone() const = 0; +}; + +struct IGpClipboardContentsText : public IGpClipboardContents +{ + virtual const uint8_t *GetBytes() const = 0; + virtual size_t GetSize() const = 0; // In bytes +}; diff --git a/GpCommon/IGpSystemServices.h b/GpCommon/IGpSystemServices.h index 5327afa..50907b9 100644 --- a/GpCommon/IGpSystemServices.h +++ b/GpCommon/IGpSystemServices.h @@ -12,6 +12,7 @@ struct IGpMutex; struct IGpThreadEvent; +struct IGpClipboardContents; struct IGpSystemServices { @@ -34,4 +35,6 @@ public: virtual void SetTextInputEnabled(bool isEnabled) = 0; virtual bool IsTextInputEnabled() const = 0; virtual bool AreFontResourcesSeekable() const = 0; + virtual IGpClipboardContents *GetClipboardContents() const = 0; + virtual void SetClipboardContents(IGpClipboardContents *contents) = 0; }; diff --git a/PortabilityLayer/MemoryManager.cpp b/PortabilityLayer/MemoryManager.cpp index 41d5763..5697df2 100644 --- a/PortabilityLayer/MemoryManager.cpp +++ b/PortabilityLayer/MemoryManager.cpp @@ -109,7 +109,8 @@ namespace PortabilityLayer uint8_t *bytes = static_cast(buf); const MMBlock *mmBlock = reinterpret_cast(bytes - MMBlock::AlignedSize()); - free(bytes - MMBlock::AlignedSize() - mmBlock->m_offsetFromAllocLocation); + void *freeLoc = bytes - MMBlock::AlignedSize() - mmBlock->m_offsetFromAllocLocation; + free(freeLoc); } MMHandleBlock *MemoryManagerImpl::AllocHandle(size_t size) diff --git a/PortabilityLayer/PLCore.cpp b/PortabilityLayer/PLCore.cpp index b628c9d..b927a92 100644 --- a/PortabilityLayer/PLCore.cpp +++ b/PortabilityLayer/PLCore.cpp @@ -15,6 +15,7 @@ #include "IGpDirectoryCursor.h" #include "HostSuspendCallArgument.h" #include "HostSuspendHook.h" +#include "IGpClipboardContents.h" #include "IGpCursor.h" #include "IGpDisplayDriver.h" #include "IGpFileSystem.h" @@ -45,6 +46,8 @@ #include "PLTimeTaggedVOSEvent.h" #include "PLWidgets.h" +#include "UTF8.h" + #include #include @@ -688,6 +691,141 @@ WindowPtr PL_GetPutInFrontWindowPtr() return PortabilityLayer::WindowManager::GetInstance()->GetPutInFrontSentinel(); } +class PLClipboardContentsText : public IGpClipboardContentsText +{ +public: + static PLClipboardContentsText *CreateFromMacRomanStr(const uint8_t *chars, size_t size); + + GpClipboardContentsType_t GetContentsType() const override; + void Destroy() override; + IGpClipboardContents *Clone() const override; + const uint8_t *GetBytes() const override; + size_t GetSize() const override; + +private: + PLClipboardContentsText(uint8_t *utf8Bytes, size_t size); + ~PLClipboardContentsText(); + + uint8_t *m_utf8Bytes; + size_t m_size; +}; + + +PLClipboardContentsText *PLClipboardContentsText::CreateFromMacRomanStr(const uint8_t *chars, size_t length) +{ + PortabilityLayer::MemoryManager *mm = PortabilityLayer::MemoryManager::GetInstance(); + + size_t numUTF8Bytes = 0; + + for (size_t i = 0; i < length; i++) + { + uint8_t utf8Bytes[PortabilityLayer::UTF8Processor::kMaxEncodedBytes]; + + uint16_t codePoint = MacRoman::ToUnicode(chars[i]); + + size_t numBytesEmitted = 0; + PortabilityLayer::UTF8Processor::EncodeCodePoint(utf8Bytes, numBytesEmitted, codePoint); + + numUTF8Bytes += numBytesEmitted; + } + + uint8_t *utf8Bytes = nullptr; + + if (numUTF8Bytes) + { + utf8Bytes = static_cast(mm->Alloc(numUTF8Bytes)); + if (!utf8Bytes) + return nullptr; + + numUTF8Bytes = 0; + for (size_t i = 0; i < length; i++) + { + uint16_t codePoint = MacRoman::ToUnicode(chars[i]); + + size_t numBytesEmitted = 0; + PortabilityLayer::UTF8Processor::EncodeCodePoint(utf8Bytes + numUTF8Bytes, numBytesEmitted, codePoint); + + numUTF8Bytes += numBytesEmitted; + } + } + + void *storage = mm->Alloc(sizeof(PLClipboardContentsText)); + if (!storage) + { + mm->Release(utf8Bytes); + return nullptr; + } + + return new (storage) PLClipboardContentsText(utf8Bytes, numUTF8Bytes); +} + +PLClipboardContentsText::PLClipboardContentsText(uint8_t *utf8Bytes, size_t size) + : m_utf8Bytes(utf8Bytes) + , m_size(size) +{ +} + +PLClipboardContentsText::~PLClipboardContentsText() +{ + PortabilityLayer::MemoryManager::GetInstance()->Release(m_utf8Bytes); +} + +GpClipboardContentsType_t PLClipboardContentsText::GetContentsType() const +{ + return GpClipboardContentsTypes::kText; +} + +void PLClipboardContentsText::Destroy() +{ + this->~PLClipboardContentsText(); + PortabilityLayer::MemoryManager::GetInstance()->Release(this); +} + +IGpClipboardContents *PLClipboardContentsText::Clone() const +{ + PortabilityLayer::MemoryManager *mm = PortabilityLayer::MemoryManager::GetInstance(); + uint8_t *bytesCopy = nullptr; + if (m_size) + { + bytesCopy = static_cast(mm->Alloc(m_size)); + if (!bytesCopy) + return nullptr; + + memcpy(bytesCopy, m_utf8Bytes, m_size); + } + + void *storage = mm->Alloc(sizeof(PLClipboardContentsText)); + if (!storage) + { + if (bytesCopy) + mm->Release(bytesCopy); + + return nullptr; + } + + return new (storage) PLClipboardContentsText(bytesCopy, m_size); +} + +const uint8_t *PLClipboardContentsText::GetBytes() const +{ + return m_utf8Bytes; +} + +size_t PLClipboardContentsText::GetSize() const +{ + return m_size; +} + + +void PL_CopyStringToClipboard(const uint8_t *chars, size_t length) +{ + if (IGpClipboardContentsText *clipboardText = PLClipboardContentsText::CreateFromMacRomanStr(chars, length)) + { + PLDrivers::GetSystemServices()->SetClipboardContents(clipboardText); + clipboardText->Destroy(); + } +} + Window::Window() : m_surface(PortabilityLayer::QDPortType_Window) , m_wmX(0) diff --git a/PortabilityLayer/PLCore.h b/PortabilityLayer/PLCore.h index 7b23249..a56f482 100644 --- a/PortabilityLayer/PLCore.h +++ b/PortabilityLayer/PLCore.h @@ -279,3 +279,5 @@ void PL_NotYetImplemented(); void PL_NotYetImplemented_Minor(); void PL_NotYetImplemented_TODO(const char *category); void PL_Init(); + +void PL_CopyStringToClipboard(const uint8_t *chars, size_t length); diff --git a/PortabilityLayer/PLEditboxWidget.cpp b/PortabilityLayer/PLEditboxWidget.cpp index 0db6057..19e13d8 100644 --- a/PortabilityLayer/PLEditboxWidget.cpp +++ b/PortabilityLayer/PLEditboxWidget.cpp @@ -2,6 +2,7 @@ #include "FontFamily.h" #include "FontManager.h" +#include "IGpClipboardContents.h" #include "IGpSystemServices.h" #include "InputManager.h" #include "MacRomanConversion.h" @@ -9,14 +10,16 @@ #include "RenderedFont.h" #include "GpRenderedFontMetrics.h" #include "ResolveCachingColor.h" -#include "TextPlacer.h" #include "Rect2i.h" +#include "TextPlacer.h" +#include "UTF8.h" #include "PLDrivers.h" #include "PLKeyEncoding.h" #include "PLQDraw.h" #include "PLStandardColors.h" #include "PLTimeTaggedVOSEvent.h" +#include "PLCore.h" #include @@ -237,6 +240,7 @@ namespace PortabilityLayer const KeyDownStates *downStates = PortabilityLayer::InputManager::GetInstance()->GetKeys(); const bool isShiftHeld = downStates->m_special.Get(GpKeySpecials::kLeftShift) || downStates->m_special.Get(GpKeySpecials::kRightShift); const bool isWords = downStates->m_special.Get(GpKeySpecials::kLeftCtrl) || downStates->m_special.Get(GpKeySpecials::kRightCtrl); + const bool isCtrlHeld = downStates->m_special.Get(GpKeySpecials::kLeftCtrl) || downStates->m_special.Get(GpKeySpecials::kRightCtrl); if (keyEvent.m_keyIDSubset == GpKeyIDSubsets::kSpecial) { @@ -281,6 +285,39 @@ namespace PortabilityLayer return WidgetHandleStates::kDigested; } } + + if (isCtrlHeld && keyEvent.m_keyIDSubset == GpKeyIDSubsets::kASCII) + { + if (keyEvent.m_key.m_asciiChar == kCopyShortcutKey) + { + if (m_selStartChar != m_selEndChar) + HandleCopy(); + return WidgetHandleStates::kDigested; + } + if (keyEvent.m_key.m_asciiChar == kCutShortcutKey) + { + if (m_selStartChar != m_selEndChar) + HandleCut(); + return WidgetHandleStates::kDigested; + } + else if (keyEvent.m_key.m_asciiChar == kPasteShortcutKey) + { + HandlePaste(keyEvent.m_repeatCount); + return WidgetHandleStates::kDigested; + } + else if (keyEvent.m_key.m_asciiChar == kSelectAllShortcutKey) + { + m_selStartChar = 0; + m_selEndChar = m_length; + + m_caratSelectionAnchor = CaratSelectionAnchor_Start; + m_caratScrollLocked = false; + AdjustScrollToCarat(); + + m_caratTimer = 0; + Redraw(); + } + } } } else if (evt.m_vosEvent.m_eventType == GpVOSEventTypes::kMouseInput) @@ -642,7 +679,91 @@ namespace PortabilityLayer Redraw(); } } + + void EditboxWidget::HandleCopy() + { + if (m_selStartChar == m_selEndChar) + return; + PL_CopyStringToClipboard(m_chars + m_selStartChar, m_selEndChar - m_selStartChar); + } + + void EditboxWidget::HandleCut() + { + HandleCopy(); + HandleBackspace(1); + } + + void EditboxWidget::HandlePaste(size_t repeatCount) + { + IGpClipboardContents *clipboardContents = PLDrivers::GetSystemServices()->GetClipboardContents(); + + if (clipboardContents == nullptr) + return; + + if (clipboardContents->GetContentsType() != GpClipboardContentsTypes::kText) + { + clipboardContents->Destroy(); + return; + } + + IGpClipboardContentsText *textContents = static_cast(clipboardContents); + const size_t utf8Size = textContents->GetSize(); + const uint8_t *utf8Bytes = textContents->GetBytes(); + + size_t numCodePoints = 0; + for (size_t i = 0; i < utf8Size; ) + { + uint32_t codePoint = 0; + size_t numDigested = 0; + if (!UTF8Processor::DecodeCodePoint(utf8Bytes + i, utf8Size - i, numDigested, codePoint)) + { + clipboardContents->Destroy(); + return; + } + + i += numDigested; + numCodePoints++; + } + + PortabilityLayer::MemoryManager *mm = PortabilityLayer::MemoryManager::GetInstance(); + uint8_t *decodedChars = static_cast(mm->Alloc(numCodePoints)); + + if (!decodedChars) + { + clipboardContents->Destroy(); + return; + } + + numCodePoints = 0; + for (size_t i = 0; i < utf8Size; ) + { + uint32_t codePoint = 0; + size_t numDigested = 0; + if (!UTF8Processor::DecodeCodePoint(utf8Bytes + i, utf8Size - i, numDigested, codePoint)) + { + clipboardContents->Destroy(); + return; + } + + if (codePoint > 0xffff || !MacRoman::FromUnicode(decodedChars[numCodePoints], static_cast(codePoint))) + decodedChars[numCodePoints] = '?'; + + numCodePoints++; + + i += numDigested; + } + + // This is extremely suboptimal due to the embedded redraw... + for (size_t i = 0; i < repeatCount; i++) + { + for (size_t j = 0; j < numCodePoints; j++) + HandleCharacter(decodedChars[j], 1); + } + + mm->Release(decodedChars); + clipboardContents->Destroy(); + } void EditboxWidget::HandleRightArrow(const uint32_t numRepeatsRequested, bool shiftHeld, bool wholeWords) { diff --git a/PortabilityLayer/PLEditboxWidget.h b/PortabilityLayer/PLEditboxWidget.h index 0e6f85e..9da7192 100644 --- a/PortabilityLayer/PLEditboxWidget.h +++ b/PortabilityLayer/PLEditboxWidget.h @@ -90,6 +90,9 @@ namespace PortabilityLayer void HandleHome(bool shiftHeld); void HandleEnd(bool shiftHeld); + void HandleCopy(); + void HandleCut(); + void HandlePaste(size_t repeatCount); size_t FindVerticalMovementCaratPos(const Vec2i &desiredPos, bool &isOutOfRange, CaratCharacterAlignment *optOutAlignment) const; void ExpandSelectionToWords(size_t caratPos, size_t &outStartChar, size_t &outEndChar); @@ -137,5 +140,10 @@ namespace PortabilityLayer void *m_characterFilterContext; static const CharacterCategorySpan gs_characterCategorySpans[]; + + static const int kCopyShortcutKey = 'C'; + static const int kCutShortcutKey = 'X'; + static const int kPasteShortcutKey = 'V'; + static const int kSelectAllShortcutKey = 'A'; }; } diff --git a/PortabilityLayer/UTF8.h b/PortabilityLayer/UTF8.h index 1cd9a64..4a68004 100644 --- a/PortabilityLayer/UTF8.h +++ b/PortabilityLayer/UTF8.h @@ -11,5 +11,7 @@ namespace PortabilityLayer static void EncodeCodePoint(uint8_t *characters, size_t &outCharactersEmitted, uint32_t codePoint); static bool DecodeToMacRomanPascalStr(const uint8_t *inChars, size_t inSize, uint8_t *outChars, size_t maxOutSize, size_t &outSize); + + static const unsigned int kMaxEncodedBytes = 4; }; }