다음 이전 차례

17. Selection 관리하기

17.1 개요

GTK가 제공하는 프로세스 사이의 통신형태 중 하나는 selection이다. Selection은 사용자가 마우스로 긁거나 해서 선택된 텍스트의 일부같은, 데이터 조각들을 인식한다. 사용자가 어떤 순간에 선택하고 있을 수 있는 어플은 하나 뿐이며, 따라서 어떤 어플에 의해 선택이 요구되었을 때, 이전의 소유자는 selection이 포기되었음을 사용자에게 표시해 주어야 한다. 다른 어플들은 target이라 불리는, 다른 형태의 selection을 요청한다. Selection의 갯수는 제한이 없지만, 대부분의 X윈도 어플들은 primary selection이라고 부르는 단 하나만을 다룬다.

대부분 경우, GTK 어플이 selection 자체를 다룰 필요는 없다. Entry widget 등 표준적인 widget들은 사용자가 텍스트 위로 마우스를 드래그하는 등의 합당한 경우에 selection을 제기할 능력을 이미 가지고 있으며, 그리고 사용자가 마우스 의 두번째 버튼을 클릭하는 경우처럼 다른 어플이나 widget에 의해 소유된 selection 항목들을 되찾을 수도 있다. 그러나 우리가 다른 widget들이 selection 을 제공하는 능력을 가지도록 하고싶은 경우도 있을 것이며, 디폴트로 제공되지 않는 target을 되찾고 싶을 때도 있을 것이다.

Selection 다루기를 이해하기 위해 필요한 기본적인 개념은 atom이라는 것이다. Atom이란 어떤 display에서, 한 문자열을 유일하게 구별할 수 있는 완전한 것이다. 어떤 atom들은 X 서버에 의해 미리 정의되어 있으며, 어떤 경우엔 이런 atom들에 대한 constant들이 gtk.h에 있을 수도 있다. 예를들어 상수 GDK_PRIMARY_SELECTION은 문자열 "PRIMARY"에 해당된다. 다른 경우라면, 우리는 어떤 문자열 에 대응하는 atom을 취하기 위해 gdk_atom_interm()을, 그리고 atom의 이름을 취하기 위해선 gdk_atom_name()을 이용해야 한다. Selection과 target들은 모두 atom에 의하여 식별된다.

17.2 Selection을 복구하기

Selection을 되찾는다는 것은 하나의 비동시성의 과정이다. 이 과정을 시작 하기 위해 이 함수를 이용한다.

gint gtk_selection_convert   (GtkWidget           *widget,
                              GdkAtom              selection,
                              GdkAtom              target,
                              guint32              time)

이것은 target에 의해 설정된 형태로 selection을 변환한다. 만약 가능하다면, time 인자는 selection을 결정한 이벤트로부터의 시간이 되어야 한다. 이것은 사용자가 요청한 순서대로 이벤트가 발생하는 것을 확실히 해준다. 그러나 만약 이것이 쓰여질 수 없다면(예를들어 변환이 "clicked"시그널에 의해 이루어졌다면) 우리는 상수 GDK_CURRENT_TIME를 이용할 수 있을 것이다.

Selecting owner가 어떤 요구에 반응하면 "selection_received"라는 시그널이 우리의 어플에 보내지게 된다. 이 시그널에 대한 핸들러는 아래와 같이 정의된 GtkSelectionData 구조체에 대한 포인터를 받는다.

struct _GtkSelectionData
{
  GdkAtom selection;
  GdkAtom target;
  GdkAtom type;
  gint    format;
  guchar *data;
  gint    length;
};

인자 selectiontarget은 우리가 gtk_selection_convert()함수에 준 값들이다. 인자 type은 selection owner에 의해 리턴된 데이터 타입을 식별하는 atom이다. 몇가지 가능한 값으로는 라틴 문자의 문자열 "STRING", atom의 시리즈 "ATOM", 하나의 정수 "INTEGER" 등이 있다. 대부분의 target들은 오직 한 가지 type을 리턴할 수 있다. 인자 format은 각 단위의 길이를 비트 단위로 준 것이다. 보통, 데이터를 받을 땐 이것에 대해서 신경쓸 필요가 없다. 인자 data는 리턴된 데이터에 대한 포인터며, length는 이 데이터의 길이를 바이트 단위로 준 것이다. 만약 length가 음수라면 에러가 발생한 것이고 selection은 복구될 수 없을 것이 다. 이것은 그 selection을 소유한 어플이 없거나, 또는 어플이 지원하지 않는 target을 요청했을 때 일어날 수 있는 일이다. 실제로 버퍼는 length보다 1바이트 길게 되는 것을 보장받는다. 남는 바이트는 언제나 zero가 될 것이고, 따라서 NULL로써 문자열을 끝내기 위해 따로 문자열의 복사본을 만들어 둘 필요가 없다.

