minimize

事業拡大のため、新しい仲間を募集しています。
→詳しくはこちら

Natureについて

Natureとは耳慣れない言葉かもしれません。
各プロジェクトには、そのプロジェクトが持つ属性のようなものが用意されていて
それをNatureと呼びます。

例えば、Javaプロジェクトは org.eclipse.jdt.core.javanature というNatureを持っています。
このNatureによって、JavaファイルをSaveしたときに
自動的にclassファイルを作成したりしているのです(実際にはNatureが持つBuilder)。

Natureは属性ですので、一つのプロジェクトが複数のNatureを持つことが可能です。

Propertyについて

Natureを作る前にまず、Property(プロパティ)の説明から始めます。
プロパティはPreferenceとよく似ています。
後者が「ワークスペース単位」の設定であるのに対して、
前者は「エレメント単位」の設定と考えることが出来ます。

エレメントとは、Package Explorerビューなどに表示されるアイテムの事です。
ファイル、フォルダ、ソースフォルダ、プロジェクトなど様々な種類があります。

Natureはプロジェクト単位なので、プロジェクト単位の設定をするような
プロパティページを作りましょう。

PropertyPageの作り方

では実際に PropertyPage を作ってみます。
基本的には Preferenceの作り方 と似ています。
ただし、PropertyPage を継承します。
FieldEditorPreferencePage などと違い、ストアとの連動などは自分で記述する必要があります。
以下に簡単な例を載せます。

public class SamplePropertyPage extends PropertyPage implements
        IWorkbenchPropertyPage {
    
    private Text txtSample;
    
    @Override
    protected Control createContents(Composite parent) {
        
        Composite comp = new Composite(parent, SWT.NULL);
        comp.setLayout(new FormLayout());
        
        IPreferenceStore store = getStore();

        txtSample = new Text(comp, SWT.BORDER | SWT.SINGLE);
        txtSample.setText(store.getString("txtSample"));
        
        noDefaultAndApplyButton();
        return comp;
    }
    
    @Override
    public boolean performOk() {
        IPreferenceStore store = getStore();
        store.setValue("txtSample", txtSample.getText());
        return super.performOk();
    }

}

※ 一部記述を省略しているのでこのままではまだ動作しません

このように、performOk() をオーバーライドして
その中でストアへの書き込みを行います。
これで、プロパティページでOKを押したときにテキストの内容がストアに書き込まれ
次回このページを開いたときには

txtSample.setText(store.getString("txtSample"));

によってストアの内容がテキストコンポーネントの初期値として表示されることになります。

PropertyPage では Default / Apply が実装されていないので
その部分を自分で記述するか、上の例のように noDefaultAndApplyButton() を使って
Default / Apply ボタンを非表示にしておくか、どちらかにしましょう。

先にコードから書いてしまいましたが、当然これも plugin.xml に記述する必要があります。

<extension
      point="org.eclipse.ui.propertyPages">
   <page
   	    objectClass="org.eclipse.jdt.core.IJavaProject"
         class="sample.SamplePropertyPage"
         name="Sample"
         id="sample.SamplePropertyPage">
   </page>
</extension>
objectClass

プロパティページの表示対象となるクラスまたはインターフェイスを指定します。
この例では IJavaProject となっていますから、
Javaプロジェクトのプロパティページを開いたときにのみ表示対象となります。

具体的に言えば、ビュー内で選択されたアイテムが
objectClass で指定したインターフェイスを持つときにのみ
プロパティページ内のツリーにここで指定したプロパティページ用の項目が表示されることになります。

例えば、Javaファイル用のプロパティページでは「Resource」しか表示されませんが
Javaプロジェクト用のプロパティページでは他にも「Java Build Path」や「Java Compiler」など
多数のページが表示されます。
これは、それらのプロパティページが IJavaProject に関連付けられている為です。

Eclipse3.3では、この指定がより細かくできるようになるようです。
そのため、objectClass 属性は deprecated となっています。

class

プロパティページの実装クラスを指定します。
PropertyPage の派生クラスである必要があります。

name

ページ内のツリーに表示される名称です。

id

プロパティページのIDです。classと同じ値にしておけば間違いは無いです。

実際の表示内容はこんな感じになります。
画面レイアウト

エレメント単位のストアについて

先程のコードで一部省略した部分について説明します。
それはストアについてです。
Preferenceはワークスペース全体の設定なので、ストアの取得は簡単です。

IPreferenceStore getPreferenceStore() {
    return MyPlugin.getDefault().getPreferenceStore();
}

