ページ

2013年5月19日日曜日

JavaFX - Custom Editable ListCell (3)

前回は、FXMLで定義したViewを使用して、ListCellに表示する方法をみていきました。今回は、それを再利用できるようなクラスとして作成してみたいと思います。

まずは、ListCellに表示するViewのインターフェイスを以下のように定義します。

public interface GraphicListCellView<T> {
    ObjectProperty<T> modelProperty();

    void appearOn(ListCell<T> cell);

    <E extends Event> void addEventHandler(EventType<E> eventType, EventHandler<? super E> eventHandler);
    <E extends Event> void removeEventHandler(EventType<E> eventType, EventHandler<? super E> eventHandler);
    void requestFocus();

    public static class EditEvent<T> extends Event {
        private static final EventType EDIT_ANY = new EventType(Event.ANY, "EDIT");
        private static final EventType EDIT_CANCEL = new EventType(editAny(), "EDIT_CANCEL");
        private static final EventType EDIT_COMMIT = new EventType(editAny(), "EDIT_COMMIT");

        public static <T> EventType<EditEvent<T>> editAny() { return EDIT_ANY; }
        public static <T> EventType<EditEvent<T>> editCancel() { return EDIT_CANCEL; }
        public static <T> EventType<EditEvent<T>> editCommit() { return EDIT_COMMIT; }

        private final T newValue;

        public EditEvent(EventType<? extends EditEvent> eventType, T newValue) {
            super(eventType);

            this.newValue = newValue;
        }

        public T getNewValue() {
            return newValue;
        }
    }
}

Viewに表示するモデルのプロパティとListCellに表示するためのメソッドを定義しています。また、編集時のイベントの登録と編集時にフォーカスを要求できるように、Nodeに定義されているメソッドも定義しています。さらに、編集時のイベントも定義しています。

次に、表示用のViewのモデルの内容と、編集用のViewのモデルの内容の変換を行うために、以下のインターフェイスを定義しておきます。

public interface GraphicListCellModelConverter<T> {
    void mergeEditModel(T editModel, T model);
    T toEditModel(T model);
}

mergeEditModelメソッドでは、editModelの内容をmodelの内容に適用し、toEditModelメソッドでは、modelの内容をコピー返し、編集用のViewのモデルに設定できるようにします。

以上で、準備ができましたので、ListCellを継承したクラスを作成していきます。名称は、GraphicListCellとしてみます。

public class GraphicListCell<T> extends ListCell<T> {
}

表示用のViewと編集用のViewを生成するための、Callbackインターフェイスを実装したファクトリを指定してインスタンスを生成できるように、以下のようなファクトリメソッドを定義します。

public static <T> Callback<ListView<T>, ListCell<T>> forListView(Callback<GraphicListCell<T>, GraphicListCellView<T>> viewFactory) {
    return forListView(viewFactory, null);
}

public static <T> Callback<ListView<T>, ListCell<T>> forListView(
    Callback<GraphicListCell<T>, GraphicListCellView<T>> viewFactory,
    Callback<GraphicListCell<T>, GraphicListCellView<T>> editViewFactory
) {
    return forListView(viewFactory, editViewFactory, null);
}

public static <T> Callback<ListView<T>, ListCell<T>> forListView(
    final Callback<GraphicListCell<T>, GraphicListCellView<T>> viewFactory,
    final Callback<GraphicListCell<T>, GraphicListCellView<T>> editViewFactory,
    final GraphicListCellModelConverter<T> converter
) {
    return new Callback<ListView<T>, ListCell<T>>() {
        @Override
        public ListCell<T> call(ListView<T> listView) {
            return new GraphicListCell<>(viewFactory, editViewFactory, converter);
        }
    };
}

protected GraphicListCell(
    Callback<GraphicListCell<T>, GraphicListCellView<T>> viewFactory,
    Callback<GraphicListCell<T>,GraphicListCellView<T>> editViewFactory,
    GraphicListCellModelConverter<T> converter
) {
    setViewFactory(Objects.requireNonNull(viewFactory));
    setEditViewFactory(editViewFactory);
    setConverter(converter);

    getStyleClass().add("graphic-list-cell");
    setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}

public ObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>> viewFactoryProperty() { return viewFactory; }
private final ObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>> viewFactory = new SimpleObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>>(this, "viewFactory");
public final Callback<GraphicListCell<T>, GraphicListCellView<T>> getViewFactory() { return viewFactory.get(); }
public final void setViewFactory(Callback<GraphicListCell<T>, GraphicListCellView<T>> viewFactory) { this.viewFactory.set(viewFactory); }

public ObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>> editViewFactoryProperty() { return editViewFactory; }
private final ObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>> editViewFactory = new SimpleObjectProperty<Callback<GraphicListCell<T>, GraphicListCellView<T>>>(this, "editViewFactory");
public final Callback<GraphicListCell<T>, GraphicListCellView<T>> getEditViewFactory() { return editViewFactory.get(); }
public final void setEditViewFactory(Callback<GraphicListCell<T>, GraphicListCellView<T>> editViewFactory) { this.editViewFactory.set(editViewFactory); }

public ObjectProperty<GraphicListCellModelConverter<T>> converterProperty() { return converter; }
private final ObjectProperty<GraphicListCellModelConverter<T>> converter = new SimpleObjectProperty<>(this, "converter");
public final GraphicListCellModelConverter<T> getConverter() { return converter.get(); }
public final void setConverter(GraphicListCellModelConverter<T> converter) { this.converter.set(converter); }

private GraphicListCellView<T> view;
private GraphicListCellView<T> editView;

