notepad2 컴파일 삽질기 7 : 한글 IME 패치 재작성

Notepad2에서 한글을 정상적으로 입력할 수 있도록 하기 위해 다음과 같은 패치를 공개했다.


이 패치의 가장 큰 문제는 너무 복잡하다는 점이다.
그런데, 또 하나의 문제가 있다. 바로 한글 IME 외의 IME에서 완벽하게 동작하지 않는다는 것...

사실, 모든 IME에서 완벽하게 동작하게 만들기 위해 이리저리 손을 많이 대었는데, 알고 보니 한글 IME만 엉망인 거다. OTL

한글 외의 일본어, 중국어 IME는 원래부터 정상적으로 동작했음


이 문제를 한꺼번에 해결하고, 좀 더 간결한 패치를 유지하기 위해 다시 패치를 해봤다.
참고로, 이 패치는 기본적으로 행복한고니 님의 패치[각주:1]를 기본으로 작성했다.
이 패치를 하려면, 위의 세 패치는 안한 셈 치고 처음부터 다시 시작해야 한다.

패치할 대상은 ScintillaWin.cxx 하나다. (얏호! 드디어 정상적인 패치를 만든 거다!)


1. WndProc(unsigned int iMessage, uptr_t wParam, sptr_t lParam) 수정

WM_IME_STARTCOMPOSITION: 을 찾아 아래와 같이 수정한다.
이 부분은 정확히는 행복한고니 님이 참고한 sixman님의 패치(헥헥 길다)에서 그대로 가져온 것이다.

case WM_IME_STARTCOMPOSITION:     // dbcs
    ImeStartComposition();
    // added from here-------------------------------------------------
    if (LOWORD(GetKeyboardLayout(0))==MAKELANGID(LANG_KOREAN, SUBLANG_KOREAN)) {
        // if the current IME is the Korean IME, do not show the default IME window
        return 0;
    }
    // added to here-------------------------------------------------
    return ::DefWindowProc(MainHWND(), iMessage, wParam, lParam);


2. ScintillaWin::HandleComposition(uptr_t wParam, sptr_t lParam) 대체

HandleComposition()을 찾아 아래 내용으로 대체한다.
삽입/덮어쓰기/행의 끝에서 덮어쓰기에서 각각 다르게 동작하도록 작성했다.
따라서, 행복한고니 님 패치의 버그[각주:2]는 발생하지 않는다.