しかし、エレメント単位のストアの場合このように簡単には行きません。
具体的には、以下のようにします。

IPreferenceStore getProjectStore(IProject project) {
    return new ScopedPreferenceStore(new ProjectScope(project), 
        MyPlugin.getDefault().getBundle().getSymbolicName());
}

ここで注目するのは project の部分です。
ここを可変にすることで、エレメント単位のストアが取得可能になります。

最後にもう一つ。エレメント単位のストアは自動では外部記憶装置に保存されません。
手動で save() メソッドを呼び出してやる必要があります。
以上を踏まえて、完全なコードを以下に載せます。

public class SamplePropertyPage extends PropertyPage implements
        IWorkbenchPropertyPage {
    
    private IProject project; // 選択されたProject
    private Text txtSample;
    
    @Override
    public void setElement(IAdaptable element) {
        super.setElement(element);
        project = ((IJavaProject)element).getProject();
    }
    
    @Override
    protected Control createContents(Composite parent) {
        
        Composite comp = new Composite(parent, SWT.NULL);
        comp.setLayout(new FormLayout());
        
        IPreferenceStore store = getProjectStore();
    
        txtSample = new Text(comp, SWT.BORDER | SWT.SINGLE);
        txtSample.setText(store.getString("txtSample"));
        
        noDefaultAndApplyButton();
        return comp;
    }
    
    @Override
    public boolean performOk() {
        IPreferenceStore store = getProjectStore();
        store.setValue("txtSample", txtSample.getText());
        try {
            ((IPersistentPreferenceStore)store).save();
        } catch (IOException e) {
            ...
        }
        return super.performOk();
    }
    
    IPreferenceStore getProjectStore() {
        return new ScopedPreferenceStore(new ProjectScope(project), 
            MyPlugin.getDefault().getBundle().getSymbolicName());
    }
    
}

これで完成です。
setElement() については後ほど説明します。

Natureの追加・削除

先程も説明したようにNatureは属性なので
追加・削除することが出来ます。
どちらも手順は似ています。

追加方法

void enableNature(IProject project) {
    IProjectDescription desc = project.getDescription();
    List<String> natureIds = new ArrayList<String>(Arrays.asList(desc.getNatureIds()));
    natureIds.add(SampleNature.NATURE_ID);
    desc.setNatureIds(natureIds.toArray(new String[natureIds.size()]));
    project.setDescription(desc, monitor);
}

IProject.getDescription() で、プロジェクトの description が取得できます。
ここから IProjectDescription.getNatureIds() によってNatureIDの一覧を取得して
そこに今回追加したいNatureIDを追加します。
そして setNatureIds() によって description にセットし、最後に setDescription() で完了です。

IProjectDescription.appendNatureId() とかあれば便利なんですが、無いようです。
削除も全く同様です。

削除方法

void enableNature(IProject project) {
    IProjectDescription desc = project.getDescription();
    List<String> natureIds = new ArrayList<String>(Arrays.asList(desc.getNatureIds()));
    while (natureIds.contains(SampleNature.NATURE_ID)) {
        natureIds.remove(SampleNature.NATURE_ID);
    }
    desc.setNatureIds(natureIds.toArray(new String[natureIds.size()]));
    project.setDescription(desc, monitor);
}

さて、上で突然 SampleNature なんてものが登場していますが
これが Nature の正体です。作り方は後で説明します。

その前に、Nature用のプロパティページを作りましょう。
チェックボックス型のフィールドを一つ作成します。
ストアは使いません。

public class SamplePropertyPage ... {
    
    private IProject project; // 選択されたProject
    private boolean hasNature; // 現在プロジェクトがNatureを持つかどうか
    
    private Button chkNature; // チェックボックス用コンポーネント
    
    @Override
    public void setElement(IAdaptable element) {
        super.setElement(element);
        // 選択されたエレメントから IProject を取得
        project = ((IJavaProject)element).getProject();
        try {
            hasNature = project.hasNature(SampleNature.NATURE_ID);
        } catch (CoreException e) {
            ...
        }
        
    }
    
    @Override
    protected Control createContents(Composite parent) {
        ...
        chkNature = new Button(comp, SWT.CHECK);
        chkNature.setText("Enable Nature");
        chkNature.setSelection(hasNature);
        ...
    }
    
    @Override
    public boolean performOk() {
        if (hasNature != chkNature.getSelection()) {
            // チェックボックスが変更されたらNatureを追加または削除
            if (!hasNature) {
                enableNature();
            } else {
                disableNature();
            }
        }
    }
    
}

そんなに難しいことはやっていませんが、いくつか補足を。

