S-JIS[2013-03-24/2013-05-01] 変更履歴

Eclipse GEF アウトラインページ

Eclipseプラグイン開発GEFのアウトラインページについて。


概要

GEFでは、エディター(ダイアグラム)上に表示されている図をサムネイル(縮小図)としてアウトラインページに表示することが出来る。

図全体がエディターに入り切っていない(はみ出している)場合は、アウトラインページに縮小された全体図が表示され、現在エディター上に表示されている範囲も図示される。
この図示された領域(四角形)をマウスでドラッグすることによってエディター上の表示範囲もスクロールするので、非常に便利。


アウトラインページにサムネイル(縮小図)を表示する例。

アウトラインページクラスを定義する際に親クラスとするContentOutlinePageには、GEFが提供しているorg.eclipse.gef.ui.partsパッケージのものと標準のorg.eclipse.ui.views.contentoutlineパッケージのものがあるが
ここではGEFの方を指定している。

GraphicalEditorクラス:

import org.eclipse.draw2d.LightweightSystem;
import org.eclipse.draw2d.Viewport;
import org.eclipse.draw2d.parts.ScrollableThumbnail;

import org.eclipse.gef.LayerConstants;
import org.eclipse.gef.ui.parts.ContentOutlinePage;
import org.eclipse.gef.ui.parts.ScrollingGraphicalViewer;

import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;

import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
public class MyEditor extends GraphicalEditorWithFlyoutPalette {
〜
	@Override
	public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
		if (adapter == IPropertySheetPage.class) {
			return new TabbedPropertySheetPage(this);
		}
		if (adapter == ZoomManager.class) {
			return ((ScalableRootEditPart) getGraphicalViewer().getRootEditPart()).getZoomManager();
		}
		if (adapter == IContentOutlinePage.class) {
			return new MyOutlinePage();
		}
		return super.getAdapter(adapter);
	}

サムネイルのアウトラインページを作るにはGraphicalEditorのgetGraphicalViewer()を呼び出す必要があるので、
とりあえずアウトラインページクラスをGraphicalEditorのインナークラスとして定義する。

	private class MyOutlinePage extends ContentOutlinePage {
		private Canvas canvas;
		private ScrollableThumbnail thumbnail;
		public MyOutlinePage() {
			super(new ScrollingGraphicalViewer());
		}
		@Override
		public void createControl(Composite parent) {
			canvas = new Canvas(parent, SWT.BORDER);
			LightweightSystem lws = new LightweightSystem(canvas);

			ScalableRootEditPart root = (ScalableRootEditPart) getGraphicalViewer().getRootEditPart();
			thumbnail = new ScrollableThumbnail((Viewport) root.getFigure());
			thumbnail.setSource(root.getLayer(LayerConstants.PRINTABLE_LAYERS));
			lws.setContents(thumbnail);
		}
		@Override
		public Control getControl() {
			return canvas;
		}
		@Override
		public void dispose() {
			thumbnail.deactivate();
			super.dispose();
		}
	}
}

他の人のサンプルを見ると、thumbnailの破棄にはDisposeListenerを使っているんだけど、dispose()メソッドでやっちゃいけない理由は何なんだろう?


Tamenaga Toshirouさんのアウトラインビューと縮小ビューの表示では、アウトラインページを上下に2分割し、上側にツリー、下側にサムネイルを表示している。
他の例を見てもSashFormを使っているものが多いようなのだが、SashFormはSwingのJSplitPaneのようなもので、ひとつの領域を分割して使用するのに使う。
今回はサムネイルだけ表示したいので、SashFormは使っていない。


ツリーの例

アウトラインページにツリーも表示してみる。[2013-04-28]
参考: Tamenaga Toshirouさんのアウトラインビューと縮小ビューの表示

SashFormを使って、上側にサムネイル、下側にツリーを表示する。

ツリーを表示するには、GEFのTreeViewerを使う。
ツリーのデータを管理するのにツリー用のEditPart(AbstractTreeEditPart)を使う。

MyOutlinePage.java

import org.eclipse.draw2d.LightweightSystem;
import org.eclipse.draw2d.Viewport;
import org.eclipse.draw2d.parts.ScrollableThumbnail;
import org.eclipse.gef.EditPartViewer;
import org.eclipse.gef.LayerConstants;
import org.eclipse.gef.editparts.ScalableRootEditPart;
import org.eclipse.gef.ui.parts.ContentOutlinePage;
import org.eclipse.gef.ui.parts.TreeViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.SashForm;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
public class MyOutlinePage extends ContentOutlinePage {
	private final MyEditor myEditor;

