glib, gtk 팝업 사전 만들기 7화


6화에 파일 올렸는데, 중간 중간 짤려먹어서 파일만 다시 올려봄.

이것도 짤리면 말구~


----------

tiandict.c

----------

/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 2; tab-width: 2 -*- */


#include

#include

#include

#include

#include


#define TIAN_WIDTH  300

#define TIAN_HEIGHT 100


typedef struct _Tiandict

{

  DBM           *dbm;

  GtkWidget     *window;

  GtkTextBuffer *text_buffer; /* 요거는 GtkTextView 용도 */

  GtkClipboard  *clipboard;

  gulong         handler_id;

  /* 요거는 offset 맞추기 위한 변수, GUI 프로그래밍하다보면 offset, 좌표 맞추는 일이 자주 발생함. */

  int            titlebar_height;

} Tiandict;


void

on_owner_change (GtkClipboard *clipboard,

                 GdkEvent     *event,

                 Tiandict     *dict)

{

  GdkWindow *gdk_window;

  GdkSeat   *seat;

  GdkDevice *device;

  gchar     *text;

  gint       x;

  gint       y;


  text = gtk_clipboard_wait_for_text (clipboard);


  if (text == NULL)

    return;


  gtk_window_set_title (GTK_WINDOW (dict->window), text);


  gdk_window = gtk_widget_get_window (GTK_WIDGET (dict->window));

  seat       = gdk_display_get_default_seat (gdk_display_get_default ());

  device     = gdk_seat_get_pointer (seat);


  if (gdk_window && device)

  {

    gdk_device_get_position (device, NULL, &x, &y);

    /* 아래는 적절한 위치로 이동하기 위해 좌표를 좀 조절함 */

    gtk_window_move (GTK_WINDOW (dict->window), x - 25,

                                                y + dict->titlebar_height);

    gtk_window_set_keep_above (GTK_WINDOW (dict->window), TRUE);

    gtk_window_set_keep_above (GTK_WINDOW (dict->window), FALSE);

  }


  /* 아래는 db 에서 데이터 인출해오는 코드, 매우 간단함. */

  datum key;

  datum content;


  key.dptr  = text;

  key.dsize = strlen (text) + 1;

  content = dbm_fetch (dict->dbm, key);


  if (dbm_error (dict->dbm))

    perror (NULL);


  if (content.dptr)

    gtk_text_buffer_set_text (dict->text_buffer, content.dptr, -1);

  else

    gtk_text_buffer_set_text (dict->text_buffer, "찾는 단어가 없습니다.", -1);


  g_free (text);

}


gboolean

on_enter_notify_event (GtkWidget *widget,

                       GdkEvent  *event,

                       Tiandict  *dict)

{

  if (dict->handler_id)

  {

    g_signal_handler_disconnect (dict->clipboard, dict->handler_id);

    dict->handler_id = 0;

  }


  return FALSE;

}


gboolean

on_leave_notify_event (GtkWidget *widget,

                       GdkEvent  *event,

                       Tiandict  *dict)

{

  if (!dict->handler_id)

    dict->handler_id = g_signal_connect (dict->clipboard, "owner-change",

                                         (GCallback) on_owner_change, dict);


  return FALSE;

}


static void

on_destroy (GtkWidget *widget,

            gpointer   user_data)

{

  gtk_main_quit ();

}


/* 이 함수는 실제로 저장하는 함수임 */

static void

on_save_button_clicked (GtkButton *button,

                        Tiandict  *dict)

