특집기사:GEF에서 네이티브 드래그 앤 드랍 사용하기

위클립스
이동: 둘러보기, 찾기
Article.png 특집기사 정보
원문 보기
저자 Eric Bodeau, IBM
역자 이지율
Recipe.gif 이 문서는 번역되고 있는 중입니다. 번역을 희망하시는 분은, 적절한 세션에 틀:찜을 이용해 찜 해주세요.

운영체제 드래그 앤 드랍은 한 GUI 객체가 다른 응용 프로그램 안에 있는 GUI 객체에게 데이터를 주고 받을 수 있게 한다. GEFSWT를 통해 운영체제의 드래그 앤 드랍 기능을 사용할 수 있게 한다.[1] 이 문서는 GEF의 드래그 앤 드랍 기능을 예제를 통하여 자세히 살펴보고, 이 API들을 사용할 때의 잇점을 설명한다.

목차

[편집] 소개

네이티브 드래그 앤 드랍은 운영체제 하단부의 드래그 앤 드랍 매커니즘을 의미한다. 이는 EditPart를 한 뷰어 안에서 드래그 하는 것과는 다르다. 네이티브 드래그 앤 드랍을 사용함으로써, 뷰나 에디터, 그리고 윈도우나 심지어 다른 프로그램과도 데이터를 주고 받을 수 있다[2].

[편집] SWT 예제

운영체제의 드래그 앤 드랍 기능을 사용하기 위해 GEF는 SWTDragSourceDropTarget API 를 이용한다.[3] SWT의 DND를 사용하여 데이터를 보내려면 DragSource를 만들어 Control을 훅 해야 하고, 비슷한 접근 법으로 데이터를 수신하기 위해서는 DropTarget을 만들어야 한다. 이후에 DragSourceListener나 DropTargetListener 를 추가하여 원하는 작업을 기술 할 수 있다. 다음은 트리아이템을 드래그 하여 텍스트 필드에 떨어뜨리는 예제이다:

import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.*;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.*;
 
public class DNDExample {
 
 public static void main(String[] args) {
   Shell shell = new Shell();
   shell.setBackground(new Color(null, 200, 200, 200));
   shell.setLayout(new GridLayout(2, false));
 
   // 트리 및 트리 아이템 만들기
   final Tree tree = new Tree(shell, SWT.NONE);
   TreeItem item1 = new TreeItem(tree, SWT.NONE);
   item1.setText("Item 1");
   TreeItem item2 = new TreeItem(tree, SWT.NONE);
   item2.setText("Item 2");
 
   // 트리에 드래그 소스를 만듬
   DragSource ds = new DragSource(tree, DND.DROP_MOVE);
   ds.setTransfer(new Transfer[] {TextTransfer.getInstance()});
   ds.addDragListener(new DragSourceAdapter() {
      public void dragSetData(DragSourceEvent event) {
         // Set the data to be the first selected item's text
         event.data = tree.getSelection()[0].getText();
      }
   });
 
   // 텍스트 필드 만들기
   final Text text = new Text(shell, SWT.NONE);
 
   // 텍스트 필드에 드롭 타겟 만들기
   DropTarget dt = new DropTarget(text, DND.DROP_MOVE);
   dt.setTransfer(new Transfer[] {TextTransfer.getInstance()});
   dt.addDropListener(new DropTargetAdapter() {
      public void drop(DropTargetEvent event) {
         // Set the text field's text to the text being dropped
         text.setText((String)event.data);
      }
   });
 
   shell.pack();
   shell.open();
   Display display = Display.getDefault();
   while (!shell.isDisposed())
      if (!display.readAndDispatch())
         display.sleep();
   display.dispose();
 }
}

DragSourceListener는 dragSetData()메소드 안에서 이벤트에 데이터를 셋팅 해야 하고, DropTargetListener는 드롭 이벤트에 반응하여 데이터를 처리하는 일을 수행해야 한다.

GEF는 드래그 앤 드랍 매커니즘을 위해 SWTDragSourceDropTarget을 이용한다. EditPartViewer에 드래그 리스너를 추가할 때, 드래그 소스가 만들어지고, 기반 컨트롤[4]을 훅한다. 드롭 타겟 리스너를 추가할 때에도 마찬가지 방식으로 드롭 타겟이 만들어져 훅한다. 하지만 SWT 예제에서 처럼 드래그 소스나 타겟에 직접 리스너를 부착하는 것과 달리, GEF Viewer는 DelegatingDragSourceAdapter나 DelegatingDropTargetListener를 만든다. 그리고 사용자가 추가한 리스너는 이 위임 어댑터에 붙게 된다. 드래그 소스나 타겟을 통해 드래그 이벤트가 디스패치 되면, 위임 어댑터는 그 이벤트를 수신하여, 적절한 리스너에게 위임하게 된다.

