2015/05/06

Qt 5.4 fcitx immodule Build 및 한글 사용


이전 글에서 우분투 15.04 Unity Desktop 환경에서 한글 입력기로 fcitx를 추전했는데, 막상 Qt 5.4의 Qt creator에서는 한영키가 동작하지 않아 황당했다. 당연히, 이 Qt creator로 build한 Qt 5.4 애플리케이션에서도 한영키가 동작할리 없다. 이전 글에서 테스트한 Qt 앱은 Qt 5.3에서 build 했던것 같다. 한마디로 우분투 패키지로 다운 받은 fcitx는 최신 버전의 Qt 5.4를 지원하지 않는 것 같다. 뭔가 달라진 듯...

구글링하니까 마침 일본 블로그에 fcitx build하는 방법이 올라와 있어서 다시 build 한 후 Qt5 immodule를 복사해 넣었더니 모든 Qt 애플리케이션에서 한글입력이 잘 된다. 다만, 그 블로그 내용을 그대로 따라하니까 안되더라... 그래서 다시 정리한다.

Qt 5.4에서 fcitx Qt5 immodule build

$ sudo apt-get install git cmake

$ git clone https://github.com/fcitx/fcitx-qt5.git
$ cd fcitx-qt5
$ git checkout 0.1.3

build 하기 전에 Qt 5.4가 설치된 홈 폴더를 미리 알아 두어야 한다. 나의 PC에는 /opt/OpenSrc/Qt에 설치하였다.

$ cmake . -DCMAKE_PREFIX_PATH=/opt/OpenSrc/Qt/5.4/gcc_64
$ make

기존 fcitx 패키지로 설치된 Qt5 immodule 대체 설치

기존의 우분투 fcitx 패키지에 딸려온 Qt5 immodule을 대체해버리는 것이 가장 좋은 설치 방법이다. 안전을 위해 원래 파일은 org.libfcitxplatforminputcontextplugin.so로 백업했다.

$ sudo mv /usr/lib/x86_64-linux-gnu/qt5/plugins/platforminputcontexts/libfcitxplatforminputcontextplugin.so /usr/lib/x86_64-linux-gnu/qt5/plugins/platforminputcontexts/org.libfcitxplatforminputcontextplugin.so

$ sudo cp ./src/libfcitxplatforminputcontextplugin.so /usr/lib/x86_64-linux-gnu/qt5/plugins/platforminputcontexts/

또 한가지 대체해야 할 파일이 있다는 것을 나중에 알았다. Qt5 immodule이 사용하는 runtime library 파일 하나가 같이 build 되었다. 이 놈까지 버전이 일치해야 한다.

$ sudo mv /usr/lib/x86_64-linux-gnu/libfcitx-qt.so.0.1 /usr/lib/x86_64-linux-gnu/org.libfcitx-qt.so.0.1

$ sudo cp ./fcitx-qt5/libfcitx-qt5.so.0.1 /usr/lib/x86_64-linux-gnu/libfcitx-qt.so.0.1

참고 사항

참조한 일본 블로그에는 환경 변수 설정 같은 것들이 있다. 그렇지만 .bashrc에 입력기 환경변수를 넣으면 X-Window의 실행 순서 때문에 환경변수가 제대로 동작하지 않을 수도 있다. 편하고도 안전한 방법은 먼저 fcitx 패키지를 설치하는 것이다. 이 글은 fcitx 패키지가 먼저 설치된 것을 전제로 했다.

일본 블로그에서 설명하듯이 기존 파일들을 대체하지 않고도 새로 build한 Qt5 immodule을 사용할 수도 있는데 runtime library가 두 개라 환경 설정을 제대로 해주어야 한다(기존 패키지와의 충돌때문에 설정이 다소 복잡하므로 세부 설명은 안한다). 다만, Qt5 immodule을 복사해야 할 폴더는 /opt/OpenSrc/Qt/5.4/gcc_64/plugins/platforminputcontexts가 아니고, /opt/OpenSrc/Qt/Tools/QtCreator/bin/plugins/platforminputcontexts이더라. 뭐, 두 곳에 다 복사해도 상관은 없다.

2015/05/05

우분투 15.04 fcitx-hangul 설치 및 설정


