notepad2 컴파일 삽질기 2 : IME 메시지를 처리하도록 수정

step2. IME 메시지를 처리하도록 수정

IME 메시지를 처리하도록 수정하는 방법은 CodeWiz님의 방법을 거의 그대로 사용했으며, 티끌만큼 수정했습니다.
그런데도, 굳이 여기에 적은 이유는 아래와 같습니다.

1. 앞뒤 포스팅과의 일관성 유지
2. CodeWiz님의 방법을 쥐꼬리만큼이라도 더 쉽게 설명
3. 제 스스로 까먹을까봐

수정과정은 역시 파일 단위로 적겠습니다.
수정 대상 파일은 3개(ScintillaWin.cxx, Editor.h, Editor.cxx)인데, 모두 Scintilla의 파일들입니다.


1. ScintillaWin.cxx

WM_IME_COMPOSITION 메시지를 처리하기 위해 관련된 메쏘드를 수정하거나 추가해야 합니다.
수정할 메쏘드와 추가할 메쏘드는 다음과 같습니다.

수정: HandleComposition(), UpdateSystemCaret(), CreateSystemCaret(), DestroySystemCaret(), HasCaretSizeChanged()
추가: GetCompositionString(), AddImeCompositionString(), MoveCompositionWindow(), GetCaretSize()


HandleComposition()은 아래와 같이 수정합니다.
sptr_t ScintillaWin::HandleComposition(uptr_t wParam, sptr_t lParam) {
#ifdef __DMC__
    // Digital Mars compiler does not include Imm library
    return 0;
#else
    if(lParam & GCS_COMPSTR)
    {
        if(inComposition)
            DelCharBack(false);

        if(AddImeCompositionString(GCS_COMPSTR) == 0)
            inComposition = FALSE;
        else
            inComposition = TRUE;
    }
    else if(lParam & GCS_RESULTSTR)
    {
        if(inComposition)
            DelCharBack(false);

        AddImeCompositionString(GCS_RESULTSTR);
        inComposition = FALSE;
    }

    MoveCompositionWindow();
    UpdateSystemCaret();
    return 0;

#endif
}


UpdateSystemCaret()은 아래와 같이 수정합니다.
void ScintillaWin::UpdateSystemCaret() 
{
    if (hasFocus)
    {
        if (HasCaretSizeChanged())
        {
            DestroySystemCaret();
            CreateSystemCaret();
        }

        if(inComposition)
        {
            Point pos = LocationFromPosition(currentPos-1);
            ::SetCaretPos(pos.x-sysCaretWidth, pos.y);
        }
        else
        {
            Point pos = LocationFromPosition(currentPos);
            ::SetCaretPos(pos.x, pos.y);
        }
    }
}


CreateSystemCaret()은 아래와 같이 수정합니다.
BOOL ScintillaWin::CreateSystemCaret() 
{

    Point cs = GetCaretSize();

    sysCaretHeight = cs.y;
    sysCaretWidth = cs.x;

    BOOL retval = ::CreateCaret(MainHWND()
        , NULL
        , sysCaretWidth
        , sysCaretHeight);

    ::ShowCaret(MainHWND());
    return retval;
}


DestroySystemCaret()은 아래와 같이 수정합니다.
BOOL ScintillaWin::DestroySystemCaret() 
{
    ::HideCaret(MainHWND());
    return ::DestroyCaret();
}


HasCaretSizeChanged()은 아래와 같이 수정합니다.
bool ScintillaWin::HasCaretSizeChanged() 
{
    Point cs = GetCaretSize();
    if(cs.x != sysCaretWidth || cs.y != sysCaretHeight)
        return true;
    return false;
}


다음, 추가해야할 메쏘드 4개를 ScintillaWin.cxx 파일 맨 마지막에 추가합니다.
LONG ScintillaWin::GetCompositionString(DWORD index, LPVOID buf, DWORD len)
{
    LONG bytes = 0;
    HIMC hIMC = ::ImmGetContext(MainHWND());
    if (hIMC)
    {
        bytes = ::ImmGetCompositionStringW(hIMC, index, buf, len);
        ::ImmReleaseContext(MainHWND(), hIMC);
    }

    return bytes;
}

