Разработка плагина IntelliJ IDEA. Часть 2

Original author: JetBrains
  • Translation
Продолжаем неравный бой с документаций Intellij IDEA. Предыдущая часть находится здесь.

Конфигурационный файл плагина


Вся функциональность, предоставляемая плагином, а также описание и данные об авторе перечисляются в файле plugin.xml.
Рассмотрим структуру файла, в котором присутствуют практически все значимые элементы.

<!-- url="..." определяет URL домашней страницы плагина (отображается на Welcome Screen и в настройках "Plugins") -->
<idea-plugin url="http://www.jetbrains.com/idea">
    <!-- Имя плагина -->
    <name>VssIntegration</name>

    <!-- Уникальный идентификатор плагина. Не может быть изменен в последующих версиях. Если не указан, то по-умолчанию равен <name> -->
    <id>VssIntegration</id>
    
    <!-- Описание плагина -->
    <description>Vss integration plugin</description>
    
    <!-- Описание изменений в последних версиях. Отображается в настройках плагинов и Web-интерфейсе -->
    <change-notes>Initial release of the plugin.</change-notes>
     
    <!-- Версия плагина -->
    <version>1.0</version>
     
    <!-- Разработчик плагина. Необязательный атрибут "url" указывает на домашнюю страницу разработчика. Необязательный атрибут "email" содержит адрес электронной почты разработчика. Необязательный атрибут "logo" указывает на логотип 16х16, сохраненный в JAR  -->
    <vendor url="http://www.jetbrains.com" email="support@jetbrains.com" logo="icons/plugin.png">Foo Inc.</vendor>
     
    <!-- Уникальные идентификаторы плагинов от которых зависит текущий -->
    <depends>MyFirstPlugin</depends>
    <!-- Опциональные зависимости -->
    <depends optional="true" config-file="mysecondplugin.xml">MySecondPlugin</depends>
 
    <!-- Позволяет плагину интегрировать его справочную систему (в JavaHelp формате) с IDEA. Атрибут "file" определяет имя JAR в поддиректории "help". Атрибут "path" определяет имя файла с helpset. -->
    <helpset file="myhelp.jar" path="/Help.hs" />
    
    <!-- Минимальный и максимальный номер билда, совместимого с плагином -->
    <idea-version since-build="3000" until-build="3999"/>    
 
    <!-- Имя бандла с текстовыми ресурсами -->
    <resource-bundle>messages.MyPluginBundle</resource-bundle>
     
    <!-- Компоненты уровня приложения -->
    <application-components>
        <component>
            <!-- Интерфейс -->
            <interface-class>com.foo.Component1Interface</interface-class>
            <!-- Класс с реализацией -->
            <implementation-class>com.foo.impl.Component1Impl</implementation-class>
        </component>
    </application-components>
     
    <!-- Компоненты уровня проекта -->
    <project-components>
        <component>
            <!-- Интерфейс и реализация совпадают -->
            <interface-class>com.foo.Component2</interface-class>
            <!-- Если "workspace" установлено в "true", компонент сохраняет состояние в *.iws файл вместо *.ipr (имеет смысл только для реализации JDOMExternalizable). -->
            <option name="workspace" value="true" />            
            <!-- Если представлен тег "loadForDefaultProject", компонент загружается для проекта по-умолчанию -->
            <loadForDefaultProject>
        </component>
    </project-components>
     
    <!-- Компоненты уровня модуля -->
    <module-components>
        <component>
            <interface-class>com.foo.Component3</interface-class>
        </component>
    </module-components>
 
    <!-- Действия -->
    <actions>    
        <action id="VssIntegration.GarbageCollection" class="com.foo.impl.CollectGarbage" text="Collect _Garbage" description="Run garbage collector">
            <keyboard-shortcut first-keystroke="control alt G" second-keystroke="C" keymap="$default"/>
        </action>
    </actions>
     
    <!-- Точки расширения, определенные в плагине. Точки расширения позволяют другим плагинам предоставлять различные данные. Атрибут "beanClass" определяет реализацию, используемую при расширении. -->
    <extensionPoints>    
        <extensionPoint 