드래그 앤 드랍 이벤트를 위임하는 방식의 장점 중 하나는, 트랜스퍼 타입에 따라 리스너를 분리시키기 쉽다는 점이다. 각각의 리스너는 자신이 관심을 가지는 트랜스퍼 타입에 대해서만 기술하므로 코드의 가독성도 좋아진다. 또한 드래그앤 드랍 기능을 확장할 때 리스너를 추가하기만 하면 된다. 그리고 이러한 위임 기법은 리스너들이 조화[5]를 이루게 한다.

[편집] GEF에디터에 리스너 추가하기

일단 코드를 살펴보자. 이클립스 GEF 예제로 포함된 Logic 에디터에 드랍 기능을 추가해 볼것이다. 이 기능은 사용자들에게 네비게이터나 패키지 익스플로러(혹은 파일을 드래그 할수 있는 운영체제의 익스플로러)등에서 로직 에디터에 파일을 끌어다 놓을 수 있도록 할 것이다. 그러면 에디터는 파일 이름으로 레이블을 하나 만들어 부착할 것이다. 먼저 TransferDropTargetListenr[6]를 하나 만들어야 한다:

package org.eclipse.gef.examples.logicdesigner.dnd;
 
import org.eclipse.swt.dnd.FileTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.gef.EditPartViewer;
import org.eclipse.gef.dnd.AbstractTransferDropTargetListener;
 
public class FileTransferDropTargetListener 
   extends AbstractTransferDropTargetListener 
{
 
   public FileTransferDropTargetListener(EditPartViewer viewer, Transfer xfer) {
      super(viewer, xfer);
   }
 
   public FileTransferDropTargetListener(EditPartViewer viewer) {
      super(viewer, FileTransfer.getInstance());
   }
 
   protected void updateTargetRequest() {}
 
}

맨 처음 해야 할 일은 적절한 요청을 만들기 위해 createTargetRequest()를 오버라이드 하는 것이다. CreateRequest는 파일이 에디터에 드롭되었을 때, 새 레이블을 만들기 위해 사용된다. CreateRequest는 커맨드에 포함될 새 모델 객체를 만들기 위해 CreationFactory가 필요하다. 드랍된 파일 이름으로 부터 새 LogicLabel을 만드는 팩토리를 구현해야 한다.

package org.eclipse.gef.examples.logicdesigner.dnd;
 
import org.eclipse.gef.requests.CreationFactory;
import org.eclipse.gef.examples.logicdesigner.model.LogicLabel;
 
public class FileLabelFactory implements CreationFactory {
 
   private String text = "";
 
   public Object getNewObject() {
      LogicLabel label = new LogicLabel();
      label.setLabelContents(text);
      return label;
   }
 
   public Object getObjectType() {
      return LogicLabel.class;
   }
 
   public void setText(String s) {
      text = s;
   }
}

이제 팩토리가 준비되었으니, 드랍 리스너에 필드로 이 팩토리를 추가한다.

private FileLabelFactory factory = new FileLabelFactory();

그리고 createTargetRequest()가 호출되는 동안 이 팩토리를 사용하도록 한다.

protected Request createTargetRequest() {
   CreateRequest request = new CreateRequest();
   request.setFactory(factory);
   return request;
}

다음으로, 마우스가 드래그 될 때 마다, 드랍 위치를 CreateRequest 객체에 업데이트 하도록 updateTargetRequest()를 구현한다.

protected void updateTargetRequest() {
   ((CreateRequest)getTargetRequest()).setLocation(getDropLocation());
}


파일들이 드래그 되 었을 때, 원본 위치에서 제거되는 것을 막기 위해, handleDragOver()를 오버라이드 하여, detail 필드를 DND.DROP_COPY로 지정한다. 만약 이 필드를 수정하지 않으면 기본값(DND.DROP_MOVE)가 사용되는데, 이 경우 드래그 소스는 파일이 이동한 것으로 간주한다.

protected void handleDragOver() {
   getCurrentEvent().detail = DND.DROP_COPY;
   super.handleDragOver();
}