ViewのファクトリとGraphicListModelConverterについては、プロパティとして定義しています。

まず、updateItemメソッドをオーバーライドします。

@Override
protected void updateItem(T item, boolean empty) {
    super.updateItem(item, empty);

    if (empty) {
        setGraphic(null);
        return;
    }

    if (view == null) { view = getViewFactory().call(this); }
    view.modelProperty().set(item);
    view.appearOn(this);
}

Viewの生成には、コンストラクタで指定したファクトリを利用して行い、GraphicListCellViewインターフェイスのメソッドを利用して必要な処理を行っています。

次に、startEditメソッドをオーバーライドします。

@Override
public void startEdit() {
    if (!isEditable()) { return; }
    if (!getListView().isEditable()) { return; }
    if (editView == null && getEditViewFactory() == null) { return; }

    super.startEdit();

    if (editView == null) { editView = createEditView(); }
    editView.modelProperty().set(
        getConverter() == null ? view.modelProperty().get() : getConverter().toEditModel(view.modelProperty().get())
    );
    editView.appearOn(this);
    Platform.runLater(new Runnable() {
        @Override
        public void run() {
            editView.requestFocus();
        }
    });
}

protected GraphicListCellView<T> createEditView() {
    GraphicListCellView<T> editView = getEditViewFactory().call(this);
    editView.addEventHandler(GraphicListCellView.EditEvent.<T>editCommit(), new EventHandler<GraphicListCellView.EditEvent<T>>() {
        @Override
        public void handle(GraphicListCellView.EditEvent<T> event) {
            GraphicListCell.this.commitEdit(event.getNewValue());
        }
    });
    editView.addEventHandler(GraphicListCellView.EditEvent.<T>editCancel(), new EventHandler<GraphicListCellView.EditEvent<T>>() {
        @Override
        public void handle(GraphicListCellView.EditEvent<T> event) {
            GraphicListCell.this.cancelEdit();
        }
    });
    return editView;
}

Viewの生成には、コンストラクタで指定したファクトリを利用して行い、GraphicListCellViewインターフェイスのメソッドを利用して必要な処理を行っています。また、GraphicListCellModelConverterが設定されていない場合は、表示用のViewのモデルをそのまま編集用のViewのモデルに設定していますので、編集内容がそのまま表示内容に反映されることになります。

次に、cancelEditメソッドをオーバーライドします。

@Override
public void cancelEdit() {
    if (!isEditing()) { return; }

    super.cancelEdit();

    view.appearOn(this);
}

GraphicListCellViewインターフェイスのメソッドを利用して必要な処理を行なっています。

最後に、commitEditメソッドをオーバーライドします。

@Override
public void commitEdit(T newValue) {
    if (!isEditing()) { return; }

    if (getConverter() != null) { getConverter().mergeEditModel(newValue, view.modelProperty().get()); }

    super.commitEdit(newValue);
}

GraphicListCellModelConverterが設定されている場合に、更新された内容を表示用のViewのモデルに反映するようにしています。

以上で、完了になります。

では、作成したGraphicListCellを前回の内容に適用してみたいと思います。

まず、前回作成したUserListCellは不要になります。また、UserViewについては、以下のように、GraphicListCellViewを実装し、appearOnメソッドの実装を追加します。

public class UserView extends GridPane implements GraphicListCellView<UserModel> {
    // 前回と同じ内容

    @Override
    public void appearOn(ListCell<UserModel> cell) {
        Objects.requireNonNull(cell).setGraphic(this);
    }
}

UserEditViewについても同様に、GraphicListCellViewを実装し、appearOnメソッドの実装を追加します。また、EditEventの定義については、GraphicListCellViewで行なっていますので、不要となりますので削除します。

public class UserEditView extends GridPane implements GraphicListCellView<UserModel> {
    // 前回と同じ内容で、EditEventの定義を削除

    @Override
    public void appearOn(ListCell<UserModel> cell) {
        Objects.requireNonNull(cell).setGraphic(this);
    }
}

また、EditEventの定義が変わりましたので、イベントの発生部分を以下のように修正しておきます。

applyButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent actionEvent) {
        fireEvent(new EditEvent<>(EditEvent.editCommit(), getModel()));
    }
});

cancelButton.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent actionEvent) {
        fireEvent(new EditEvent<>(EditEvent.editCancel(), null));
    }
});

最後に、ListViewのcellFactoryの設定部分をGraphicListCellを用いて、以下のようにします。

userListView.setCellFactory(
    GraphicListCell.<UserModel>forListView(
        new Callback<GraphicListCell<UserModel>, GraphicListCellView<UserModel>>() {
            @Override
            public GraphicListCellView<UserModel> call(GraphicListCell<UserModel> cell) {
                return new UserView();
            }
        },
        new Callback<GraphicListCell<UserModel>, GraphicListCellView<UserModel>>() {
            @Override
            public GraphicListCellView<UserModel> call(GraphicListCell<UserModel> cell) {
                return new UserEditView();
            }
        },
        new GraphicListCellModelConverter<UserModel>() {
            @Override
            public void mergeEditModel(UserModel editModel, UserModel model) {
                model.merge(editModel);
            }

            @Override
            public UserModel toEditModel(UserModel model) {
                return model.clone();
            }
        })
);

以上で、修正は完了になります。あとは、前回と同じ内容で実行することができます。

FXMLで定義したViewを使用して、ListCellに表示するための再利用可能なクラスを作成してみました。TableCellやTreeCellについても、同様になりますので、このようなクラスを作成しておくと、ListView・TableView・TreeViewに表示する内容をFXMLで定義できますので、簡単に表示内容をいろいろとカスタマイズすることができるようになります。