name="testExtensionPoint" beanClass="com.foo.impl.MyExtensionBean"/>
    </extensionPoints>
 
    <!-- Расширения, которые плагин добавляет к точкам расширения, 
         определенным в ядре IDEA или других плагинах.  Атрибут "defaultExtensionNs" должен соответствовать ID расширяемого плагина или "com.intellij" если используется расширение из IDEA Core. 
         Имя тегов внутри <extensions> должно совпадать с точками расширения -->
    <extensions xmlns="VssIntegration">    
        <testExtensionPoint implementation="com.foo.impl.MyExtensionImpl"/>
    </extensions>
</idea-plugin>

Выполнение и обновление действий


Система действий позволяет плагинам добавлять собственные пункты в меню и тулбары IDEA. Действие – это класс, унаследованный от AnAction, чей метод actionPerformed() вызывается, когда выбран элемент меню или соответствующая ему кнопка тулбара. Например, один и тот же класс действия отвечает за пункт меню «File | Open File…» и кнопку «Open File» на тулбаре.

Действия можно объединять в группы, которые, в свою очередь, могут содержать подгруппы.
Каждому действию или группе должен быть присвоен уникальный идентификатор. Идентификаторы большинства стандартных действий определены в классе com.intellij.openapi.actionSystem.IdeActions.
Каждое действие может быть включено в несколько групп, таким образом, они способны отображаться в различных местах пользовательского интерфейса. Места, в которых могут отображаться действия, определены как константы в интерфейсе ActionPlaces (из того же пакета). Для каждого места создается экземпляр класса Presentation, поэтому одно и то же действие может иметь различный текст или иконку в разных местах интерфейса. Разные представления создаются копированием возвращаемого значения из метода AnAction.getTemplatePresentation().

Для обновления состояния действия периодически вызывается метод AnAction.update(). В метод передается объект типа AnActionEvent, давая информацию о текущем контексте и, в частности, специфическое представление, которое следует обновить.

Для получения информации о текущем состоянии IDE, включая активный проект, выделенный файл, выделенный текст в редакторе и т.д., может быть использован метод AnActionEvent.getData(). Различные ключи, принимаемые этим методом, описаны в классе DataKeys. Объект типа AnActionEvent также передается в метод actionPerformed().

Регистрация действий


Существует два варианта регистрации действия: перечислением в файле plugin.xml, либо вручную на стадии инициализации компонента.
Рассмотрим первый вариант.
<actions>
    <!-- Элемент <action> определяет действие, которое мы хотим зарегистрировать.
         Обязательный атрибут "id" содержит уникальный идентификатор действия.
         Обязательный атрибут "class" определяет имя класса, реализующего действие.
         Обязательный атрибут "text" определяет название элемента меню или тулбара.
         Опциональный атрибут "use-shortcut-of" содержит идентификатор другого действия, позволяя переиспользовать связанную с ним комбинацию клавиш.  
         Необязательный атрибут "description" определяет подсказку в строке состояния.
         Необязательный атрибут "icon" указывает какую иконку отображать пользователю. -->
        <action id="VssIntegration.GarbageCollection" class="com.foo.impl.CollectGarbage" text="Collect _Garbage" description="Run garbage collector"
                icon="icons/garbage.png">
    <!-- Узел <add-to-group> указывает, что действие необходимо добавить к существующей группе (нескольким группам).
         Обязательный атрибут "group-id" указывает на идентификатор группы.
         Группа должна реализовать класс DefaultActionGroup.
         Обязательный атрибут "anchor" определяет относительную позицию ("first", "last", "before" или "after").
         Атрибут "relative-to-action" обязателен, если anchor равен "before" или "after". -->
    <add-to-group group-id="ToolsMenu" relative-to-action="GenerateJavadoc" anchor="after"/>
    <!-- Узел <keyboard-shortcut> определяет комбинации клавиш. 
         Обязательный атрибут "first-keystroke" определяет главную комбинацию. Значение должно быть корректной Swing-комбинацией.
         Необязательный параметр "second-keystroke" определяет дополнительное сочетание.
         Обязательный атрибут "keymap" определяет раскладку. Идентификаторы описаны в классе com.intellij.openapi.keymap.KeymapManager. -->
    <keyboard-shortcut first-keystroke="control alt G" second-keystroke="C" keymap="$default"/>
    <!-- Узел <mouse-shortcut> определяет сочетания с клавишами мыши.
         Они определяются как последовательность слов, разделенных пробелами: "button1", "button2", "button3" для мыши; "shift", "control", "meta", "alt", "altGraph" для клавиатуры; "doubleClick" – для двойного клика. -->
       <mouse-shortcut keystroke="control button3 doubleClick" keymap="$default"/>
     </action>
        <!-- Элемент <group> определяет группу. Элементы <action>, <group> и <separator> определенные внутри автоматически добавляются в группу.
               Необязательный атрибут "popup" определяет, как будет показано меню. Если popup="true", действия будут в подменю; если popup="false", действия расположатся в родительском меню. -->
        <group class="com.foo.impl.MyActionGroup" id="TestActionGroup" text="Test Group" description="Group with test actions"
               icon="icons/testgroup.png" popup="true">
            <action id="VssIntegration.TestAction" class="com.foo.impl.TestAction" text="My Test Action" description="My test action"/>
            <!-- Элемент <separator> определяет разделитель между пунктами. Он может иметь дочерний элемент <add-to-group>. -->
            <separator/>
            <group id="TestActionGroup"/>
             <!-- Элемент <reference> позволяет добавить существующее действие к группе. Обязательный атрибут "ref" содержит идентификатор действия. -->
            <reference ref="EditorCopy"/>
            <add-to-group 
                  group-id="MainMenu" relative-to-action="HelpMenu" anchor="before"/>
        </group>
    </actions>