이번 예제에서, 우리는 "TARGETS"라는 특별한 target을 복구할 것이다. 이것은 selection이 변환될 수 있는 모든 target의 리스트이다.

#include <gtk/gtk.h>

void selection_received (GtkWidget *widget,
                         GtkSelectionData *selection_data,
                         gpointer data);

/* 사용자가 "Get Targets" 버튼을 클릭했을 때 요청되는 시그널 핸들러 */
void
get_targets (GtkWidget *widget, gpointer data)
{
  static GdkAtom targets_atom = GDK_NONE;

  /* 문자열 "TARGETS"에 해당하는 atom을 취한다. */
  if (targets_atom == GDK_NONE)
    targets_atom = gdk_atom_intern ("TARGETS", FALSE);

  /* 그리고 primary selection으로서 "TARGETS"라는 target을 요청한다. */
  gtk_selection_convert (widget, GDK_SELECTION_PRIMARY, targets_atom,
                         GDK_CURRENT_TIME);
}

/* Selection owner가 데이터를 리턴했을 때 불려지는 시그널 핸들러 */
void
selection_received (GtkWidget *widget, GtkSelectionData *selection_data,
                      gpointer data)
{
  GdkAtom *atoms;
  GList *item_list;
  int i;

  /* **** 중요 **** Selection의 복구가 성공하는지 체크할 것. */
  if (selection_data->length < 0)
    {
      g_print ("Selection retrieval failed\n");
      return;
    }
  /* 기대한 형태로 데이터를 취했음을 확인한다. */
  if (selection_data->type != GDK_SELECTION_TYPE_ATOM)
    {
      g_print ("Selection \"TARGETS\" was not returned as atoms!\n");
      return;
    }

  /* 우리가 전해받은 atom을 프린트한다. */
  atoms = (GdkAtom *)selection_data->data;

  item_list = NULL;
  for (i=0; i<selection_data->length/sizeof(GdkAtom); i++)
    {
      char *name;
      name = gdk_atom_name (atoms[i]);
      if (name != NULL)
        g_print ("%s\n",name);
      else
        g_print ("(bad atom)\n");
    }

  return;
}

int
main (int argc, char *argv[])
{
  GtkWidget *window;
  GtkWidget *button;

  gtk_init (&argc, &argv);

  /* Toplevel 윈도를 만든다. */

  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_title (GTK_WINDOW (window), "Event Box");
  gtk_container_border_width (GTK_CONTAINER (window), 10);

  gtk_signal_connect (GTK_OBJECT (window), "destroy",
                      GTK_SIGNAL_FUNC (gtk_exit), NULL);

  /* 사용자가 target을 취하려고 클릭할 수 있는 버튼을 만든다. */

  button = gtk_button_new_with_label ("Get Targets");
  gtk_container_add (GTK_CONTAINER (window), button);

  gtk_signal_connect (GTK_OBJECT(button), "clicked",
                        GTK_SIGNAL_FUNC (get_targets), NULL);
  gtk_signal_connect (GTK_OBJECT(button), "selection_received",
                        GTK_SIGNAL_FUNC (selection_received), NULL);

  gtk_widget_show (button);
  gtk_widget_show (window);

  gtk_main ();

  return 0;
}

17.3 Selection을 제공하기

Selection을 제공하는 것은 조금 더 복잡하다. 우리는 우리의 selection이 요청되었을 때 호출될 핸들러들을 등록해 둬야 한다. 우리가 다룰 각각의 selection/target 쌍들에 대해 이런 호출을 하는 것이다.

void gtk_selection_add_handler (GtkWidget           *widget,
                                GdkAtom              selection,
                                GdkAtom              target,
                                GtkSelectionFunction function,
                                GtkRemoveFunction    remove_func,
                                gpointer             data);

widget, selection, target은 이 핸들러가 관리할 요청(request)을 식별한다. remove_func 인자는 시그널 핸들러가 제거될 때 NULL이 아닌 값이 된다. 이건 예를들어, 인터프리터 언어같이 데이터에 대한 reference count의 트랙을 유지할 필요성이 있는 경우 등에 유용하다.

이런 모양의 callback 함수를 보자.

typedef void (*GtkSelectionFunction) (GtkWidget *widget,
                                      GtkSelectionData *selection_data,
                                      gpointer data);

GtkSelectionData는 위에서의 경우와 같은 것이지만, 이번엔 우리는 필드를 type, format, data, 그리고 length로 채워야 한다. (필드 format은 여기서 실제로 중요하다. X 서버는 데이터가 byte-swap되어야 하는지의 여부를 이것 으로써 결정한다. 보통 이것은 8 즉 하나의 문자이거나, 또는 32 즉 정수가 된다.) 이건 이 함수를 호출해서 이루어진다.

