notepad2 컴파일 삽질기 3 : undo/redo가 정상동작하도록 수정 (버그패치)

step3. undo/redo가 정상동작하도록 수정

앞의 두 포스팅과 달리, 이번 3번에서 수정되는 내용은 우아한 수정과는 거리가 멉니다.
private로 선언된 클래스를 굳이 public으로 끌어올리는 것 외에도 좀 지저분한 수정이 곳곳에서 자행(?)됩니다.

하지만, 그래도 꼭 직접 수정해봐야겠단 일념으로 수정을 했습니다.
Scintilla는 편집은 Editor 클래스에서, undo/redo는 Cellbuffer 클래스에서 처리하는 구조인데, 편집기를 담당하는 Editor 클래스에서 이를 직접 제어하도록 수정했습니다.
(이런 식으로 패치하면 안돼요. 엉엉엉)


1. Editor.h

Editor 클래스에 앞의 포스팅에서 추가한 inComposion 외에 2개의 bool 변수를 더 추가합니다.
역할이 조금씩 다르기 때문에 이렇게 지정했는데, 막상 끝내고 보니 지저분해지는 근본적인 원인 중 하나가 되더군요.
class Editor : public DocWatcher {
    bool inComposition;
    bool bCompositioning;
    bool bCompletingComposition;


2. CellBuffer.h

undo/redo를 처리하는 부분을 외부에서 제어할 수 있도록 수정합니다.
UndoHistory 클래스의 선언부에 아래와 같은 내용을 추가합니다.
class UndoHistory {
protected:
    bool bEnabled;
public:
    void EnableUndoAction(bool bEnable);


다음으로 CellBuffer 클래스의 선언부에서 UndoHistory uh; 의 상속형태를 private: 에서 public: 으로 바꿔줍니다.
class CellBuffer {
private:
    UndoHistory uh;

class CellBuffer {
public:
    UndoHistory uh;    
private:
와 같이 말이죠.



3. Document.h

Document 클래스 역시 위와 같은 이유에서 CellBuffer cb; 의 상속형태를 private: 에서 public: 으로 바꿔줍니다.
class Document : PerLine {
private:
    CellBuffer cb;

class Document : PerLine {
public:
    CellBuffer cb;
private:
와 같이 말입니다.



4. CellBuffer.cxx

UndoHistory 클래스의 생성자에 아래와 같은 한 줄을 추가합니다.
UndoHistory::UndoHistory() {
    bEnabled = true;

    
그리고, AppendAction(), BeginUndoAction(), EndUndoAction()의 맨앞에 if (!bEnabled) return; 를 추가합니다.
void UndoHistory::AppendAction(actionType at, int position, char *data, int lengthData, bool &startSequence) {
    if (!bEnabled) return;
    EnsureUndoRoom(); // 여기부터는 원래의 메쏘드
   
void UndoHistory::BeginUndoAction() {
    if (!bEnabled) return;
    EnsureUndoRoom(); // 여기부터는 원래의 메쏘드
   
void UndoHistory::EndUndoAction() {
    if (!bEnabled) return;
    PLATFORM_ASSERT(undoSequenceDepth > 0); // 여기부터는 원래의 메쏘드

    
마지막으로 Undo 여부를 제어하는 메쏘드를 추가합니다.
void UndoHistory::EnableUndoAction(bool bEnable)
{
    bEnabled = bEnable;
}

이것으로 외부에서 CellBuffer를 손댈 수 있게 되었습니다.



5. Editor.cxx

이제부터는 외부에서 직접 CellBuffer를 손대어 한글 조립시 적절하게 undo를 enable/disable시킬 수 있도록 수정합니다.
우선, AddCharUTF()를 대폭 수정해서 아래 내용으로 바꿔치기합니다.
void Editor::AddCharUTF(char *s, unsigned int len, bool treatAsDBCS) {
    bool wasSelection = currentPos != anchor;
    ClearSelection();
    static bool charReplaceAction = false;

    if (inOverstrike && !wasSelection && !RangeContainsProtected(currentPos, currentPos + 1)) {
        bool bOneMore = false;
        
        if (!inComposition)
        {
            if (currentPos < (pdoc->Length())) {
                if (!IsEOLChar(pdoc->CharAt(currentPos))) {
                    charReplaceAction = true;
                    pdoc->BeginUndoAction();
                    pdoc->DelChar(currentPos);
                }
            }
            if (bCompositioning) pdoc->cb.uh.EnableUndoAction(false);
        }

        else if (bCompletingComposition)
        {
            if (len>2)
            {
                len--;
                bOneMore = true;
            }
            pdoc->cb.uh.EnableUndoAction(true);
        }

        else if (!bCompositioning) pdoc->cb.uh.EnableUndoAction(true);

        if (pdoc->InsertString(currentPos, s, len)) {
            SetEmptySelection(currentPos + len);
        }

        if (charReplaceAction && (!bCompositioning || (inComposition && !bCompositioning))) {
            pdoc->cb.uh.EnableUndoAction(true);
            pdoc->EndUndoAction();
            charReplaceAction = false;
        }

        if (bCompletingComposition && bOneMore)
        {
            if (currentPos < (pdoc->Length())) {
                if (!IsEOLChar(pdoc->CharAt(currentPos))) {
                    charReplaceAction = true;
                    pdoc->BeginUndoAction();
                    pdoc->DelChar(currentPos);
                }
            }
            if (pdoc->InsertString(currentPos, s+2, 1)) {
                SetEmptySelection(currentPos + 1);
            }
            if (charReplaceAction) {
                pdoc->EndUndoAction();
                charReplaceAction = false;
            }
        }
        bCompletingComposition = false;
    }
    else if (pdoc->InsertString(currentPos, s, len)) {
        SetEmptySelection(currentPos + len);
    }

    // If in wrap mode rewrap current line so EnsureCaretVisible has accurate information
    if (wrapState != eWrapNone) {
        AutoSurface surface(this);
        if (surface) {
            WrapOneLine(surface, pdoc->LineFromPosition(currentPos));
        }
        SetScrollBars();
    }
    EnsureCaretVisible();
    // Avoid blinking during rapid typing:
    ShowCaretAtCurrentPosition();
    if (!caretSticky) {
        SetLastXChosen();
    }

    if (treatAsDBCS) {
        NotifyChar((static_cast<unsigned char>(s[0]) << 8) |
                static_cast<unsigned char>(s[1]));
    } else {
        int byte = static_cast<unsigned char>(s[0]);
        if ((byte < 0xC0) || (1 == len)) {
            // Handles UTF-8 characters between 0x01 and 0x7F and single byte
            // characters when not in UTF-8 mode.
            // Also treats \0 and naked trail bytes 0x80 to 0xBF as valid
            // characters representing themselves.
        } else {
            // Unroll 1 to 3 byte UTF-8 sequences.  See reference data at:
            // http://www.cl.cam.ac.uk/~mgk25/unicode.html
            // http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt
            if (byte < 0xE0) {
                int byte2 = static_cast<unsigned char>(s[1]);
                if ((byte2 & 0xC0) == 0x80) {
                    // Two-byte-character lead-byte followed by a trail-byte.
                    byte = (((byte & 0x1F) << 6) | (byte2 & 0x3F));
                }
                // A two-byte-character lead-byte not followed by trail-byte
                // represents itself.
            } else if (byte < 0xF0) {
                int byte2 = static_cast<unsigned char>(s[1]);
                int byte3 = static_cast<unsigned char>(s[2]);
                if (((byte2 & 0xC0) == 0x80) && ((byte3 & 0xC0) == 0x80)) {
                    // Three-byte-character lead byte followed by two trail bytes.
                    byte = (((byte & 0x0F) << 12) | ((byte2 & 0x3F) << 6) |
                            (byte3 & 0x3F));
                }
                // A three-byte-character lead-byte not followed by two trail-bytes
                // represents itself.
            }
        }
        NotifyChar(byte);
    }
}

다음으로,  sptr_t Editor::WndProc(unsigned int iMessage, uptr_t wParam, sptr_t lParam)의 switch-case 에서 case 일부를 수정합니다.
두 case문 모두, 맨앞에 pdoc->cb.uh.EnableUndoAction(true); 한 줄을 추가한 것입니다.
    case SCI_UNDO:
        pdoc->cb.uh.EnableUndoAction(true);
        Undo();
        SetLastXChosen();
        break;
       
    case SCI_PAGEDOWNRECTEXTEND:
    case SCI_SELECTIONDUPLICATE:
        pdoc->cb.uh.EnableUndoAction(true);
        return KeyCommand(iMessage);

        
        
6. ScintillaWin.cxx

sptr_t HandleComposition(uptr_t wParam, sptr_t lParam) 메쏘드를 아래 내용으로 교체합니다.
sptr_t ScintillaWin::HandleComposition(uptr_t wParam, sptr_t lParam) {
#ifdef __DMC__
    // Digital Mars compiler does not include Imm library
    return 0;
#else
    if (inOverstrike)
    {
ClearSelection();
        if(lParam & GCS_COMPSTR)
        {
            if(inComposition) DelCharBack(false);
            bCompositioning = true;
            inComposition = !(AddImeCompositionString(GCS_COMPSTR) == 0);
        }
        else if(lParam & GCS_RESULTSTR)
        {
            if(inComposition) DelCharBack(false);

            bCompositioning = false;
            AddImeCompositionString(GCS_RESULTSTR);
            inComposition = FALSE;
        }
    }
    else
    {
ClearSelection();
        pdoc->cb.uh.EnableUndoAction(false);

        if(lParam & GCS_COMPSTR)
        {
            if(inComposition) DelCharBack(false);

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

            pdoc->cb.uh.EnableUndoAction(true);
            AddImeCompositionString(GCS_RESULTSTR);
            inComposition = FALSE;
        }
    }

    MoveCompositionWindow();
    UpdateSystemCaret();
    return 0;

#endif
}

마지막으로,  sptr_t ScintillaWin::WndProc(unsigned int iMessage, uptr_t wParam, sptr_t lParam)의 switch-case에서 WM_SETFOCUS:에 한 줄을 추가합니다.
추가된 결과는 아래와 같습니다.
  case WM_SETFOCUS:
if (inComposition) PostMessage(this->MainHWND(), WM_IME_ENDCOMPOSITION, 0, 0); //추가된 줄
SetFocusState(true);
RealizeWindowPalette(false);
DestroySystemCaret();

이렇게 수정하고 컴파일해보면 한글 입력 및 한글을 포함한 undo/redo가 정상적으로 동작하는 것을 볼 수 있습니다.

그리고, 이런 글에는 언제나 붙는 말씀입니다만...
이 패치로 발생하는 어떠한 손해 내지는 피해에 대해서도 저는 책임지지 않습니다.


덧. 다음 두 가지 버그를 수정했습니다.
 -  블럭 선택이 된 상태에서 undo/redo 기능이 정상동작하지 않는 버그 수정
 -  한글 입력 중 포커스를 빼았겼다 다시 포커스를 가져오면 윈도우 IME의 한글입력창이 나타나는 버그 수정
(수정 내용이 작아서 별도 포스팅은 하지 않고 내용을 수정했습니다)