LONG ScintillaWin::AddImeCompositionString(DWORD index)
{
    const int maxImeBuf = 200;
    wchar_t wcs[maxImeBuf];

    LONG bytes = GetCompositionString(index, wcs, (maxImeBuf-1)*2);
    if(bytes == 0)
        return bytes;

    LONG wides = bytes / 2;

    if (IsUnicodeMode()) {
        char utfval[maxImeBuf * 3];
        unsigned int len = UTF8Length(wcs, wides);
        UTF8FromUTF16(wcs, wides, utfval, len);
        utfval[len] = '\0';
        AddCharUTF(utfval, len);
    } else {
        char dbcsval[maxImeBuf * 2];
        int size = ::WideCharToMultiByte(InputCodePage()
            , 0
            , wcs
            , wides
            , dbcsval
            , sizeof(dbcsval) - 1
            , 0
            , 0);
        AddCharUTF(dbcsval, size);
    }

    return bytes;
}

BOOL ScintillaWin::MoveCompositionWindow()
{
    HIMC hIMC = ::ImmGetContext(MainHWND());
    if(!hIMC)
        return FALSE;

    Point pos = LocationFromPosition(currentPos);
    COMPOSITIONFORM CompForm;
    CompForm.dwStyle = CFS_POINT;
    CompForm.ptCurrentPos.x = pos.x;
    CompForm.ptCurrentPos.y = pos.y;
    ::ImmSetCompositionWindow(hIMC, &CompForm);
    ::ImmReleaseContext(MainHWND(), hIMC);
    return TRUE;
}

Point ScintillaWin::GetCaretSize()
{
    Point cs;

    if(inComposition)
    {
        Point end = LocationFromPosition(currentPos);
        Point start = LocationFromPosition(currentPos-2);
        cs.x = end.x - start.x;
        cs.y = vs.lineHeight;

    }
    else if(inOverstrike)
    {
        cs.y = vs.lineHeight;
        Point end = LocationFromPosition(currentPos+1);
        Point start = LocationFromPosition(currentPos);
        cs.x = end.x - start.x;
        if(cs.x <= 0)
        {
            cs.x = vs.aveCharWidth;
        }
    }
    else
    {
        cs.y = vs.lineHeight;
        cs.x = vs.caretWidth;
        if (cs.x == 0)
            cs.x = 1;
    }

    return cs;
}


마지막으로, ScintillaWin.cxx에서 추가할 메쏘드를 선언해줍니다.
class ScintillaWin :
    public ScintillaBase {
public:
    LONG GetCompositionString(DWORD index, LPVOID buf, DWORD len);
    LONG AddImeCompositionString(DWORD index);
    BOOL MoveCompositionWindow();
    Point GetCaretSize();


2. Editor.h

IME 조합중인지 여부를 저장하는 변수를 선언합니다.
class Editor : public DocWatcher {
public:
    bool inComposition;



3. Editor.cxx

IME 조합여부를 처리하는 부분을 추가합니다.
Editor의 선언부AddCharUTF()에 조금의 코드를 추가하면 됩니다.

선언부에서는 아래와 같이 초기화 작업만 해주면 됩니다.
Editor::Editor() {
    inComposition = false;


다음, void AddCharUTF(char *s, unsigned int len, bool treatAsDBCS) 메쏘드에서
if (inOverstrike && !wasSelection && !RangeContainsProtected(currentPos, currentPos + 1)) {
를 찾아서
if (!inComposition && inOverstrike && !wasSelection && !RangeContainsProtected(currentPos, currentPos + 1)) {
로 수정하면 됩니다.


마지막으로, void Paint(Surface *surfaceWindow, PRectangle rcArea) 메쏘드에서 캐럿을 그려주는 부분을 찾아 주석처리합니다.
아래의
        // Draw the Caret
        if (lineDoc == lineCaret) {
를 찾아서
        // Draw the Caret
        if (false && lineDoc == lineCaret) {
로 바꿔주면 됩니다.


여기까지 수정된 것만으로 자연스러운 한글 입출력은 가능합니다.
하지만, undo/redo에서 문제가 발생합니다.
(Scintilla/Notepad2의 undo/redo 구조때문입니다)

다음 포스팅에서는 undo/redo를 자연스럽게 수정하는 방법을 소개하겠습니다.