В случае регистрации действия непосредственно из Java кода, следует выполнить два шага. Для начала, экземпляр класса, производного от AnAction необходимо передать в метод ActionManager.registerAction() для создания привязки к идентификатору. Далее, действие нужно добавить к одной или более группам. Для получения экземпляра группы по идентификатору, следует вызвать ActionManager.getAction() и привести результат к типу DefaultActionGroup.
Следующая последовательность шагов требуется для регистрации действий во время запуска IDEA:
  1. создать класс, реализующий интерфейс ApplicationComponent;
  2. переопределить методы getComponentName, initComponent, и disposeComponent;
  3. зарегистрировать класс в секции <application-components> файла plugin.xml.

Для наглядности приведем пример кода, выполняющий описанную процедуру.
public class MyPluginRegistration implements ApplicationComponent {
// Должен возвращать уникальное имя компонента
    @NotNull public String getComponentName() {
        return "MyPlugin";               
    }
// После регистрации класса MyPluginRegistration в секции <application-components>
// этот метод выполнится при старте IDEA.
    public void initComponent() {
        ActionManager am = ActionManager.getInstance();
        TextBoxes action = new TextBoxes();
      // Регистрируем действие.
        am.registerAction("MyPluginAction", action);
      // Получаем группу, к которой будет присоединено наше действие.
        DefaultActionGroup windowM = (DefaultActionGroup) am.getAction("WindowMenu");
      // Добавляем к группе разделитель и действие. 
        windowM.addSeparator();
        windowM.add(action);
    }
// Этот метод используется для освобождения ресурсов.
    public void disposeComponent() {       
    }
}

Создание интерфейса для действий


Если плагину необходимо отобразить тулбар или всплывающее меню со специфическим интерфейсом это может быть выполнено с помощью классов ActionPopupMenu и ActionToolbar. Их объекты могут быть созданы вызовами метода ActionManager.createActionPopupMenu() и ActionManager.createActionToolbar().

Если действие содержится в специфическом компоненте (например, панели), обычно требуется вызвать ActionToolbar.setTargetComponent() и передать ему экземпляр компонента как параметр. Это гарантирует, что состояние кнопок тулбара зависит только от состояния связанного компонента, а не от фокуса на фрейме IDE.

Сохранение состояния компонентов


IntelliJ IDEA предоставляет API, позволяющий компонентам или сервисам сохранять их состояние между запусками IDE. Сохранять можно как примитивные типы, так и составные объекты благодаря использованию интерфейса PersistentStateComponent.

Использование PropertiesComponent для хранения простых значений

Если требуется сохранить лишь несколько примитивных типов, то простейший путь достичь этого – использовать сервис com.intellij.ide.util.PropertiesComponent. Он может использоваться для сохранения значений как уровня приложения, так и уровня проекта (данные помещаются в файл workspace).
Для сохранения значений уровня приложения используется метод PropertiesComponent.getInstance(), а для значений уровня проекта – PropertiesComponent.getInstance(Project).

Использование интерфейса PersistentStateComponent

Интерфейс PersistentStateComponent дает более гибкие возможности управления сохраняемыми значениями, их форматом и расположением хранилища. Для того чтобы использовать его, необходимо реализовать в сервисе или компоненте сам интерфейс, определить класс, отвечающий за состояние, а также указать место хранения используя аннотацию @State.
Необходимо отметить, что экземпляры расширений не могут сохранять свое состояние с помощью PersistentStateComponent. Если это необходимо, то рекомендуется создать отдельный сервис, ответственный за управление состоянием данного расширения.