우분투 15.04에서 기본 입력기인 ibus가 큰 문제가 있는 건 아니지만, 최근에 Qt5를 배우면서 Qt5 입력기 모듈을 제공하는 fcitx를 설치해 보기로 했다. ibus나 uim 입력기는 Qt5 애플리케이션에서 한글 입력시 띄어쓰기 등의 문제가 발생한다. ibus의 경우 이 문제에 대해 버그 리포팅 한지 1년이 다 돼가는데 Qt5.5 이후에나 사용할 수 있을 듯 하다.

참고로, ibus나 fcitx 주 개발자는 중국인이고 uim은 일본인이다. ibus-hangul과 nabi 개발자는 한국 사람인데 그가 만든 libhangul을 fcitx-hangul 입력기에서 사용하고 있다. 그래서 그런지 몰라도 ibus-hangul에서 발생하는 마지막 입력글자가 마우스 이동시 따라 다니는 문제는 fcitx-hangul에서도 발생한다. uim-byeoru를 만든이도 또 다른 한국인인듯 하다. 한글 입력기의 문제는 한국 개발자들이 주개발자가 아니고 한글 입력기 모듈만을 만든는데서 비롯되는 것인지도 모른다. 워낙에 개발자 저변이 열악하다 보니 한번 만들면 유지보수나 사용자 지원이 잘 이루어지지 않고 있는 것이 현실이다.

fcitx를 설치 후 사용해 본 바로는 현재까지의 입력기들 중에선 우분투 유니티 데스크탑 환경에서 가장 좋은 한글입력기라고 말하고 싶다. 기본적으로 유니티 상단 패널에서 한영 전환 상태를 구분할 수 있고, 한자 입력은 물론이고 virtual keyboard를 이용해서 일본어나 특수문자 입력도 가능하다. Qt5 입력 모듈이 기본 제공되고 있어 Qt5 애플리케이션에서도 한글입력이 잘 된다.


fcitx 설치

아래와 같이 fcitx-hangul 패키지만 설치하면 필요한 패키지들이 모두 설치된다.

$ sudo apt-get install fcitx-hangul

패키지 설치 후 기본 입력기인 ibus대신 아래 화면과 같이 [System Settings] > [Language Support]에서 [Keyboard input method system:]의 [fcitx]를 선택한 후, 다시 로그인해야 한다.


한글 키보드 추가 및 한영키 설정

재로그인 후, fcitx 설정 프로그램을 실행해야 한다. 유니티 패널 상단의 키보드를 클릭하여 Configure를 선택하거나, <Super> Key를 눌러서 유니티 대쉬에서 fcitx를 검색해서 나오는 [fcitx Configuration] 아이콘을 선택해서 기동해도 되고, 터미널에서 아래 명령으로 실행해도 된다.

$ fcitx-config-gtk3

먼저, 아래 화면과 같이 [Input Method] 탭 하단 왼쪽의 [+] 버튼을 눌러 Hangul 키보드를 검색한 후 선택하여 추가해 준다. 참고로, 내 경우엔 우분투 기본언어를 영어로 사용하고 있어서, 한글을 기본 언어로 사용하는 경우에는 반대로 Keyboard-English(US)를 추가해야 할지도 모르겠다. 그리고, 두개의 키보드가 추가된 상태에서 위에 있는 키보드가 기본 키보드가 된다. 즉, Hanul 키보드가 위에 있도록 순서를 조정하면 터미널 등의 애플리케이션 실행 후 한글 입력 상태가 된다.


참고로, 이렇게 추가된 키보드는 [System Settings] > [Text Entry]에서도 확인할 수 있는데 한글 키보드는 Hangul (Fcitx)로 표시된다.

이제, 한영전환키로 한영키만 설정해 주면 된다. 아래 화면과 같이 [Global Config] 탭에서 [Trigger Input Method]에 [Ctrl+Space]와 [Empty]로 표시되어 있는 버튼을 각각 눌러서 [한/영]키와 [Shift+Space]키로 바꿔주면 된다.


추가 설정

위의 과정까지만 해 주어도 fcitx를 입력기로 쓰는데 큰 지장은 없는데, 아래 화면과 같이 한글로 전환시 [Hangul]이라고 표시되는 작은 창이 뜨는게 좀 귀찮아서 없애기로 하였다.


