MPS 2024.3 ヘルプ

HowTo- 追加のツールを追加する (別名再生)

追加のツールを追加する (別名再生)

このドキュメントでは、MPS 用の新しいツール(Eclipse ユーザーの場合、MPS のツールは Eclipse のビューのようなもの)を作成する方法について説明します。これは、MPS に任意の追加ツールを組み込むための例として役立ちます。このテキストは、ツール開発の側面、つまり言語とメニューシステムに新しいツールを追加する方法、および現在編集されているものとビューを同期する方法を強調しています。

他のすべての点で、ツールは単なる Swing UI プログラムです。洗練されたビューを実装するには、Java Swing の経験が必要です。

このチュートリアルでは、Swing を使用してツリービューを構築する複雑さにあまり焦点を当てません。これは驚くほど重要な作業です。

アウトラインツール自体

MPS プラグイン言語は、新しいツールの定義をサポートしています。

Tool の新しいインスタンスを作成し、名前を設定します。別のキャプションとアイコンを付けることもできます。デフォルトの場所(下、左、右、上)も定義できます。

ここで、ツールに 3 つのフィールドを追加します。1 つ目はプロジェクトを記憶するために使用され、2 つ目はツールを MPS のメッセージバスに接続し、3 つ目はアクティブなエディターが変更された場合に ToolSynchronizer. The ToolSynchronizer がアウトラインビューを更新することを記憶します。

メッセージバスは、イベント配信のための IDEA プラットフォームのインフラストラクチャです。現在アクティブなエディターでの選択の変更の通知を受け取るために使用します。これら 2 つについては後で詳しく説明します。

private Project project; private MessageBusConnection messageBusConn; private ToolSynchronizer synchronizer;

これで、ツールの 3 つの主要なメソッド initdispose and getComponent を実装する準備が整いました。

これがコードです(コメント付きで、参照してください ! ):

init(project)->void { this.project = project; } dispose(project)->void { // disconnect from message bus -- connected in getComponent() if ( this.messageBusConn != null ) this.messageBusConn.disconnect(); } getComponent()->JComponent { // create a new JTree with a custom cell renderer (for rendering custom icons) JTree tree = new JTree(new Object[0]); tree.setCellRenderer(new OutlineTreeRenderer(tree.getCellRenderer())); // create a new synchronizer. It needs the tree to notify it of updates. this.synchronizer = new ToolSynchronizer(tree); // connect to the message bus. The synchronizer receives editor change events // and pushes them on to the tree. The synchronizer implements // FileEditorChangeListener to be able to make this work this.messageBusConn = this.project.getMessageBus().connect(); this.messageBusConn.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this.synchronizer); // we finally return a ScrollPane with the tree in it return new JScrollPane(tree); }

ツールを開くためのアクション

アクションは、MPS UI に存在するコマンドです。アウトラインビューを開くためのアクションを追加する必要があります。

アクションは、言語のプラグインの側面に存在します。アクションは、期待どおりにキーボードショートカット、キャプション、アイコンを定義できます。また、アクションパラメーターも宣言します。これらは、アクションを実行できるようにするために使用可能である必要があるコンテキストを定義します。これにより、メニュー内のアクションの存在が決定され、そのコンテキストをアクション自体に配信することがサポートされます。この場合、コンテキストには、現在開いているプロジェクトと現在開いているファイルエディターが含まれます。

action context parameters ( always visible = false ) Project project key: PROJECT required FileEditor editor key: FILE_EDITOR required

アクションの execute メソッドで、アウトラインツールを作成して開きます。

setOutline メソッドは、アウトラインツール自体に適用される通常のメソッドです。渡されたエディターを格納するだけです。

execute(event)->void { tool<Outline> outline = this.project.tool<Outline>; outline.setEditor(this.editor); outline.openTool(true); }

アクショングループ

メニュー項目はグループを介して追加されます。アウトラインを開くアクションをメニューシステムに追加できるようにするには、新しいグループを定義する必要があります。グループはその内容(アクションと 2 つのセパレーター)を定義し、メニューのどこに移動するかを決定します。グループはプラグインの側面にも存在します。

group OutlineGroup is popup: false contents <---> OpenOutlineAction <---> modifications add to Tools at position customTools

ツールのライフサイクルを管理する

このツールは、MPS の他の部分とうまく連携する必要があります。多くのイベントをリッスンし、適切に対応する必要があります。このタスク専用のリスナーが 2 人います。EditorActivationListener は、ユーザーが現在使用しているエディターのアウトラインをアウトラインビューに表示する必要があるため、開いている可能性のある多くのエディターのどれが現在アクティブであるかを追跡します。また、選択ステータスとモデルの変更を処理するリスナーをさらに接続する責任もあります(以下を参照)。ModelLifecycleListener は、現在アクティブなエディターによって編集されているモデルのライフサイクルイベントを追跡します。モデルは、編集中に、たとえば、VCS 操作を元に戻すことによって置き換えることができます。

エディターのアクティブ化とフォーカス

EditorActivationListener は、潜在的に多くのオープンエディターのどれが現在アクティブであるかを追跡します。次のコード(すでに上に表示)によって作成されるため、ツールによってインスタンス化され、MPS メッセージバスに接続されます。

this.messageBusConn = this.project.getMessageBus().connect(); this.messageBusConn.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, this.synchronizer);

