S-JIS[2013-01-18/2013-01-19] 変更履歴

EclipseプラグインDMDLエディター(色の設定)

Eclipseプラグイン開発自作DMDLエディターで色を設定できるようにする。


概要

プラグインは、EclipseのPreference(設定)で設定を変更できるのが普通。
DMDLエディターの色も変更できるようにしてみる。

色を設定するには、FieldEditorPreferencePage用の色設定フィールドとしてColorFieldEditorというクラスがあるので、これを参考にする。
(一行には「ラベル」「色」「BOLD有無」の3種類(3列)を出力したいので、2列しか出力できないFieldEditorPreferencePageは今回は使えない)


修正内容

修正内容を全部挙げているとけっこう量が多いので、抜粋する。

plugin.xml

拡張ポイントのpreferencespreferencePagesを追加。

ColorManager.java

色の管理はColorManagerクラスで行うようになっていたので、PreferenceStoreから色コードを取得するように変更。

import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.resource.StringConverter;
	public Color getColor(String key) {
		IPreferenceStore store = Activator.getDefault().getPreferenceStore();
		String s = store.getString(key);
		RGB rgb = StringConverter.asRGB(s);
		return getColor(rgb); //これは今までにも在ったgetColor()
	}

設定ストア(store)から値を取ってくるところは特に不思議なことは無い。
StringConverterクラスにRGBとStringの変換メソッドがあるので、それを使ってRGBクラスに変換する。

DMDLConfiguration.java・DMBlockScanner.java・DMDefaultScanner.java

TextAttributeを作って色を指定していた箇所は、設定ストアから色を取ってくるColorManager#getColor()を使うように修正。

DMDLEditorPreferenceInitializer.java

デフォルトの色を指定するクラスを新設。

public class DMDLEditorPreferenceInitializer extends AbstractPreferenceInitializer {

	@Override
	public void initializeDefaultPreferences() {
		IPreferenceStore store = Activator.getDefault().getPreferenceStore();

		String red   = StringConverter.asString(new RGB(192, 0, 0));
		String green = StringConverter.asString(new RGB(0, 192, 0));
		String blue  = StringConverter.asString(new RGB(0, 0, 192));

		store.setDefault(COLOR_COMMENT, green);
		store.setDefault(STYLE_COMMENT, SWT.NORMAL);

		store.setDefault(COLOR_ANNOTATION, red);
		store.setDefault(STYLE_ANNOTATION, SWT.BOLD);
〜
	}
}

DMDLEditorPreferencePage.java

設定ページを表示するクラスを新設。

public class DMDLEditorPreferencePage extends PreferencePage implements IWorkbenchPreferencePage {
	public DMDLEditorPreferencePage() {
		super("DMDLEditorPreferencePage");
		setPreferenceStore(Activator.getDefault().getPreferenceStore());
	}
	@Override
	protected Control createContents(Composite parent) {
		setTitle("color preference");
		IPreferenceStore store = getPreferenceStore();

		Composite composite = new Composite(parent, SWT.NONE);
		{
			GridLayout layout = new GridLayout();
			layout.numColumns = 3; // 列数
			composite.setLayout(layout);
		}
		{ // ラベル(一番上の行)
			Label label1 = new Label(composite, SWT.CENTER);
			label1.setText("column");
			Label label2 = new Label(composite, SWT.CENTER);
			label2.setText("color");
			Label label3 = new Label(composite, SWT.LEFT);
			label3.setText("bold");
		}

		{ // コメントの色
			Label label = new Label(composite, SWT.NONE);
			label.setText("comment");

			RGB rgb = PreferenceConverter.getColor(store, COLOR_COMMENT);
			commentColor = new ColorSelector(composite);
			commentColor.setColorValue(rgb);

			int style = store.getInt(STYLE_COMMENT);
			commentCheck = new Button(composite, SWT.CHECK | SWT.CENTER);
			commentCheck.setSelection((style & SWT.BOLD) != 0);
		}
〜
		return composite;
	}

PreferenceConverterを使うと、設定ストア(store)からRGBを直接取得できる。(内部ではStringConverterを使っている)
色の設定にはColorSelectorクラスを使用する。画面上はボタンになり、そのボタンを押すと色ダイアログが出て色を指定できる。(このダイアログの具体的な表現方法はOSによって異なる)

チェックボックスにはButtonクラスを使い、スタイルとしてSWT.CHECKを指定する。