void gtk_selection_data_set (GtkSelectionData *selection_data,
                             GdkAtom           type,
                             gint              format,
                             guchar           *data,
                             gint              length);

이 함수는 적절히 데이터의 복사본을 만들도록 해주기 때문에 우리는 따로 이것에 신경쓸 필요가 없다. (우리는 직접 GtkSelectionData의 필드들을 채워 주지 않아도 된다는 말이다.)

우리는 다음 함수를 호출해서 selection의 소유권(ownership)을 제시할 수 있다.

gint gtk_selection_owner_set (GtkWidget           *widget,
                              GdkAtom              selection,
                              guint32              time);

만약 또다른 어플이 selection의 소유권을 제시한다면, 우리는 "selection_ clear_event"를 받게될 것이다.

Selection을 제공하는 예제로서, 다음 프로그램은 어떤 토글버튼에 selection 기능을 첨가할 것이다. 이 토글버튼이 눌려진 상태라면, 프로그램은 primary selection을 제기할 것이다. GTK 자체에 의해 주어지는 "TARGETS" 같은 것은 제쳐 두고, 여기서 주어진 유일한 target은 "STRING" target이다. 이 target이 요청되면, 시각을 보여주는 한 문자열이 리턴된다.

#include <gtk/gtk.h>
#include <time.h>

/* 사용자가 selection을 토글할 때 호출되는 callback. */
void
selection_toggled (GtkWidget *widget, gint *have_selection)
{
  if (GTK_TOGGLE_BUTTON(widget)->active)
    {
      *have_selection = gtk_selection_owner_set (widget,
                                                 GDK_SELECTION_PRIMARY,
                                                 GDK_CURRENT_TIME);
      /* Selection을 요구하는 데 실패하면, out state로 그 버튼을
       * 리턴한다. */
      if (!*have_selection)
        gtk_toggle_button_set_state (GTK_TOGGLE_BUTTON(widget), FALSE);
    }
  else
    {
      if (*have_selection)
        {
          /* Selection owner를 NULL로 해서 selection을 비우기 전에,
           * 우리가 현재 실제의 owner인지 체크하자. */
          if (gdk_selection_owner_get (GDK_SELECTION_PRIMARY) == widget->window)
            gtk_selection_owner_set (NULL, GDK_SELECTION_PRIMARY,
                                     GDK_CURRENT_TIME);
          *have_selection = FALSE;
        }
    }
}

/* 다른 어플이 selection을 제기했을 때 호출된다. */
gint
selection_clear (GtkWidget *widget, GdkEventSelection *event,
                 gint *have_selection)
{
  *have_selection = FALSE;
  gtk_toggle_button_set_state (GTK_TOGGLE_BUTTON(widget), FALSE);

  return TRUE;
}

/* Selection으로서 현재 시각을 제공한다. */
void
selection_handle (GtkWidget *widget,
                  GtkSelectionData *selection_data,
                  gpointer data)
{
  gchar *timestr;
  time_t current_time;

  current_time = time (NULL);
  timestr = asctime (localtime(&current_time));
  /* 우리가 하나의 스트링을 리턴할 때, 따로 NULL 문자로 끝을 내지
   * 않아도 된다.  이미 처리되어 있기 때문이다. */

  gtk_selection_data_set (selection_data, GDK_SELECTION_TYPE_STRING,
                          8, timestr, strlen(timestr));
}

int
main (int argc, char *argv[])
{
  GtkWidget *window;

  GtkWidget *selection_button;
  static int have_selection = FALSE;

  gtk_init (&argc, &argv);

  /* Toplevel 윈도를 만든다. */

  window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
  gtk_window_set_title (GTK_WINDOW (window), "Event Box");
  gtk_container_border_width (GTK_CONTAINER (window), 10);

  gtk_signal_connect (GTK_OBJECT (window), "destroy",
                      GTK_SIGNAL_FUNC (gtk_exit), NULL);

  /* Selection으로서 동작할 토글버튼을 하나 만든다. */

  selection_button = gtk_toggle_button_new_with_label ("Claim Selection");
  gtk_container_add (GTK_CONTAINER (window), selection_button);
  gtk_widget_show (selection_button);

  gtk_signal_connect (GTK_OBJECT(selection_button), "toggled",
                      GTK_SIGNAL_FUNC (selection_toggled), &have_selection);
  gtk_signal_connect (GTK_OBJECT(selection_button), "selection_clear_event",
                      GTK_SIGNAL_FUNC (selection_clear), &have_selection);

  gtk_selection_add_handler (selection_button, GDK_SELECTION_PRIMARY,
                             GDK_SELECTION_TYPE_STRING,
                             selection_handle, NULL, NULL);

  gtk_widget_show (selection_button);
  gtk_widget_show (window);

  gtk_main ();

  return 0;
}


다음 이전 차례