	private SashForm sash;
	private ScrollableThumbnail thumbnail;
	public MyOutlinePage(MyEditor myEditor) {
		super(new TreeViewer());
		this.myEditor = myEditor;
	}
	@Override
	public void createControl(Composite parent) {
		sash = new SashForm(parent, SWT.VERTICAL);

		// thumbnail
		Canvas canvas = new Canvas(sash, SWT.BORDER);
		LightweightSystem lws = new LightweightSystem(canvas);

		ScalableRootEditPart root = myEditor.getScalableRootEditPart();
		thumbnail = new ScrollableThumbnail((Viewport) root.getFigure());
		thumbnail.setSource(root.getLayer(LayerConstants.PRINTABLE_LAYERS));
		lws.setContents(thumbnail);

		// tree
		EditPartViewer viewer = getViewer();
		viewer.createControl(sash);
		viewer.setEditDomain(myEditor.getEditDomain());
		viewer.setEditPartFactory(new MyOutlineTreePartFactory());
		viewer.setContents(myEditor.getRootModel());
		myEditor.addSelectionSynchronizerViewer(viewer);

		sash.setWeights(new int[] { 3, 7 });
	}

コンストラクターでTreeViewerをセットしているので、getViewer()でTreeViewerが取れる。
viewerのcreateControl()を呼び出すと、内部では「new Tree(sash, SWT.MULTI〜)」のような処理が実行され、ツリーのコンポーネントが作られる。
EditPartFactoryはツリー構造を構築するためのクラス。
setContents()でツリーのルート要素のデータを指定する。
addSelectionSynchronizerViewer()はダイアグラムとの連携をとるようにするもの。エディター上で図形を選択したときにツリーの要素を選択状態にしたり、ツリーの要素を選択したときにダイアグラム上の図形が選択されたりする優れもの。

sashはデフォルトでは2つの領域のサイズが同じになる。[2013-05-01]
変更したい場合はsetWeights()で割合をセットする。
 

	@Override
	public Control getControl() {
		return sash;
	}
	@Override
	public void dispose() {
		thumbnail.deactivate();
		super.dispose();
	}
}

ツリー用のEditPartクラスを用意する。これはAbstractTreeEditPartを使用する。
(エディター(ダイアグラム)を構築するときに使ったもの(AbstractGraphicalEditPart)ではない)

TreeEditPartはツリーの階層の種類毎に用意する。(下記の例では1種類で全てを賄っている)

MyTreeEditPart.java

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

import org.eclipse.gef.editparts.AbstractTreeEditPart;
import org.eclipse.swt.graphics.Image;
public abstract class MyTreeEditPart extends AbstractTreeEditPart implements PropertyChangeListener {
	@Override
	public MyModel getModel() {
		return (MyModel) super.getModel();
	}

getModel()でツリー要素の対象モデルのインスタンスが取れる。
色々なところから呼び出されるメソッドだが、呼び出す側で毎回キャストするのが面倒なので、共変戻り値の構文を使って自分のクラスで返すようにしておく。

	@Override
	protected List<MyModel> getModelChildren() {
		return getModel().getChildren();
	}

子要素があるときは、getModelChildren()で子要素の一覧を返す。無いときは空のリストを返す。

	@Override
	protected String getText() {
		MyModel model = getModel();
		return model.getName();
	}

	@Override
	protected Image getImage() {
		return super.getImage(); // TODO アイコン
	}

getText()でツリー要素(ラベル)に表示するテキストを返す。
同様にgetImage()でアイコンが指定できる。

	@Override
	public void activate() {
		super.activate();
		getModel().addPropertyChangeListener(this);
	}

	@Override
	public void deactivate() {
		getModel().removePropertyChangeListener(this);
		super.deactivate();
	}
	@Override
	public void propertyChange(PropertyChangeEvent event) {
		String name = event.getPropertyName();
		if ("name".equals(name)) {
			refreshVisuals();
		} else if ("children".equals(name)) {
			refreshChildren();
		}
	}
}

TreeEditPartにはPropertyChangeListenerを仕込んでおく。
イベント内容が「ツリー要素(ラベル)に表示すべき内容が変わった」ものだったら、refreshVisuals()を呼び出して表示を更新する。
「子要素が増減した」場合はrefreshChildren()を呼び出す。


どのモデルがどのTreeEditPartを使うかは、ツリー用のEditPartFactoryで決定する。

MyOutlineTreePartFactory.java

