Переключающий адаптер для титульного стекла

#javafx

Вопрос:

В моем приложении я хочу применить поведение a ToggleGroup к группе TitledPane s. Для этого я реализовал это:

ToggleAdapter.java

 package sample;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.scene.control.TitledPane;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;

public class ToggleAdapter implements Toggle {
    final private TitledPane titledPane;
    final private ObjectProperty<ToggleGroup> toggleGroupProperty = new SimpleObjectProperty<>();

    private ToggleAdapter(TitledPane titledPane) {
        this.titledPane = titledPane;
    }

    @Override
    public ToggleGroup getToggleGroup() {
        return toggleGroupProperty.get();
    }

    @Override
    public void setToggleGroup(ToggleGroup toggleGroup) {
        toggleGroupProperty.set(toggleGroup);
    }

    @Override
    public ObjectProperty<ToggleGroup> toggleGroupProperty() {
        return toggleGroupProperty;
    }

    @Override
    public boolean isSelected() {
        return titledPane.isExpanded();
    }

    @Override
    public void setSelected(boolean selected) {
        titledPane.setExpanded(selected);
    }

    @Override
    public BooleanProperty selectedProperty() {
        return titledPane.expandedProperty();
    }

    @Override
    public Object getUserData() {
        return titledPane.getUserData();
    }

    @Override
    public void setUserData(Object value) {
        titledPane.setUserData(value);
    }

    @Override
    public ObservableMap<Object, Object> getProperties() {
        return FXCollections.emptyObservableMap();
    }

    public static Toggle asToggle(final TitledPane titledPane) {
        return new ToggleAdapter(titledPane);
    }
}
 

образец.fxml

 <?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.VBox?>

<VBox spacing="7.0" xmlns="http://javafx.com/javafx/15.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
   <children>
      <TitledPane fx:id="titledPane1" text="Title 1">
         <content>
            <TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
         </content>
      </TitledPane>
      <TextField />
      <TitledPane fx:id="titledPane2" expanded="false" text="Title 2">
         <content>
            <TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
         </content>
      </TitledPane>
      <TitledPane fx:id="titledPane3" expanded="false" text="Title 3">
         <content>
            <TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
         </content>
      </TitledPane>
   </children>
   <padding>
      <Insets bottom="14.0" left="14.0" right="14.0" top="14.0" />
   </padding>
</VBox>
 

Controller.java

 package sample;

import javafx.fxml.FXML;
import javafx.scene.control.TitledPane;
import javafx.scene.control.ToggleGroup;

public class Controller {
    @FXML private TitledPane titledPane1;
    @FXML private TitledPane titledPane2;
    @FXML private TitledPane titledPane3;

    @FXML
    private void initialize() {
        final var toggleGroup = new ToggleGroup();

        final var toggle1 = ToggleAdapter.asToggle(titledPane1);
        toggle1.setToggleGroup(toggleGroup);

        final var toggle2 = ToggleAdapter.asToggle(titledPane2);
        toggle2.setToggleGroup(toggleGroup);

        final var toggle3 = ToggleAdapter.asToggle(titledPane3);
        toggle3.setToggleGroup(toggleGroup);

        toggleGroup.selectToggle(toggle1);

    }
}
 

Main.java

 package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {

    @Override
    public void start(Stage primaryStage) throws Exception{
        Parent root = FXMLLoader.load(getClass().getResource("sample.fxml"));
        primaryStage.setTitle("Hello World");
        final Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }


    public static void main(String[] args) {
        launch(args);
    }
}
 

Мой наивный подход не работает, но я понятия не имею, почему бы и нет. Есть какие-нибудь идеи?

ПРАВКА: Я знаю об Acordion этом , но это не подходит, потому что я не могу поместить все три TitledPane s в один родительский контейнер.

Комментарии:

1. Вот реализация для ToggleButton . Может быть, вы сможете почерпнуть из этого какие-нибудь идеи. github.com/openjdk/jfx/blob/master/modules/javafx.controls/src/…

2. Не изобретайте велосипед заново. Элемент управления уже реализован в стандартном API Accordion .

3. Привет @kleopatra, я только что изменил пример.

Ответ №1:

Во-первых, вы изобретаете велосипед заново. Не делайте этого; уже Accordion есть элемент управления, который реализует именно то , что вы пытаетесь здесь сделать.

Все, что вам нужно, это:

 <?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Accordion?>
<?import javafx.scene.control.TextArea?>
<?import javafx.scene.control.TitledPane?>

<Accordion expandedPane="${titledPane1}" xmlns="http://javafx.com/javafx/15.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.Controller">
   <panes>
      <TitledPane fx:id="titledPane1" text="Title 1" expanded="true">
         <content>
            <TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
         </content>
      </TitledPane>
      <TitledPane fx:id="titledPane2" expanded="false" text="Title 2">
         <content>
            <TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
         </content>
      </TitledPane>
      <TitledPane fx:id="titledPane3" expanded="false" text="Title 3">
         <content>
            <TextArea minHeight="-Infinity" prefHeight="125.0" prefRowCount="1" wrapText="true" />
         </content>
      </TitledPane>
   </panes>
   <padding>
      <Insets bottom="14.0" left="14.0" right="14.0" top="14.0" />
   </padding>
</Accordion>
 

и

 public class Controller {
    
    @FXML private Accordion accordion ;
    @FXML private TitledPane titledPane1;
    @FXML private TitledPane titledPane2;
    @FXML private TitledPane titledPane3;

    @FXML
    private void initialize() {
        accordion.setExpandedPane(titledPane1);
    }
}
 

Это ToggleAdapter не требуется.

