Eclipseプラグインを作ってみよう!

第10回画面の作成(5)

前回でMaster(一覧)とDetails(詳細)の連携が完了しました。今回からはバリデータ定義フォームの実装です。これは実装が複雑なため、詳細をひとつひとつ解説するのではなく、ポイントを絞って解説していきます。

プラグインプロジェクトのダウンロード

今回はポイントを絞って解説するために、完成したプラグインプロジェクトを用意しました。バリデータ定義フォームを実装したプラグインプロジェクトを下記のリンクからダウンロードし、お使いのワークスペースにインポートしてください。前回の連載までに実装されたプラグインプロジェクトはインポート前にリネームすることで残しておくことができます。

画面とクラスの関係

今回の実装でフォームデザイナーの画面が完成しました。画面とそれを実装しているクラスの関係を示します。なお、バリデータ定義フォームは設計時は一覧と詳細を横に並べていましが、画面に表示しきれない場合があるので、縦に並べるように変更しました。

フォームデザイナーのクラスと画面の関係
フォームデザイナーのクラスと画面の関係

Details(詳細)の入力項目定義フォーム、バリデータ定義フォームをそれぞれ実装しているFieldDetailsGeneralSectionクラス、 FieldDetailsValidatorSectionクラスはAbstractFieldDetailsSectionクラスを継承しています。Details(詳細)を管理するFieldDetailsPageクラスでは、各フォームをListオブジェクトとして管理し、ウィジェットの生成・イベント通知を抽象的に行うことができます。

以下のソースコードはFieldDetailsPageクラスのselectionChanged()メソッドです。この実装ではフィールドが選択された場合に、入力項目定義フォーム、バリデータ定義フォームのウィジェットを再生成するようにしています。その後、各フォームに変更を通知するようにしています。

FieldDetailsPageクラスのselectionChanged()メソッド
public class FieldDetailsPage implements IDetailsPage {
    ...
    public void selectionChanged(IFormPart part,
                                 ISelection selection) {
        boolean fieldChange = true;
        if (fSections != null) {
            for (AbstractFieldDetailsSection section : fSections) {
                if (section.getSectionPart() == part) {
                    fieldChange = false;
                }
            }
        }

        if (fieldChange) {
            if (fSections != null) {
                for (AbstractFieldDetailsSection section : fSections) {
                    section.getSectionPart().getSection().dispose();
                }
                fSections.removeAll(
                        new ArrayList<AbstractFieldDetailsSection>());
            }

            fSections = new ArrayList<AbstractFieldDetailsSection>();
            fSections.add(new FieldDetailsGeneralSection());
            fSections.add(new FieldDetailsValidatorSection());

            for (AbstractFieldDetailsSection section : fSections) {
                section.createContents(
                        fManagedForm.getToolkit().createSection(
                                fParent, Section.TITLE_BAR),
                                fManagedForm);
            }
            fParent.pack();
        }

        for (AbstractFieldDetailsSection section : fSections) {
            section.selectionChanged(part, selection);
        }
    }
}

定義されたバリデータを表すDefinedValidatorクラス

フォームデザイナーはPHPのバリデーションフレームワークPiece_Rightのバリデーション定義ファイルを編集するためのエディターです。従って、Piece_Rightによって定義されているバリデータしか設定できないようにしなければなりません。そこで、新たにmodel.definedパッケージを追加し、そこにDefinedValidatorクラス、DefinedRuleクラスを作成しました。

ひとつのバリデータ定義にはひとつ以上のルールを設定する必要があります。例えば、メールアドレスかどうかを検証するEmailバリデータではアットマークより前にドットが入ることを許可するかを決定するルール(allowDotBeforeAtmark)を設定する必要があります。 EmailバリデータのDefinedValidator, DefinedRulesオブジェクトの生成は以下のようになります。

Emailバリデータの定義
List<DefinedRule> emailRules = new ArrayList<DefinedRule>();
emailRules.add(new DefinedRule(
                        "allowDotBeforeAtmark", 
                        DefinedRule.WidgetType.YesNo, 
                        "false"));

