Skip to content

Commit

Permalink
Cancel IME composing on clear text input client
Browse files Browse the repository at this point in the history
When a text input client is cleared, such as when the user changes focus
from one text input widget to another, or when the window itself loses
focus, cancel IME composing mode, close the composing candidates window
(if any), and reset the composing text.

Issue: flutter/flutter#90503
  • Loading branch information
cbracken committed Oct 12, 2021
1 parent 9683106 commit 769b087
Show file tree
Hide file tree
Showing 16 changed files with 125 additions and 45 deletions.
4 changes: 4 additions & 0 deletions shell/platform/windows/flutter_window_win32.cc
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ void FlutterWindowWin32::OnCursorRectUpdated(const Rect& rect) {
UpdateCursorRect(Rect(origin, size));
}

void FlutterWindowWin32::OnResetImeComposing() {
AbortImeComposing();
}

bool FlutterWindowWin32::OnBitmapSurfaceUpdated(const void* allocation,
size_t row_bytes,
size_t height) {
Expand Down
3 changes: 3 additions & 0 deletions shell/platform/windows/flutter_window_win32.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ class FlutterWindowWin32 : public WindowWin32, public WindowBindingHandler {
// |FlutterWindowBindingHandler|
void OnCursorRectUpdated(const Rect& rect) override;

// |FlutterWindowBindingHandler|
void OnResetImeComposing() override;

// |WindowWin32|
void OnScroll(double delta_x,
double delta_y,
Expand Down
2 changes: 2 additions & 0 deletions shell/platform/windows/flutter_window_win32_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class SpyTextInputPlugin : public KeyboardHandlerBase,
void(const std::u16string& text, int cursor_pos));

virtual void OnCursorRectUpdated(const Rect& rect) {}
virtual void OnResetImeComposing() {}

private:
std::unique_ptr<TextInputPlugin> real_implementation_;
Expand Down Expand Up @@ -155,6 +156,7 @@ class MockFlutterWindowWin32 : public FlutterWindowWin32,
MOCK_METHOD0(GetDpiScale, float());
MOCK_METHOD0(IsVisible, bool());
MOCK_METHOD1(UpdateCursorRect, void(const Rect&));
MOCK_METHOD0(OnResetImeComposing, void());

protected:
virtual BOOL Win32PeekMessage(LPMSG lpMsg,
Expand Down
5 changes: 5 additions & 0 deletions shell/platform/windows/flutter_window_winuwp.cc
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,11 @@ void FlutterWindowWinUWP::OnCursorRectUpdated(const Rect& rect) {
// TODO(cbracken): Implement IMM candidate window positioning.
}

void FlutterWindowWinUWP::OnResetImeComposing() {
// TODO(cbracken): Cancel composing, close the candidates view, and clear the
// composing text.
}

void FlutterWindowWinUWP::OnWindowResized() {}

FlutterWindowWinUWP::~FlutterWindowWinUWP() {}
Expand Down
3 changes: 3 additions & 0 deletions shell/platform/windows/flutter_window_winuwp.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ class FlutterWindowWinUWP : public WindowBindingHandler {
// |WindowBindingHandler|
void OnCursorRectUpdated(const Rect& rect) override;

// |FlutterWindowBindingHandler|
void OnResetImeComposing() override;

// |WindowBindingHandler|
void OnWindowResized() override;

Expand Down
4 changes: 4 additions & 0 deletions shell/platform/windows/flutter_windows_view.cc
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,10 @@ void FlutterWindowsView::OnCursorRectUpdated(const Rect& rect) {
binding_handler_->OnCursorRectUpdated(rect);
}

void FlutterWindowsView::OnResetImeComposing() {
binding_handler_->OnResetImeComposing();
}

void FlutterWindowsView::InitializeKeyboard() {
auto internal_plugin_messenger = internal_plugin_registrar_->messenger();
#ifdef WINUWP
Expand Down
3 changes: 3 additions & 0 deletions shell/platform/windows/flutter_windows_view.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate,
// |TextInputPluginDelegate|
void OnCursorRectUpdated(const Rect& rect) override;

// |TextInputPluginDelegate|
void OnResetImeComposing() override;

protected:
// Called to create the keyboard hook handlers.
//
Expand Down
85 changes: 43 additions & 42 deletions shell/platform/windows/testing/mock_window_binding_handler.h
Original file line number Diff line number Diff line change
@@ -1,42 +1,43 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_
#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_

#include <windowsx.h>

#include "flutter/shell/platform/windows/window_binding_handler.h"
#include "gmock/gmock.h"

namespace flutter {
namespace testing {

/// Mock for the |WindowWin32| base class.
class MockWindowBindingHandler : public WindowBindingHandler {
public:
MockWindowBindingHandler();
virtual ~MockWindowBindingHandler();

// Prevent copying.
MockWindowBindingHandler(MockWindowBindingHandler const&) = delete;
MockWindowBindingHandler& operator=(MockWindowBindingHandler const&) = delete;

MOCK_METHOD1(SetView, void(WindowBindingHandlerDelegate* view));
MOCK_METHOD0(GetRenderTarget, WindowsRenderTarget());
MOCK_METHOD0(GetPlatformWindow, PlatformWindow());
MOCK_METHOD0(GetDpiScale, float());
MOCK_METHOD0(IsVisible, bool());
MOCK_METHOD0(OnWindowResized, void());
MOCK_METHOD0(GetPhysicalWindowBounds, PhysicalWindowBounds());
MOCK_METHOD1(UpdateFlutterCursor, void(const std::string& cursor_name));
MOCK_METHOD1(OnCursorRectUpdated, void(const Rect& rect));
MOCK_METHOD3(OnBitmapSurfaceUpdated,
bool(const void* allocation, size_t row_bytes, size_t height));
};

} // namespace testing
} // namespace flutter

#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_
#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_

#include <windowsx.h>

#include "flutter/shell/platform/windows/window_binding_handler.h"
#include "gmock/gmock.h"

namespace flutter {
namespace testing {

/// Mock for the |WindowWin32| base class.
class MockWindowBindingHandler : public WindowBindingHandler {
public:
MockWindowBindingHandler();
virtual ~MockWindowBindingHandler();

// Prevent copying.
MockWindowBindingHandler(MockWindowBindingHandler const&) = delete;
MockWindowBindingHandler& operator=(MockWindowBindingHandler const&) = delete;

MOCK_METHOD1(SetView, void(WindowBindingHandlerDelegate* view));
MOCK_METHOD0(GetRenderTarget, WindowsRenderTarget());
MOCK_METHOD0(GetPlatformWindow, PlatformWindow());
MOCK_METHOD0(GetDpiScale, float());
MOCK_METHOD0(IsVisible, bool());
MOCK_METHOD0(OnWindowResized, void());
MOCK_METHOD0(GetPhysicalWindowBounds, PhysicalWindowBounds());
MOCK_METHOD1(UpdateFlutterCursor, void(const std::string& cursor_name));
MOCK_METHOD1(OnCursorRectUpdated, void(const Rect& rect));
MOCK_METHOD0(OnResetImeComposing, void());
MOCK_METHOD3(OnBitmapSurfaceUpdated,
bool(const void* allocation, size_t row_bytes, size_t height));
};

} // namespace testing
} // namespace flutter

#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_H_
15 changes: 15 additions & 0 deletions shell/platform/windows/text_input_manager_win32.cc
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,21 @@ std::optional<std::u16string> TextInputManagerWin32::GetResultString() const {
return GetString(GCS_RESULTSTR);
}

void TextInputManagerWin32::AbortComposing() {
ImmContext imm_context(window_handle_);
if (imm_context.IsValid()) {
// Cancel composing and close the candidates window.
::ImmNotifyIME(imm_context.get(), NI_COMPOSITIONSTR, CPS_CANCEL, 0);
::ImmNotifyIME(imm_context.get(), NI_CLOSECANDIDATE, 0, 0);

// Clear the composing string.
wchar_t completion_str[] = L"";
wchar_t reading_str[] = L"";
::ImmSetCompositionStringW(imm_context.get(), SCS_SETSTR, completion_str,
sizeof(wchar_t), reading_str, sizeof(wchar_t));
}
}

std::optional<std::u16string> TextInputManagerWin32::GetString(int type) const {
if (window_handle_ == nullptr || !ime_active_) {
return std::nullopt;
Expand Down
6 changes: 6 additions & 0 deletions shell/platform/windows/text_input_manager_win32.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ class TextInputManagerWin32 {
// be committed in the composing region when composition is ended.
std::optional<std::u16string> GetResultString() const;

/// Aborts IME composing.
///
/// Aborts composing, closes the candidates window, and clears the contents
/// of the composing string.
void AbortComposing();

private:
// Returns either the composing string or result string based on the value of
// the |type| parameter.
Expand Down
2 changes: 1 addition & 1 deletion shell/platform/windows/text_input_plugin.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
#include <windows.h>

#include <cstdint>
#include <iostream>

#include "flutter/shell/platform/common/json_method_codec.h"
#include "flutter/shell/platform/windows/flutter_windows_view.h"
Expand Down Expand Up @@ -148,6 +147,7 @@ void TextInputPlugin::HandleMethodCall(
if (method.compare(kShowMethod) == 0 || method.compare(kHideMethod) == 0) {
// These methods are no-ops.
} else if (method.compare(kClearClientMethod) == 0) {
delegate_->OnResetImeComposing();
active_model_ = nullptr;
} else if (method.compare(kSetClientMethod) == 0) {
if (!method_call.arguments() || method_call.arguments()->IsNull()) {
Expand Down
3 changes: 3 additions & 0 deletions shell/platform/windows/text_input_plugin_delegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ class TextInputPluginDelegate {
// Notifies the delegate of the updated the cursor rect in Flutter root view
// coordinates.
virtual void OnCursorRectUpdated(const Rect& rect) = 0;

// Notifies the delegate that the system IME composing state should be reset.
virtual void OnResetImeComposing() = 0;
};

} // namespace flutter
Expand Down
23 changes: 21 additions & 2 deletions shell/platform/windows/text_input_plugin_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <memory>

#include "flutter/shell/platform/common/json_message_codec.h"
#include "flutter/shell/platform/common/json_method_codec.h"
#include "flutter/shell/platform/windows/flutter_windows_view.h"
#include "flutter/shell/platform/windows/testing/test_binary_messenger.h"
#include "gmock/gmock.h"
Expand All @@ -30,8 +31,10 @@ static std::unique_ptr<std::vector<uint8_t>> CreateResponse(bool handled) {

class EmptyTextInputPluginDelegate : public TextInputPluginDelegate {
public:
// Notifies delegate that the cursor position has changed.
void OnCursorRectUpdated(const Rect& rect) {}
void OnCursorRectUpdated(const Rect& rect) override {}
void OnResetImeComposing() override { ime_was_reset = true; }

bool ime_was_reset = false;
};
} // namespace

Expand Down Expand Up @@ -59,5 +62,21 @@ TEST(TextInputPluginTest, TextMethodsWorksWithEmptyModel) {
// Passes if it did not crash
}

TEST(TextInputPluginTest, ClearClientResetsComposing) {
TestBinaryMessenger messenger([](const std::string& channel,
const uint8_t* message, size_t message_size,
BinaryReply reply) {});
BinaryReply reply_handler = [](const uint8_t* reply, size_t reply_size) {};

EmptyTextInputPluginDelegate delegate;
TextInputPlugin handler(&messenger, &delegate);

auto& codec = JsonMethodCodec::GetInstance();
auto message = codec.EncodeMethodCall({"TextInput.clearClient", nullptr});
messenger.SimulateEngineMessage("flutter/textinput", message->data(),
message->size(), reply_handler);
EXPECT_TRUE(delegate.ime_was_reset);
}

} // namespace testing
} // namespace flutter
4 changes: 4 additions & 0 deletions shell/platform/windows/window_binding_handler.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ class WindowBindingHandler {
virtual bool OnBitmapSurfaceUpdated(const void* allocation,
size_t row_bytes,
size_t height) = 0;

// Invoked when the app ends IME composing, such when the active text input
// client is cleared.
virtual void OnResetImeComposing() = 0;
};

} // namespace flutter
Expand Down
4 changes: 4 additions & 0 deletions shell/platform/windows/window_win32.cc
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,10 @@ void WindowWin32::OnImeRequest(UINT const message,
// https://github.com/flutter/flutter/issues/74547
}

void WindowWin32::AbortImeComposing() {
text_input_manager_.AbortComposing();
}

void WindowWin32::UpdateCursorRect(const Rect& rect) {
text_input_manager_.UpdateCaretRect(rect);
}
Expand Down
4 changes: 4 additions & 0 deletions shell/platform/windows/window_win32.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,10 @@ class WindowWin32 {
WPARAM const wparam,
LPARAM const lparam);

// Called when the app ends IME composing, such as when the text input client
// is cleared or changed.
virtual void AbortImeComposing();

// Called when the cursor rect has been updated.
//
// |rect| is in Win32 window coordinates.
Expand Down

0 comments on commit 769b087

Please sign in to comment.