{

  /* 아래 변수는 포인터가 아니잖아. 함수에 포인터로 넘길 때, &iter1 이렇게 함

     아래처럼 변수를 선언하면 malloc, free 안 써도 됨. 함수 내에서 한번 쓰고 버리는 함수들은

     이렇게 선언하면 됨 */

  GtkTextIter  iter1;

  GtkTextIter  iter2;

  gchar       *text;


  /* text buffer 에서 문자열 가져오는 작업임. api 문서 보면 다 나옴.

     문서를 얼마나 빨리 찾냐가 기술임 ㅋㅋㅋㅋ */

  gtk_text_buffer_get_start_iter  (dict->text_buffer, &iter1);

  gtk_text_buffer_get_end_iter    (dict->text_buffer, &iter2);

  text = gtk_text_buffer_get_text (dict->text_buffer, &iter1, &iter2, TRUE);


  /* text 가 NULL 이면 프로그램 죽음. 그거 방지하려고 넣는 코드.

     text[0] 이거는 뭐하는 거냐면

     text = ""; 이렇게 하면 text[0] 여기에 0('\0', NULL) 값 들어있는거임.

     문자열은 문자열인데 strlen (text) 하면 0 나오는 문자열이거든. 그거 저장할 필요 없잖아 */

  if (text && text[0])

  {

    datum key;

    datum content;


    /* 마우스로 더블 클릭하면 PRIMARY Atom 에 저장된다고 했잖어. 그걸 GtkClipboard 이라는 걸로

       인출해오고, 인출 후에 set_title () 했거덩. 그래서 title 로부터 key 되는 단어를 가져오는

       거임. 별도로 변수 만들기 귀찮거덩. 있는거 그냥 쓰자 */

    key.dptr  = (char *) gtk_window_get_title (GTK_WINDOW (dict->window));

    /* 문자열은 NULL 바이트까지 저장하는게 나중에 써먹기 좋아. */

    key.dsize = strlen (key.dptr) + 1;


    content.dptr  = text;

    content.dsize = strlen (content.dptr) + 1;

    /* 저장하는 함수. 예외처리해야 하는데 귀찮아서 ㅋㅋㅋ */

    dbm_store (dict->dbm, key, content, DBM_REPLACE);

  }


  /* text[0] 에 0 들어 있을 경우도 해제되는거임. */

  if (text) /* if (text != NULL) 이거랑 같은 코드. 일부러 길게 칠 필요 없거덩 */

    g_free (text);

}


Tiandict *tian_dict_new ()