위의 화면에 없애는 방법도 같이 보여주고 있는데, gedit로 ~/.config/fcitx/config 파일을 열어서 "ShowInputWindowAfterTriggering=False" 부분을 설정해서 저장한 후,

$ gedit ~/.config/fcitx/config

아래 명령으로 fcitx를 재기동 해주면 된다.

$ fcitx -r

Virtual Keyboard 사용

맨 위의 스크린 샷에 보이는 virtual keyboad를 사용하려면 유니티 상단 패널의 키보드나 태극문양 아이콘을 클릭해서 [Toggle Virtual Keyboard]를 선택하면 키보드가 나타나고, 한번더 선택하면 사라진다. 마우스로 Virtual Keyboard의 [Latin] 부분을 선택하면 번갈아 가면서 일본어 등 다른 키보드들을 선택할 수 있다. 또한, 마우스로 특수문자를 입력해도 되고 해당 키보드가 선택된 상태에서 실제 키보드를 입력해도 해당 키의 특수문자가 입력된다.

fcitx 자잘한 문제들

앞서 언급했던 ibus-hangul과 동일한 한글 마지막 입력글자가 마우스 따라다니는 문제 외에, 우분투 VirtualBox 게스트에서 영문 모드 상태의 유니티 Dash에서 한영키가 동작하지 않아 한글이 입력되지 않는 문제가 있었다. 우분투 호스트에서는 그런 문제는 안생기고 있다. 또, 한가지는 ibus-hangul에서도 발생하는 문제인데, 영문모드 상태에서 구글 chrome 브라우저를 실행하자마자 주소창에서 한영키를 누를 경우 한영전환이 안되는 문제가 있다. 주소창이 아닌 검색창에서 한영키를 누르면 한영전환이 잘 된다.

추가적으로, 다시 ibus를 사용하고자 할 경우 [System Settings] > [Language Support]에서  [fcitx] 대신 [ibus]를 선택하고 재로그인 해 주면 되는데, 한영키를 사용하려면 Hangul (ibus) 키보드를 다시 추가해 주어야 한다. 이때 불필요한 fcitx 프로세스 하나가 같이 살아있는 문제도 있다.

2015/05/01

Qt5 Mouse Event 처리


Qt5도 배울 겸 재미삼아 마우스 이벤트 처리 프로그램을 만들어 본다. UI 프로그램을 거의 짜 본적이 없어서 그런지 마우스 이벤트 처리가 생각보다 복잡하다. 소시적부터 관심이 있었으면 Libre Office Impress 같은 것을 만들었을지도... 도형 들을 마우스로 선택하고 옮기고 크기를 조절하고 Widget 창 크기 조절시 창크기에 비례해서 도형 크기도 변해야 한다. 마우스 커서도 선택 모드, 드래그 모드, 리사이즈 모드에 따라 바꿔주고... 우분투의 Screenshot으로 캡춰했더니 마우스 커서는 모양이 안바뀌네...

그런데, Qt에서 사각형 Class인 QRect가 좀 웃긴다. QRect(x, y, width, height)로 생성할 수 있는데 x2 = x+width-1, y2 = y+height-1로 정의하고 있다. 가령, rect.x2()나 rect.y2()를 호출하면 각각 1pixel이 모자라게 된다. 그런데 놀라운 것은 painter.drawRect(rect)로 그리면 1pixel이 모자라지 않게 잘 그려준다. 별 문제가 없어 보이지만, 계산을 할때는 x2 = x+width, y2 = y+height로 계산해야 1pixel에 대해 제대로 고려가 된다. 또, 한가지 이로 인해 파생되는 문제는 rect.normalized()라는 함수가 width나 height가 음수일 때 양수로 사각형 좌표를 바꿔주는데 x와 x2를 swapping 해버림으로써 사각형 크기가 어긋나게 된다.

