From 6aa2201797a9ed64ce0178215ae941d0c5f09579 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 30 Oct 2020 00:07:57 +0800 Subject: ... --- src/win/gui/InputMethod.cpp | 278 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 src/win/gui/InputMethod.cpp (limited to 'src/win/gui/InputMethod.cpp') diff --git a/src/win/gui/InputMethod.cpp b/src/win/gui/InputMethod.cpp new file mode 100644 index 00000000..d6f2146d --- /dev/null +++ b/src/win/gui/InputMethod.cpp @@ -0,0 +1,278 @@ +#include "cru/win/gui/InputMethod.hpp" + +#include "cru/common/Logger.hpp" +#include "cru/common/StringUtil.hpp" +#include "cru/platform/Check.hpp" +#include "cru/win/Exception.hpp" +#include "cru/win/gui/Window.hpp" + +#include + +namespace cru::platform::gui::win { +AutoHIMC::AutoHIMC(HWND hwnd) : hwnd_(hwnd) { + Expects(hwnd); + handle_ = ::ImmGetContext(hwnd); +} + +AutoHIMC::AutoHIMC(AutoHIMC&& other) + : hwnd_(other.hwnd_), handle_(other.handle_) { + other.hwnd_ = nullptr; + other.handle_ = nullptr; +} + +AutoHIMC& AutoHIMC::operator=(AutoHIMC&& other) { + if (this != &other) { + Object::operator=(std::move(other)); + this->hwnd_ = other.hwnd_; + this->handle_ = other.handle_; + other.hwnd_ = nullptr; + other.handle_ = nullptr; + } + return *this; +} + +AutoHIMC::~AutoHIMC() { + if (handle_) { + if (!::ImmReleaseContext(hwnd_, handle_)) + log::TagWarn(log_tag, u"Failed to release HIMC."); + } +} + +// copied from chromium +namespace { +// Determines whether or not the given attribute represents a target +// (a.k.a. a selection). +bool IsTargetAttribute(char attribute) { + return (attribute == ATTR_TARGET_CONVERTED || + attribute == ATTR_TARGET_NOTCONVERTED); +} +// Helper function for ImeInput::GetCompositionInfo() method, to get the target +// range that's selected by the user in the current composition string. +void GetCompositionTargetRange(HIMC imm_context, int* target_start, + int* target_end) { + int attribute_size = + ::ImmGetCompositionString(imm_context, GCS_COMPATTR, NULL, 0); + if (attribute_size > 0) { + int start = 0; + int end = 0; + std::vector attribute_data(attribute_size); + ::ImmGetCompositionString(imm_context, GCS_COMPATTR, attribute_data.data(), + attribute_size); + for (start = 0; start < attribute_size; ++start) { + if (IsTargetAttribute(attribute_data[start])) break; + } + for (end = start; end < attribute_size; ++end) { + if (!IsTargetAttribute(attribute_data[end])) break; + } + if (start == attribute_size) { + // This composition clause does not contain any target clauses, + // i.e. this clauses is an input clause. + // We treat the whole composition as a target clause. + start = 0; + end = attribute_size; + } + *target_start = start; + *target_end = end; + } +} +// Helper function for ImeInput::GetCompositionInfo() method, to get underlines +// information of the current composition string. +CompositionClauses GetCompositionClauses(HIMC imm_context, int target_start, + int target_end) { + CompositionClauses result; + int clause_size = + ::ImmGetCompositionString(imm_context, GCS_COMPCLAUSE, NULL, 0); + int clause_length = clause_size / sizeof(std::uint32_t); + if (clause_length) { + result.reserve(clause_length - 1); + std::vector clause_data(clause_length); + ::ImmGetCompositionString(imm_context, GCS_COMPCLAUSE, clause_data.data(), + clause_size); + for (int i = 0; i < clause_length - 1; ++i) { + CompositionClause clause; + clause.start = clause_data[i]; + clause.end = clause_data[i + 1]; + clause.target = false; + // Use thick underline for the target clause. + if (clause.start >= target_start && clause.end <= target_end) { + clause.target = true; + } + result.push_back(clause); + } + } + return result; +} + +std::u16string GetString(HIMC imm_context) { + LONG string_size = + ::ImmGetCompositionString(imm_context, GCS_COMPSTR, NULL, 0); + std::u16string result((string_size / sizeof(char16_t)), 0); + ::ImmGetCompositionString(imm_context, GCS_COMPSTR, result.data(), + string_size); + return result; +} + +std::u16string GetResultString(HIMC imm_context) { + LONG string_size = + ::ImmGetCompositionString(imm_context, GCS_RESULTSTR, NULL, 0); + std::u16string result((string_size / sizeof(char16_t)), 0); + ::ImmGetCompositionString(imm_context, GCS_RESULTSTR, result.data(), + string_size); + return result; +} + +CompositionText GetCompositionInfo(HIMC imm_context) { + // We only care about GCS_COMPATTR, GCS_COMPCLAUSE and GCS_CURSORPOS, and + // convert them into underlines and selection range respectively. + + auto text = GetString(imm_context); + + int length = static_cast(text.length()); + // Find out the range selected by the user. + int target_start = length; + int target_end = length; + GetCompositionTargetRange(imm_context, &target_start, &target_end); + + auto clauses = GetCompositionClauses(imm_context, target_start, target_end); + + int cursor = ::ImmGetCompositionString(imm_context, GCS_CURSORPOS, NULL, 0); + + return CompositionText{std::move(text), std::move(clauses), + TextRange{cursor}}; +} + +} // namespace + +WinInputMethodContext::WinInputMethodContext( + gsl::not_null window) + : native_window_(window) { + event_guard_ += window->NativeMessageEvent()->AddHandler( + std::bind(&WinInputMethodContext::OnWindowNativeMessage, this, + std::placeholders::_1)); +} + +WinInputMethodContext::~WinInputMethodContext() {} + +void WinInputMethodContext::EnableIME() { + const auto hwnd = native_window_->GetWindowHandle(); + if (::ImmAssociateContextEx(hwnd, nullptr, IACE_DEFAULT) == FALSE) { + log::TagWarn(log_tag, u"Failed to enable ime."); + } +} + +void WinInputMethodContext::DisableIME() { + const auto hwnd = native_window_->GetWindowHandle(); + AutoHIMC himc{hwnd}; + + if (!::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0)) { + log::TagWarn(log_tag, + u"Failed to complete composition before disable ime."); + } + + if (::ImmAssociateContextEx(hwnd, nullptr, 0) == FALSE) { + log::TagWarn(log_tag, u"Failed to disable ime."); + } +} + +void WinInputMethodContext::CompleteComposition() { + auto himc = GetHIMC(); + if (!::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0)) { + log::TagWarn(log_tag, u"Failed to complete composition."); + } +} + +void WinInputMethodContext::CancelComposition() { + auto himc = GetHIMC(); + if (!::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_CANCEL, 0)) { + log::TagWarn(log_tag, u"Failed to complete composition."); + } +} + +CompositionText WinInputMethodContext::GetCompositionText() { + auto himc = GetHIMC(); + return GetCompositionInfo(himc.Get()); +} + +void WinInputMethodContext::SetCandidateWindowPosition(const Point& point) { + auto himc = GetHIMC(); + + ::CANDIDATEFORM form; + form.dwIndex = 1; + form.dwStyle = CFS_CANDIDATEPOS; + + form.ptCurrentPos = native_window_->DipToPixel(point); + + if (!::ImmSetCandidateWindow(himc.Get(), &form)) + log::TagDebug(log_tag, + u"Failed to set input method candidate window position."); +} + +IEvent* WinInputMethodContext::CompositionStartEvent() { + return &composition_start_event_; +} + +IEvent* WinInputMethodContext::CompositionEndEvent() { + return &composition_end_event_; +}; + +IEvent* WinInputMethodContext::CompositionEvent() { + return &composition_event_; +} + +IEvent* WinInputMethodContext::TextEvent() { + return &text_event_; +} + +void WinInputMethodContext::OnWindowNativeMessage( + WindowNativeMessageEventArgs& args) { + const auto& message = args.GetWindowMessage(); + switch (message.msg) { + case WM_CHAR: { + const auto c = static_cast(message.w_param); + if (IsUtf16SurrogatePairCodeUnit(c)) { + // I don't think this will happen because normal key strike without ime + // should only trigger ascci character. If it is a charater from + // supplementary planes, it should be handled with ime messages. + log::TagWarn(log_tag, + u"A WM_CHAR message for character from supplementary " + u"planes is ignored."); + } else { + char16_t s[1] = {c}; + text_event_.Raise({s, 1}); + } + args.HandleWithResult(0); + break; + } + case WM_IME_COMPOSITION: { + composition_event_.Raise(nullptr); + auto composition_text = GetCompositionText(); + log::TagDebug(log_tag, u"WM_IME_COMPOSITION composition text:\n{}", + composition_text); + if (message.l_param & GCS_RESULTSTR) { + auto result_string = GetResultString(); + text_event_.Raise(result_string); + } + break; + } + case WM_IME_STARTCOMPOSITION: { + composition_start_event_.Raise(nullptr); + break; + } + case WM_IME_ENDCOMPOSITION: { + composition_end_event_.Raise(nullptr); + break; + } + } +} + +std::u16string WinInputMethodContext::GetResultString() { + auto himc = GetHIMC(); + auto result = win::GetResultString(himc.Get()); + return result; +} + +AutoHIMC WinInputMethodContext::GetHIMC() { + const auto hwnd = native_window_->GetWindowHandle(); + return AutoHIMC{hwnd}; +} +} // namespace cru::platform::gui::win -- cgit v1.2.3 From 9c9a9c988b5d03642f931341c97b672d054936c8 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 30 Oct 2020 00:20:52 +0800 Subject: ... --- include/cru/platform/gui/DebugFlags.hpp | 8 ++++++++ src/win/gui/InputMethod.cpp | 7 +++++-- src/win/gui/Window.cpp | 9 +++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 include/cru/platform/gui/DebugFlags.hpp (limited to 'src/win/gui/InputMethod.cpp') diff --git a/include/cru/platform/gui/DebugFlags.hpp b/include/cru/platform/gui/DebugFlags.hpp new file mode 100644 index 00000000..2b7c7c19 --- /dev/null +++ b/include/cru/platform/gui/DebugFlags.hpp @@ -0,0 +1,8 @@ +#pragma once + +namespace cru::platform::gui { +struct DebugFlags { + static constexpr int paint = 0; + static constexpr int input_method = 0; +}; +} // namespace cru::platform::gui diff --git a/src/win/gui/InputMethod.cpp b/src/win/gui/InputMethod.cpp index d6f2146d..34638516 100644 --- a/src/win/gui/InputMethod.cpp +++ b/src/win/gui/InputMethod.cpp @@ -3,6 +3,7 @@ #include "cru/common/Logger.hpp" #include "cru/common/StringUtil.hpp" #include "cru/platform/Check.hpp" +#include "cru/platform/gui/DebugFlags.hpp" #include "cru/win/Exception.hpp" #include "cru/win/gui/Window.hpp" @@ -246,8 +247,10 @@ void WinInputMethodContext::OnWindowNativeMessage( case WM_IME_COMPOSITION: { composition_event_.Raise(nullptr); auto composition_text = GetCompositionText(); - log::TagDebug(log_tag, u"WM_IME_COMPOSITION composition text:\n{}", - composition_text); + if constexpr (DebugFlags::input_method) { + log::TagDebug(log_tag, u"WM_IME_COMPOSITION composition text:\n{}", + composition_text); + } if (message.l_param & GCS_RESULTSTR) { auto result_string = GetResultString(); text_event_.Raise(result_string); diff --git a/src/win/gui/Window.cpp b/src/win/gui/Window.cpp index dda8a36a..174b8931 100644 --- a/src/win/gui/Window.cpp +++ b/src/win/gui/Window.cpp @@ -4,6 +4,7 @@ #include "cru/common/Logger.hpp" #include "cru/platform/Check.hpp" #include "cru/platform/gui/Base.hpp" +#include "cru/platform/gui/DebugFlags.hpp" #include "cru/win/graphics/direct/WindowPainter.hpp" #include "cru/win/gui/Cursor.hpp" #include "cru/win/gui/Exception.hpp" @@ -132,7 +133,9 @@ bool WinNativeWindow::ReleaseMouse() { } void WinNativeWindow::RequestRepaint() { - log::TagDebug(log_tag, u"A repaint is requested."); + if constexpr (DebugFlags::paint) { + log::TagDebug(log_tag, u"A repaint is requested."); + } if (!::InvalidateRect(hwnd_, nullptr, FALSE)) throw Win32Error(::GetLastError(), "Failed to invalidate window."); if (!::UpdateWindow(hwnd_)) @@ -376,7 +379,9 @@ void WinNativeWindow::OnDestroyInternal() { void WinNativeWindow::OnPaintInternal() { paint_event_.Raise(nullptr); ValidateRect(hwnd_, nullptr); - log::TagDebug(log_tag, u"A repaint is finished."); + if constexpr (DebugFlags::paint) { + log::TagDebug(log_tag, u"A repaint is finished."); + } } void WinNativeWindow::OnResizeInternal(const int new_width, -- cgit v1.2.3 From 72d801f9b2a41ab2bcd9023c65422e089d885107 Mon Sep 17 00:00:00 2001 From: crupest Date: Fri, 30 Oct 2020 00:26:35 +0800 Subject: ... --- src/win/gui/InputMethod.cpp | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'src/win/gui/InputMethod.cpp') diff --git a/src/win/gui/InputMethod.cpp b/src/win/gui/InputMethod.cpp index 34638516..492f1c2f 100644 --- a/src/win/gui/InputMethod.cpp +++ b/src/win/gui/InputMethod.cpp @@ -258,10 +258,16 @@ void WinInputMethodContext::OnWindowNativeMessage( break; } case WM_IME_STARTCOMPOSITION: { + if constexpr (DebugFlags::input_method) { + log::TagDebug(log_tag, u"WM_IME_STARTCOMPOSITION received."); + } composition_start_event_.Raise(nullptr); break; } case WM_IME_ENDCOMPOSITION: { + if constexpr (DebugFlags::input_method) { + log::TagDebug(log_tag, u"WM_IME_ENDCOMPOSITION received."); + } composition_end_event_.Raise(nullptr); break; } -- cgit v1.2.3 From 2d4a5df468f8bc13fbb657e010c393365ef79bda Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 10 Nov 2020 15:52:15 +0800 Subject: ... --- include/cru/platform/GraphBase.hpp | 6 ++++++ include/cru/platform/gui/InputMethod.hpp | 3 ++- include/cru/ui/DebugFlags.hpp | 2 +- src/ui/controls/TextBox.cpp | 2 -- src/ui/controls/TextControlService.hpp | 26 +++++++++++++++++++++++++- src/ui/host/LayoutPaintCycler.cpp | 4 ++-- src/win/gui/InputMethod.cpp | 2 +- 7 files changed, 37 insertions(+), 8 deletions(-) (limited to 'src/win/gui/InputMethod.cpp') diff --git a/include/cru/platform/GraphBase.hpp b/include/cru/platform/GraphBase.hpp index 6700765e..2b40898e 100644 --- a/include/cru/platform/GraphBase.hpp +++ b/include/cru/platform/GraphBase.hpp @@ -22,6 +22,12 @@ struct Point final { return fmt::format(u"({}, {})", ToUtf16String(x), ToUtf16String(y)); } + constexpr Point& operator+=(const Point& other) { + this->x += other.x; + this->y += other.y; + return *this; + } + float x = 0; float y = 0; }; diff --git a/include/cru/platform/gui/InputMethod.hpp b/include/cru/platform/gui/InputMethod.hpp index 53a8d671..9d090eab 100644 --- a/include/cru/platform/gui/InputMethod.hpp +++ b/include/cru/platform/gui/InputMethod.hpp @@ -37,7 +37,8 @@ struct IInputMethodContext : virtual INativeResource { virtual CompositionText GetCompositionText() = 0; - // Set the candidate window lefttop. Use this method to prepare typing. + // Set the candidate window lefttop. Relative to window lefttop. Use this + // method to prepare typing. virtual void SetCandidateWindowPosition(const Point& point) = 0; // Triggered when user starts composition. diff --git a/include/cru/ui/DebugFlags.hpp b/include/cru/ui/DebugFlags.hpp index fceef081..7c600d48 100644 --- a/include/cru/ui/DebugFlags.hpp +++ b/include/cru/ui/DebugFlags.hpp @@ -4,5 +4,5 @@ namespace cru::ui::debug_flags { constexpr bool routed_event = false; constexpr bool layout = false; constexpr bool shortcut = false; -constexpr bool text_service = false; +constexpr bool text_service = true; } // namespace cru::ui::debug_flags diff --git a/src/ui/controls/TextBox.cpp b/src/ui/controls/TextBox.cpp index 4a8d6658..6ba6ecb2 100644 --- a/src/ui/controls/TextBox.cpp +++ b/src/ui/controls/TextBox.cpp @@ -10,9 +10,7 @@ namespace cru::ui::controls { using render::BorderRenderObject; -using render::CanvasRenderObject; using render::ScrollRenderObject; -using render::StackLayoutRenderObject; using render::TextRenderObject; TextBox::TextBox() diff --git a/src/ui/controls/TextControlService.hpp b/src/ui/controls/TextControlService.hpp index 92a66f5e..d50621ea 100644 --- a/src/ui/controls/TextControlService.hpp +++ b/src/ui/controls/TextControlService.hpp @@ -298,6 +298,24 @@ class TextControlService : public Object { selection.GetStart(), selection.GetEnd()); } + void UpdateInputMethodPosition() { + if (auto input_method_context = this->GetInputMethodContext()) { + Point right_bottom = + this->GetTextRenderObject()->GetTotalOffset() + + this->GetTextRenderObject()->GetCaretRect().GetRightBottom(); + right_bottom.x += 5; + right_bottom.y += 5; + + if constexpr (debug_flags::text_service) { + log::TagDebug(log_tag, + u"Calculate input method candidate window position: {}.", + right_bottom.ToDebugString()); + } + + input_method_context->SetCandidateWindowPosition(right_bottom); + } + } + template void SetupOneHandler(event::RoutedEvent* (Control::*event)(), void (TextControlService::*handler)( @@ -449,6 +467,12 @@ class TextControlService : public Object { if (text == u"\b") return; this->ReplaceSelectedText(text); }); + + host::WindowHost* window_host = control_->GetWindowHost(); + if (window_host) + input_method_context_event_guard_ += + window_host->AfterLayoutEvent()->AddHandler( + [this](auto) { this->UpdateInputMethodPosition(); }); } } @@ -481,5 +505,5 @@ class TextControlService : public Object { // nullopt means not selecting std::optional select_down_button_; -}; // namespace cru::ui::controls +}; } // namespace cru::ui::controls diff --git a/src/ui/host/LayoutPaintCycler.cpp b/src/ui/host/LayoutPaintCycler.cpp index 9b319da4..fd581e00 100644 --- a/src/ui/host/LayoutPaintCycler.cpp +++ b/src/ui/host/LayoutPaintCycler.cpp @@ -29,7 +29,7 @@ void LayoutPaintCycler::OnCycle() { host_->Repaint(); } } - layout_dirty_ = true; - paint_dirty_ = true; + layout_dirty_ = false; + paint_dirty_ = false; } } // namespace cru::ui::host diff --git a/src/win/gui/InputMethod.cpp b/src/win/gui/InputMethod.cpp index 492f1c2f..0ef9a403 100644 --- a/src/win/gui/InputMethod.cpp +++ b/src/win/gui/InputMethod.cpp @@ -198,7 +198,7 @@ void WinInputMethodContext::SetCandidateWindowPosition(const Point& point) { auto himc = GetHIMC(); ::CANDIDATEFORM form; - form.dwIndex = 1; + form.dwIndex = 0; form.dwStyle = CFS_CANDIDATEPOS; form.ptCurrentPos = native_window_->DipToPixel(point); -- cgit v1.2.3 From acb7462be0b1d6b002a9e50a81b2b74fdc16a3b3 Mon Sep 17 00:00:00 2001 From: crupest Date: Sat, 26 Dec 2020 19:44:12 +0800 Subject: ... --- src/win/gui/InputMethod.cpp | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'src/win/gui/InputMethod.cpp') diff --git a/src/win/gui/InputMethod.cpp b/src/win/gui/InputMethod.cpp index 0ef9a403..6643ea58 100644 --- a/src/win/gui/InputMethod.cpp +++ b/src/win/gui/InputMethod.cpp @@ -165,10 +165,7 @@ void WinInputMethodContext::DisableIME() { const auto hwnd = native_window_->GetWindowHandle(); AutoHIMC himc{hwnd}; - if (!::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0)) { - log::TagWarn(log_tag, - u"Failed to complete composition before disable ime."); - } + ::ImmNotifyIME(himc.Get(), NI_COMPOSITIONSTR, CPS_COMPLETE, 0); if (::ImmAssociateContextEx(hwnd, nullptr, 0) == FALSE) { log::TagWarn(log_tag, u"Failed to disable ime."); -- cgit v1.2.3 From 7b6ca6e8a158868da316351b64e64ab1cdb5872c Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 28 Dec 2020 23:05:06 +0800 Subject: ... --- src/common/StringUtil.cpp | 2 +- src/ui/controls/TextHostControlService.cpp | 1 - src/win/gui/InputMethod.cpp | 10 +++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) (limited to 'src/win/gui/InputMethod.cpp') diff --git a/src/common/StringUtil.cpp b/src/common/StringUtil.cpp index b13c0193..0cadc545 100644 --- a/src/common/StringUtil.cpp +++ b/src/common/StringUtil.cpp @@ -255,7 +255,7 @@ gsl::index Utf16ForwardUntil(std::u16string_view str, gsl::index position, UnreachableCode(); } -inline bool IsSpace(CodePoint c) { return c == 0x20; } +inline bool IsSpace(CodePoint c) { return c == 0x20 || c == 0xA; } gsl::index Utf16PreviousWord(std::u16string_view str, gsl::index position, bool* is_space) { diff --git a/src/ui/controls/TextHostControlService.cpp b/src/ui/controls/TextHostControlService.cpp index 502f63f9..07b4f1e8 100644 --- a/src/ui/controls/TextHostControlService.cpp +++ b/src/ui/controls/TextHostControlService.cpp @@ -346,7 +346,6 @@ void TextHostControlService::GainFocusHandler( input_method_context_event_guard_ += input_method_context->TextEvent()->AddHandler( [this](const std::u16string_view& text) { - if (text == u"\b") return; this->ReplaceSelectedText(text); }); diff --git a/src/win/gui/InputMethod.cpp b/src/win/gui/InputMethod.cpp index 6643ea58..cc237e88 100644 --- a/src/win/gui/InputMethod.cpp +++ b/src/win/gui/InputMethod.cpp @@ -226,7 +226,7 @@ void WinInputMethodContext::OnWindowNativeMessage( const auto& message = args.GetWindowMessage(); switch (message.msg) { case WM_CHAR: { - const auto c = static_cast(message.w_param); + auto c = static_cast(message.w_param); if (IsUtf16SurrogatePairCodeUnit(c)) { // I don't think this will happen because normal key strike without ime // should only trigger ascci character. If it is a charater from @@ -235,8 +235,12 @@ void WinInputMethodContext::OnWindowNativeMessage( u"A WM_CHAR message for character from supplementary " u"planes is ignored."); } else { - char16_t s[1] = {c}; - text_event_.Raise({s, 1}); + if (c != '\b') { // ignore backspace + if (c == '\r') c = '\n'; // Change \r to \n + + char16_t s[1] = {c}; + text_event_.Raise({s, 1}); + } } args.HandleWithResult(0); break; -- cgit v1.2.3