コンストラクターでは、アウトラインツリーを記憶しています。次に、ツリー自体でユーザーが行った選択の変更をリッスンする新しいアウトラインツリー選択リスナーを設定します。

this.outlineSelectionListener = new OutlineSelectionListener(this); tree.addTreeSelectionListener(this.outlineSelectionListener);

次に、モデルのライフサイクル(ロード、アンロード、置換)を追跡するリスナーを設定し、イベントコレクター(両方とも以下で説明)に接続します。

modelLifecycleListener = new ModelLifecycleListener(tree); eventsCollector = new ModelChangeListener(tree);

EditorActivationListener implements FileEditorManagerListener なので、次の 3 つのメソッドを実装する必要があります。

void fileOpened(FileEditorManager p0, VirtualFile p1); void fileClosed(FileEditorManager p0, VirtualFile p1); void selectionChanged(FileEditorManagerEvent p0);

場合、selectionChanged イベントに関心があります。これは、他のすべての種類のリスナーをクリーンアップして接続する必要があるためです。これが実装です。

public void selectionChanged(FileEditorManagerEvent event) { // read action is required since we access the model read action { FileEditor oldEditor = event.getOldEditor(); // grab the old editor and clean it up if there is one if (oldEditor != null) { this.cleanupOldEditor(oldEditor); } // call a helper method that sets up the new editor this.newEditorActivated(event.getNewEditor()); } }

cleanupOldEditor メソッドは、古い、現在は非アクティブなエディターから既存のリスナーを削除します。

private void cleanupOldEditor(FileEditor oldEditor) { // Downcast from IDEA level to MPS specifics and // grab the NodeEditor component IEditor oldNodeEditor = ((MPSFileNodeEditor) oldEditor).getNodeEditor(); if (oldNodeEditor != null && this.editorSelectionListener != null) { // remove the selection listener from the old editor oldNodeEditor.getCurrentEditorComponent().getSelectionManager(). removeSelectionListener(this.editorSelectionListener); // grab the descriptor of the model edited by the old editor // and remove the model listener (cleanup!) SModelDescriptor descriptor = oldNodeEditor.getEditorContext().getModel(). getModelDescriptor(); descriptor.removeModelListener(modelLifecycleListener); // remove the model edited by the old editor from the events collector // ...we are not interested anymore. eventsCollector.remove(descriptor); } }

次の方法では、インフラストラクチャを新しく選択したエディターとその基になるモデルに接続します。ノードへの参照を保持するときはいつでも、SNodePointer をどのように使用するかに注意してください。これはプロキシとして機能し、モデルが置き換えられた場合のノードの解決を処理し、実際のノードへの「弱い参照」を含むため、モデルがアンロードされるとガベージコレクションされる可能性があります。これは、メモリリークを回避するために重要です。

public void newEditorActivated(FileEditor fileEditor) { if (fileEditor != null) { // remember the current editor this.currentEditor = ((MPSFileNodeEditor) fileEditor); // grab the root node of that new editor... SNode rootNode = this.currentEditor.getNodeEditor(). getCurrentlyEditedNode().getNode(); // ...wrap it in an SNodePointer... SNodePointer treeRoot = new SNodePointer(rootNode); // and create a new outline tree model OutlineModel model = new OutlineModel(treeRoot); tree.setModel(model); // create a new selection listener and hook it up // with the newly selected editor this.editorSelectionListener = new EditorSelectionListener(tree, outlineSelectionListener); SelectionManager selectionManager = this.currentEditor.getNodeEditor(). getCurrentEditorComponent().getSelectionManager(); selectionManager.addSelectionListener(this.editorSelectionListener); // This is needed to detect reloading of a model ((MPSFileNodeEditor) fileEditor).getNodeEditor(). getEditorContext().getModel().getModelDescriptor(). addModelListener(modelLifecycleListener); eventsCollector.add(this.currentEditor.getNodeEditor(). getEditorContext().getModel().getModelDescriptor()); } else { tree.setModel(new OutlineModel(null)); } }

モデルライフサイクルの追跡

MPS によって提供される ModelLifecycleListener extends the SModelAdapter クラス。モデルの置き換えに関心があるため、modelReplaced メソッドをオーバーロードします。現在保持されているモデルが新しいモデルに置き換えられるたびに呼び出されます。VCS リバー操作中。

実装では、同じルートに対して新しい新しいツリーモデルを作成します。このコードは少し無意味に見えますが、新しいモデルでプロキシされたノードを自動的に再解決する SNodePointer を内部で使用していることに注意してください。

また、新しいモデルの記述子にリスナーとして自分自身を追加して、後続のモデル置換イベントが通知されるようにします。

@Override public void modelReplaced(SModelDescriptor descriptor) { tree.setModel(new OutlineModel(((OutlineModel) tree.getModel()).getRealRoot())); descriptor.addModelListener(this); }

ノード選択の同期

トラッキングエディターの選択

これは、現在アクティブなエディターで選択されたノードが変更されると、ツリーの選択(および拡張ステータス)を更新します。アウトラインビューのツリーが現在アクティブなエディターと同期していることを(上記で)確認しました。

単一ノードの選択のみに関心があるため、このクラスは MPS' SingularSelectionListenerAdapter を拡張します。

selectionChangedTo メソッドを次のように上書きします。

protected void selectionChangedTo(EditorComponent component, SingularSelection selection) { // do nothing is disabled -- prevents cyclic, never ending updates if (disabled) { return; } // read action, because we access the model read action { // gran the current selection SNode selectedNode = selection.getSelectedNodes().get(0); // ... only if it has changed... if (selectedNode != lastSelection) { lastSelection = selectedNode; // disable the tree selection listener, once again to prevent cyclic // never ending updates treeSelectionListener.disable(); // select the actual node in the tree tree.setSelectionPath(((OutlineModel) tree.getModel()). gtPathTo(selectedNode)); treeSelectionListener.enable(); } } }

モデル構造の変化を追跡する

エディターでノードを追加、変更(名前変更)、移動、削除した場合は、アウトラインビューのツリー構造を更新する必要があります。ツリー変更イベントは非常にきめ細かくすることができ、オーバーヘッドを回避するために、MPS はそれらをより粗いコマンドに関連するバッチに収集します。MPS の EventsCollector 基本クラスを使用することにより、イベントの重要なバッチが発生したときに通知を受け取り、訪問者の使用に関心のあるイベントリストを調べることができます。

ModelChangeListener はこのタスクを実行します。そのためには、eventsHappened メソッドを実装する必要があります。イベントのリストを取得し、SModelEventVisitorAdapter を拡張する内部クラスを使用してイベントにアクセスし、関心のあるイベントに対応します。

protected void eventsHappened(List<SModelEvent> list) { super.eventsHappened(list); foreach evt in list { evt.accept(new ModelChangeVisitor()); } }

ツリーに通知するビジターとして機能する ModelChangeVisitor 内部クラスは、visitPropertyEvent をオーバーライドして、現在のバッチでプロパティが変更されたノードを検出します。次に、ツリーモデルのすべてのリスナーに通知します。

public void visitPropertyEvent(SModelPropertyEvent event) { OutlineModel outlineModel = ((OutlineModel) tree.getModel()); foreach l in outlineModel.getListeners() { l.treeNodesChanged(new TreeModelEvent(this, outlineModel.getPathTo(event.getNode()))); } }

また、visitChildEvent を上書きして、ノードの子の追加 / 削除の通知を受け取ります。JTree の API が少し煩わしいことを除いて、次のコメント付きコードは、それが何をするかについて明確にする必要があります。

@Override public void visitChildEvent(SModelChildEvent event) { // grab the model OutlineModel outlineModel = ((OutlineModel) tree.getModel()); // we need the following arrays later for the JTree API Object[] child = new Object[]{event.getChild()}; int[] childIndex = new int[]{event.getChildIndex()}; // we create a tree path to the parent notify all listeners // of adding or removing children TreePath path = outlineModel.getPathTo(event.getParent()); if (path == null) { return; } // notify the tree model's listeners about what happened foreach l in outlineModel.getListeners() { if (event.isAdded()) { l.treeNodesInserted(new TreeModelEvent(this, path, childIndex, child)); } else if (event.isRemoved()) { l.treeNodesRemoved(new TreeModelEvent(this, path, childIndex, child)); } } }

帰り道: 追跡木の選択

JTree の選択の追跡は、Swing の TreeSelectionListener and overwriting it's valueChanged メソッドを次の方法で実装することによって行われます。

public void valueChanged(TreeSelectionEvent event) { // don't do anything if disabled --- preventing cyclic updates! if (!(disableEditorUpdate)) { JTree tree = ((JTree) event.getSource()); if (editorActivationListener.currentEditor != null && tree.getLastSelectedPathComponent() instanceof SNodePointer) { // grab the selected treee node SNodePointer pointer = ((SNodePointer) tree. getLastSelectedPathComponent()); // disable the editor selection listener to prevent // cyclic, never ending updates editorActivationListener.editorSelectionListener.disable(); // update the selection in the editor editorActivationListener.currentEditor.getNodeEditor(). getCurrentEditorComponent().selectNode(pointer.getNode()); editorActivationListener.editorSelectionListener.enable(); } } }

Swing のアーティファクト: ツリーモデルとレンダラ

このセクションでは、Swing アーティファクトの実装における MPS 固有の興味深い側面をいくつか説明したいと思います。

ツリーセルレンダラー

ツリーセルレンダラーは、ツリー内のセルのレンダリングを担当します。getPresentation method on the nodes, and the IconManager を使用して、それぞれのノードのアイコン(のキャッシュバージョン)を取得します。

public Component getTreeCellRendererComponent(JTree tree, Object object, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) { if (object instanceof SNodePointer) { string presentation; DefaultTreeCellRenderer component; read action { SNode node = ((SNodePointer) object).getNode(); presentation = node.getPresentation(); component = ((DefaultTreeCellRenderer) renderer.getTreeCellRendererComponent( tree, presentation, selected, expanded, leaf, row, hasFocus)); component.setIcon(IconManager.getIconFor(node)); } } else { return renderer.getTreeCellRendererComponent(tree, object, selected, expanded, leaf, row, hasFocus); } }

木モデル

概念に ShowConceptInOutlineAttribute 属性のアノテーションが付けられている子のみをツリーノードに含めるため、ツリーモデルは興味深いものです。storeInOutline ロールに保存されます。

ツリーモデルでは、この属性の存在についてノードの子をフィルタリングする必要があります。それぞれのヘルパーメソッドは次のとおりです。

private List findAllChildrenWithAttribute(SNodePointer pointer) { List result = new ArrayList(); SNode node = pointer.getNode(); if (node == null) { return new ArrayList(); } foreach child in node.getChildren() { SNode attribute = AttributeOperations.getNodeAttribute( child.getConceptDeclarationNode(), "showInOutline"); if (attribute != null) { result.add(child); } } return result; }