Статьи

Динамическое обновление деревьев обозревателя платформы NetBeans с помощью значков расширения

Цель этой статьи — расширить записи в блоге Geertjan о деревьях проводника и значках расширения. Первая статья, найденная по адресу https://blogs.oracle.com/geertjan/entry/no_expansion_key_when_no,   хорошо работает для статических деревьев. Когда узел создается, он определяет, есть ли у нижележащего объекта данных или бина дочерние элементы. Если нет детей, он становится листовым узлом. Если есть дети, его можно развернуть, чтобы показать его детям. Это хорошо работает для статических деревьев, но в этом примере решение принимается только один раз. Если узел определен как листовой узел, но базовый компонент позднее добавляет дочерний узел, узел остается листовым узлом.
 
Вторая статья почти привела нас туда. Его можно найти по адресу https://blogs.oracle.com/geertjan/entry/no_expansion_icon_when_no, Когда ребенок добавляется, изменение обнаруживается. После добавления дочернего узла узел использует фабрику узлов и продолжает это делать. Но если все дочерние элементы удалены, узел не становится листом.
 
Решение состоит в том, чтобы отслеживать два состояния нашего родительского узла. Мы хотим знать, есть ли у узла дочерние элементы при его создании. Мы также хотим знать, есть ли у узла дочерние элементы, когда дочерние элементы добавляются или удаляются.
 
Давайте посмотрим на базовый компонент MyObject:

public class MyObject {
    public static final String ADD_CHILD = "ADD";
    public static final String CHILDREN_TYPE = "CHILDREN";
    public static final String LABEL_TYPE = "LABEL";
    public static final String REMOVE_CHILD = "REMOVE";
    
    private transient final PropertyChangeSupport propertyChangeSupport =
            new PropertyChangeSupport(this);

    private final MyObject parent;

    private String label;
    private List children = new ArrayList();
    
    
    public MyObject(final MyObject parent, final String label) {
        this.parent = parent;
        this.label = label;
    }
    
    public MyObject(final String label) {
        this(null, label);
    }
    
    

    public void addChild(final MyObject child) {
        final boolean oldState = this.hasChildren();
        final int oldChildren = children.size();
        
        children.add(child);
        propertyChangeSupport.firePropertyChange(
                CHILDREN_TYPE, oldState, this.hasChildren());
        propertyChangeSupport.firePropertyChange(
                ADD_CHILD, oldChildren, children.size());
    }

    
    public List getChildren() {
        return children;
    }
    
    public MyObject getParent() {
        return parent;
    }

    public String getLabel() {
        return label;
    }
    
    public boolean hasChildren() {
        return !children.isEmpty();
    }
    
    public boolean hasParent() {
        return parent != null;
    }
    
    public void removeChild(final MyObject child) {
        final boolean oldState = this.hasChildren();
        final int oldChildren = children.size();
        
        children.remove(child);
        propertyChangeSupport.firePropertyChange(
                CHILDREN_TYPE, oldState, this.hasChildren());
        propertyChangeSupport.firePropertyChange(
                REMOVE_CHILD, oldChildren, children.size());
    }

    public void setLabel(String label) {
        final String oldLabel = this.label;
        
        this.label = label;
        
        propertyChangeSupport.firePropertyChange(LABEL_TYPE, oldLabel, label);
    }

    public void addPropertyChangeListener(
            final PropertyChangeListener listener) {
        propertyChangeSupport.addPropertyChangeListener(listener);
    }

    /**
     * Remove PropertyChangeListener.
     *
     * @param listener
     */
    public void removePropertyChangeListener(
            final PropertyChangeListener listener) {
        propertyChangeSupport.removePropertyChangeListener(listener);
    }
}

Это базовый компонент, имеющий встроенную поддержку изменения свойств. Сохраняется метка для объекта, а также необязательный родительский объект. Также ведется список детей. Когда происходят изменения в этом bean-компоненте, он запускает соответствующее изменение свойства. Есть четыре:

 

  • LABEL_TYPE вызывается, если изменяется поле метки компонента.
  • ADD_CHILD запускается при добавлении новых детей.
  • REMOVE_CHILD запускается при удалении детей.
  • CHILDREN_TYPE запускается, когда бин переходит от наличия детей к отсутствию детей или наоборот. Это работает, потому что объекты PropertyChangeSupport не запускают события, когда параметры oldValue и newValue совпадают. Таким образом, даже если это событие вызывается при каждом добавлении или удалении дочернего элемента, слушатели будут получать PropertyChangeEvent только тогда, когда два значения различаются.