{

  Tiandict  *dict;

  GtkWidget *sw; /* 이거는 scrolled window, 길어서 그냥 sw */

  GtkWidget *editor;

  GtkWidget *titlebar;    /* GtkHeader */

  GtkWidget *save_button; /* GtkButton */

  gchar     *path1, *path2; /* 패스1은 디렉토리 만들 때 써먹을 거임 */


  dict = g_slice_new (Tiandict);

  path1 = g_build_path (G_DIR_SEPARATOR_S, g_get_user_config_dir (),

                       "tiandict", NULL);

  path2 = g_build_path (G_DIR_SEPARATOR_S, g_get_user_config_dir (),

                       "tiandict", "dict.db", NULL);

  g_mkdir_with_parents (path1, 0700); /* 디렉토리 한방에 만드는거 mkdir -p */

  dict->dbm = dbm_open (path2, O_RDWR | O_CREAT | O_SYNC, S_IRUSR | S_IWUSR);


  if (!dict->dbm)

  {

    /* glib 에 g_log, g_info, g_message, g_warning, g_critical, g_debug

       이런 함수들 있음. 참고로 logging 도 됨 */

    g_critical ("Can't open %s", path2);

    exit (EXIT_FAILURE);

  }


  gtk_init (NULL, NULL);


  /* GtkButton */

  /* button 은 이렇게 만드는데, icon 이름에,

     /usr/share/icons/Adwaita/22x22/legacy/document-save.png

     document-save 요 부분만 넣으면 됨. 각종 사이즈, 각종 theme 별로,

     document-save 아이콘이 엄청 나게 많음.

     /usr/share/icons/ 디렉토리에 있는 거니까.. 고민하지 말라구. */

  save_button = gtk_button_new_from_icon_name ("document-save",

                                               GTK_ICON_SIZE_SMALL_TOOLBAR);

  /* 전편에 설명했지? 버튼 클릭하면 clicked 신호가 발생하는데,

     on_save_button_clicked 함수 실행하라는 얘기임. user_data 로 dict 넘어가는거구 */

  g_signal_connect (save_button, "clicked",

                    G_CALLBACK (on_save_button_clicked), dict);


  /* GtkHeaderBar */

  titlebar = gtk_header_bar_new ();

  gtk_header_bar_set_decoration_layout (GTK_HEADER_BAR (titlebar),

                                        "menu:minimize,close");

  gtk_header_bar_set_show_close_button (GTK_HEADER_BAR (titlebar), TRUE);

  /* gtk_container_add (GTK_HEADER_BAR (titlebar), aoeuao) 이런 함수도 있지만,

     아래처럼 직접 제공하면 그걸 써!!.

     pack_start, pack_end 함수 많이 보게 될 텐데,

     pack_start 는 시작부분(왼쪽이겠지?)부터 패킹하라는 얘기. 쳐넣으라는 얘기.

     pack_end 는 끝에서부터 (오른쪽이겠지?) 패킹하라는 얘기. 쳐넣으라는 얘기.

     이런 짓을 왜하지? 이러지 말구, 창 최대화하면 그게 layout 에 영향을 주거덩.

     왜 필요한지 사이즈, 간격 맞추다보면 별 뻘짓을 다하게 될꺼다 흠핫핫핫 ㅋㅋ */

  gtk_header_bar_pack_start (GTK_HEADER_BAR (titlebar), save_button);


  /* GtkTextView */

  editor = gtk_text_view_new ();

  /* 이거는 줄바꿈, 편집기에 이렇거 많이 보이지? */

  gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (editor), GTK_WRAP_WORD_CHAR);

  /* 이거는 버퍼 얻어오는거. 나중에 해제 시키지 말구 */

  dict->text_buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (editor));

  gtk_text_buffer_set_text (dict->text_buffer,

    "티엔 사전입니다. 마우스로 더블 클릭하면 단어에 대한 뜻이 나옵니다. 찾는 단어가 없을 때, "

    "이 곳에 단어에 대한 의미를 입력후에 위에 제목 줄에 저장 버튼 누르시면 되겠습니다.", -1);


  /* GtkScrolledWindow */

  /* 옵션 안 주면 스크롤바가 알아서 다 자동으로 됨 */

  /* 스코롤러 정책 옵션 바꿀려면 devhelp 로 gtk_scrolled 입력하고 검색하면 되구.

     GUI 프로그래밍은 api 검색 능력이 곧 실력이야. 진짜임!!! */

  sw = gtk_scrolled_window_new (NULL, NULL);

  gtk_container_add (GTK_CONTAINER (sw), editor);


  /* GtkWindow */

  dict->window = gtk_window_new (GTK_WINDOW_TOPLEVEL);

  gtk_window_set_titlebar (GTK_WINDOW (dict->window), titlebar);

  gtk_window_set_default_size (GTK_WINDOW (dict->window), TIAN_WIDTH, TIAN_HEIGHT);

  gtk_window_set_position (GTK_WINDOW (dict->window), GTK_WIN_POS_CENTER);

  gtk_window_set_gravity  (GTK_WINDOW (dict->window), GDK_GRAVITY_CENTER);

  gtk_window_set_title    (GTK_WINDOW (dict->window), "Tiandict");

  gtk_container_add (GTK_CONTAINER (dict->window), sw);


  /* GtkClipboard */

  dict->clipboard  = gtk_clipboard_get (GDK_SELECTION_PRIMARY);

  dict->handler_id = g_signal_connect (dict->clipboard, "owner-change",

                                       (GCallback) on_owner_change, dict);


  g_signal_connect (dict->window, "destroy", (GCallback) on_destroy, NULL);

  g_signal_connect (sw, "enter-notify-event", (GCallback) on_enter_notify_event, dict);

  g_signal_connect (sw, "leave-notify-event", (GCallback) on_leave_notify_event, dict);


  gtk_widget_show_all (dict->window);


  /* 요거는 진짜 설명이 필요함. 위에 gtk_header_bar_new() 했잖아. 그때는 height 를

     알 수 없어. 그걸 나중에 allocate 한 후에 알 수 있거든.

     gtk_widget_show_all gtk_widget_show 한 이후에는 확실히 알 수 있지.

     그거 모르면 엄청 해멜거다.. ㅋㅋㅋ "draw" 신호가 발생하거든.

     그 신호에 g_signal_connect 로 연결하고, 콜백에서 아래 함수 실행해도 크기 알 수 있지

     이거 진짜 노하우야... 잘 기억해둬. 나 옛날에 이거 몰라서 한 두달 삽질했다 !!!!

     그 옛날이 벌써 4년전이네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

     */

  dict->titlebar_height = gtk_widget_get_allocated_height (titlebar);


  g_free (path1);

  g_free (path2);


  return dict;

}