	// デフォルト値に戻す
	@Override
	protected void performDefaults() {
		IPreferenceStore store = getPreferenceStore();

		{ // コメントの色
			RGB rgb = PreferenceConverter.getDefaultColor(store, COLOR_COMMENT);
			commentColor.setColorValue(rgb);

			int style = store.getDefaultInt(STYLE_COMMENT);
			commentCheck.setSelection((style & SWT.BOLD) != 0);
		}
〜
	}
	// 設定を反映させる
	@Override
	public boolean performOk() {
		IPreferenceStore store = getPreferenceStore();

		{ // コメントの色
			RGB rgb = commentColor.getColorValue();
			PreferenceConverter.setValue(store, COLOR_COMMENT, rgb);

			int style = SWT.NORMAL;
			if (commentCheck.getSelection()) {
				style |= SWT.BOLD;
			}
			store.setValue(STYLE_COMMENT, style);
		}
〜
		return true;
	}

これで、メニューバーの「ウィンドウ(W)」→「設定(P)」で開く設定ダイアログにDMDLエディターのメニューが出て、色を設定できる。

しかし、色を設定して「OK」ボタンを押しても、すぐには色は反映されない。
現在開いているdmdlファイル(DMDLエディター)を一回閉じて再度開くと反映される。


OKボタン押下時に色を反映させる

開いているdmdlファイル(DMDLエディター)を開き直さなくてもOKボタン(あるいは適用(Apply)ボタン)を押したときに色を反映させたい。

これには、ScannerやDamagerRepairerに指定しているTextAttributeを作り直す必要がある。

設定ストアが変更されたときにそれを通知するリスナー機構があるので、それを利用して
通知があったらTextAttributeを作り直すようにしてやる。
その後再描画を行うと新しい色が反映される。

DMDLEditor.java

再描画を行うメソッドはDMDLEditor(の親クラスであるTextEditor)にあるので、設定ストアの変更通知を受け取るリスナーはDMDLEditorに実装する。

import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
public class DMDLEditor extends TextEditor implements IPropertyChangeListener {
	/**
	 * コンストラクター.
	 */
	public DMDLEditor() {
		setDocumentProvider(new DMDLDocumentProvider());
		setSourceViewerConfiguration(new DMDLConfiguration(colorManager));

		// Preferenceの更新リスナーを登録
		IPreferenceStore store = Activator.getDefault().getPreferenceStore();
		store.addPropertyChangeListener(this);
	}
	@Override
	public void dispose() {
		// Preferenceの更新リスナーを削除
		IPreferenceStore store = Activator.getDefault().getPreferenceStore();
		store.removePropertyChangeListener(this);

		colorManager.dispose();
		super.dispose();
	}
	// Preferenceの更新イベント処理
	@Override
	public void propertyChange(PropertyChangeEvent event) {
		// 色をPreferenceから取得し直す
		DMDLConfiguration config = (DMDLConfiguration) getSourceViewerConfiguration();
		config.updatePreferences(); //←これは今回新設したメソッド

		// エディターを再描画する
		getSourceViewer().invalidateTextPresentation();
	}
}

DMDLConfiguration.java

DMDLConfigurationで各種Scannerの初期設定を行っている。
そこで、設定ストアから設定を取り直す処理を追加する。

public class DMDLConfiguration extends SourceViewerConfiguration {
〜
	private PresentationReconciler reconciler;
	private NonRuleBasedDamagerRepairer commentDamagerPepairer;

	@Override
	public IPresentationReconciler getPresentationReconciler(ISourceViewer sourceViewer) {
		reconciler = new PresentationReconciler();

		{ // デフォルトの色の設定
			DMDefaultScanner scanner = getDefaultScanner();
			DefaultDamagerRepairer dr = new DefaultDamagerRepairer(scanner);
			reconciler.setDamager(dr, IDocument.DEFAULT_CONTENT_TYPE);
			reconciler.setRepairer(dr, IDocument.DEFAULT_CONTENT_TYPE);
		}
		{ // コメントの色の設定
			TextAttribute attr = new TextAttribute(colorManager.getCommentColor());
			NonRuleBasedDamagerRepairer dr = new NonRuleBasedDamagerRepairer(attr);
			commentDamagerPepairer = dr;
			reconciler.setDamager(dr, DMDLPartitionScanner.DMDL_COMMENT);
			reconciler.setRepairer(dr, DMDLPartitionScanner.DMDL_COMMENT);
		}

		return reconciler;
	}
	/**
	 * Preference更新時に呼ばれる処理.
	 * <p>
	 * 色を設定し直す。
	 * </p>
	 */
	public void updatePreferences() {
		// デフォルトの色の設定
		getDefaultScanner().initialize();	//←initialize()は今回新設したメソッド

		// コメントの色の設定
		TextAttribute attr = new TextAttribute(colorManager.getCommentColor());
		commentDamagerPepairer.setDefaultTextAttribute(attr);	//←setDefaultTextAttribute()は今回新設したメソッド
	}
	private DMDefaultScanner defaultScanner;

