Merge branch 'ime-control'

Provide some control over IME and use it to fix wxGrid when using IME.

See #27026.
This commit is contained in:
Vadim Zeitlin
2026-09-25 00:05:20 +02:00
15 changed files with 575 additions and 36 deletions
+4
View File
@@ -3223,6 +3223,10 @@ private:
// the edit control is shown, but just supposes that it is.
void DoSaveEditControlValue();
// Update the state of the IME when anything affecting it changed.
void UpdateIME();
// these sets contain the indices of fixed, i.e. non-resizable
// interactively, grid rows or columns and are null if there are no fixed
// elements (which is the default)
+4
View File
@@ -427,6 +427,10 @@ public:
wxGridWindowType GetType() const { return m_type; }
// Update the position of the input method windows to correspond to the
// current cell.
void UpdateIMECursorRect();
private:
const wxGridWindowType m_type;
+5
View File
@@ -339,6 +339,9 @@ public:
// "commit" signal handler.
bool GTKDoInsertTextFromIM(const char* text);
// Actual implementation of DoUpdateInputMethodCursorRect().
void GTKUpdateIMCursorRect(GtkIMContext* imContext) const;
// indices for the arrays below
enum ScrollDir { ScrollDir_Horz, ScrollDir_Vert, ScrollDir_Max };
@@ -416,6 +419,8 @@ protected:
virtual void DoSetClientSize(int width, int height) override;
virtual void DoMoveWindow(int x, int y, int width, int height) override;
virtual void DoEnable(bool enable) override;
virtual void DoEnableInputMethod(bool enable) override;
virtual void DoUpdateInputMethodCursorRect() override;
#if wxUSE_MENUS_NATIVE
virtual bool DoPopupMenu( wxMenu *menu, int x, int y ) override;
+1
View File
@@ -688,6 +688,7 @@ protected:
virtual void DoReleaseMouse() override;
virtual void DoEnable(bool enable) override;
virtual void DoEnableInputMethod(bool enable) override;
virtual void DoFreeze() override;
virtual void DoThaw() override;
+39
View File
@@ -1304,6 +1304,27 @@ public:
wxCaret *GetCaret() const { return m_caret; }
#endif // wxUSE_CARET
// input method (IME) support
// --------------------------
// enable or disable input method for this window, it's enabled by
// default but can be disabled for windows not accepting text input
void EnableInputMethod(bool enable = true);
bool IsInputMethodEnabled() const { return m_enableIME; }
// set the rectangle, in client coordinates, where the text is being
// input: the input method windows are positioned near it
//
// this must be called by the windows accepting text input whenever
// this rectangle changes, e.g. because the insertion point moved or
// the window was scrolled
void UpdateInputMethodCursorRect(const wxRect& rect);
// return the rectangle set by the function above for this window, or
// an empty rectangle, meaning that the input method default position
// is used, if it wasn't set for it
wxRect GetInputMethodCursorRect() const;
// get the (average) character size for the current font
virtual int GetCharHeight() const = 0;
virtual int GetCharWidth() const = 0;
@@ -1799,6 +1820,15 @@ protected:
// widgets state are necessary
virtual void DoEnable(bool WXUNUSED(enable)) { }
// this method should be implemented to really enable or disable input
// method for this window, it's only called if the state changes
virtual void DoEnableInputMethod(bool WXUNUSED(enable)) { }
// this method can be implemented to really update the input method
// windows position, using GetInputMethodCursorRect(), it's only called if
// the input method is enabled
virtual void DoUpdateInputMethodCursorRect() { }
// the window id - a number which uniquely identifies a window among
// its siblings unless it is wxID_ANY
@@ -1890,6 +1920,9 @@ protected:
bool m_disableFocusFromKbd:1;
bool m_enableFocusFromKbd:1;
// flag controlling the use of IME (enabled by default)
bool m_enableIME:1;
// window attributes
long m_windowStyle,
m_exStyle;
@@ -2077,6 +2110,12 @@ private:
// (i.e. not being updated) if it is positive
unsigned int m_freezeCount;
// The window for which the input method cursor rectangle was set and the
// rectangle itself: as only the window having the focus can use it, we
// don't need to store it in every window.
static const wxWindowBase* ms_imeCursorWindow;
static wxRect ms_imeCursorRect;
wxDECLARE_ABSTRACT_CLASS(wxWindowBase);
wxDECLARE_NO_COPY_CLASS(wxWindowBase);
wxDECLARE_EVENT_TABLE();
+7
View File
@@ -688,6 +688,13 @@ public:
If the key is F2 (special), editing will always start and this
method will not be called at all (but StartingKey() will)
Note that this function must not change the editor contents: this should
be only done in StartingKey() which will be called later.
Also note that this function is called with a special synthetic event
for the letter "A" to check if the editor accepts text input and whether
the IME should be enabled for it.
*/
virtual bool IsAcceptedKey(wxKeyEvent& event);
+88
View File
@@ -603,6 +603,94 @@ public:
///@}
/**
@name Input method functions
Input methods (IMEs) are used for entering text in languages such as
Chinese or Japanese, in which a single character is typically composed
using several key presses.
Native controls, such as wxTextCtrl, handle input methods on their own,
but custom windows may need to use these functions to indicate whether
they accept text input at all and where the IME window should be shown
if they do.
*/
///@{
/**
Enable or disable input method for this window.
Input method is enabled for all windows by default, but it may be
useful to disable it for custom windows which don't accept text input,
as otherwise the input method windows may still be shown when the user
types in them, even though the text entered in them is ignored.
Note that input method can be disabled or enabled at any time, e.g. a
custom control could enable it only when its current item is
editable.
This function is currently implemented for wxMSW, wxGTK and wxOSX. In
wxGTK and wxOSX it doesn't affect native controls which always handle
input method themselves.
@see IsInputMethodEnabled()
@since 3.3.4
*/
void EnableInputMethod(bool enable = true);
/**
Return @true if input method is enabled for this window.
Note that this function simply returns @false if EnableInputMethod()
was called with @false argument and doesn't check whether the input
method is actually used by the window or not.
@since 3.3.4
*/
bool IsInputMethodEnabled() const;
/**
Set the area where the text is being entered.
Custom windows accepting text input should call this function to let
the input method know where to show its windows, e.g. the one with the
text being composed or the list of candidates for it. For a custom
text control, @a rect would be the rectangle corresponding to the
current insertion point position.
This function must be called whenever this rectangle changes, e.g.
when the insertion point moves or the window is scrolled. Note that
it is not sufficient to update the rectangle when a key is pressed
because some input methods, e.g. Fcitx under Linux, process the keys
before the window receives them.
This function is currently implemented for wxMSW, wxGTK and wxOSX.
@param rect
The rectangle in client coordinates or an empty rectangle to let
the input method use its default position.
@see GetInputMethodCursorRect()
@since 3.3.4
*/
void UpdateInputMethodCursorRect(const wxRect& rect);
/**
Return the area where the text is being entered.
This is the rectangle last passed to UpdateInputMethodCursorRect() for
this window or an empty rectangle if it hadn't been called for it or
if another window has called it since.
@since 3.3.4
*/
wxRect GetInputMethodCursorRect() const;
///@}
/**
@name Child management functions
*/
+45
View File
@@ -317,6 +317,8 @@ wxWindowBase::wxWindowBase()
m_disableFocusFromKbd = false;
m_enableFocusFromKbd = false;
m_enableIME = true;
#if wxUSE_DRAG_AND_DROP
m_dropTarget = nullptr;
#endif // wxUSE_DRAG_AND_DROP
@@ -461,6 +463,9 @@ wxWindowBase::~wxWindowBase()
// immediately: don't leave dangling pointers.
wxPendingDelete.DeleteObject(this);
if ( ms_imeCursorWindow == this )
ms_imeCursorWindow = nullptr;
// Just in case we've loaded a top-level window via LoadNativeDialog but
// we weren't a dialog class
wxTopLevelWindows.DeleteObject(this);
@@ -1788,6 +1793,46 @@ void wxWindowBase::SetCaret(wxCaret *caret)
}
#endif // wxUSE_CARET
// ----------------------------------------------------------------------------
// input method support
// ----------------------------------------------------------------------------
void wxWindowBase::EnableInputMethod(bool enable)
{
if ( enable == m_enableIME )
return;
m_enableIME = enable;
DoEnableInputMethod(enable);
}
// Store just a single IME rectangle for all windows instead of adding wxRect
// member to each window because only one window can have the IME focus at a
// time and it would be wasteful to increase the size of all windows when we
// can avoid it.
const wxWindowBase* wxWindowBase::ms_imeCursorWindow = nullptr;
wxRect wxWindowBase::ms_imeCursorRect;
wxRect wxWindowBase::GetInputMethodCursorRect() const
{
return ms_imeCursorWindow == this ? ms_imeCursorRect : wxRect();
}
void wxWindowBase::UpdateInputMethodCursorRect(const wxRect& rect)
{
if ( ms_imeCursorWindow == this && rect == ms_imeCursorRect )
return;
ms_imeCursorWindow = this;
ms_imeCursorRect = rect;
// Don't bother updating the position if the input method is not used, it
// will be updated when it is enabled.
if ( m_enableIME )
DoUpdateInputMethodCursorRect();
}
#if wxUSE_VALIDATORS
// ----------------------------------------------------------------------------
// validators
+4
View File
@@ -2154,6 +2154,10 @@ wxDataViewMainWindow::wxDataViewMainWindow( wxDataViewCtrl *parent, wxWindowID i
m_count = -1;
m_underMouse = nullptr;
// Disable IME for this window: it accepts focus but doesn't accept text
// input, only in-place text controls created when editing do.
EnableInputMethod(false);
UpdateDisplay();
}
+96
View File
@@ -2790,6 +2790,9 @@ void wxGrid::ScrollWindow( int dx, int dy, const wxRect *rect )
m_rowLabelWin->ScrollWindow( 0, dy, rect );
m_colLabelWin->ScrollWindow( dx, 0, rect );
// The position of the current cell has changed.
UpdateIME();
}
void wxGridWindow::OnMouseEvent( wxMouseEvent& event )
@@ -2853,10 +2856,40 @@ void wxGridWindow::OnFocus(wxFocusEvent& event)
Refresh(true, &cursor);
}
// Update IME state in case the current cell editability changed while we
// didn't have focus.
if ( event.GetEventType() == wxEVT_SET_FOCUS )
m_owner->UpdateIME();
if ( !m_owner->ProcessWindowEvent( event ) )
event.Skip();
}
void wxGridWindow::UpdateIMECursorRect()
{
// We don't have any caret, so put the input method windows over the
// current cell.
const wxGridCellCoords& coords = m_owner->GetGridCursorCoords();
if ( coords == wxGridNoCellCoords )
{
UpdateInputMethodCursorRect(wxRect());
return;
}
// The current cell may be in a different grid window when using frozen
// rows or columns, so compute its position in this window coordinates.
wxGridWindow* const cellWin = m_owner->CellToGridWindow(coords);
wxRect rect = m_owner->CellToRect(coords);
rect.Offset(-m_owner->GetGridWindowOffset(cellWin));
rect.SetPosition(m_owner->CalcGridWindowScrolledPosition(rect.GetPosition(),
cellWin));
if ( cellWin != this )
rect.SetPosition(ScreenToClient(cellWin->ClientToScreen(rect.GetPosition())));
UpdateInputMethodCursorRect(rect);
}
// Unlike XToCol() and YToRow() these macros always return a valid column/row,
// so their results don't need to be checked, while the results of the public
// functions always must be.
@@ -6281,6 +6314,50 @@ void wxGrid::UpdateColours()
}
}
// ----------------------------------------------------------------------------
// keyboard handling
// ----------------------------------------------------------------------------
// Enable input method for the grid windows only if the current cell can be
// edited: otherwise typing just shows the IME windows without doing anything.
//
// Also update the IME windows position, as it depends on the current cell.
void wxGrid::UpdateIME()
{
bool enable = CanEnableCellControl();
if ( enable )
{
// Even if the current cell can be edited, its editor may not accept
// any text at all, as is the case for the boolean editor, for
// example, and showing IME for it would be useless. Check for this by
// asking the editor whether it would start editing if a letter were
// typed.
wxKeyEvent event(wxEVT_CHAR);
event.m_keyCode = 'a';
event.m_uniChar = 'a';
enable = GetCurrentCellEditorPtr()->IsAcceptedKey(event);
}
wxGridWindow* const allGridWindows[] =
{
m_gridWin,
m_frozenCornerGridWin,
m_frozenRowGridWin,
m_frozenColGridWin,
};
for ( wxGridWindow* win : allGridWindows )
{
if ( !win )
continue;
win->EnableInputMethod(enable);
win->UpdateIMECursorRect();
}
}
void wxGrid::OnKeyDown( wxKeyEvent& event )
{
// propagate the event up and see if it gets processed
@@ -6603,6 +6680,18 @@ void wxGrid::OnChar( wxKeyEvent& event )
event.Skip();
}
}
else if ( IsCellEditControlEnabled() )
{
// We can get here if more than one character is input at once, as it
// happens when using IME: the first character starts editing, but the
// subsequent ones are still sent to the grid window and not to the
// editor, so forward them to it.
wxGridCellEditorPtr editor = GetActiveCellEditorPtr();
if ( editor->IsAcceptedKey(event) )
editor->StartingKey(event);
else
event.Skip();
}
else
{
event.Skip();
@@ -6698,6 +6787,8 @@ bool wxGrid::SetCurrentCell( const wxGridCellCoords& coords )
m_currentCellCoords = coords;
UpdateIME();
RefreshBlock(coords);
#if wxUSE_ACCESSIBILITY
@@ -7927,6 +8018,8 @@ void wxGrid::EnableEditing( bool edit )
if (!edit)
EnableCellEditControl(edit);
m_editable = edit;
UpdateIME();
}
}
@@ -10320,6 +10413,9 @@ void wxGrid::SetReadOnly(int row, int col, bool isReadOnly)
if ( CanHaveAttributes() )
{
GetOrCreateCellAttrPtr(row, col)->SetReadOnly(isReadOnly);
if ( wxGridCellCoords(row, col) == m_currentCellCoords )
UpdateIME();
}
}
+7 -3
View File
@@ -147,11 +147,17 @@ void wxScrollHelper::DoScrollOneDir(int orient,
m_win->SetScrollPos(orient, pos);
pos = m_win->GetScrollPos(orient);
const int diff = (*posOld - pos)*pixelsPerLine;
// Update the position before calling ScrollWindow(), as it may use
// GetViewStart(), which must already return the new position, as it
// does in the other ports.
*posOld = pos;
const bool scrollingEnabled =
orient == wxHORIZONTAL ? m_xScrollingEnabled : m_yScrollingEnabled;
if ( scrollingEnabled )
{
int diff = (*posOld - pos)*pixelsPerLine;
m_targetWindow->ScrollWindow(orient == wxHORIZONTAL ? diff : 0,
orient == wxHORIZONTAL ? 0 : diff);
}
@@ -161,8 +167,6 @@ void wxScrollHelper::DoScrollOneDir(int orient,
// physical move of the pixels is disabled.
m_targetWindow->Refresh(true, GetScrollRect());
}
*posOld = pos;
}
}
+53 -4
View File
@@ -1473,8 +1473,52 @@ gtk_window_key_press_callback( GtkWidget *WXUNUSED(widget),
int wxWindowGTK::GTKIMFilterKeypress(GdkEventKey* event) const
{
return m_imContext ? gtk_im_context_filter_keypress(m_imContext, event)
: FALSE;
if ( !m_imContext || !IsInputMethodEnabled() )
return FALSE;
// Note that the input method may handle the keys before we get them, e.g.
// Fcitx does this by default, so updating the cursor location here is not
// enough and UpdateInputMethodCursorRect() must be called when it
// changes, but still do it here to be sure it's up to date.
GTKUpdateIMCursorRect(m_imContext);
return gtk_im_context_filter_keypress(m_imContext, event);
}
void wxWindowGTK::DoEnableInputMethod(bool enable)
{
// We don't need to do anything if we don't have the focus, as the input
// method state will be taken into account when we get it.
if ( !m_imContext || gs_currentFocus != this )
return;
if ( enable )
{
GTKUpdateIMCursorRect(m_imContext);
gtk_im_context_focus_in(m_imContext);
}
else
{
gtk_im_context_reset(m_imContext);
gtk_im_context_focus_out(m_imContext);
}
}
void wxWindowGTK::DoUpdateInputMethodCursorRect()
{
if ( m_imContext )
GTKUpdateIMCursorRect(m_imContext);
}
// Let the input method know where to show its windows, if we know it.
void wxWindowGTK::GTKUpdateIMCursorRect(GtkIMContext* imContext) const
{
const wxRect rect = GetInputMethodCursorRect();
if ( rect.IsEmpty() )
return;
GdkRectangle area = { rect.x, rect.y, rect.width, rect.height };
gtk_im_context_set_cursor_location(imContext, &area);
}
extern "C" {
@@ -5059,8 +5103,13 @@ bool wxWindowGTK::GTKHandleFocusIn()
"handling focus_in event for %s",
wxDumpWindow(this));
if (m_imContext)
if (m_imContext && IsInputMethodEnabled())
{
// Set the cursor location before giving focus to the input method,
// as it may use it immediately.
GTKUpdateIMCursorRect(m_imContext);
gtk_im_context_focus_in(m_imContext);
}
gs_currentFocus = this;
@@ -5146,7 +5195,7 @@ void wxWindowGTK::GTKHandleFocusOutNoDeferring()
gs_lastFocus = this;
if (m_imContext)
if (m_imContext && IsInputMethodEnabled())
gtk_im_context_focus_out(m_imContext);
if ( gs_currentFocus != this )
+153 -29
View File
File diff suppressed because it is too large Load Diff
+30
View File
@@ -1085,8 +1085,38 @@ void wxOSX_insertText(NSView* self, SEL _cmd, NSString* text);
{
wxUnusedVar(aRange);
wxUnusedVar(actualRange);
// We don't support ranges, but at least return the position where the
// text is being input, if the window provides it.
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if ( wxWindowMac* const win = impl ? impl->GetWXPeer() : nullptr )
{
const wxRect rect = win->GetInputMethodCursorRect();
if ( !rect.IsEmpty() )
{
// This function must return the rectangle in screen coordinates.
const wxRect rectScreen(win->ClientToScreen(rect.GetPosition()),
rect.GetSize());
return wxToNSRect(nil, rectScreen);
}
}
return NSMakeRect(0, 0, 0, 0);
}
- (NSTextInputContext *)inputContext
{
// Returning nil from here disables the input method for this view.
wxWidgetCocoaImpl* impl = (wxWidgetCocoaImpl* ) wxWidgetImpl::FindFromWXWidget( self );
if ( wxWindowMac* const win = impl ? impl->GetWXPeer() : nullptr )
{
if ( !win->IsInputMethodEnabled() )
return nil;
}
return [super inputContext];
}
- (NSUInteger)characterIndexForPoint:(NSPoint)aPoint
{
wxUnusedVar(aPoint);
+39
View File
@@ -156,21 +156,30 @@ public:
m_scrollWindowCallCount++;
m_lastScrollWindowDelta = wxPoint(dx, dy);
m_lastScrollWindowViewStart = GetViewStart();
}
void ResetScrollWindowCalls()
{
m_scrollWindowCallCount = 0;
m_lastScrollWindowDelta = wxPoint();
m_lastScrollWindowViewStart = wxDefaultPosition;
}
int GetScrollWindowCallCount() const { return m_scrollWindowCallCount; }
wxPoint GetLastScrollWindowDelta() const { return m_lastScrollWindowDelta; }
// Return the view start as it was during the last ScrollWindow() call.
wxPoint GetLastScrollWindowViewStart() const
{
return m_lastScrollWindowViewStart;
}
private:
int m_scrollWindowCallCount = 0;
wxPoint m_lastScrollWindowDelta;
wxPoint m_lastScrollWindowViewStart;
};
#endif // wxUSE_SCROLLBAR
@@ -216,6 +225,36 @@ TEST_CASE_METHOD(WindowTestCase, "Window::ScrolledWindowPhysicalScrolling",
CHECK( win->GetLastScrollWindowDelta() == wxPoint(-10, 0) );
}
TEST_CASE_METHOD(WindowTestCase, "Window::ScrolledWindowViewStart",
"[window][scroll]")
{
auto win = make_unique<ScrollCountingWindow>(wxTheApp->GetTopWindow());
// ScrollWindow() must be called after updating the scroll position, so
// that it can be used, e.g. to convert between logical and device
// coordinates, in the overridden version of this function.
win->Scroll(0, 3);
REQUIRE( win->GetScrollWindowCallCount() == 1 );
CHECK( win->GetLastScrollWindowDelta() == wxPoint(0, -30) );
CHECK( win->GetLastScrollWindowViewStart() == wxPoint(0, 3) );
win->ResetScrollWindowCalls();
win->Scroll(2, -1);
REQUIRE( win->GetScrollWindowCallCount() == 1 );
CHECK( win->GetLastScrollWindowDelta() == wxPoint(-20, 0) );
CHECK( win->GetLastScrollWindowViewStart() == wxPoint(2, 3) );
// When scrolling in both directions, ScrollWindow() is called twice and
// the view start must be fully updated by the time of the last call.
win->ResetScrollWindowCalls();
win->Scroll(1, 1);
REQUIRE( win->GetScrollWindowCallCount() == 2 );
CHECK( win->GetLastScrollWindowViewStart() == wxPoint(1, 1) );
}
#endif // wxUSE_SCROLLBAR
TEST_CASE_METHOD(WindowTestCase, "Window::ShowHideEvent", "[window]")