void tian_dict_free (Tiandict *dict)

{

  dbm_close (dict->dbm);

  g_slice_free (Tiandict, dict);

}


int main (int argc, char **argv)

{

  Tiandict *dict;


  dict = tian_dict_new ();


  gtk_main ();


  tian_dict_free (dict);


  return EXIT_SUCCESS;

}


---------

configure

---------

#!/bin/sh


# default variables

prefix="/usr/local"

EXTRA_CFLAGS="-Wall -Werror"

EXTRA_LDFLAGS="-Wl,-z,relro"

hardening="-D_FORTIFY_SOURCE=2 -O1"


for i in "$@"

do

case $i in

  --disable-hardening)

    hardening=""

    ;;

  --libdir=*)

    libdir="${i#*=}"

    ;;

  --prefix=*)

    prefix="${i#*=}"

    ;;

  --help|*)

    echo "Usage: ./configure [OPTION]..."

    echo "  --help                      Show help messages"

    echo "  --disable-hardening         Disable hardening"

    echo "  --libdir=PATH               Set PATH to libdir"

    echo "  --prefix=DIRECTORY          Set DIRECTORY to prefix"

    exit 1

    ;;

esac

done


[ -z $libdir ] && libdir="$prefix/lib"

eval libdir=$libdir


EXTRA_CFLAGS=$EXTRA_CFLAGS" $hardening"

errmsg=""


# check required packages


if ! pkg-config --exists glib-2.0;    then errmsg+=" glib-2.0";    fi

if ! pkg-config --exists gobject-2.0; then errmsg+=" gobject-2.0"; fi

if ! pkg-config --exists gtk+-3.0;    then errmsg+=" gtk+-3.0";    fi

if [ ! -f /usr/lib/libgdbm_compat.so.4 ] &&

   [ ! -f /usr/lib/x86_64-linux-gnu/libgdbm_compat.so.4 ]

  then errmsg+=" libgdbm_compat.so.4"

fi


# print error messages


if test -n "$errmsg"; then

  echo "Not found: $errmsg"

  exit 1

fi


# generate config.mk


cat > config.mk <<- EOF

EXTRA_CFLAGS      = $EXTRA_CFLAGS

EXTRA_LDFLAGS     = $EXTRA_LDFLAGS

VERSION           = 2020.03.07

CC                = cc

top_srcdir        = $(pwd)

prefix            = $prefix

libdir            = $libdir

datadir           = $prefix/share

GETTEXT_PACKAGE   = tiandict

TARGET_LOCALE_DIR = $prefix/share/locale

MKDIR_P           = mkdir -p

EOF


echo "Generated config.mk"

echo "----------------------------------------------------------------"

cat config.mk

echo "----------------------------------------------------------------"



--------

Makefile

--------

include ./config.mk


TARGET = tiandict


TARGET_DEPS_CFLAGS = `pkg-config --cflags glib-2.0 gobject-2.0 gtk+-3.0`

TARGET_DEPS_LIBS   = `pkg-config --libs   glib-2.0 gobject-2.0 gtk+-3.0` -lgdbm_compat


SOURCES = tiandict.c

CFLAGS = \

$(TARGET_DEPS_CFLAGS) \

$(EXTRA_CFLAGS) \

-DVERSION=\"$(VERSION)\" \

-DTARGET_LOCALE_DIR=\"$(TARGET_LOCALE_DIR)\"


LDFLAGS = $(TARGET_DEPS_LIBS) $(EXTRA_LDFLAGS)


all: $(TARGET)


$(TARGET): $(SOURCES) Makefile

$(CC) $(CFLAGS) $(SOURCES) \

  -Wl,--as-needed $(LDFLAGS) -o $(TARGET)


clean:

rm -f $(TARGET) config.mk


install:

install -D -m 755 $(TARGET) -t $(DESTDIR)$(prefix)/bin


uninstall:

rm -f $(DESTDIR)$(prefix)/bin/$(TARGET)

-rmdir -p $(DESTDIR)$(prefix)/bin