Причина, по которой ваш код не работает, заключается в том, что вы предполагаете, я думаю, что ToggleGroup он наблюдает за selected состоянием каждого из своих переключателей и обновляет состояние другого переключателя, когда один из них выбран. Это не так; на самом деле реализация переключения несет ответственность за сохранение единого выбора в своей группе переключений, если она того пожелает. Вы можете сделать это, добавив прослушиватель в выбранное состояние в ToggleAdapter (но опять же, чтобы подчеркнуть, всегда неправильно заново изобретать функциональность, определенную в стандартном API).

 private ToggleAdapter(TitledPane titledPane) {
    this.titledPane = titledPane;
    selectedProperty().addListener(obs -> {
        ToggleGroup tg = getToggleGroup();
        if (tg != null) {
            if (isSelected()) {
                tg.selectToggle(this);
            } else if (tg.getSelectedToggle() == this) {
                tg.selectToggle(null);
            }
        }
    });
}
 

Комментарии:

1. Как упоминалось в моем вопросе: я действительно хочу применить поведение a ToggleGroup к списку TitledPane . Accordion является a Control и работает только в том случае, если все TitlePane s, о которых идет речь, являются детьми аккордеона.

2. @Hannes До сих пор не ясно, почему Accordion (или, на худой конец, несколько Accordion s) не обеспечивают ту функциональность, которую вы ищете. Но если у вас есть какой-то другой вариант использования, разве модификация ToggleAdapter не решит проблему?

3. просто побочный комментарий: на самом деле реализация переключения отвечает за поддержание единого выбора , который является истинным и реализован, но мне кажется странным (как в полностью перевернутом 😉 для класса, который должен управлять единственным выбором внутри группы.

4. @клеопатра Согласилась, это странно. Я уверен, что в то время это имело смысл для разработчиков API, но я даже не могу предположить, о чем они думали.

5. Я считаю Accordion , что это не решает мою проблему, потому TitledPane что s в моем реальном случае использования находится в разных Parent s, которые расположены в разных местах пользовательского интерфейса. Поэтому я не могу поместить все TitledPane буквы s в одного родителя, так как это необходимо для правильной работы аккордеона. В своем MRE я попытался устранить сложность пользовательского интерфейса и сузить его до чисто технических требований.

Ответ №2:

Изменение реализации ToggleAdapter на это фактически решает проблему:

 package sample;

import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.ObjectPropertyBase;
import javafx.collections.FXCollections;
import javafx.collections.ObservableMap;
import javafx.scene.control.TitledPane;
import javafx.scene.control.Toggle;
import javafx.scene.control.ToggleGroup;

import java.util.Optional;

public class ToggleAdapter implements Toggle {
    final private TitledPane titledPane;
    final private ObjectProperty<ToggleGroup> toggleGroupProperty = new ObjectPropertyBase<>() {
        private ToggleGroup old;

        @Override
        protected void invalidated() {
            if (old != null) {
                old.getToggles().remove(ToggleAdapter.this);
            }
            old = get();
            if (get() != null amp;amp; get().getToggles().contains(ToggleAdapter.this) == false) {
                get().getToggles().add(ToggleAdapter.this);
            }
        }

        @Override
        public Object getBean() {
            return ToggleAdapter.this;
        }

        @Override
        public String getName() {
            return "toggleGroup";
        }
    };

    @Override
    public ToggleGroup getToggleGroup() {
        return toggleGroupProperty.get();
    }

    @Override
    public void setToggleGroup(ToggleGroup toggleGroup) {
        toggleGroupProperty.set(toggleGroup);
    }

    @Override
    public ObjectProperty<ToggleGroup> toggleGroupProperty() {
        return toggleGroupProperty;
    }

    @Override
    public boolean isSelected() {
        return titledPane.isExpanded();
    }

    @Override
    public void setSelected(boolean selected) {
        titledPane.setExpanded(selected);
    }

    @Override
    public BooleanProperty selectedProperty() {
        return titledPane.expandedProperty();
    }

    @Override
    public Object getUserData() {
        return titledPane.getUserData();
    }

    @Override
    public void setUserData(Object value) {
        titledPane.setUserData(value);
    }

    @Override
    public ObservableMap<Object, Object> getProperties() {
        return FXCollections.emptyObservableMap();
    }

    public static Toggle asToggle(final TitledPane titledPane) {
        return new ToggleAdapter(titledPane);
    }

    public ToggleAdapter(TitledPane titledPane) {
        this.titledPane = titledPane;

        selectedProperty().addListener(obs -> {
            Optional.ofNullable(getToggleGroup()).ifPresent(toggleGroup -> {
                if (isSelected()) {
                    toggleGroup.selectToggle(this);
                } else if (toggleGroup.getSelectedToggle() == this) {
                    toggleGroup.selectToggle(null);
                }
            });
        });
    }
}
 

Спасибо, James_D за идею о том, как изменить реализацию.

Комментарии:

1. неправильно не реализовывать сломанный инвариант, о котором я упоминал в ответе Джеймса

2. @клеопатра, ты имеешь в виду это? github.com/openjdk/jfx/blob/… Я не совсем уверен, с каким делом он справляется, тбх.

3. скопированный комментарий: как бы ни была плоха ToggleGroup, я думаю, что любое решение должно поддерживать инвариант assertTrue(tg.getToggles().contains(toggle)) для всех управляемых переключений, которые могут быть обработаны в свойстве ToggleGroup адаптера (посмотрите на ToggleButton, как уже отметил @Sedrick 🙂

4. Я не понимаю, когда может возникнуть этот инвариант и в каком случае с ним необходимо справиться. Тем более, что я не вижу, как свойство ToggleGroup относится к нему.

5. речь идет не о вас или обо мне — речь идет о будущих читателях: поэтому ответы должны быть как можно более хорошими … дерьмовый оригинал (в ядре javafx) не является оправданием для воспроизведения его дерьмовости *выкл