モデルの編集機能の追加
モデルの編集機能は、EditPartにEditPolicyをインストールすることで追加されます。サンプル・アプリケーションで行う編集操作は、ノードの作成・削除と、移動・サイズ変更、およびコネクションの作成・削除、再接続です。これらの編集操作に伴って発生するRequestのタイプと、各Requestを処理するためのEditPolicyは、以下の通りです。
| Role | EditPolicyクラス(スーパークラス) | 処理するRequestのタイプ |
LAYOUT_ROLE |
DiagramXYLayoutEditPolicy( XYLayoutEditPolicy) |
REQ_MOVE_CHILDREN、REQ_RESIZE_CHILDREN、REQ_CREATE |
COMPONENT_ROLE |
ExampleComponentEditPolicy( ComponentEditPolicy) |
REQ_DELETE |
CONNECTION_ROLE |
ExampleConnectionEditPolicy( ConnectionEditPolicy) |
REQ_DELETE |
GRAPHICAL_NODE_ROLE |
ExampleNodeEditPolicy( GraphicalNodeEditPolicy) |
REQ_CONNECTION_START、REQ_CONNECTION_END、REQ_RECONNECT_SOURCE、REQ_RECONNECT_TARGET |
これらのEditPolicyクラスは、EditPartのcreateEditPolicyメソッド内でインストールされます。
public class ExampleDiagramEditPart extends BaseGraphicalEditPart { ... protected void createEditPolicies() { // EditPolicyのインストール installEditPolicy(EditPolicy.COMPONENT_ROLE, new RootComponentEditPolicy()); installEditPolicy(EditPolicy.LAYOUT_ROLE, new DiagramXYLayoutEditPolicy()); } ... }
EditPolicyの実装とコマンド・クラスの作成
EditPolicyクラスは、通常GEFに用意されているEditPolicyクラスを拡張して作成します。ユーザーの操作によって発生したRequestは、EditPolicyのgetCommandメソッドに渡され、そのRequestが表すモデルの変更要求にそったコマンドの作成が要求されますが、GEFに用意されているEditPolicyクラスは、getCommandメソッドで、送られてきたRequestのタイプを調べ、RequestがそのEditPolicyで処理すべきタイプの場合は、対応するメソッドを呼び出すようになっています。
例えば、XYLayoutEditPolicyクラスは、REQ_MOVE_CHILDREN、およびREQ_RESIZE_CHILDRENタイプのRequestを受け取ると、createChangeConstraintCommandメソッドを呼び出して、モデルの制約を変更するコマンドの作成を要求します。また、REQ_CREATEタイプのRequestを受け取ると、getCreateCommandメソッドを呼び出します。
したがって、これらのクラスを利用すれば、必要なメソッドをオーバーライドして、コマンドを返すようにするだけでEditPolicyの実装を行うことができます。次に、GEFに用意されたクラスを使用したEditPolicyの実装例をいくつか示します。
DiagramXYLayoutEditPolicy
このEditPolicyは、ExampleDiagramEditPartにインストールされるEditPolicyです。ExampleDiagramEditPartは、FreeformLayout(XYLayoutのサブクラス)をレイアウト・マネージャとして設定したフィギュアをビューとして作成するので、このEditPartが受け持つ編集動作は、XYLayoutに関連するものになります。このEditPartで処理するRequestのタイプは、REQ_CREATEと、REQ_MOVE_CHILDREN、およびREQ_RESIZE_CHILDRENです。
public class DiagramXYLayoutEditPolicy extends XYLayoutEditPolicy { protected Command createAddCommand(EditPart child, Object constraint) { return null; // REQ_ADD } /**
* ノードの位置・サイズ変更コマンドの作成(REQ_MOVE_CHILDREN,REQ_RESIZE_CHILDREN) */ protected Command createChangeConstraintCommand(EditPart child, Object constraint) { ChangeConstraintCommand command = new ChangeConstraintCommand ((NodeElement)child.getModel()); command.setConstraint((Rectangle)constraint); return command; } /**
* ノード作成コマンドの作成(REQ_CREATE)
*/ protected Command getCreateCommand(CreateRequest request) { // 作成するノードの追加先を取得 ExampleDiagram parent = (ExampleDiagram) getHost().getModel(); // 新しく作成するノード・モデルを取得 NodeElement node = (NodeElement) request.getNewObject(); CreateNodeCommand command = new CreateNodeCommand(parent, node); // 新しく作成するノードの制約(位置とサイズ)を設定 command.setConstraint((Rectangle)getConstraintFor(request)); return command; } protected Command getDeleteDependantCommand(Request request) { return null; // REQ_DELETE_DEPENDANT } }
ここでは、Requestの持つ情報を利用してコマンドを作成しています。コマンド・クラスは、モデルのプロパティを変更するだけの単純なものです。また、コマンドは、モデルの情報のみを持ち、EditPartやビューについては、参照しないようにします。
public class ChangeConstraintCommand extends Command { private NodeElement nodeElement; private Rectangle oldConstraint,newConstraint; public ChangeConstraintCommand(NodeElement node) { super("制約の変更"); nodeElement = node; oldConstraint = node.getConstraint(); } public void execute() { // 制約の変更 nodeElement.setConstraint(newConstraint); } public void setConstraint(Rectangle rect) { newConstraint = rect; } public void undo() { // 制約を元にもどす nodeElement.setConstraint(oldConstraint); } }
public class CreateNodeCommand extends Command { private ExampleDiagram diagram; private NodeElement node; private Rectangle constraint; public CreateNodeCommand (ExampleDiagram diagram, NodeElement node) { super("ノードの作成"); this.diagram = diagram; this.node = node; } public void execute() { // 制約の設定 node.setConstraint(constraint); // ノードの追加 diagram.addChild(node); } public void setConstraint(Rectangle r) { constraint = r; } public void undo() { // ノードの削除 diagram.removeChild(node); } }
ExampleNodeEditPolicy
このEditPolicyは、コネクションの作成と再接続の操作を処理します。この内、コネクションの作成は、他の編集操作とは少し異なります。これは、コネクションEditPartには、ソースとターゲットが必要となるためです。
コネクションの作成コマンドは、2段階に分けて作成されます。まず最初の段階では、ソースとなるEditPartが決定されます。次に、ターゲットEditPartが決定され、コネクションの作成コマンドが作成されることになります。
public class ExampleNodeEditPolicy extends GraphicalNodeEditPolicy { // フィードバック用フィギュア private Shape targetFeedback; /**
* コネクション作成コマンドを作成する第2段階
*/ protected Command getConnectionCompleteCommand( CreateConnectionRequest request) { CreateConnectionCommand command = (CreateConnectionCommand)request.getStartCommand(); command.setTargetNode((NodeElement)getHost().getModel()); return command; } /**
* コネクション作成コマンドを作成する最初の段階
*/ protected Command getConnectionCreateCommand(CreateConnectionRequest request) { CreateConnectionCommand command = new CreateConnectionCommand ((ConnectionElement)request.getNewObject()); command.setSourceNode((NodeElement)getHost().getModel()); request.setStartCommand(command); return command; } ... protected void eraseTargetConnectionFeedback(DropRequest request) { // フィードバックを消去 if (targetFeedback == null) return; getFeedbackLayer().remove(targetFeedback); targetFeedback = null; } protected void showTargetConnectionFeedback(DropRequest request) { if (targetFeedback != null) return; // フィードバック用のフィギュアの作成 targetFeedback = new RectangleFigure(); targetFeedback.setBackgroundColor(ColorConstants.darkBlue); targetFeedback.setLineWidth(2); // 背景をXORで描画して対象のノードに重ねる targetFeedback.setFillXOR(true); targetFeedback.setBounds(getHostFigure().getBounds()); // フィードバック用フィギュアを追加するためのレイヤーに追加 getFeedbackLayer().add(targetFeedback); } }
このEditPolicyでは、コネクションの作成、および再接続の過程をユーザーに視覚的に示すために、独自のフィードバックを表示します。フィードバックは、RootEditPartが提供するフィードバック・レイヤーに配置されます。このフィードバックは、コネクションの作成時、あるいは再接続時に、対象となるノードをハイライト表示するためのものです。
フィードバックの表示は、getCommandメソッドではなく、showSourceFeedback、およびshowTargetFeedbackメソッドを通して、要求されます。上記のコードでオーバーライドしているshowTargetConnectionFeedbackメソッドは、スーパークラスのshowTargetFeedbackメソッドが、コネクションに関連するRequestタイプを受け取ったときに呼び出されます。また、フィードバックの消去についても同様です。
DeleteNodeCommand
ノードは、コネクションが接続されている可能性があります。削除されるノードにコネクションが接続されていた場合は、コネクションも一緒に削除する必要があります。したがって、ノードの削除を行うコマンド・クラスは、これに注意して作成する必要があります。
public class DeleteNodeCommand extends Command { private ExampleDiagram diagram; private NodeElement node; // 削除対象のノード // 削除されるノードをソースとするコネクション private List oldSourceConnections = new ArrayList(); // 削除されるノードをターゲットするコネクション private List oldTargetConnections = new ArrayList(); public DeleteNodeCommand (ExampleDiagram parent, NodeElement child) { super(ExamplePlugin.getResourceString ("DeleteNodeCommand.Label")); //$NON-NLS-1$ diagram = parent; node = child; } public void execute() { // ノードに接続されているコネクションを削除する removeConnections(); // ノードを削除 diagram.removeChild(node); } private void removeConnections() { // 削除されるノードに接続されているコネクションを記録 oldSourceConnections.addAll(node.getSourceConnections()); oldTargetConnections.addAll(node.getTargetConnections()); // 削除されるノードをソースとするコネクションを削除 Iterator it = oldSourceConnections.iterator(); while(it.hasNext()) { ((ConnectionElement)it.next()).disconnect(); } // 削除されるノードをターゲットとするコネクションを削除 it = oldTargetConnections.iterator(); while(it.hasNext()) { ((ConnectionElement)it.next()).disconnect(); } } private void restoreConnections() { // このノードをソースとするコネクションの復元 Iterator it = oldSourceConnections.iterator(); while(it.hasNext()) { ((ConnectionElement)it.next()).connect(); } // このノードをターゲットとするコネクションの復元 it = oldTargetConnections.iterator(); while(it.hasNext()) ((ConnectionElement)it.next()).connect(); oldSourceConnections.clear(); oldTargetConnections.clear(); } public void undo() { // ノードの再追加 diagram.addChild(node); // コネクションの復元 restoreConnections(); } }
グラフィカル・エディターの作成
モデル、ビュー、EditPartの準備が終わったら、EditPartViewerを用意します。EditPartViewerは、SWTコントロールを貼り付けることができる場所であれば、どこでも配置できますが、一般的には、Eclipseの「エディター・ペイン」に配置されます。
GEFには、GEFアプリケーションをEclipseのエディタ・プラグインとして作成するために、GraphicalEditorクラス、およびそのサブクラスのGraphicalEditorWithPaletteクラス、GraphicalEditorWithFlyoutPaletteクラスの3つのクラスが用意されています。サンプル・アプリケーションでは、この内のGraphicalEditorWithFlyoutPaletteクラスを使用しています。
GraphicalEditorWithFlyoutPaletteは、Eclipseのエディタ・ペインに、グラフィカル・ビューアーと、エディタ・ペインの左右に配置することができる、折りたたみ可能なパレットを提供します。

EditDomainとグラフィカル・ビューアーの設定
エディタ・ペインに配置される、グラフィカル・ビューアーや、パレットを構成するパレット・ビューアーを含む、全てのEditPartViewerは、「EditDomain」によって管理されます。EditDomainは、EditPartViewerの他にも、コマンド・スタックや、パレット、現在アクティブなツールなどを管理します。
EditDomainは、通常エディタ・クラスのコンストラクタで設定されます(下記コード(1))。これにより、エディタ・ペインに配置されるグラフィカル・ビューアーは、このEditDomainによって管理されることになります。他のEditPartViewerを追加する場合は、EditDomain#addViewerメソッドを使用して、そのビューアーを追加します。このようにしておくと、複数のEditPartViewer間で、同一のモデルを同時に編集することができるようになります。
グラフィカル・ビューアーの設定は、configureGraphicalViewerメソッド内で行います。EditPartFactoryは、このメソッド内で設定する必要があります(下記コード(2))。また、トップレベル・モデルの設定は、initializeGraphicalViewerメソッド内で行います(下記コード(3))。
public class ExampleEditor extends GraphicalEditorWithFlyoutPalette { private ExampleDiagram diagram = null; // トップレベルのモデル ... public ExampleEditor() { // エディット・ドメインの設定 setEditDomain(new DefaultEditDomain(this)); // (1) } ... protected void configureGraphicalViewer() { super.configureGraphicalViewer(); GraphicalViewer viewer = getGraphicalViewer(); // コンテンツEditPartのビューにFreeformLayoutを使用する場合は // RootEditPartをFreeformGraphicalRootEditPartに // 変更する必要がある viewer.setRootEditPart(new FreeformGraphicalRootEditPart()); // EditPartFactoryの設定 viewer.setEditPartFactory (new GraphicalEditPartsFactory()); // (2) ... } protected void initializeGraphicalViewer() { super.initializeGraphicalViewer(); GraphicalViewer viewer = getGraphicalViewer(); // トップレベル・モデルの設定 viewer.setContents(getExampleDiagram()); // (3) } ... protected void setInput(IEditorInput input) { super.setInput(input); IFile file = (IFile)input.getAdapter(IFile.class); if (file != null) { setPartName(file.getName()); diagram = load(file); // モデルのロード } if(diagram == null) diagram = new ExampleDiagram(); } private ExampleDiagram load(IFile file) { ExampleDiagram model = null; try { ObjectInputStream in = new ObjectInputStream(file.getContents()); model = (ExampleDiagram) in.readObject(); in.close(); } catch (IOException e) { e.printStackTrace(); } catch (CoreException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } return model; } ... }
ドキュメントのダーティ・チェック
前述の通り、モデルの編集は、全てコマンドを使用して行われます。したがって、コマンド・スタックを調べれば、ドキュメントが変更されたかどうかを知ることができます。
コマンド・スタックの変更は、CommandStackListenerを使用して知ることができます。GraphicalEditorクラスのサブクラスでは、スーパークラスによって、このリスナーが登録されていて、コマンド・スタックに変更があると、commandStackChangedが呼び出されるようになっています。
public class ExampleEditor extends GraphicalEditorWithFlyoutPalette { ... public void commandStackChanged(EventObject event) { super.commandStackChanged(event); // エディターのプロパティを更新 firePropertyChange(IEditorPart.PROP_DIRTY); } ... public boolean isDirty() { // ダーティー・チェックにはコマンド・スタックを使用する return getCommandStack().isDirty(); } ... }
パレットの作成
GraphicalEditorWithFlyoutPaletteクラスは、折りたたみ可能なパレットを提供します。パレットは、getPaletteRootメソッドで返されるPaletteRootに、GEFツールを格納することで作成されます。
また、パレットはEditDomainが管理するので、createPaletteViewerProviderメソッド内で、EditDomainへの登録を行います。
public class ExampleEditor extends GraphicalEditorWithFlyoutPalette { ... protected PaletteViewerProvider createPaletteViewerProvider() { return new PaletteViewerProvider(getEditDomain()); } protected FlyoutPreferences getPalettePreferences() { // パレット設定の保存と復元 return new FlyoutPreferences() { ... }; } protected PaletteRoot getPaletteRoot() { return ExamplePaletteFactory.createPaletteRoot(); } ... }
public class ExamplePaletteFactory { public static PaletteRoot createPaletteRoot() { PaletteRoot palette = new PaletteRoot(); palette.add(createToolGroup(palette)); palette.add(createConnectionCreationGroup()); palette.add(createNodeCreationGroup()); return palette; } private static PaletteEntry createToolGroup(PaletteRoot root) { PaletteGroup group = new PaletteGroup("tools"); //$NON-NLS-1$ // 選択ツールをデフォルトでアクティブになるように設定し、 // パレットに追加する ToolEntry entry = new SelectionToolEntry(); root.setDefaultEntry(entry); group.add(entry); // 囲み枠ツールの追加 group.add(new MarqueeToolEntry()); return group; } ... }
アクションの作成
GEFには、操作の取り消し・やり直しや、モデルの削除といった一般的な編集操作に関するアクションや、ビューの幅や高さを揃えたり、ビューを整列させるといったグラフィカルな編集に必要なアクションが用意されています。
また、GEFアプリケーションをエディタ・プラグインとして作成する場合には、GEFで定義されているコントリビューター・クラスを使用してアクションを登録することができます。
public class ExampleActionContributor extends ActionBarContributor { public ExampleActionContributor() { } protected void buildActions() { addRetargetAction(new DeleteRetargetAction()); addRetargetAction(new UndoRetargetAction()); addRetargetAction(new RedoRetargetAction()); // 整列アクション addRetargetAction(new AlignmentRetargetAction(PositionConstants.LEFT)); addRetargetAction(new AlignmentRetargetAction(PositionConstants.CENTER)); addRetargetAction(new AlignmentRetargetAction(PositionConstants.RIGHT)); addRetargetAction(new AlignmentRetargetAction(PositionConstants.TOP)); addRetargetAction(new AlignmentRetargetAction(PositionConstants.MIDDLE)); addRetargetAction(new (PositionConstants.BOTTOM)); // 幅、高さを合わせるためのアクション addRetargetAction(new MatchWidthRetargetAction()); addRetargetAction(new MatchHeightRetargetAction()); } protected void declareGlobalActionKeys() { addGlobalActionKey(ActionFactory.SELECT_ALL.getId()); } public void contributeToToolBar(IToolBarManager toolBarManager) { toolBarManager.add(getAction(ActionFactory.DELETE.getId())); toolBarManager.add(new Separator()); toolBarManager.add(getAction(ActionFactory.UNDO.getId())); toolBarManager.add(getAction(ActionFactory.REDO.getId())); toolBarManager.add(new Separator()); toolBarManager.add(getAction(GEFActionConstants.ALIGN_LEFT)); toolBarManager.add( getAction(GEFActionConstants.ALIGN_CENTER)); toolBarManager.add( getAction(GEFActionConstants.ALIGN_RIGHT)); toolBarManager.add(new Separator()); toolBarManager.add(getAction(GEFActionConstants.ALIGN_TOP)); toolBarManager.add( getAction(GEFActionConstants.ALIGN_MIDDLE)); toolBarManager.add( getAction(GEFActionConstants.ALIGN_BOTTOM)); toolBarManager.add(new Separator()); toolBarManager.add( getAction(GEFActionConstants.MATCH_WIDTH)); toolBarManager.add( getAction(GEFActionConstants.MATCH_HEIGHT)); } }
public class ExampleEditor extends GraphicalEditorWithFlyoutPalette { ... protected void createActions() { super.createActions(); // アクションを登録するレジストリ ActionRegistry registry = getActionRegistry(); IAction action; // 整列アクションの作成 action = new AlignmentAction ((IWorkbenchPart)this, PositionConstants.LEFT); // アクションの登録 registry.registerAction(action); // 選択によってアクションの有効・無効が // 切り替わるように設定する getSelectionActions().add(action.getId()); ... } }
おわりに
これまで見てきたとおり、GEFアプリケーションは、何らかの図を描画するものです。したがって、どのようなモデルを編集するにしても、モデルの編集方針は、共通する部分が多くなるはずです。
GEFは、モデルの形態に依存しないように設計されているので、EditPolicyやEditPartを実装するうえで、共通する処理があった場合でも、これらのクラスを他のGEFアプリケーションで再利用することは難しいでしょう。しかし、モデルの削除や、移動といった一般的な編集操作を行うためのコマンドや、EditPolicyクラスの実装方法は、異なるGEFアプリケーションでもほとんど同じものになります。また、EditPartの実装方法も、対応するビューや、モデルのタイプによって大体決まっています。
つまり、どのようなモデルを扱うにしても、GEFアプリケーションの実装方法は、同じようなものになるということができます。
GEFがサポートするその他の機能
最後に、今回は取り上げなかったGEFの主な機能を一部紹介します。これらの機能の使用法は、Eclipse CVS レポジトリーから入手できる、「org.eclipse.gef.examples.logic」サンプルを参照してください(Webページの[Project Root]メニューから[Tools_Project]を選択)。
- ズーム機能:グラフィカル・ビューアーは、図を拡大・縮小表示する機能を備えることができます。
- コンテンツ・アウトライン:GEFのツリー・ビューアーを使用して、グラフィカル・エディタで編集中のモデルの構造を「アウトライン」ビューに表示することができます。
- 図のサムネイル表示:グラフィカル・ビューアーで表示している図を基に、サムネイルを表示することができます。
- 図の印刷:グラフィカル・ビューアーで表示している図は、印刷することもできます。
- ルーラーとグリッド:ビューアー上にグリッドやルーラーを表示することができます。また、ビューは、スナップも行えます。
- ドラッグ・アンド・ドロップ:GEFアプリケーションには、ネイティブなDND機能を追加することができます。これにより、他のアプリケーションからテキストをドラッグして、ビューアーにドロップした場合に、そのテキストを使用してモデルを作成するといったようなことが可能になります。

(REQ_MOVE_CHILDREN,REQ_RESIZE_CHILDREN) */