sptr_t ScintillaWin::HandleComposition(uptr_t wParam, sptr_t lParam) {
#ifdef __DMC__
    // Digital Mars compiler does not include Imm library
    return 0;
#else
    static int cs    = -1;
    static int undo  = -1;
    static bool comp = false;
    static bool bEndOfLine, bOverstrike;
    static bool wasSelection = false;
    bool bKoreanIME = LOWORD(GetKeyboardLayout(0))==MAKELANGID(LANG_KOREAN, SUBLANG_KOREAN);
 
    if (bKoreanIME && (lParam & GCS_COMPSTR)) {
        HIMC hIMC = ::ImmGetContext(MainHWND());
        if (hIMC) {
            const int maxLenInputIME = 200;
            wchar_t wcs[maxLenInputIME];
            LONG bytes = ::ImmGetCompositionStringW(hIMC, GCS_COMPSTR, wcs, (maxLenInputIME-1)*2);
            int wides = bytes / 2;
            int selBegin = currentPos;

            if (bytes) {
                //comp==false 이면 최초 진입
                //undo를 마비시키기 전에 삭제할 글자/블럭 삭제
                if (!comp)
                {
                    wasSelection = currentPos != anchor;
                    if (wasSelection && anchor<currentPos) selBegin = anchor;
                    ClearSelection();
                    bEndOfLine = (selBegin == pdoc->Length()) || IsEOLChar(pdoc->CharAt(selBegin));
                    bOverstrike = inOverstrike;
                    if (bOverstrike && !wasSelection && !bEndOfLine && !RangeContainsProtected(selBegin, selBegin + 1)) DelChar();
                }
               
                if (cs < 0 && !bOverstrike) {
                    cs = vs.caretStyle;
                    vs.caretStyle = CARETSTYLE_BLOCK;
                }
               
                if (undo < 0) {
                    undo = pdoc->IsCollectingUndo()?1:0;
                    pdoc->SetUndoCollection(false);
                }
               
                if (!comp) {
                    comp = true;
                } else {
                    DelChar();
                }
            } else {
                //조합 중 조합중인 글자를 다 지운 경우
                if (cs >= 0) {
                    vs.caretStyle = cs;
                    cs = -1;
                }
 
                if (comp) {
                    comp = false;
                    DelChar();
                }

                if (undo >= 0) {
                    pdoc->SetUndoCollection(undo==1);
                    undo = -1;
                }
            }
           
            inOverstrike = false;
            if (IsUnicodeMode()) {
                char utfval[maxLenInputIME * 3];
                unsigned int len = UTF8Length(wcs, wides);
                UTF8FromUTF16(wcs, wides, utfval, len);
                utfval[len] = '\0';
                AddCharUTF(utfval, len);
            } else {
                char dbcsval[maxLenInputIME * 2];
                int size = ::WideCharToMultiByte(InputCodePage(),
                    0, wcs, wides, dbcsval, sizeof(dbcsval) - 1, 0, 0);
                for (int i=0; i<size; i++)
                    AddChar(dbcsval[i]);
            }
            inOverstrike = bOverstrike;
   
            MovePositionTo(selBegin);
        }
    }
   
    if (lParam & GCS_RESULTSTR) {
        //앞의 if문 6개는 모두 한글 입력기에서만 동작
        //다른 언어 IME에서는 패스
        if (comp) {
            comp = false;
            DelChar();
        }
       
        if (cs >= 0) {
            vs.caretStyle = cs;
            cs = -1;
        }
        if (undo >= 0){
            pdoc->SetUndoCollection(undo==1);
            undo = -1;
        }

        //덮어쓰기에서 한글 조합 중 마지막에 숫자나 기호를 붙인 경우 한 글자 더 삭제(가9)
        bool bKoreanPlusOneMore = false;
        if (bKoreanIME && bOverstrike)
        {
            HIMC hIMC = ::ImmGetContext(MainHWND());
            if (hIMC) {
                const int maxLenInputIME = 200;
                wchar_t wcs[maxLenInputIME];
                LONG bytes = ::ImmGetCompositionStringW(hIMC,
                    GCS_RESULTSTR, wcs, (maxLenInputIME-1)*2);
                int wides = bytes / 2;
               
                char dbcsval[maxLenInputIME * 2];
                int size = ::WideCharToMultiByte(InputCodePage(),
                    0, wcs, wides, dbcsval, sizeof(dbcsval) - 1, 0, 0);
   
                bKoreanPlusOneMore = (size==3);
                ::ImmReleaseContext(MainHWND(), hIMC);
            }
        }
        if (bKoreanPlusOneMore) DelChar();

        if (bKoreanIME) inOverstrike = false;

        HIMC hIMC = ::ImmGetContext(MainHWND());
        if (hIMC) {
            const int maxLenInputIME = 200;
            wchar_t wcs[maxLenInputIME];
            LONG bytes = ::ImmGetCompositionStringW(hIMC,
                GCS_RESULTSTR, wcs, (maxLenInputIME-1)*2);
            int wides = bytes / 2;
           
            if (IsUnicodeMode()) {
                char utfval[maxLenInputIME * 3];
                unsigned int len = UTF8Length(wcs, wides);
                UTF8FromUTF16(wcs, wides, utfval, len);
                utfval[len] = '\0';
                AddCharUTF(utfval, len);
            } else {
                char dbcsval[maxLenInputIME * 2];
                int size = ::WideCharToMultiByte(InputCodePage(),
                    0, wcs, wides, dbcsval, sizeof(dbcsval) - 1, 0, 0);
                for (int i=0; i<size; i++)
                    AddChar(dbcsval[i]);
            }
           
            // Set new position after converted
            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);
        }
        if (bKoreanIME) inOverstrike = bOverstrike;
       
        return 0;
    }
   
    return ::DefWindowProc(MainHWND(), WM_IME_COMPOSITION, wParam, lParam);
#endif
}


덧1. 드디어 완전체를 만들었다는 생각에 뿌듯함도 잠시... Scintilla 라이브러리가 그 사이에 2.x로 업데이트 되었다.
바뀐 부분의 코드를 읽어보니... 다중선택시 입력을 하면 전체 선택영역에 똑같은 내용을 출력하는 기능이 있더라.
이건 대체 어케 패치해야 되는 거냐... OTL

덧2. 한글날이 지나가기 전에 패치를 공개하기 위해 고군분투했다.
패치의 버그를 찾기 위해 열심히 뛰어주신 okto 님께 감사드린다.

  1. 이 패치는 간결하긴 하지만, 덮어쓰기 모드에서 치명적인 버그가 둘이나 있다.
    (뒤에 다시 언급됨) [본문으로]
  2. 1. undo/redo가 정상동작하지 않음
    2. 행의 끝에서 글을 입력하면 이상하게 동작함 [본문으로]