1pixel 차이 가지고 뭐 그리 쫀쫀하게 구냐고 할지도 모른다. 예전에 MS Powerpoint 발표자료 만들때 그룹 resize 같은 거 하면 pixel이 어긋나는 한 두놈이 꼭 생겨서 그거 맟추느라 고생했던 기억이 남아 있다. 왜냐하면 1pixel이라도 어긋나면 시청자에게 굉장히 허술하게 보이기 때문에... 최근 버전의 Powerpoint에서는 도형을 움직이거나 resize 하면 근처 도형에 정렬할 수 있도록 기능이 강화된 듯하다. 하지만 근본적인 문제가 해결된 것은 아니었다. 이 문제는, 표를 만들 때 무식한 방법으로 사각형 하나를 복제해서 한 줄을 만든 뒤 이 줄을 다시 복제해서 여러 줄을 만들고, 그룹 resize 후에 cell 들을 정렬시키고 나서 Powerpoint Window 화면을 resize해 보면, 금방 확인해 볼 수 있다. 표의 경계선 굵기가 일정하지 않게 되는 문제가 생긴다. Libre Office Impress에서는 resize시에도 정렬이 잘 되는 편이다.

그리고, Qt는 개발자에게 모든 필요한 것을 제공해 주려는 의도가 강해 보인다. 잡다하게 많은 Class 들이 뭐 썩먹을 수야 있지만 좀 난잡해 보인달까... 가령, int 버전의 QRect와 qreal(float) 버전의 QRectF와 같이 종류별로 int 버전과 qreal 버전을 구비하고 있다. 화면에서의 pixel들은 궁극적으로 int형이긴 하지만 resize나 reshaping 같은 것을 하기 위해서는 qreal 버전이 필요하기는 하다. 위의 pixel이 어긋나는 문제가 생기는 원인도 실수를 정수로 변환하면서 발생하는 rounding 오류 때문에 생긴다. 해결 방법은 가능한한 rounding 오류가 발생하지 않도록 해주면 된다. 가령, 특정 정수의 배수를 사용하여 나머지가 항상 0이 되도록 만들어 준다든지...


