. 중첩 처리

그룹에 속하는 레코드의 종류에는 제한이 없으며 심지어 그룹이 그룹에 포함될 수도 있다. 즉 그룹끼리는 서로 중첩(Nesting)이 가능해야 한다. 예를 들자면 매크로 실행문에 바꾸기 명령이 들어 있다거나 매크로가 매크로를 호출하는 경우가 그룹 중첩에 해당한다. 2단계 이상의 중첩도 물론 가능하다. 매크로에 속한 매크로가 바꾸기 동작을 하면 3중으로 중첩된다.

이때 안쪽에 포함된 그룹은 단독으로 그룹을 구성해서는 안되며 바깥쪽 그룹과 같은 그룹 ID를 가져야 한다. 이렇게 하려면 그룹을 구성하는 편집 동작이더라도 이미 그룹 내에 있을 때 고유한 그룹 ID를 만들지 말고 이미 적용되고 있는 그룹 ID를 찾아 취하면 된다. 이쯤 되면 무척 복잡하게 느껴지겠지만 막상 코드를 작성해보면 그리 어렵지 않다. 코드를 작성하면서 중첩 처리를 어떻게 구현하는지 보도록 하자.

먼저 취소 레코드에 그룹 ID를 기억하기 위한 멤버변수를 추가한다. 단순한 ID값이므로 정수형으로 선언하고 이름은 tick이라고 주었다.

 

struct UndoRecord

{

     BYTE action;

     BYTE status;

     int pos;

     union {

          TCHAR *data;

          int *dest;

     };

    int tick;

};

 

이 값이 0이면 단독 레코드이며 0이 아니면 그룹에 속하는 레코드이다. 같은 그룹끼리는 같은 그룹 ID를 가지므로 이 값이 같은 그룹을 통째로 취소/재실행하면 된다. 취소 레코드에 멤버가 늘어났지만 레코드 할당문이나 초기화문에서는 레코드 크기를 sizeof 연산자로 계산하도록 되어 있으므로 수정하지 않아도 된다. 다음은 그룹핑을 관리하기 위한 세 개의 멤버변수를 추가한다.

 

class CApiEdit

{

     ....

     int URtick;

     int URnest;

     BOOL bPrevCompoGroup;

 

URtick은 현재 적용할 그룹 ID값이며 URnest는 중첩 레벨이다. 중첩된 그룹을 작성할 때는 바깥쪽 그룹의 ID를 사용해야 하므로 필요할 때 ID를 직접 생성해서 쓸 수 없으며 사용중인 ID를 멤버변수가 따로 기억하고 있어야 한다. 그래야 중첩된 그룹들이 항상 같은 그룹 ID를 가질 수 있다. 두 값 모두 InitDoc에서 0으로 초기화한다.

 

void CApiEdit::InitDoc()

{

     ....

     URtick=0;

     URnest=0;

     bPrevCompoGroup=FALSE;

     Invalidate(-1);

}

 

최초 편집을 시작할 때는 그룹 상태도 아니고 중첩된 상태도 아니다. 다음 두 함수는 그룹을 시작하고 끝내기 위한 처리를 하며 비록 길이는 짧지만 그룹 처리의 핵심 함수이다. CApiEdit 클래스에 이 두 멤버함수를 작성하도록 하자.

 

void CApiEdit::StartUndoGroup()

{

     if (URnest==0) {

          URtick=((nowur << 16) | (GetTickCount() & 0xffff));

     }

     URnest++;

}

 

void CApiEdit::EndUndoGroup()

{

     URnest--;

     if (URnest==0) {

          URtick=0;

     }

}

 

StartUndoGroup은 그룹을 시작한다. 중첩 레벨이 0이면, 즉 이전에 그룹 편집 상태가 아니었다면 새로 그룹 ID를 생성한다. 그룹 ID는 앞에서 설명했다시피 지금 작성되는 취소 레코드의 ID nowur GetTickCount 함수로 구한 틱 값의 조합으로 작성된다. 그룹이 시작되면 URnest 1증가하여 그룹 안에 들어와 있음을 기억한다.

만약 URnest 0이 아닌 상태, 즉 이미 그룹에 들어와 있는 상태에서 StartUndoGroup 함수가 호출되었다면 이때는 새로 그룹 ID를 작성하지 않고 URnest 1증가시킨다. 따라서 여전히 그룹 상태이지만 안쪽에 속한 그룹은 바깥 그룹에서 작성해놓은 ID를 같이 사용하게 될 것이다. 3, 4중으로 그룹이 중첩되더라도 StartUndoGroup 함수는 최초의 그룹이 시작될 때, 즉 제일 바깥쪽의 그룹이 시작될 때만 그룹 ID를 작성한다. 이것이 바로 중첩 그룹의 핵심이다.

EndUndoGroup은 그룹을 끝낸다. URnest 1감소시키고 만약 URnest 0이면 그룹 ID 0으로 초기화한다. 그룹 상태가 해제되면 이후부터 작성되는 레코드는 그룹에 속하지 않게 될 것이다. 만약 중첩된 안쪽 그룹이 끝나는 상황이라면 중첩 레벨만 하나 감속하고 그룹 ID는 계속 유지한다. 모든 중첩된 그룹이 완전히 종료될 때(URnest 0)만 그룹 ID 0이 된다.

이 두 함수에 의해 관리되는 그룹 ID는 취소 레코드를 작성하는 URInsert, URDelete, URMove 함수에 의해 사용된다. 세 함수에 다음 코드를 추가한다.

 

void CApiEdit::URInsert(int nPos, TCHAR *str)

{

     ....

     pUR[nowur].action=UR_INSERT;

     pUR[nowur].pos=nPos;

    pUR[nowur].tick=URtick;

     lstrcpy(pUR[nowur].data,str);

}

 

void CApiEdit::URDelete(int nPos, TCHAR *str)

{

     ....

     pUR[nowur].action=UR_DELETE;

     pUR[nowur].pos=nPos;

    pUR[nowur].tick=URtick;

     lstrcpy(pUR[nowur].data,str);

}

 

void CApiEdit::URMove(int nPos, int Dest,TCHAR *str)

{

     ....

     pUR[nowur].action=UR_MOVE;

     pUR[nowur].pos=nPos;

    pUR[nowur].tick=URtick;

     pUR[nowur].dest[0]=Dest;

     pUR[nowur].dest[1]=lstrlen(str);

}

 

URtick값은 Start(End)UndoGroup 함수가 관리하므로 이 함수들은 이 값을 취소 레코드의 tick 멤버에 기록하기만 하면 된다. 그룹 상태라면 URtick StartUndoGroup 함수에 의해 적절한 그룹 ID값을 가질 것이고 그렇지 않다면 0의 값을 가질 것이다.