Реализация интерфейса PersistentStateComponent

При реализации интерфейс необходимо параметризовать классом, описывающим состояние. Класс состояния может быть обычным JavaBean-классом или же самостоятельно реализовывать PersistentStateComponent.
В первом случае, экземпляр класса состояния обычно сохраняется как поле в классе компонента.
class MyService implements PersistentStateComponent<MyService.State> {
  class State {
    public String value;
  }
 
  State myState;
 
  public State getState() {
    return myState;
  }
 
  public void loadState(State state) {
    myState = state;
  }
}

Во втором случае, можно использовать следующий шаблон:
class MyService implements PersistentStateComponent<MyService> {
  public String stateValue;
 
  public MyService getState() {
    return this;
  }
 
  public void loadState(MyService state) {
    XmlSerializerUtil.copyBean(state, this);
  }
}

Реализация класса состояния

Перед сохранением публичные поля и свойства bean-класса сериализуются в XML-формат. Следующие типы могут быть сохранены:
  • целые и дробные числа;
  • булевы значения;
  • строки;
  • коллекции;
  • словари;
  • перечисления.

Для того чтобы исключить публичное поле из сериализуемых значений, его необходимо пометить аннотацией @Transient.
Класс состояния обязан иметь конструктор без параметров, который инициализирует все поля значениями по-умолчанию (это необходимо для приведения класса в консистентное состояние до сохранения в XML-файл).

Определение места хранения

Для того чтобы задать где конкретно сохранять данные, следует добавить аннотацию @State к классу компонента, в которой необходимо указать следующие поля:
  • «Name» (обязательно) – определяет имя состояния (название корневого тега);
  • одна или несколько аннотаций @Storage (обязательно) – определяет места хранения *.ipr файлов;
  • «roamingType» – тип роуминга (опционально) – определяет, будет ли синхронизироваться состояние между различными экземплярами IDEA, когда использован плагин Server;
  • «Reloadable» (опционально) – если равно false требуется полный перезапуск среды для перезагрузки изменившегося файла состояния.

Примеры использования аннотации @Storage:
  • @Storage(id="other", file = StoragePathMacros.APP_CONFIG + "/other.xml") – для значений уровня приложения;
  • @Storage(id="other", file = StoragePathMacros.PROJECT_FILE) – для значений, сохраняемых в файле проекта (.ipr);
  • @Storage(id = "dir", file = StoragePathMacros.PROJECT_CONFIG_DIR + "/other.xml", scheme = StorageScheme.DIRECTORY_BASED)}) – для значений, сохраняемых в директории проекта;
  • @Storage(id="other", file = StoragePathMacros.WORKSPACE_FILE) – для значений, сохраняемых в workspace-файле.

Параметр «id» аннотации @Storage может быть использован для исключения полей при сериализации в определенный формат. Если исключать ничего не надо, параметру можно присвоить произвольное строковое значение.
Определяя различные значения параметра «file», можно сохранять состояние в различных файлах. Если необходимо определить, где сохранять значения, когда используется формат проекта, основанный на директории, нужно добавить вторую аннотацию @Storage со значением параметра «scheme» равным StorageScheme.DIRECTORY_BASED, например:
@State(
    name = "AntConfiguration",
    storages = {
      @Storage(id = "default", file = StoragePathMacros.PROJECT_FILE),
      @Storage(id = "dir", file = StoragePathMacros.PROJECT_CONFIG_DIR + "/ant.xml", scheme = StorageScheme.DIRECTORY_BASED)
    }
)


Настройка XML-формата

Если сохраняемое состояние не отображается на простой JavaBean, то возможно класс состояния унаследовать от org.jdom.Element. В этом случае, метод getState() используется для построения XML-элемента с произвольной структурой. В методе loadState() необходимо произвести десериализацию JDOM-элемента.
Для использования стандартной сериализации Bean-классов, совместно с настраиваемым форматом, разрешается использовать аннотации @Tag, @Attribute, @Property, @MapAnnotation, @AbstractCollection.

В следующей части: структура проекта, VFS.

Все статьи цикла: 1, 2, 3, 4, 5, 6, 7.
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 0

Only users with full accounts can post comments. Log in, please.