마지막으로, DropTargetEvent로 부터 실제 파일 이름을 얻기 위해 handleDrop() 메서드를 구현해야 한다. 이벤트 객체의 data 필드는 Object형으로 지정되 있지만, 실제 전달될 데이터 타입은 트랜스퍼 타입에 의해 결정된다. FileTransfer의 경우, 드래깅 중인 파일들의 풀 패스(Full Path)의 목록이 담긴 String 배열을 전달한다. 문제를 단순하게 하기 위해, 첫번째 파일만 처리하도록 하겠다. 또한 파일의 풀 패스가 아닌, 파일 이름만 표시하도록 한다. 일단 이벤트로 부터 필요한 정보를 얻어냈다면, 팩토리에 이 텍스트를 지정한다. 나머지 작업은 부모 클래스가 처리한다.

protected void handleDrop() {
   String s = ((String[])getCurrentEvent().data)[0];
   String separator = System.getProperty("file.separator");
   s = s.substring(s.lastIndexOf(separator) + 1);
   factory.setText(s);
   super.handleDrop();
}


드랍 리스너에서 구현해야 할 것은 위의 내용이 전부이다. 대상 편집부에서 명령을 얻어오고 실행하는 건 부모 클래스가 맡아서 처리할 것이다. 이제 남은 것이라곤, 이 리스너를 우리의 에디터에 추가하고 어떤 일이 일어나는지 지켜보는 것 뿐이다. 이를 위해, 우리는 LogicEditor 클래스의 initializeGraphicalViewer() 를 변경할 필요가 있다. 이미 그 곳에 2개의 드랍 리스너가 추가되어 있다는 걸 주목하자. 그 중 LogicTemplateTransferDropTargetListener는 템플릿을 팔레트에서 드래그해서 에디터로 드랍하면 생성 해주는 것이고, TextTransferDropTargetListener는 사용자가 (워드 프로세서 같은) 텍스트를 드래그해서 기존의 레이블로 드랍하면 레이블의 텍스트가 바뀌도록 해주는 것이다. 이 메서드에 다음 코드를 추가함으로써 우리의 리스너를 뷰어에 추가해야 한다.

getGraphicalViewer().addDropTargetListener(
       new FileTransferDropTargetListener(getGraphicalViewer()));

[편집] 장막 너머의 진실

대부분의 사람들은, 언제 리스너를 뷰어에 추가할 것인지 까지만 알면 되고, DragSource 나 DropTarget 가 아직 생성되지 않았다면, 뷰어에서 새로 생성한다. 또한 뷰어는, 추가된 모든 리스너로부터의 전송을 DragSource/DropTarget 이 인지하고 있음을 보장해준다. 좀 더 복잡하지만, 장막 너머 어떤 일들이 일어나는지 보고 싶은 사람들은 계속 읽어보라. (나는 드랍 리스너가 어떻게 추가되는지 설명할 것이다. 드래그 리스너는 비슷한 방식으로 동작하므로 생략한다.)

EditPartViewer 에 TransferDropTargetListener 를 추가하면, 뷰어에선 DelegatingDropAdapter 에 리스너를 추가한다. 그 뒤, DropTarget 이 갱신된다. 갱신 과정에서, 뷰어가 DelegatingDropAdapter 가 비어있는지(리스너가 없는지)를 검토한 후, 비어있다면 DropTarget (SWT 위젯)을 null 로 설정한다. 이는, 드랍 이벤트를 위임할 리스너가 없다면 이 드랍 이벤트를 통지할 필요가 없어지기 때문이다. DelegatingDropAdapter에 첫번째 리스너가 추가되면 (즉, DelegatingDropAdapter 가 비어있지 않지만, DropTarget 은 여전히 null 인 경우), 뷰어가 자신의 컨트롤에 DropTarget 을 새로 생성하고 모든 가능한 스타일 (DND.DROP_MOVE, DND.DROP_COPY, DND.DROP_LINK)을 설정한다. 이로써 위임받은 리스너에서 모든 스타일이 가능하게 된다, 실제로 사용하지 않더라도 말이다. 마지막으로, 뷰어가 트랜스퍼 타입을 DelegatingDropAdapter 에서 얻어와서 DropTarget 에 설정한다. DelegatingDropAdapter는 트랜스퍼 타입들을, 모든 리스너에게 각자의 트랜스퍼 타입을 질의해서, 배열에 모두 추가하는 방식으로 얻어온다. 이제 DropTarget 설정이 끝나고 DelegatingDropAdapter 가 DropTargetEvents 를 통지받게 되었다. 주의사항: 뷰어에서 생성된 DropTarget 과 사용자가 만든 DropTarget 을 같이 사용할 수 없다. 그 이유는, 다음과 같이 DropTarget 자바독에 나와있다. "컨트롤과 DropTarget은 일대일 매핑만이 허용된다." 동일한 컨트롤에 또 다른 DropTarget 을 만든다면, 에러가 뜨게 될 것이다.