	protected DMDefaultScanner getDefaultScanner() {
		if (defaultScanner == null) {
			defaultScanner = new DMDefaultScanner(attrManager);
			defaultScanner.setDefaultReturnToken(new Token(new TextAttribute(colorManager.getDefaultColor())));
		}
		return defaultScanner;
	}
}

Scannerは最初に作ったインスタンスを使い回すようにする。
で、更新リスナーからupdatePreferences()が呼ばれる度にTextAttributeだけ新しくする。

これは@ITのエディタに設定内容を反映するを参考に作ったのだが、その方法ではDamagerPepairerオブジェクトを作り直している。
その方法を真似ると、テスト実行時には問題なかったのに、実際のEclipseにインストールして動かしてみるとNullPointerExceptionが発生した。
ログを出力して調べてみると、NonRuleBasedDamagerRepairer内のfDocumentフィールドがnullになっていた。ドキュメントの設定タイミングがテストと本番で違うのかなぁ。
そこで、今回はsetDefaultTextAttribute()を新設し、DamagerPepairerオブジェクトを作り直さずにTextAttributeだけ差し替えるようにした。

NonRuleBasedDamagerRepairer.java

public class NonRuleBasedDamagerRepairer implements IPresentationDamager, IPresentationRepairer {
	public void setDefaultTextAttribute(TextAttribute defaultTextAttribute) {
		fDefaultTextAttribute = defaultTextAttribute;
	}

DMDefaultScanner.java

public class DMDefaultScanner extends RuleBasedScanner {
	/**
	 * コンストラクター.
	 *
	 * @param colorManager
	 */
	public DMDefaultScanner(ColorManager colorManager) {
		this.colorManager = colorManager;
		initialize();
	}
	public void initialize() {
		IToken modelToken = new Token(new TextAttribute(colorManager.getModelColor()));
		IToken annToken   = new Token(new TextAttribute(colorManager.getAnnotationColor()));
		IToken descToken  = new Token(new TextAttribute(colorManager.getDescriptionColor()));

		IRule[] rules = {
			new DMWordRule(MODEL_TYPE, modelToken),
			new DMAnnotationRule(annToken),
			new SingleLineRule("\"", "\"", descToken),
		};
		setRules(rules);
	}

今までコンストラクター内で行っていたルールの初期化処理を、initialize()メソッドに外出しした。
そして、設定ストアの更新リスナーからinitialize()メソッドを呼び出して色を変更する。


即時に色を反映させる

OKボタン(あるいは適用(Apply)ボタン)を押したときにdmdlファイル(DMDLエディター)に色を反映させる方式だと、「変な色になっちゃった!」という時には既に反映済みなので、戻すのが面倒。
やはり色を選択したときに実際にエディター上の色を変えて、雰囲気を見たい。[2013-01-19]

色ダイアログを出すColorSelectorには設定変更時に通知してくれるリスナーがあるので、このイベントを捕捉してエディターを再描画すればよい。
エディターの再描画をどうやって行うかが問題だが、で“設定ストアを更新すれば自動的に再描画される”ように作ったので、その機構を使う。
つまり、設定ストアに一時的な色を示すキーを追加し、ColorManagerで一時的な色を優先して使うようにする。
設定ストアに余計な情報(本来は永続化する必要のない情報)が保存されてしまうのが気に入らないが…。
また、OK・Cancel時に一時情報を削除する処理を入れる必要がある。
と思ったのだが、逆の発想で、色を変更する度に正式に設定ストアに保存してしまい、Cancel時に元に戻せば余分なキーは作らなくて済む。