プロパティページでは、選択されたエレメントを認識するために
setElement() をオーバーライドする必要があります。
ここの element に選択されたエレメントが渡されてくるので
これをフィールド(例で言えば project, hasNature)に格納しておきます。

hasNature はプロジェクトがNatureを持っているかどうかのフラグで
フィールドに持つ必要も無いかとは思われますが(projectから取得できる為)
取得時に例外が発生する可能性があるので、例外処理をまとめる意味で
setElement() 内で処理を済ませています。

紛らわしい事に IJavaProject は IProject の派生インターフェイスではないので
IJavaProject.getProject() によって IProject を取得してやる必要があります。

performOk() 内で、現在のNature有無とチェックボックスの内容を比較して
変更されているようだったらNatureを追加または削除します。

Natureの作成

まずは plugin.xml です。

<extension
      id="SampleNature"
      point="org.eclipse.core.resources.natures">
   <runtime>
      <run
            class="sample.SampleNature">
      </run>
   </runtime>
</extension>

実装は以下のようになります。

public class SampleNature implements IProjectNature {
    
    public static final String NATURE_ID = MyPlugin.PLUGIN_ID + ".SampleNature";
    
    ...
}

.SampleNature の部分は、plugin.xml に記述した id と合わせる必要があります。

これで実際にプロパティページからNatureのON/OFFを行うことが出来ます。
ストアを使わなくても情報が保持されるのは、
Natureの情報はプロジェクトの description に格納されるからです。

しかしまだこれだけでは何にもわかりません。
プロジェクトにNatureが追加されたからといって、見た目上の変化は(まだ)どこにも無いからです。

Builderの作成

Natureで何をするかというのは色々あると思いますが
一番身近なのが Builder の使用でしょう。
JavaProject は JavaNature 属性を持っていて、
そのNature内で JavaBuilder の追加・削除を制御しています。

Builder の追加・作成手順は、Nature のそれとほぼ同じです。
というわけで、早速コードを載せてしまいましょう。

public class SampleNature implements IProjectNature {
    
    public static final String NATURE_ID = MyPlugin.PLUGIN_ID + ".SampleNature";
    
    private IProject project;
    
    public void configure() throws CoreException {
        IProjectDescription desc = project.getDescription();
        List<ICommand> commands = new ArrayList<ICommand>(Arrays.asList(desc.getBuildSpec()));
        if (!commands.contains(SampleBuilder.BUILDER_ID)) {
            ICommand command = desc.newCommand();
            command.setBuilderName(SampleBuilder.BUILDER_ID);
            commands.add(command);
            desc.setBuildSpec(commands.toArray(new ICommand[commands.size()]));
            project.setDescription(desc, null);
        }
    }
    
    public void deconfigure() throws CoreException {
        IProjectDescription desc = project.getDescription();
        List<ICommand> commands = new ArrayList<ICommand>(Arrays.asList(desc.getBuildSpec()));
        for (ListIterator<ICommand> it = commands.listIterator(); it.hasNext(); ) {
            ICommand command = it.next();
            if (command.getBuilderName().equals(SampleBuilder.BUILDER_ID)) {
                it.remove();
            }
        }
        desc.setBuildSpec(commands.toArray(new ICommand[commands.size()]));
        project.setDescription(desc, null);
    }
    
    public IProject getProject() {
        return project;
    }
    
    public void setProject(IProject project) {
        this.project = project;
    }
    
}

ねっ?Natureとほとんど同じです。
余談ではありますが、リストをループさせて追加・削除を行うときには
ListIterator を使うと便利です。知らない人、結構多いですよ。

Builder の実装は以下のようになります。

public class SampleBuilder extends IncrementalProjectBuilder {
    
    public static final String BUILDER_ID = MyPlugin.PLUGIN_ID + ".SampleBuilder"; 
    
    @Override
    protected IProject[] build(int kind, Map args, IProgressMonitor monitor)
            throws CoreException {
        ...
    }
    
}

Builderの詳しい実装についてはまた今度。とりあえず今は空実装です。
あとはこのBuilderの定義を plugin.xml に書くだけです。

<extension
      id="SampleBuilder"
      name="sample builder"
      point="org.eclipse.core.resources.builders">
   <builder>
      <run
            class="sample.SampleBuilder">
      </run></builder>
</extension>

これで完成~。
実際にプロパティページから「Enable Nature」のチェックボックスをONにしてみましょう。
Builders のページに「sample builder」が表示されていれば成功です。
NatureをOFFにすれば、Builderも一緒に削除されます。