import org.eclipse.gef.EditPart;
import org.eclipse.gef.EditPartFactory;
public class MyOutlineTreePartFactory implements EditPartFactory {
	@Override
	public EditPart createEditPart(EditPart context, Object model) {
		EditPart part = null;
		if (model instanceof MyModel) {
			part = new MyTreeEditPart();
		}
		if (part != null) {
			part.setModel(model);
		}
		return part;
	}
}

最後に、MyEditorの方をちょっと修正する。
OutlinePageクラスをMyEditorのインナークラスでなくトップレベルのクラスとして定義した場合、MyEditor内のメソッドを呼びたいがpublicでないものがあるので、呼べるようにする。

GraphicalEditorクラス:

public class MyEditor extends GraphicalEditorWithFlyoutPalette {
〜
	public MyModel getRootModel() {
		GraphicalViewer viewer = getGraphicalViewer();
		MyModel model = ((MyModelEditPart) viewer.getContents()).getModel();
		return model;
	}
	public ScalableRootEditPart getScalableRootEditPart() {
		return (ScalableRootEditPart) getGraphicalViewer().getRootEditPart();
	}
	@Override
	public DefaultEditDomain getEditDomain() {
		return super.getEditDomain();
	}
	public void addSelectionSynchronizerViewer(EditPartViewer viewer) {
		getSelectionSynchronizer().addViewer(viewer);
	}
	@Override
	public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
〜
		if (adapter == IContentOutlinePage.class) {
			return new MyOutlinePage(this);
		}
		return super.getAdapter(adapter);
	}

ツリーのプロパティー

上述の方法でアウトラインページにツリーを表示できるが、ツリーの要素を選択した場合にプロパティービューは表示されない。[2013-05-01]
(ツリー要素をクリックするとエディター(ダイアログ)上の図形は選択されるのだが、その図形のプロパティーがプロパティービューに表示されず、「Properties are not available」になってしまう)

plugin.xmlのプロパティーのセクションを表示する為の条件判定に使うtypeMapperツリーのEditPartを登録してやると、プロパティービューに表示されるようになる。

TypeMapperクラス:

public class MyTypeMapper extends AbstractTypeMapper {
	@Override
	public Class<?> mapType(Object object) {
		if (object instanceof AbstractGraphicalEditPart) {
			AbstractGraphicalEditPart part = (AbstractGraphicalEditPart) object;
			return part.getModel().getClass();
		}
		if (object instanceof MyTreeEditPart) {
			MyTreeEditPart part = (MyTreeEditPart) object;
			return part.getModel().getClass();
		}

		return super.mapType(object);
	}
}

※typeMapperを使わない場合、plugin.xmlのセクションの表示条件(input)にTreeEditPartを加えていくことになるので、面倒。


ちなみに、AbstractGraphicalEditPartとAbstractTreeEditPartには共通の親クラス(AbstractEditPart)があるので、それを指定すれば1回の判定で済む。

import org.eclipse.gef.editparts.AbstractEditPart;
	@Override
	public Class<?> mapType(Object object) {
		if (object instanceof AbstractEditPart) {
			AbstractEditPart part = (AbstractEditPart) object;
			return part.getModel().getClass();
		}

		return super.mapType(object);
	}
}

セクションクラスでは、selectionから取れるEditPartがAbstractGraphicalEditPartだけでなくTreeEditPartも渡ってくることになるので、決め打ちしている場合は修正が必要になる。

public class ExampleSection extends AbstractPropertySection {
〜
	@Override
	public void setInput(IWorkbenchPart part, ISelection selection) {
		super.setInput(part, selection);

		IStructuredSelection ss = (IStructuredSelection) selection;
		AbstractEditPart editPart = (AbstractEditPart) ss.getFirstElement();
		this.model = (MyModel)editPart.getModel();
	}

なお、OutlinePageクラスに(エディターでやった事と同様に)ITabbedPropertySheetPageContributorを実装する方法では、プロパティーは表示されなかった。
(親クラスのContentOutlinePageはIAdaptableではないので、それも実装してみたのだが)

駄目な例:

public class MyOutlinePage extends ContentOutlinePage implements ITabbedPropertySheetPageContributor, IAdaptable {
	private final MyEditor myEditor;
	@Override
	public String getContributorId() {
		return "my-outline-page";
	}
	@Override
	public Object getAdapter(@SuppressWarnings("rawtypes") Class adapter) {
		if (adapter == IPropertySheetPage.class) {
			return new TabbedPropertySheetPage(this);
		}
		return Platform.getAdapterManager().getAdapter(this, adapter);
	}
}

GEFへ戻る / Eclipseプラグインへ戻る / Eclipseへ戻る / 技術メモへ戻る
メールの送信先:ひしだま