I’ve made a little example implementation. As far as I can tell it works like it is supposed to.
It would be great if you could have a look at it and maybe even implement this in JUCE!
Here is a good link if you need additional references: http://stackoverflow.com/questions/434048/how-do-you-use-ime
In order to draw all information provided by the IME’s Composition Window, we need to extend the TextInputTarget interface a little.
I propose to add something like the following functions:
virtual void setCaretPosition (int newIndex) = 0;
virtual int getCaretPosition() const = 0;
// virtual const Rectangle<int> getCaretRectangle() = 0;
virtual void setHighlightedRegionWithoutMovingCaret (const Range<int>& newRange) = 0;
virtual const Rectangle<int> getHighlightedRegionRectangle() const = 0;
virtual void replaceTextInRange (const Range<int>& range, const String& newText) = 0;
virtual void setUnderlineRegions (const Array<Range<int> >& regions) = 0;
I’ll provide my implementations of these functions in TextEditor as well, but note that I just hacked them to get something working, not considering all of TextEditor’s capabilities:
void TextEditor::replaceTextInRange (const Range<int>& range, const String& newText)
{
String text = getText();
text = text.replaceSection (range.getStart(), range.getLength(), newText);
setText (text, true); // ?
}
void TextEditor::setUnderlineRegions(const Array<Range<int> >& regions)
{
underlineRegions = regions;
repaint();
}
void TextEditor::setHighlightedRegionWithoutMovingCaret (const Range<int>& newRange)
{
selection = newRange;
repaint();
}
const Rectangle<int> TextEditor::getHighlightedRegionRectangle() const
{
// XXX: this bit is especially wrong; just hack to get something going
Range<int> sel = getHighlightedRegion();
String preText = getTextInRange (Range<int> (0, sel.getStart()));
String selText = getTextInRange (sel);
const Font font = getFont();
int b = font.getStringWidth (preText);
int w = font.getStringWidth (selText);
return Rectangle<int> (borderSize.getLeft() + textHolder->getX() + leftIndent + b - viewport->getX(),
borderSize.getTop() + textHolder->getY() - viewport->getY(),
w, roundToInt (font.getHeight()) + 2*topIndent + 8); // 8 needed because rest of code is incorrect?
}
Btw, I think the implementation of TextEditor::getCaretRectangle() may be incorrect.
Also change TextEditor::paintOverChildren() to draw underlines and clear underlines on TextEditor::focusLost():
void TextEditor::paintOverChildren (Graphics& g)
{
const Font font = getFont();//g.getCurrentFont();
for (int i = 0; i < underlineRegions.size(); ++i)
{
Range<int> region = underlineRegions[i];
if (region.intersects (getHighlightedRegion()))
continue; // do not underline highlighted region
String preText = getTextInRange (Range<int> (0, region.getStart()));
String ulText = getTextInRange (region);
int b = font.getStringWidth (preText);
int w = font.getStringWidth (ulText);
float ulX0 = float (borderSize.getLeft() + textHolder->getX() + leftIndent + b);
float ulX1 = float (ulX0 + w);
float ulY = float (borderSize.getTop() + textHolder->getY() + topIndent + roundToInt (cursorY) + roundToInt (cursorHeight));
Line<float> ul (ulX0, ulY, ulX1, ulY);
float dashes[2];
dashes[0] = 4.0f;
dashes[1] = 4.0f;
g.setColour (Colours::white);
g.drawDashedLine (ul, dashes, 2, 1.0f, 0);
}
...
}
void TextEditor::focusLost (FocusChangeType)
{
...
underlineRegions.clear();
...
}
Finally, you need to handle the WM_IME_* messages:
// NOTE:
// Here all Win32 IME handling is contained in a single class to facilitate
// showing the code changes.
// If this code were to be merged in the JUCE source tree, it would perhaps
// be better suited to go into juce_win32_Windowing.cpp directly.
#pragma comment(lib, "imm32.lib")
class Win32ImeHandler
{
public:
Win32ImeHandler();
LRESULT handleImeWndProc(ComponentPeer &owner, HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, bool &handled);
private:
void initialize();
// Get either the current 'in-progress' composition string or the 'finalized' composition string:
String getCompositionString(HIMC hImc, bool wantFinalized);
// Get caret position while doing composition:
// curImeCompString is current IME composition string, not currently displayed
// composition string (although they may be identical)
// returned position is relative to beginning of TextInputTarget, not composition string
int getCompositionCaretPos(HIMC hImc, LPARAM lParam, const String &curImeCompString);
// Get selected/highlighted range while doing composition:
// returned range is relative to beginning of TextInputTarget, not composition string
Range<int> getCompositionSelection(HIMC hImc, LPARAM lParam);
// Get underline/clause ranges while doing composition:
Array<Range<int> > getCompositionUnderlines(HIMC hImc, LPARAM lParam);
Array<Range<int> > getDefaultCompositionUnderlines(const String &compString, const Range<int> &selection);
// Move Candidate Window to correct position on screen:
void moveCandidateWindowToLeftAlignWithSelection(ComponentPeer &owner, TextInputTarget *target, HIMC hImc);
private:
// Begin and end positions of composition in number of characters relative to the
// beginning of the TextInputTarget. These values refer to the *currently displayed*
// composition string, not the composition string currently in the IME.
int bPosComposition;
int ePosComposition;
// Flag to see if there was a composition that didn't end yet, used to
// check in WM_IME_ENDCOMPOSITION for end of composition by cancellation.
bool hasPendingComposition;
};
// -------------------------------------------------------------------------------------------------
Win32ImeHandler::Win32ImeHandler()
{
initialize();
}
LRESULT Win32ImeHandler::handleImeWndProc(ComponentPeer &owner, HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam, bool &handled)
{
switch (msg)
{
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
case WM_IME_SETCONTEXT:
{
bool windowActive = (wParam == TRUE);
// Complete pending composition when losing focus:
// Note:
// Here findCurrentTextInputTarget() already returns NULL, so we can't update
// the TextInputTarget's state here; so each concrete TextInputTarget should handle this on its own
// when losing focus.
if (hasPendingComposition && !windowActive)
{
hasPendingComposition = false; // clear pending composition, so triggering WM_IME_ENDCOMPOSITION on ImmNotifyIME() does not consider this a canceled composition
HIMC hImc = ImmGetContext(hWnd);
if (hImc)
{
ImmNotifyIME(hImc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0);
ImmReleaseContext(hWnd, hImc);
}
}
// We handle the IME Composition Window ourselves (but let IME Candidates Window be
// handled by IME through DefWindowProc()), so we clear the ISC_SHOWUICOMPOSITIONWINDOW flag:
lParam &= ~ISC_SHOWUICOMPOSITIONWINDOW;
// Call DefWindowProc() ourselves, instead of simply setting handled to false and returning,
// because modified lParam is local variable:
handled = true;
return DefWindowProc(hWnd, msg, wParam, lParam);
}
break;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
case WM_IME_STARTCOMPOSITION:
{
initialize();
// Delete current selection in TextInputTarget (overwrite):
TextInputTarget *target = owner.findCurrentTextInputTarget();
if (target)
{
target->replaceTextInRange(target->getHighlightedRegion(), String::empty);
}
handled = true;
return 0;
}
break;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
case WM_IME_ENDCOMPOSITION:
{
// Check whether WM_IME_ENDCOMPOSITION was send because user canceled operation
// (e.g. by pressing escape button) and if so handle properly in TextInputTarget:
if (hasPendingComposition)
{
// Notify TextInputTarget:
TextInputTarget *target = owner.findCurrentTextInputTarget();
if (target)
{
// Remove composition string:
target->replaceTextInRange(Range<int>(bPosComposition, ePosComposition), String::empty);
ePosComposition = bPosComposition;
// Set caret to end of composition string:
target->setCaretPosition(ePosComposition);
// Set selection to no selection:
target->setHighlightedRegionWithoutMovingCaret(Range<int>::emptyRange(ePosComposition));
// Disable underlines:
Array<Range<int> > underlines;
target->setUnderlineRegions(underlines);
}
// Notify IME:
HIMC hImc = ImmGetContext(hWnd);
if (hImc)
{
// Close Candidate Window if needed:
ImmNotifyIME(hImc, NI_CLOSECANDIDATE, 0, 0);
ImmReleaseContext(hWnd, hImc);
}
}
initialize();
// Let DefWindowProc() handle this message so the Candidate Window is closed
// when you press enter, escape, or click on a candidate in the list.
handled = false;
return 0; // unused
}
break;
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
case WM_IME_COMPOSITION:
{
// Get TextInputTarget:
TextInputTarget *target = owner.findCurrentTextInputTarget();
if (!target)
{
handled = true; // ?
return 0;
}
// Get IMM context:
HIMC hImc = ImmGetContext(hWnd);
if (!hImc)
{
handled = true; // ?
return 0;
}
if (bPosComposition == -1 || ePosComposition == -1)
bPosComposition = ePosComposition = target->getCaretPosition();
// Handle result of composition (usually when the user presses enter, or clicks on an item
// in the Candidate Window):
if (lParam & GCS_RESULTSTR)
{
// Get 'finalized' composition string:
String compString = getCompositionString(hImc, true);
// Replace currently displayed composition string with new composition string:
target->replaceTextInRange(Range<int>(bPosComposition, ePosComposition), compString);
ePosComposition = bPosComposition + compString.length(); // update begin/end positions
// Set caret to end of composition string:
target->setCaretPosition(ePosComposition);
// Set selection to no selection:
target->setHighlightedRegionWithoutMovingCaret(Range<int>::emptyRange(ePosComposition));
// Disable underlines:
Array<Range<int> > underlines;
target->setUnderlineRegions(underlines);
//initialize();
hasPendingComposition = false;
}
// Handle not-yet-finalized composition:
if (lParam & GCS_COMPSTR)
{
// Get 'in-progress' composition string:
String compString = getCompositionString(hImc, false);
// Replace currently displayed composition string with new composition string:
target->replaceTextInRange(Range<int>(bPosComposition, ePosComposition), compString);
ePosComposition = bPosComposition + compString.length(); // update begin/end positions
// Update caret position:
int caretPos = getCompositionCaretPos(hImc, lParam, compString);
target->setCaretPosition(caretPos);
// Update selected range:
Range<int> selection = getCompositionSelection(hImc, lParam);
target->setHighlightedRegionWithoutMovingCaret(selection);
// XXX: when there's a selection, this means the
// candidates window was opened; while going through
// candidates the caret should be at the end of the composition string,
// but getCompositionCaretPos() return begin of the composition string for some reason
// XXX: this is a work-around.. probably there's something else wrong
if (!selection.isEmpty())
target->setCaretPosition(ePosComposition);
// Update underlines:
Array<Range<int> > underlines = getCompositionUnderlines(hImc, lParam);
if (underlines.size() == 0)
underlines = getDefaultCompositionUnderlines(compString, selection);
target->setUnderlineRegions(underlines);
// Mark that we are currently compositing:
hasPendingComposition = true;
}
// Move Candidate Window position so that the highlighted part of the
// displayed composition string and the candidates are left aligned into
// a single column (of course fonts will probably not match):
// Note: Do *after* updating selection.
moveCandidateWindowToLeftAlignWithSelection(owner, target, hImc);
// Release IMM context:
ImmReleaseContext(hWnd, hImc);
handled = true;
return 0;
}
break;
}
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
handled = false;
return 0; // unused
}
// -------------------------------------------------------------------------------------------------
void Win32ImeHandler::initialize()
{
// initialize to invalid
bPosComposition = -1;
ePosComposition = -1;
hasPendingComposition = false;
}
String Win32ImeHandler::getCompositionString(HIMC hImc, bool wantFinalized)
{
if (!hImc)
return String::empty;
DWORD dwIndex = GCS_COMPSTR;
if (wantFinalized)
dwIndex = GCS_RESULTSTR;
// Get string length:
int lenCompStrBytes = ImmGetCompositionString(hImc, dwIndex, NULL, 0); // or negative on error
if (lenCompStrBytes < 0)
return String::empty;
int lenCompStrChars = lenCompStrBytes/sizeof(TCHAR);
// Get string data:
TCHAR *tmp = new TCHAR[lenCompStrChars+1];
if (lenCompStrBytes > 0)
ImmGetCompositionString(hImc, dwIndex, tmp, lenCompStrBytes);
tmp[lenCompStrChars] = 0; // null terminate string
// Convert to JUCE string:
String compStr(CharPointer_UTF16(tmp), lenCompStrChars);
delete[] tmp;
tmp = NULL;
return compStr;
}
int Win32ImeHandler::getCompositionCaretPos(HIMC hImc, LPARAM lParam, const String &curImeCompString)
{
if (!hImc)
return -1;
if (lParam & CS_NOMOVECARET)
{
return bPosComposition; // do not move caret (by request of IME), bPosComposition = original caret position
}
else if (lParam & GCS_CURSORPOS)
{
int localCaretPos = ImmGetCompositionString(hImc, GCS_CURSORPOS, NULL, 0);
if (localCaretPos >= 0)
return bPosComposition + localCaretPos;
else
return bPosComposition; // error
}
else
{
return bPosComposition + curImeCompString.length();
}
}
Range<int> Win32ImeHandler::getCompositionSelection(HIMC hImc, LPARAM lParam)
{
if (!hImc)
return Range<int>::emptyRange(-1);
int bLocalSel = 0;
int eLocalSel = 0;
if (lParam & GCS_COMPATTR)
{
// Get size of attributes array:
int lenAttributesBytes = ImmGetCompositionString(hImc, GCS_COMPATTR, NULL, 0);
if (lenAttributesBytes > 0)
{
int lenAttributesChars = int(lenAttributesBytes/sizeof(char));
// Get attributes (8 bit flag per character):
char *attributes = new char[lenAttributesChars];
ImmGetCompositionString(hImc, GCS_COMPATTR, attributes, lenAttributesBytes);
// Find first character with ATTR_TARGET_* flag set:
bLocalSel = -1;
for (int i = 0; i < lenAttributesChars; ++i)
{
bool isCharSelected = (attributes[i] == ATTR_TARGET_CONVERTED || attributes[i] == ATTR_TARGET_NOTCONVERTED);
if (isCharSelected)
{
bLocalSel = i;
break;
}
}
if (bLocalSel == -1)
bLocalSel = lenAttributesChars;
// Find first character with ATTR_TARGET_* flag not set:
eLocalSel = -1;
for (int i = bLocalSel; i < lenAttributesChars; ++i)
{
bool isCharSelected = (attributes[i] == ATTR_TARGET_CONVERTED || attributes[i] == ATTR_TARGET_NOTCONVERTED);
if (!isCharSelected)
{
eLocalSel = i;
break;
}
}
if (eLocalSel == -1)
eLocalSel = lenAttributesChars;
delete[] attributes;
attributes = NULL;
}
}
return Range<int>(bPosComposition + bLocalSel, bPosComposition + eLocalSel);
}
Array<Range<int> > Win32ImeHandler::getCompositionUnderlines(HIMC hImc, LPARAM lParam)
{
Array<Range<int> > result;
if (!hImc)
return result;
if (lParam & GCS_COMPCLAUSE)
{
int lenClauseDataBytes = ImmGetCompositionString(hImc, GCS_COMPCLAUSE, NULL, 0);
if (lenClauseDataBytes <= 0)
return result;
int lenClauseDataItems = lenClauseDataBytes/sizeof(uint32);
uint32 *clauseData = new uint32[lenClauseDataItems];
ImmGetCompositionString(hImc, GCS_COMPCLAUSE, clauseData, lenClauseDataBytes);
for (int i = 0; i < lenClauseDataItems - 1; ++i)
{
Range<int> underline(bPosComposition + clauseData[i], bPosComposition + clauseData[i+1]);
result.add(underline);
}
delete[] clauseData;
clauseData = NULL;
}
return result;
}
Array<Range<int> > Win32ImeHandler::getDefaultCompositionUnderlines(const String &compString, const Range<int> &selection)
{
Array<Range<int> > result;
Range<int> ul1 = Range<int>(0, selection.getStart());
Range<int> ul2 = Range<int>(selection.getStart(), selection.getEnd());
Range<int> ul3 = Range<int>(selection.getEnd(), compString.length());
if (!ul1.isEmpty())
result.add(ul1);
if (!ul2.isEmpty())
result.add(ul2);
if (!ul3.isEmpty())
result.add(ul3);
return result;
}
#pragma warning(push)
#pragma warning(disable:4189) // why get this warning??
void Win32ImeHandler::moveCandidateWindowToLeftAlignWithSelection(ComponentPeer &owner, TextInputTarget *target, HIMC hImc)
{
Component *ownerComp = owner.getComponent();
Component *targetComp = ownerComp->getCurrentlyFocusedComponent();
if (targetComp != 0)
{
const int hMargin = 2; // ?
Rectangle<int> selRect = target->getHighlightedRegionRectangle();
Rectangle<int> compRect = targetComp->getBounds();
selRect.translate(compRect.getX(), compRect.getY()+hMargin);
CANDIDATEFORM posCandidateWindow = {0, CFS_CANDIDATEPOS, {selRect.getX(), selRect.getBottom()}, {0, 0, 0, 0}};
ImmSetCandidateWindow(hImc, &posCandidateWindow);
}
}
#pragma warning(pop)
One thing that’s still missing is that TextEditor::focusLost() should somehow call ImmNotifyIME(hImc, NI_COMPOSITIONSTR, CPS_COMPLETE, 0);, to close any open candidate list window, etc.; I have no idea how to do this in JUCE however…
Hope this helps!