DefinedValidator emailValidator = new DefinedValidator(
                                            "Email",
                                            "メールアドレス",
                                            emailRules));

DefinedValidatorクラスはコンストラクタをプライベートとして定義し、インスタンスはstatic初期化子でのみ生成しています。このようにすることで、外部で不正なインスタンスが生成されることを防いでいます。

Piece_Rightでは多くのバリデータを提供していますが、今回はCompare, Date, Emailの3つだけ定義しています。

ルールごとのウィジェットの定義

ルールには先ほどEmailバリデータのallowDotBeforeAtmarkルールのように「はい」⁠いいえ」で設定するものもあれば、任意の文字列を設定するルールもあります。これらの入力インタフェースにはそれぞれラジオボタン、テキストボックスを使いたいところです。そこで DefinedRuleクラスではそのルールで使用するウィジェットを指定するWidgetTypeプロパティーを用意しました。WidgetTypeはenum型で定義されており、今回は"Text"(テキストボックス)、"YesNo"(「はい」⁠いいえ」のふたつのラジオボタン)を用意しています。

バリデータ定義フォームの実装

いよいよ今回の主な機能であるバリデータ定義フォームの実装を解説します。バリデータ定義フォームは今までMaster(一覧⁠⁠、 Details(詳細)で行っていたことと同様のことをフォーム内部で行いつつ、外部のMaster(一覧)であるフィールド一覧とも連携する必要があります。また、今回は選択されるバリデータごとに詳細部分のウィジェットを切り替える必要もあります。こういった仕様をどのように解決しているか、順に解説していきます。

バリデータ選択ダイアログ

まず最初に定義されたバリデータの一覧から追加するバリデータを選択するバリデータ一覧ダイアログを作成します。Eclipseでは一覧からデータを選択するためのダイアログとしてElementListSelectionDialogクラスが用意されているので、これを利用してValidatorSelectionDialogクラスを実装します。バリデータ一覧はDefinedValidatorクラスでスタティックメソッドとして定義されているgetList()メソッドから取得します。

ElementListSelectionDialogクラスを継承することで、フィルター機能を持った選択ダイアログを簡単に実装することができます。

ValidatorSelectionDialogクラス
public class ValidatorSelectionDialog extends ElementListSelectionDialog {
    public ValidatorSelectionDialog(Shell parent) {
        super(parent, new ILabelProvider() {
            public Image getImage(Object element) {
                return null;
            }

            public String getText(Object element) {
                assert (element instanceof DefinedValidator);

                return ((DefinedValidator) element).getDisplayName();
            }

            public void addListener(ILabelProviderListener listener) {
            }

            public void dispose() {
            }

            public boolean isLabelProperty(Object element, String property) {
                return false;
            }

            public void removeListener(ILabelProviderListener listener) {
            }
        });
        setTitle("バリデータ選択");
        setMessage("使用するバリデータを選択してください。");
        setHelpAvailable(false);
        setMultipleSelection(false);
        setElements(DefinedValidator.getList().toArray());
    }
}
バリデータ選択ダイアログ
バリデータ選択ダイアログ

ValidatorUIクラス

Master(一覧)のフィールド一覧テーブルではFieldオブジェクトでやりとりしましたが、バリデータ一覧テーブルではどういったオブジェクトでやりとりすればよいでしょうか。その候補として、まずDefinedValidatorオブジェクトが考えられます。しかし、このオブジェクトではメッセージや各ルールの値などを保持することができません。逆に、これらのデータを保持するVaildatorオブジェクトでは、選択されたときにどういったウィジェットを表示すればよいかがわかりません。

そこで、DefinedValidatorオブジェクトとValidatorオブジェクトの両方を保持するValidatorUIクラスを定義することにします。

DefinedValidatorオブジェクトとValidatorオブジェクトを保持するValidatorUIクラス
public class FieldDetailsValidatorSection extends AbstractFieldDetailsSection {
    private class ValidatorUI {
        private DefinedValidator fDefinedValidator;
        private Validator fValidator;

        ValidatorUI(DefinedValidator definedValidator) {
            fDefinedValidator = definedValidator;
            fValidator = fDefinedValidator.create();
        }

        ValidatorUI(Validator validator) {
            fValidator = validator;
            fDefinedValidator = DefinedValidator.getDefinedValidator(
                                    fValidator);
        }

        public DefinedValidator getDefinedValidator() {
            return fDefinedValidator;
        }

        public Validator getValidator() {
            return fValidator;
        }
    }
    ...
}

さらにこのクラスではDefinedValidatorオブジェクト、Validatorオブジェクトのそれぞれを引数とするふたつのコンストラクターを定義します。そして引数でない方のインスタンスを生成・取得することで、バリデータの新規追加、編集の両方に対応できるようになっています。

バリデータごとのウィジェットの生成

バリデータはそれぞれひとつ以のルールを保持するので、バリデータ一覧で選択されるごとに詳細のウィジェットを再生成する必要があります。バリデータの詳細が一回以上生成されている場合は配下のすべてのコントロールを破棄してから、ウィジェットを再生成します。

FieldDetialsValidatorSectionクラスのcreateValidatorDetailsWidget()メソッド
public class FieldDetailsValidatorSection extends AbstractFieldDetailsSection {
    ...   
    private void createValidatorDetailsWidget(
                            final Composite composite, 
                            final ValidatorUI validatorUI) {
        FocusListener focusListener = new FocusListener() {
            ...
        };

        FormToolkit toolkit = getManagedForm().getToolkit();

        if (fValidatorDetails != null) {
            for (Control control : fValidatorDetails.getChildren()) {
                control.dispose();
            }
        } else {
            fValidatorDetails = toolkit.createComposite(composite);
            fValidatorDetails.setLayout(new GridLayout(2, false));

            GridData layoutData = new GridData();
            layoutData.horizontalSpan = 2;
            layoutData.grabExcessHorizontalSpace = true;
            layoutData.grabExcessVerticalSpace = true;
            layoutData.horizontalAlignment = GridData.FILL;
            layoutData.verticalAlignment = 
                    GridData.HORIZONTAL_ALIGN_BEGINNING;
            fValidatorDetails.setLayoutData(layoutData);
        }

        Label nameLabel = toolkit.createLabel(
                            fValidatorDetails, 
                            validatorUI.getDefinedValidator().getDisplayName());
        GridData layoutData = new GridData();
        layoutData.horizontalSpan = 2;
        layoutData.grabExcessHorizontalSpace = true;
        layoutData.horizontalAlignment = GridData.FILL;
        nameLabel.setLayoutData(layoutData);

        fMessageText = createLabelAndText(fValidatorDetails, "メッセージ");
        fMessageText.addFocusListener(focusListener);

        fRuleWidgets = new HashMap<DefinedRule, Widget[]>();
        for (DefinedRule definedRule
                : validatorUI.getDefinedValidator().getRules()) {
            createRuleWidget(
                    fValidatorDetails, 
                    definedRule, 
                    focusListener);
        }

        layoutData = (GridData) fViewer.getTable().getLayoutData();
        layoutData.minimumWidth = fViewer.getTable().getSize().x
                                  - fViewer.getTable().getLocation().x
                                  + 5;
        composite.getParent().pack();
        layoutData.minimumWidth = 0;
    }
    ...
}

すべてのウィジェットを再生成したあと、親ウィジェットのpack()メソッドを呼び出して、すべてのウィジェットが表示されるようにしています。この前後にバリデータ一覧テーブルのレイアウトデータのminimumWidthプロパティーを設定しています。これはpack()メソッドを呼び出した際に、バリデータ一覧テーブルが内部データに合わせてリサイズされるのを防ぐために行っています。

また各ルールのウィジェットはDefinedRuleオブジェクトのWidgetTypeに合わせて生成され、それらはDefinedRuleオブジェクトをキーとしてMapオブジェクトに保存しています。このMapオブジェクトはValidatorオブジェクトにデータを設定する際に使用されます。

FieldDetialsValidatorSectionクラスのcreateRuleWidget()メソッド
public class FieldDetailsValidatorSection extends AbstractFieldDetailsSection {
    ...   
    private void createRuleWidget(
                    final Composite parent, 
                    final DefinedRule definedRule, 
                    final FocusListener focusListener) {
        if (definedRule.getWidgetType() == DefinedRule.WidgetType.Text) {
            Text text = createLabelAndText(parent, definedRule.getName());
            text.addFocusListener(focusListener);
            fRuleWidgets.put(
                    definedRule, 
                    new Widget[]{text});
        } else if (definedRule.getWidgetType()
                        == DefinedRule.WidgetType.YesNo) {
            Button[] radioButtons = createLabelAndRadioButton(
                                        parent, 
                                        definedRule.getName());
            for (Button radioButton : radioButtons) {
                radioButton.addFocusListener(focusListener);
            }
            fRuleWidgets.put(
                    definedRule, 
                    new Widget[]{radioButtons[0], radioButtons[1]});
        } else {
            assert (false);
        }
    }
    ...
}

変更通知

バリデータの詳細で行われた変更通知は、入力項目定義フォームと同様に、各ウィジェットのフォーカスを失ったときに行います。DefinedRuleオブジェクトと対応するウィジェットを保持するMapオブジェクトから、イベントが発生したウィジェットと同じものを探し、Validatorオブジェクトにルールを設定します。

FieldDetialsValidatorSectionクラスのcreateRuleWidget()メソッド
public class FieldDetailsValidatorSection extends AbstractFieldDetailsSection {
    ...   
    private void createValidatorDetailsWidget(
                            final Composite composite, 
                            final ValidatorUI validatorUI) {
        FocusListener focusListener = new FocusListener() {
            public void focusGained(FocusEvent e) {
            }

            public void focusLost(FocusEvent event) {
                Validator validator = 
                    ((ValidatorUI) ((IStructuredSelection) fViewer
                            .getSelection()).getFirstElement()).getValidator();

                if (fMessageText == event.widget) {
                    validator.setMessage(fMessageText.getText());
                } else {
                    Iterator<DefinedRule> iterator = 
                        fRuleWidgets.keySet().iterator();
                    while (iterator.hasNext()) {
                        DefinedRule definedRule = iterator.next();
                        Widget[] widgets = fRuleWidgets.get(definedRule);

                        if (definedRule.getWidgetType() 
                                == DefinedRule.WidgetType.Text
                            && widgets[0] == event.widget) {
                            validator.setRule(
                                    definedRule.getName(), 
                                    ((Text) widgets[0]).getText());

                        } else if (definedRule.getWidgetType() 
                                        == DefinedRule.WidgetType.YesNo
                                   && (widgets[0] == event.widget 
                                       || widgets[1] == event.widget)) {
                            if (((Button) widgets[0]).getSelection()) {
                                validator.setRule(
                                        definedRule.getName(), "true");
                            } else {
                                validator.setRule(
                                        definedRule.getName(), "false");
                            }

                        } else {
                            assert (false);
                        }
                    }
                }

                getManagedForm().fireSelectionChanged(
                            getSectionPart(), 
                            new StructuredSelection(getField()));
            }
        };
        ...
    }
    ...
}

おわりに

今回はバリデータ定義フォームの実装について解説しました。前回までで説明した基礎をもとにした応用といった感じでしたが、いかがだったでしょうか。Eclipse Forms及びSWT、JFaceを使ったプログラミングとPOJOをいかにうまく連携するかを考える楽しみを感じていただければ幸いです。

次回はYAMLファイルの読み書きを実装します。

おすすめ記事

記事・ニュース一覧