Mouse Press Event
void ChartWidget::mousePressEvent(QMouseEvent* event)
{
    m_firstPos = boundPos(event->pos());
    m_lastPos = m_firstPos;

    bool newPos = true;
    bool selectStat = false;
    bool modKey = event->modifiers().testFlag(Qt::ShiftModifier);
    bool itemSelected = m_selectedItems.size() ? true : false;
    QVariant info;

    if(itemSelected) {
        // if mod key pressed, deselect the selected item.
        if(modKey) {
            foreach(Drawable* drawable, m_selectedItems) {
                if(drawable->contains(m_firstPos, &info)) {
                    removeSelectedItem(drawable);
                    selectStat |= drawable->onDeselect();
                    newPos = false;
                    break;
                }
            }
        }
        else {
            // if reclick one of selected items, check new mouse event.
            foreach(Drawable* drawable, m_selectedItems) {
                if((m_resizeIndex = drawable->selectIndex(m_firstPos))) {
                    m_resizeMode = true;
                    m_mouseItem = drawable;
                    return;
                }
                if(drawable->contains(m_firstPos, &info)) {
                    m_dragMode = true;
                    return;
                }
            }
            // if new mouse position, deselect all the selected items.
            foreach(Drawable* drawable, m_selectedItems) {
                removeSelectedItem(drawable);
                selectStat |= drawable->onDeselect();
            }
        }
    }

    if(!itemSelected || newPos) {
        Drawable* clicked = drawableAt(m_firstPos, &info);
        // if new item is clicked select the item.
        if(clicked) {
            m_selectMode = false;
            m_dragMode = true;
            addSelectedItem(clicked);
            selectStat |= clicked->onSelect(event, modKey, info);
        }
        // if no item is selected check new select event.
        else m_selectMode = true;
    }

    if(selectStat) emit selectionChanged();
    if(!m_selectMode) reDraw();
    QWidget::mousePressEvent(event);
}
Mouse Move Event
void ChartWidget::mouseMoveEvent(QMouseEvent* event)
{
    m_lastPos = boundPos(event->pos());

    m_cursor = mcPointer;
    if(m_selectedItems.size()) {
        if(m_resizeMode) {
            m_cursor = (m_resizeIndex < 3) ? mcSizeLCross :
                       (m_resizeIndex < 5) ? mcSizeRCross :
                       (m_resizeIndex < 7) ? mcSizeVert :
                       mcSizeHoriz;
            m_selectRect = m_mouseItem->region();
            foreach(Drawable* drawable, m_selectedItems) drawable->onResize(m_resizeIndex);
            m_selectRect = m_mouseItem->region();
        }
        else if(m_dragMode) {
            m_cursor = mcDrag;
            foreach(Drawable* drawable, m_selectedItems) drawable->onMouseMove(event);
            m_firstPos = m_lastPos;
        }
        else {
            foreach(Drawable* drawable, m_selectedItems) {
                if((m_resizeIndex = drawable->selectIndex(m_lastPos))) {
                    m_cursor = (m_resizeIndex < 3) ? mcSizeLCross :
                               (m_resizeIndex < 5) ? mcSizeRCross :
                               (m_resizeIndex < 7) ? mcSizeVert :
                               mcSizeHoriz;
                    break;
                }
                if(drawable->contains(m_lastPos)) {
                    m_cursor = mcDrag;
                    break;
                }
            }
        }

    }

    reDraw();
    QWidget::mouseMoveEvent(event);
}
void Drawable::onMouseMove(QMouseEvent* event)
{
    Q_UNUSED(event)
    
    QRect rect = region();
    QPoint pos = rect.topLeft() + owner()->lastPos() - owner()->firstPos();
    setRegion(QRect(pos, rect.size()));
}
Mouse Release Event
void ChartWidget::mouseReleaseEvent(QMouseEvent* event)
{
    m_lastPos = boundPos(event->pos());
    //m_lastPos = adjustPosition(m_lastPos);

    bool modKey = event->modifiers().testFlag(Qt::ControlModifier);

    // if mouse left-click position changed.
    if(event->button()==Qt::LeftButton && (m_firstPos-m_lastPos).manhattanLength()>TOL_SELECT) {
        if(!modKey && m_selectedItems.size()) {
            foreach(Drawable* drawable, m_selectedItems) drawable->onMouseRelease(event);
        }
        else {
            bool selectStat = false;
            QVariant info = 0;
            QRect rect = QRect(m_firstPos, m_lastPos);
            foreach(Layer* layer, m_layers) {
                foreach(Drawable* drawable, layer->children()) {
                    // if non-selected items are in mouse dragged region, select new items.
                    if(rect.contains(drawable->region())) {
                        addSelectedItem(drawable);
                        selectStat |= drawable->onSelect(event, false, info);
                    }
                }
            }
            if(selectStat) emit selectionChanged();
        }
    }

    m_selectMode = false;
    m_dragMode = false;
    m_resizeMode = false;
    m_resizeIndex = 0;
    if(m_mouseItem) m_mouseItem = 0;
    reDraw();
    QWidget::mouseReleaseEvent(event);
}
void Drawable::onMouseRelease(QMouseEvent* event)
{
    Q_UNUSED(event)
    
    setRegion(positiveRect(m_rect));
    QVector2D ar = owner()->aspectRatio();
    m_rectOrigin.setWidth(m_rect.width()/ar.x());
    m_rectOrigin.setHeight(m_rect.height()/ar.y());
}
Widget Resize Event
void ChartWidget::resizeEvent(QResizeEvent* event)
{
    if(m_pixmap) delete m_pixmap;
    m_pixmap = new QPixmap(event->size());

    setViewportChanged();
    setViewport(rect());
    qreal arX = m_viewport.width() / m_viewportOrigin.width();
    qreal arY = m_viewport.height() / m_viewportOrigin.height();
    setAspectRatio(arX, arY);

    reDraw();
    setViewportChanged(false);
}
void Drawable::update()
{
    QVector2D ar = owner()->aspectRatio();
    qreal arX = ar.x();
    qreal arY = ar.y();
    if(owner()->viewportChanged()) {
        qreal x1 = arX * m_posOrigin.x();
        qreal y1 = arY * m_posOrigin.y();
        qreal x2 = x1 + arX * m_rectOrigin.width();
        qreal y2 = y1 + arY * m_rectOrigin.height();
        m_posCurrent = QPointF(x1, y1);
        // rounding errors are increased if you rsize chartwidget frequently.
        m_rect = QRect(RoundToMultiple(x1), RoundToMultiple(y1), 
                       RoundToMultiple(x2-x1), RoundToMultiple(y2-y1));
    }
    else {
        m_posCurrent = QPointF(m_rect.x(), m_rect.y());
        m_posOrigin = QPointF(m_posCurrent.x()/arX, m_posCurrent.y()/arY);
    }
}