Это заботится о базовом bean-компоненте, поэтому теперь давайте посмотрим на BeanNode, который будет визуально представлять его в дереве проводника.

public class ObjectNode extends BeanNode implements PropertyChangeListener {

    private MyObject bean;
    
    public ObjectNode(MyObject bean) throws IntrospectionException {
        super(bean, Children.createLazy(new ObjectCallable(bean)),
                Lookups.singleton(bean));
        this.bean = bean;

        this.setDisplayName(bean.getLabel());
        bean.addPropertyChangeListener(this);
    }
    
    public final void checkChildren(final Object eventObject) {
        if (eventObject == Boolean.TRUE) {
            this.setChildren(Children.create(
                    new ObjectChildFactory(bean), false));
        } else if (eventObject == Boolean.FALSE) {
            this.setChildren(Children.LEAF);
        }
    }
    
    @Override
    public Action[] getActions(final boolean popup) {
        final Action[] returnActions = new Action[2];
        
        returnActions[0] = new AddAction(bean);
        returnActions[1] = new RemoveAction(bean);
        
        return returnActions;
    }
    
    @Override
    public Action getPreferredAction() {
        return new AddAction(bean);
    }

    @Override
    public void propertyChange(final PropertyChangeEvent evt) {
        if (evt.getPropertyName().equals(MyObject.LABEL_TYPE)){
            setDisplayName(bean.getLabel());
        }
        
        // We need to see if we have to update our children.
        else if (MyObject.CHILDREN_TYPE.equals(evt.getPropertyName())) {
            this.checkChildren(evt.getNewValue());
        }
    }
} 

В конструкторе мы используем объект Callable (ObjectCallable), чтобы определить, есть ли у нас дочерние элементы в начальном состоянии. Мы посмотрим на это через минуту. ObjectNode также прослушивает изменения свойств.

 

  • Если метка изменяется, ObjectNode обновляет отображаемое имя.
  • Если состояние наличия дочерних элементов (true / false) изменяется, мы обновляем то, как представлены дочерние элементы узла.

Метод checkChildren изменяет дочерние элементы на экземпляр ObjectFactory, если дочерние элементы присутствуют Если нет, то дети заменяются на Children.LEAF.

 

У нас также есть два действия; один для добавления дочерних узлов и один для удаления узла. Мы вернемся к ним позже. Действие по умолчанию (запускается двойным щелчком по узлу) — добавить новый дочерний узел.

ObjectCallable очень похож на метод checkChildren.

public class ObjectCallable implements Callable {

    private final MyObject key;
    
    public ObjectCallable(final MyObject key) {
        this.key = key;
    }
    
    @Override
    public Children call() throws Exception {
        if (!key.hasChildren()) {
            return Children.LEAF;
        } else {
            return Children.create(new ObjectChildFactory(key), true);
        }
    }
    
}

Если вы уже работали с объектами NodeFactory, ObjectChildFactory довольно прост.

public class ObjectChildFactory extends ChildFactory.Detachable
 
  
        implements PropertyChangeListener {

    private MyObject key;

    public ObjectChildFactory(final MyObject key) {
        this.key = key;
        key.addPropertyChangeListener(this);
    }

    @Override
    protected boolean createKeys(List
  
    toPopulate) {
        final List
   
     children = key.getChildren();

        for (MyObject child : children) {
            toPopulate.add(child);
        }
        return true;
    }

    @Override
    protected Node createNodeForKey(MyObject key) {
        ObjectNode node = null;
        try {
            node = new ObjectNode(key);
        } catch (IntrospectionException ex) {
            Exceptions.printStackTrace(ex);
        }
        return node;
    }

    @Override
    public void propertyChange(final PropertyChangeEvent evt) {
        if (MyObject.ADD_CHILD.equals(evt.getPropertyName())) {
            this.refresh(true);
        } else if (MyObject.REMOVE_CHILD.equals(evt.getPropertyName())) {
            this.refresh(true);
        }
    }
}

   
  
 

ObjectNodeFactory регистрируется как PropertyChangeListener на узле, для которого он создает дочерние узлы. Он прослушивает события ADD_CHILD и REMOVE_CHILD и запускает обновление при его срабатывании. Таким образом, дерево проводника синхронизируется с состоянием базовой модели компонента.