DelegatingDropAdapter 는 활성화될 하나의 리스너를 선택하고 (비활성 리스너는 이벤트를 받지 않는다) 매 이벤트 전에 활성 리스너를 갱신한다. 리스너가 활성화 되려면, 다음 세가지 조건을 만족해야 한다. 1) 리스너의 트랜스퍼 타입은 드래그되고 있는 데이터의 트랜스퍼 타입과 서로 맞아야 한다. 2) 마우스 커서 아래에 리스너 요청을 이해할 수 있는 EditPart 가 있어야 한다. 3) 리스너는, 앞의 두가지 조건을 만족하는, 리스트 내의 첫번째 리스너이어야 한다. 활성 리스너는 마우스 위치나 수식 키(alt, ctrl 같은)를 누르는 등에 따라 매 이벤트 마다 변경될 수 있다. AbstractTransferDropTargetListener.isEnabled(DropTargetEvent) 에서, 리스너는 자신의 트랜스퍼 타입을 이벤트에서 지원하는 트랜스퍼 타입 중 하나와 맞춰본다. 맞는게 있다면, 리스너는 현재 마우스 위치에서 대상의 요청을 이해할 수 있는 EditPart 를 찾아본다. 대상 편집부를 찾았다면, 리스너가 활성화되고 DelegatingDropTargetAdapter 는 이 이벤트를 처리할 리스너 찾는 일을 중지한다. 이제 실제로 이벤트가 활성 리스너에게 위임되었다. 활성 리스너가 변경된다면, 이전 리스너는 dragLeave() 이벤트를 받고 새로운 리스너는 dragEnter() 를 받아서 적절한 초기화와 정리가 되도록 보장해준다.

[편집] 정리

EditPartViewer에 드래그 앤 드랍 지원기능을 추가하는 것은 아주 간단한 작업이다. 이렇게 하면, OS레벨로 데이터를 전송하면서, GEF 응용프로그램이 다른 뷰, 에디터, 응용프로그램 들과 상호작용을 할 수 있게 될 것이다. 이를 위해 필요한 건, 단지 이 viewer에, 적절한 트랜스퍼 타입과 함께 AbstractTransferDragSourceListener 와 AbstractTransferDropTargetListener 의 서브클래스를 추가하는 것 뿐이다. 여기에서, 나는 이러한 기능을 구현하는게 어느 정도로 쉬운 일인지 보여주기 위해 아주 간단한 드랍 리스너를 소개했다. 실제 응용프로그램은 대부분 더 의미있는 작업을 수행하겠지만, 전체적인 프로세스는 동일하다.

[편집] 소스 코드

독립실행형 SWT 예제의 소스코드는 다음 링크에서 받을 수 있다. here.

[편집] 감사의 말

The author would like to thank Randy Hudson, Daniel Lee, David Williams, and Nitin Dahyabhai for providing constructive comments on the article.

Java and all Java-based trademarks and logos are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States, other countries, or both.

[편집] 참고

  1. 특집기사:SWT 애플리케이션에 드래그 & 드롭 추가하기에서 SWT에서의 DND 매커니즘을 확인 할 수 있다.
  2. 다른 프로그램과의 드래그엔 드랍: 두 응용프로그램이 같은 타입의 데이터에 대해 트랜스퍼를 공급하는 경우에 가능하다.
  3. SWT DND 메커니즘의 작동방법에 대해 자세한 정보를 원하면 베로니카 어바인의 Drag and Drop 문서를 참조할 수 있다.[1]
  4. FigureCanvas
  5. 리스너간의 조화:SWT에서 드래그 이벤트는 운영체제가 발생 시키고, 리스너들이 이 이벤트에 플래그를 조작하여 드래그 앤 드랍 여부가 결정된다. 따라서 SWT에서는 마지막 리스너가 핸들 여부를 최종 결정해 버리는 것과 달리, GEF에는 위임 어댑터가 리스너가 직접 이 이벤트 플래그를 조작하지 않게끔 하기 때문에, 리스너들은 완전히 독립적으로 소유하는 이벤트 처럼 사용할 수 있다.
  6. GEF는 AbstractTransferDropTargetListener를 서브클래싱 목적으로 공급한다.

이 기사에 대한 의견은 토론 페이지를 통해 나눌 수 있습니다.

개인 도구
이름공간
변수
행위
포탈
탐색
도움
도구모음