Всегда приятно общаться с приложением, которое запоминает твои повадки и словно чувствует тебя, того, что ты хочешь. Любая UI библиотека или платформа обладает увы лишь базовым функционалом и набором компонент. Например, если колонка в таблице не перемещается или по ней нельзя отсортировать, то такое приложение, в котором она используется сложно назвать дружелюбным. К счастью, сегодня таким функционалом никого не удивишь. Однако, не каждая программа запомнит положение этой колонки и в следующем сеансе отобразит ее именно на том же месте. Возможно, также будет раздражать каждый раз устанавливать положение разделителя в SplitPane или вводить одни и те же параметры фильтра. Как правило, такие удобства приходится обеспечивать самим разработчикам.


Примеров таких на первый взгляд маленьких доработок множество, но предлагаемых платформой решений всего два, и по сути они похожи: создать свой компонент на основе базового, создать свой Skin к базовому компоненту, переопределив поведение. Ни тот, ни другой способ не является простым в реализации, к тому же на каждый компонент необходимо будет писать свой компонент-адаптер. Я встречал не мало людей, кому этот способ был более знаком и понятен.


Но он далеко не единственный. Что, если взять возможности платформы, которая поддерживает шаблон обозреватель для дочерних Node, и при добавлении или удалении подграф Node прогонять через набор плагинов, каждый из которых занимается своей специфической работой? Один умеет все запоминать и восстанавливать при повторном сеансе, другой — указанным компонентам меняет контекстное меню, добавляя функцию копирования текста. Кто-то из них добавляет три точки в конце текста, если он не умещается, а при наведении мыши показывает подсказку с полным текстом, только если он не уместился. Самое главное, что не важно из какой библиотеки этот компонент, можем ли мы от него наследоваться и переопределить нужное нам поведение. Все что нам в таком случае надо — научить плагин работать с нужными компонентами при необходимости по разному.


Таким мог бы быть слушатель коллекции дочерних элементов:


private final ListChangeListener changeListener = (ListChangeListener<Node>) (ListChangeListener.Change<? extends Node> c) -> {
        if (c.next()) {
            c.getAddedSubList().forEach(this::applySettingsForNodeAndAddListenerForItsChild);
        }
    };

Таким бы — код обработки каждой измененной Node-ы:


private void applySettingsForNodeAndAddListenerForItsChild(Node n) {
        if (!checkApplySettings(n)) {
            apply(n);
            ObservableList<Node> children = getChildren(n);
            if (children != null) {
                addListnerForUpdateChildren(children);
            }
            markNodePropertyApplied(n);
        }
    }

А таким — непосредственно код вызова самого плагина, который зарегистрирован на этот тип компонент:


public Node apply(Node node) {
        List<SettingsPlugin> settingsPlugins = settingsMap.get(Node.class);
        if (settingsPlugins != null) {
            for (SettingsPlugin plugin : settingsPlugins) {
                node = plugin.apply(node, userSettings.getSettings());
            }
        }
        List<SettingsPlugin> settingList = settingsMap.get(node.getClass());
        if (settingList != null) {
            for (SettingsPlugin plugin : settingList) {
                node = plugin.apply(node, userSettings.getSettings());
            }
        }
        return node;
    }

Вот интерфейс самого плагина:


public interface SettingsPlugin {
    public Node apply(Node node, Map<String, Object> userSettings);
}

Необходимо только на коллекции дочерних элементов Root элемента Scene один раз зарегистрировать слушателя, а на остальном подграфе он зарегистрируется сам...


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

Комментарии (2)


  1. Lure_of_Chaos
    08.09.2018 23:22

    1. Для чего нужно возвращать передаваемую ноду? Неужто для возможности подмены одного компонента другим? И что будет, если плагину передадим, инапример, кнопку, а он вернёт поле ввода?
    2. Почему бы «плагин»(хотя это не плагин, а декоратор) не параметризировать по ноде, чтобы обойтись без проверок на тип и приведения типов?
    3. Также не определено время вызова плагина. Что приводит к следующим вопросам:
    — когда вызывать плагин? При инициализации, добавлении на форму, активации? Что будет, если вызвать плагин дважды?
    — для изменения поведения элемента необходимо слушать события. Как на них оформлять подписку и дективировать ее? Лучше бы еще одним параметром передавать и имя события либо состояния (например, «initialize»)


    1. High_Tower Автор
      08.09.2018 23:53

      Для чего нужно возвращать передаваемую ноду?

      Вы правы, в данном контексте это не имеет смысла.


      Неужто для возможности подмены одного компонента другим? И что будет, если плагину передадим, инапример, кнопку, а он вернёт поле ввода?

      Нет, никаких подмен. Код выдернут из контекста. Для рассматриваемой проблемы было бы достаточно void метода.


      Почему бы «плагин»(хотя это не плагин, а декоратор) не параметризировать по ноде, чтобы обойтись без проверок на тип и приведения типов?

      Декоратор, как правило, реализует тот же интерфейс и дополняет реализацию методов декорируемого объекта. При этом клиент общается именно с декоратором, когда тот хранит внутри себя ссылку на исходный объект. Здесь же предполагается добавление некоторых свойств объекту, при этом клиент общается с самим исходным объектом без посредника.


      Типизировать можно, но все мои плагины добавляли общий (например, текст с тремя точками) функционал разным Node (в ячейку таблиц и списков, в поле ввода и подписи), писать при этом на каждую Node отдельный плагин означало бы дублирование.


      Также не определено время вызова плагина. Что приводит к следующим вопросам:
      — когда вызывать плагин? При инициализации, добавлении на форму, активации? Что будет, если вызвать плагин дважды?

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


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


      для изменения поведения элемента необходимо слушать события. Как на них оформлять подписку и дективировать ее? Лучше бы еще одним параметром передавать и имя события либо состояния (например, «initialize»)

      В JavaFX есть Observable коллекции. Например, ObservableList будет уведомлять о добавляемых и удаляемых элементах. В моей реализации предполагалось только прослушка добавления новых Node в граф.