Это действие выполняется из контекстного меню узла или двойным щелчком по узлу. Он добавляет нового дочернего элемента к узлу с меткой, являющейся значением System.currentTimeMillis ().

public class AddAction extends AbstractAction {
    private final MyObject bean;
    
    public AddAction(final MyObject bean) {
        this.bean = bean;
        this.putValue(AbstractAction.NAME, "Add Node");
    }

    @Override
    public void actionPerformed(final ActionEvent evt) {
        bean.addChild(
                new MyObject(bean, String.valueOf(System.currentTimeMillis())));
    }
}

Это действие выполняется из контекстного меню узла. Когда узел выбирается для удаления, мы находим родителя и удаляем узел как дочерний. Если у узла нет родителя, мы информируем пользователя и не удаляем узел.

public class RemoveAction extends AbstractAction {
    
    private final MyObject bean;
    
    public RemoveAction(final MyObject bean) {
        this.bean = bean;
        this.putValue(AbstractAction.NAME, "Remove Node");
    }

    @Override
    public void actionPerformed(final ActionEvent event) {
        if (bean.hasParent()) {
            final MyObject parent = bean.getParent();
            
            parent.removeChild(bean);
        } else {
            JOptionPane.showMessageDialog(
                    null, "The head node cannot be removed!");
        }
    }   
}

Это дерево проводника, которое будет отображаться при запуске приложения платформы. Он начинается с одного головного узла «родитель» и добавляет к нему первого потомка, соответственно называемого «первым». Корневой контекст проводника устанавливается как «родительский» узел, а остальная часть нашего кода берет верх.

@TopComponent.Description(
    preferredID = "ObjectExplorerTopComponent",
    persistenceType = TopComponent.PERSISTENCE_ALWAYS)
@TopComponent.Registration(mode = "explorer", openAtStartup = true)
@ActionID(category = "Window", id = "org.o.explorer.ObjectExplorerTopComponent")
@ActionReference(path = "Menu/Window")
@TopComponent.OpenActionRegistration(
    displayName = "#CTL_ObjectExplorerAction",
preferredID = "ObjectExplorerTopComponent")
@Messages({
    "CTL_ObjectExplorerAction=ObjectExplorer",
    "CTL_ObjectExplorerTopComponent=ObjectExplorer Window",
    "HINT_ObjectExplorerTopComponent=This is a ObjectExplorer window"
})
public final class ObjectExplorerTopComponent extends TopComponent implements ExplorerManager.Provider {

    private ExplorerManager em = new ExplorerManager();
    private MyObject parent = new MyObject("parent");

    public ObjectExplorerTopComponent() throws IntrospectionException {
        initComponents();
        setName(Bundle.CTL_ObjectExplorerTopComponent());
        setToolTipText(Bundle.HINT_ObjectExplorerTopComponent());
        setLayout(new BorderLayout());
        parent.addChild(new MyObject("First"));
        em.setRootContext(new ObjectNode(parent));
        add(new BeanTreeView(), BorderLayout.CENTER);
        associateLookup(ExplorerUtils.createLookup(em, getActionMap()));
    }                                  

    @Override
    public ExplorerManager getExplorerManager() {
        return em;
    }
}

Поэтому, чтобы создать действительно динамическое дерево обозревателя, которое будет правильно поддерживать состояние и отображать значки расширения при добавлении и удалении, мы проделали следующую работу:

  • Заставил наш бин (MyObject) общаться, когда он добавляет и удаляет дочерние элементы. Это также вызывает другое событие, когда мы переходим от рождения детей к тому, чтобы иметь детей, и наоборот.
  • Сделано, чтобы наш узел (ObjectNode) определял состояние своих дочерних элементов, когда он создан (ObjectCallable) И когда состояние дочерних элементов изменяется (PropertyChangeEvent / Listener). Он знает, как при необходимости перейти с листа на фабрику (checkChildren).
  • Сделано обновление фабрики при добавлении и удалении узлов.
  • Реализованы базовые функции добавления и удаления.

Эти парадигмы могут быть повторно использованы в других проектах с динамическими деревьями проводника. Отсюда можно получить более надежную и детальную функциональность. Этот проект должен послужить хорошей отправной точкой.