  従来の方式 一時設定方式 即時反映方式
初期処理 特に無し。 特に無し。 Cancel用に「元の色」を保持しておく。
色変更時 無処理。 設定ストアの一時項目に色を書き込む。
(エディターに反映される)
設定ストアの正式項目に色を書き込む。
(エディターに反映される)
Apply時 OKと同じ。 OKと同じ。 Applyは“現在値で確定”という意味なので、
Cancel用の「元の色」を現在値に変更する。
(設定ストアに対する処理は無い。既に反映されているから)
OK時 設定ストアの正式項目に色を書き込む。
(エディターに反映される)
設定ストアの正式項目に色を書き込む。
設定ストアの一時項目を削除する。
(エディターに反映される)
無処理。(既に反映されているから)
Cancel時 無処理。 設定ストアの一時項目を削除する。
(エディターに反映される)
設定ストアの正式項目に「元の色」を書き込む。
(エディターに反映される)
Defaultボタン 設定ストアからデフォルト値を取得し
設定画面に反映させる。
設定ストアからデフォルト値を取得し
設定画面および一時項目に反映させる。
(エディターに反映される)
設定ストアからデフォルト値を取得し
設定画面および正式項目に反映させる。
(エディターに反映される)
ColorManager 正式項目から色を取得する。 一時項目があればそれを使う。
無ければ正式項目から色を取得する。
正式項目から色を取得する。(従来方式と同じ)

DMDLEditorColorPreferencePage.java

public class DMDLEditorColorPreferencePage extends PreferencePage implements IWorkbenchPreferencePage {
	@Override
	protected Control createContents(Composite parent) {
		IPreferenceStore store = getPreferenceStore();
〜
		{ // コメントの色
			Label label = new Label(composite, SWT.NONE);
			label.setText("comment");

			backupColor = PreferenceConverter.getColor(store, COLOR_COMMENT);
			commentColor = new ColorSelector(composite);
			commentColor.addListener(new ColorListener(store, COLOR_COMMENT));
			commentColor.setColorValue(backupColor);

			backupStyle = store.getInt(STYLE_COMMENT);
			commentCheck = new Button(composite, SWT.CHECK | SWT.CENTER);
			commentCheck.addSelectionListener(new CheckListener(store, STYLE_COMMENT));
			commentCheck.setSelection((backupStyle & SWT.BOLD) != 0);
		}
〜
		return composite;
	}
	// デフォルト値に戻す
	@Override
	protected void performDefaults() {
		IPreferenceStore store = getPreferenceStore();

		{ // コメントの色
			RGB rgb = PreferenceConverter.getDefaultColor(store, COLOR_COMMENT);
			commentColor.setColorValue(rgb);
			PreferenceConverter.setValue(store, COLOR_COMMENT, rgb);

			int style = store.getDefaultInt(STYLE_COMMENT);
			commentCheck.setSelection((style & SWT.BOLD) != 0);
			store.setValue(STYLE_COMMENT, style);
		}
〜
	}
	// 設定を確定させる
	@Override
	public void performApply() {
		IPreferenceStore store = getPreferenceStore();

		{ // コメントの色
			backupColor = commentColor.getColorValue();

			int style = SWT.NORMAL;
			if (commentCheck.getSelection()) {
				style |= SWT.BOLD;
			}
			backupStyle = style;
		}
〜
	}
	// キャンセル(元に戻す)
	@Override
	protected boolean performCancel() {
		IPreferenceStore store = getPreferenceStore();

		{ // コメントの色
			PreferenceConverter.setValue(store, COLOR_COMMENT, backupColor);
			store.setValue(STYLE_COMMENT, backupStyle);
		}
〜
		return true;
	}

ColorListener

ColorSelectorの色が設定されたときに通知を受け取るリスナーを用意する。
リスナー登録箇所

import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
class ColorListener implements IPropertyChangeListener {
	protected IPreferenceStore store;
	protected String colorKey;

	public ColorListener(IPreferenceStore store, String key) {
		this.store    = store;
		this.colorKey = key;
	}
	@Override
	public void propertyChange(PropertyChangeEvent event) {
		RGB rgb = (RGB) event.getNewValue();
		PreferenceConverter.setValue(store, colorKey, rgb);
	}
}

IPropertyChangeListenerでは、色が設定されるとpropertyChange()が呼ばれる。
event.getNewValue()で新しい設定値が取得できる。(戻り型はObjectなので、RGBにキャストする)

CheckListener

DMDLエディターのPreferenceでは、文字をBOLDにするかどうかをチェックボックス(Buttonクラス)で設定できるようにしている。
Buttonにはいくつかのリスナーが登録できるが、今回は(チェックボックスとして使われている場合に呼ばれる)SelectionListenerを使う。
リスナー登録箇所

import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
class CheckListener implements SelectionListener {
	protected IPreferenceStore store;
	protected String styleKey;

	public CheckListener(IPreferenceStore store, String key) {
		this.store    = store;
		this.styleKey = key;
	}
	@Override
	public void widgetSelected(SelectionEvent e) {
		Button check = (Button) e.widget;

		int style = SWT.NORMAL;
		if (check.getSelection()) {
			style |= SWT.BOLD;
		}
		store.setValue(styleKey, style);
	}
	@Override
	public void widgetDefaultSelected(SelectionEvent e) {
	}
}

チェックが付けられたり外されたりするとSelectionListener#widgetSelected()が呼ばれるのだが、(IPropertyChangeListenerと違って)SelectionEventには“チェックの値”は入っていない。
SelectionEvent#widget(ちなみに、これはフィールド。ゲッターメソッドは用意されていない)でチェックボックス(Button)が取得できるので、そこから値を取り出す。


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