Создаем расширение для Eclipse: шаг за шагом (исходники)

Натан А. Гуд

Цель этой статьи не только в том, чтобы описать процесс создания нового модуля для Eclipse, хотя и это рассматривается достаточно детально. В процессе создания модуля вы узнаете, как лучше использовать средства, предоставляемые интегрированной средой разработки Eclipse (Eclipse IDE), для того, чтобы быстро выполнить все шаги, описанные в статье. Статья также научит вас, как расширить Eclipse, чтобы добавить дополнительные функциональные возможности в ваш модуль. После ознакомления со статьей вы сможете использовать мастер создания модулей, входящий в Eclipse IDE, для создания новых расширений Eclipse. Кроме того, вы узнаете, как расширять классы и реализовать интерфейсы, которые вы можете использовать для добавления окон настроек, страниц для задания параметров, а также для копирования информации с использованием функции "drag-and-drop".

Выполнение инструкций, описанных в статье, предполагает хорошее понимание концепций объекто-ориентированного программирования, таких как разработка интерфейсов и расширение классов. Необходимо иметь четкое представление о том, что такое интегрированная среда разработки Eclipse, однако опыт создания модулей для Eclipse не требуется. Инструкции и примеры, используемые в статье, расчитаны на версию Eclipse 3.2 или более позднюю. Для тестирования модуля требуется вложенный запуск Eclipse, так что убедитесь, что объем оперативной памяти на вашем компьютере позволяет одновременный запуск двух экземпляров Eclipse.

Начинаем разработку модуля

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

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

Модуль фрагментов кода для Example.com предоставляет следующие возможности:

  1. Древовидный список фрагментов кода, позволяющий найти фрагмент по категории.
  2. Окно настроек, где вы можете определить источник загрузки фрагментов.
  3. Контекстно-зависимое меню для вставки фрагмента в целевой код, открытый в редакторе
  4. Фрагменты кода с шаблонами переменных в форме ${variable}, а также мастер для ввода значений этих переменных.

Создание проекта

Для начала создадим новый проект, предназначенный для разработки модуля. К счастью, процесс создания такого проекта может быть значительно облегчен благодаря использованию мастера, который поможет вам пройти все шаги по генерации нового модуля. Итак, выполните следующие пункты:

  1. В главном меню Eclipse выберите File > New > Project. В результате вы увидите меню для выбора мастера создания проектов.
  2. В списке предлагаемых типов проектов разверните меню Plug-in Development. Выберите пункт Plug-in Project и нажмите Next для запуска мастера создания подключаемого модуля.
  3. В поле Project name название модуля SnippetsPlugin и нажмите Next.
  4. В открывшемся окне Plug-in Content оставьте все значения по умолчанию и нажмите Next.
  5. В окне Templates выберите Custom plug-in wizard и нажмите Next.
  6. В открывшемся списке предлагаемых шаблонов щелкните на кнопке Deselect All , а затем выберите следующие шаблоны:
    1. "Hello world" Action Set
    2. Popup Menu
    3. Preference Page
    4. View

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

  7. В окне Sample Action Set:
    1. Измените значение поля Action Class Name на SnippetAction.
    2. Нажмите Next для перехода к следующему окну.
  8. В окне Sample Popup Menu:
    1. Измените значение поля Name Filter на *.*.
    2. Измените значение поля Submenu Name на Snippet Submenu.
    3. Измените значение поля Action Class на SnippetPopupAction.
    4. Нажмите Next для перехода к следующему пункту.
  9. В окне Sample Preference Page:
    1. Измените значение Page Class Name на SnippetPreferencePage.
    2. Измените значение Page Name на Snippet Preferences.
    3. Нажмите Next для перехода к следующему пункту.
  10. В окне Main View Settings:
    1. Измените значение View Class Name на SnippetsView.
    2. Измените значение View Name на Example.com Snippets View.
    3. Измените значение View Category Name на Example.com Enterprise.
    4. Выберите опцию Tree viewer в качестве основного вида.
    5. Нажмите Next для перехода к следующему пункту.
  11. В окне View Features оставьте все опции выбранными и нажмите кнопку Finish для создания проекта.

Как только Eclipse завершит процесс создания нового проекта, в вашем рабочем пространстве появится проект SnippetsPlugin, включающий в себя некий набор файлов. Шаблоны, которые были выбраны в процессе создания проекта, сгенерировали несколько пакетов, включающих в себя исходные файлы Java™. Подробнее ознакомиться с каждым из использованных шаблонов вы сможете в разделе Изучаем шаблоны. Если же вы и так хорошо знакомы с шаблонами модулей или просто не хотите тратить время, вникая во все детали, переходите к разделу Тестирование подключаемого модуля.

Изучаем шаблоны

Создание модуля Eclipse по шаблонам может вызвать у человека, незнакомого со всеми деталями разработки модулей, вполне закономерный вопрос: что именно происходит в процессе создания нового проекта? Если описанные выше шаги выполнены корректно, то созданный проект включает в себя целый набор пакетов, содержащих как файлы с исходными кодом на Java, так и другие файлы. Новичок в деле разработки модулей Eclipse скорее всего затруднится ответить на вопрос, что это за файлы, и как они попали в проект. Подобное непонимание является нежелательным побочным эффектом использования мастера создания модулей.

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

Среда Eclipse IDE определяет набор операций ( action set ) как набор возможных действий, привязанных к пунктам меню или кнопкам, которые составляют одну логическую группу. Модуль может иметь определенный набор операций, так как кажется вполне естественным объединить все операции, осуществляемые в рамках функциональности модуля, в одну группу.

Шаблон набора оперций "Hello world" Action Set добавляет файл SnippetAction.java в пакет snippetssample.action. Имя класса определяется значением поля Action Class Name, назначаемом в окне Sample Action Set как показано на рисунке 1.

Рисунок 1. Конфигурация набора операций
Конфигурация набора операций

Когда вы запускаете модуль в Eclipse, набор операций "Hello, world" добавляет пункт Sample Menu в главное меню Eclipse. Текст заголовка меню определяется в файле plugin.xml, как показано в листинге 1:

Листинг 1. Определение набора операций модуля в файле plugin.xml

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

   <extension
         point="org.eclipse.ui.actionSets">
      <actionSet
            label="Sample Action Set"
            visible="true"
            id="SnippetsPlugin.actionSet">
         <menu
               label="Sample &Menu"
               id="sampleMenu">
            <separator
                  name="sampleGroup">
            </separator>
         </menu>
         <action
               label="&Sample Action"
               icon="icons/sample.gif"
               class="snippetsplugin.actions.SnippetAction"
               tooltip="Hello, Eclipse world"
               menubarPath="sampleMenu/sampleGroup"
               toolbarPath="sampleGroup"
               id="snippetsplugin.actions.SnippetAction">
         </action>
      </actionSet>
   </extension>
...
</plugin>

Если вы щелкнете мышкой на пункте Sample Menu, выполнится код, определенный в методе run класса SnippetAction. Код, включенный в шаблон (см. листинг 2), выведет на экран текстовое сообщение:

Листинг 2. Mетод run() класса SnippetAction

                
public void run(IAction action) {
    MessageDialog.openInformation(
        window.getShell(),
        "SnippetsPlugin Plug-in",
        "Hello, Eclipse world");
}

Контекстное меню

Шаблон контекстного меню позволяет добавить в ваш модуль меню, которое выводится на экран при нажатии правой кнопки мышки на файл, - такое меню еще называют всплывающим . Если вы щелкнете правой кнопкой мышки на имени файла в представлении Package Explorer, откроется меню, озаглавленное Snippet Submenu и содержащее один элемент New Action. Конфигурация шаблона контекстного меню показана на рисунке 2.

Рисунок 2. Конфигурация контекстного меню
Конфигурация контекстного меню

По умолчанию полю Target Object's Class присвоено значение org.eclipse.core.resources.IFile. Это значит, что контекстное меню будет выводиться на экран при нажатии правой кнопки мышки на имени файла. Вам могут понадобиться и другие возможные значения этого поля: IProject или IFolder. При выборе IProject контекстное меню будет выводиться на экран при нажатии правой кнопки мышки на имени проекта, а при выборе IFolder - при нажатии правой кнопки мышки на имени директории.

Поле Name Filter позволяет показывать контекстное меню только для определенного типа файлов. Например, если определить фильтр *.html, контекстное меню появится только при нажатии правой кнопки мышки на файлах с расширением .html . После того как мастер создания модулей завершил создание нового проекта, значение фильтра определено в файле plugin.xml.

Значение поля Submenu Name определяет название подменю контекстного меню. Для уже созданного проекта это значение определяется в файле plugin.xml и может при желании быть изменено.

Поле Action Label определяет название пункта подменю. Это значение также хранится в файле plugin.xml и может быть изменено в процессе разработки модуля.

Поле Java Package Name задает имя пакета, в котором содержится новый класс. Имя класса определяется значением поля Action Class . Эти значения, так же как и предыдущие, могут быть изменены в процессе работы над модулем, однако переименование пакета и класса потребует дополнительных шагов. При изменении названий пакета и класса вам необходимо будет использовать функцию Refactor Eclipse IDE, чтобы корректно изменить все ссылки на пакет и класс в файле plugin.xml.

В листинге 3 представлен фрагмент файла plugin.xml, описывающий функциональность контекстного меню.

Листинг 3. Точка расширения контекстного меню

                
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.2"?>
<plugin>
...
   <extension
         point="org.eclipse.ui.popupMenus">
      <objectContribution
            objectClass="org.eclipse.core.resources.IFile"
            nameFilter="*.*"
            id="SnippetsPlugin.contribution1">
         <menu
               label="Snippet Submenu"
               path="additions"
               id="SnippetsPlugin.menu1">
            <separator
                  name="group1">
            </separator>
         </menu>
         <action
               label="New Action"
               class="snippetsplugin.popup.actions.SnippetPopupAction"
               menubarPath="SnippetsPlugin.menu1/group1"
               enablesFor="1"
               id="SnippetsPlugin.newAction">
         </action>
      </objectContribution>
   </extension>
...
</plugin>

При выборе пункта контекстного меню Eclipse выполняет код, определенный в методе run() (см. листинг 4) класса SnippetPopupAction. По умолчанию этот метод выводит на экран текстовое сообщение.

Листинг 4. Метод run() класса SnippetPopupAction

                
public void run(IAction action) {
    Shell shell = new Shell();
    MessageDialog.openInformation(
        shell,
        "SnippetsPlugin Plug-in",
        "New Action was executed.");
}

Окно настроек подключаемого модуля

Шаблон окна настроек модуля дает возможность определить препочтительные настройки для работы модуля. Это окно открывается при выборе пункта Window > Preferences главного меню Eclipse. На рисунке 3 показана страница мастера модулей для конфигурации окна настроек.

Рисунок 3. Конфигурация окна настроек
Конфигурация окна настроек

Поле Java Package Name определяет имя пакета, содержащего окно настроек, а также всех классов для поддержки этого окна. Значение поля Page Class Name соответствует имени класса окна настроек, а значение поля Page Name определяет название окна, которое будет выводиться в списке, открывающемся при выборе пункта Window > Preferences. Название окна настроек, которое мы при создании проекта определили как Snippet Preferences, может быть изменено в файе plugin.xml.

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

Самая приятная особенность шаблона окна настроек - это то, что вам не надо писать код для хранения и извлечения параметров настройки, - об этом автоматически позаботятся элементы управления окна настроек.

Вид

Шаблон вида создает панель, которая будет автоматически открыта в Eclipse при запуске вашего модуля (подробную информацию по тестовому запуску см. в разделе Тестирование подключаемого модуля).

Окно Main View Settings , показанное на рисунке 4, предназначено для определения конфигурации вида.

Рисунок 4. Конфигурация вида
Конфигурация вида

Как и в рассмотренных выше шаблонах, поле Java Package Name определяет имя пакета, содержащего классы для реализации вида.

Поле View Class Name определяет имя Java-класса для создания вида.

Значение поля View Name определяет название вида так, как оно будет выводиться в списке видов, отображаемом при выборе пункта Window > Show View главного меню Eclipse.

Поле View Category ID определяет идентификатор категории, к которой следует причислить новый вид, а поле View Category Name - имя этой категории. Категории - это способ группировки видов. При открытии списка видов Window > Show View, виды сгруппированы и показываются по категориям.

Класс вида (SnippetView) генерируется в зависимости от выбранного типа вида. Если при создании вида вы выбрали опцию Table viewer , вид будет содержать только список элементов. Такой список создается с помощью метода getElements().

Если же выбрана опция Tree viewer , класс SnippetView будет создан несколько иначе. Метод getElements() будет изменен для создания объектов древовидной структуры класса TreeObject, и, кроме того, внутренний класс ViewContentProvider также будет модифицирован для реализации интерфейса ITreeContentProvider.

Классы вида

Класс SnippetView, созданный по шаблону вида, является наиболее сложным из всех классов, используемых в рамках нашего проекта. В развернутом виде класс SnippetView включает в себя следующие внутренние классы:

TreeObject
Класс определяет концевой узел дерева, который не включает в себя никаких других элементов.
TreeParent
Это расширение класса TreeObject определяет узел дерева, который может содержать другие элементы
ViewContentProvider
Класс ViewContentProvider реализует интерфейсы IStructuredProvider и ITreeContentProvider, а также получает данные для отображения в панели вида. Далее в этой статье мы модифицируем этот класс для отображения подробной информации о фрагментах кода.
ViewLabelProvider
Класс, определяющий пиктограммы элементов дерева.
NameSorter
Класс, который может быть модифицирован для реализации произвольной сортировки элементов в панели вида. В данной статье мы не будем затрагивать эту функциональность. Если вас интересует более подробная информация об этом классе, посмотрите детальное описание ViewerSorter в документации по Eclipse API (см. Ресурсы).

Тестирование подключаемого модуля

В результате выполнения инструкций из предыдущих разделов вы получили набор Java-классов, сгенерированных по выбранным шаблонам. Перед тем как двинуться дальше, следует протестировать полученный модуль, чтобы увидеть, что, собственно, делают эти шаблоны. Для тестирования модуля необходимо запустить еще один экземпляр Eclipse. Самый простой способ осуществить вложенный запуск - это щелкнуть правой кнопкой мышки по названию проекта в панели Package Explorer, а затем выбрать в контекстном меню пункт Run As > Eclipse Application.

Во вновь запущенном экземпляре Eclipse вы можете проверить, как будут выглядеть компоненты вашего модуля в работе. В результате использования шаблона набора операций в главном меню Eclipse появился пункт Sample Menu. Если выбрать пункт Sample Menu > Sample Item, Eclipse выведет на экран текстовое окно с заголовком SnippetSample Plug-in и приветствием "Hello, Eclipse world."

Теперь вы можете закрыть вложенный экземпляр Eclipse и отредактировать строку, выделенную жирным шрифтом в листинге 5. Она находится в классе SnippetAction.

Листинг 5. Параметры сообщения, выводимого на экран при выборе пункта SnippetAction

                
    public void run(IAction action) {
        MessageDialog.openInformation(
            window.getShell(),
            "SnippetsPlugin Plug-in",
            "Hello, Eclipse world");
    }

Запустите еще раз обновленный модуль как вложенный экземпляр Eclipse и убедитесь, что текстовое окно выводится с новым заголовком. Если же вам не нравится название пункта меню Sample Menu или его элемента, вы можете изменить их в файле plugin.xml. Соответствующие строки выделены в листинге 6 жирным шрифтом.

Листинг 6. Названия элементов меню в plugin.xml

                
<menu
      label="Sample &Menu"
      id="sampleMenu">
   <separator
         name="sampleGroup">
   </separator>
</menu>
<action
      label="&Sample Action"
      icon="icons/sample.gif"
      class="snippetsplugin.actions.SnippetAction"
      tooltip="Hello, Eclipse world"
      menubarPath="sampleMenu/sampleGroup"
      toolbarPath="sampleGroup"
      id="snippetsplugin.actions.SnippetAction">
</action>

Контекстное меню в Eclipse

Чтобы увидеть контекстное меню, добавленное в ваш модуль соответствующим шаблоном, еще раз запустите вложенный экземпляр Eclipse. Выберите любой файл в панели Package Explorer. Если панель Package Explorer пуста, создайте проект, содержаший какой-либо файл. Если вы щелкнете правой кнопкой мышки на имени файла, открывшееся контекстное меню будет содержать подменю Snippet Submenu, которое, в свою очередь, включает в себя единственный пункт New Action. При выборе этого пункта на экран выводится текстовое окно с сообщением New Action was executed.

Окно настроек в Eclipse

Чтобы увидеть созданное по шаблону окно настроек модуля, во вложенном экземпляре Eclipse выберите Window > Preferences. Список категорий в левой панели окна настроек будет содержать категорию Snippet Preferences. При выборе этой категории в правой панели окна настроек выводятся параметры настройки вашего модуля с целым набором возможных опций. Эти опции автоматически включаются в модуль при использовании шаблона окна настроек.

В дальнейшем вы измените эти настройки так, чтобы они представляли собой реальные значения, используемые при работе вашего модуля. Пока же попробуйте изменить значения настроек и сохранить свои изменения, используя кнопки Apply или OK, а затем закройте и откройте окно настроек еще раз, чтобы убедиться, что ваши изменения сохранились. Обратите внимание, что поддержка этой функциональности не требует от вас написания дополнительного кода.

Код, создающий окно настроек, принадлежит классу SnippetPreferencePage, соответствующий метод показан в листинге 7.

Листинг 7. Метод createFieldEditors () класса SnippetPreferencePage

                
public void createFieldEditors() {
    addField(new DirectoryFieldEditor(PreferenceConstants.P_PATH, 
            "&Directory preference:", getFieldEditorParent()));
    addField(
        new BooleanFieldEditor(
            PreferenceConstants.P_BOOLEAN,
            "&An example of a boolean preference",
            getFieldEditorParent()));

    addField(new RadioGroupFieldEditor(
            PreferenceConstants.P_CHOICE,
        "An example of a multiple-choice preference",
        1,
        new String[][] { { "&Choice 1", "choice1" }, {
            "C&hoice 2", "choice2" }
    }, getFieldEditorParent()));
    addField(
        new StringFieldEditor(PreferenceConstants.P_STRING, 
        "A &text preference:", getFieldEditorParent()));
}

Вид в Eclipse

Чтобы проверить, как выглядит вид, созданный для вашего модуля, во вложенном экземпляре Eclipse выберите пункт Window > Show View > Other. . Новый вид будет включен в категорию Example.com Enterprise, как определено в конфигурации вида (см. рисунок 4), сохраненной в файле plugin.xml. Откройте вид, выбрав пункт Example.com Snippet View и нажав кнопку OK.

По умолчанию панель вида включает в себя несколько элементов. На рисунке 5 изображено дерево таких элементов, выводимое в панели Example.com Snippets View вашего модуля.

Рисунок 5. Развернутое дерево элементов
Развернутое дерево элементов

Двойной щелчок мышкой на любом из элементов дерева выведет на экран текстовое сообщение вида "Double-click detected on XXX", где ХХХ - соответствующий элемент дерева. Для этого вида также определены два дополнительные операции. Они включены в меню вида, а также выводятся в контекстном меню при нажатии правой кнопки мышки в любом месте панели вида. Это операции определяются динамически в методе makeActions(), приведенном в листинге 8.

Листинг 8. Метод makeActions()

                
    private void makeActions() {
        action1 = new Action() {
            public void run() {
                showMessage("Action 1 executed");
            }
        };
        action1.setText("Action 1");
        action1.setToolTipText("Action 1 tooltip");
        action1.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages().
            getImageDescriptor(ISharedImages.IMG_OBJS_INFO_TSK));
        
        action2 = new Action() {
            public void run() {
                showMessage("Action 2 executed");
            }
        };
        action2.setText("Action 2");
        action2.setToolTipText("Action 2 tooltip");
        action2.setImageDescriptor(PlatformUI.getWorkbench().getSharedImages().
                getImageDescriptor(ISharedImages.IMG_OBJS_INFO_TSK));
        doubleClickAction = new Action() {
            public void run() {
                ISelection selection = viewer.getSelection();
                Object obj = ((IStructuredSelection)selection).getFirstElement();
                showMessage("Double-click detected on "+obj.toString());
            }
        };
    }

Построение интерфейса SnippetProvider

Для создания классов, которые будут извлекать фрагменты кода из заданного источника, определим сначала интерфейс, а затем уже приступим к разработке классов реализации. Интерфейс будет использоваться классом SnippetView для загрузки и отображения фрагментов кода в редакторе. Однако сам класс SnippetView не располагает никакой информацией о том, откуда и каким образом фрагменты кода попадают в среду разработки. Все это - задача интерфейса.

Начнем с создания интерфейса SnippetProvider. Выберите пункт File > New > Interface, в строке Package укажите пакет com.example.plugins.snippets.providers, в который следует включить новый интерфейс. Нажмите кнопку Finish для создания интерфейса.

Измените код SnippetProvider.java как показано в листинге 9.

Листинг 9. Интерфейс SnippetProvider

                
public interface SnippetProvider {
    
    public String[] getLanguages() throws SnippetProviderException;
    public String[] getCategories(String language) throws SnippetProviderException;
    public SnippetInfo[] getSnippetInfo(String language, String category) 
      throws SnippetProviderException;
    public Snippet getSnippet(SnippetInfo info);
    public void configure(Properties props) throws SnippetProviderConfigurationException;

}

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

Таблица 1. Методы интерфейса SnippetProvider

Метод

Описание

configure(Properties props)  Метод конфигурации адаптера.
getCategories(String language)  Метод возвращает категорию, к которой относится фрагмент кода. Набор возможных категорий определяется в зависимости от языка, на котором написан фрагмент кода.
getLanguages()  Метод возвращает список языков, который является верхнеуровневым критерием организации фрагментов кода.
getSnippet(SnippetInfo info)  Метод возвращает указанный фрагмент.
getSnippetInfo(String language, String category)  Метод возвращает массив объектов SnippetInfo для отображения в панели вида.

Классы, реализующие интерфейс SnippetFileProvider

Как только определен интерфейс, создание класса реализации интерфейса становится достаточно простым. В этом разделе мы создадим класс для извлечения фрагмента кода из директории файловой системы.

Вместо того чтобы вручную добавлять методы интерфейса в новый класс, предоставим Eclipse возможность поработать за нас и создать первоначальную версию класса. Щелкните правой кнопкой мышки на пакете com.example.plugins.snippets.providers и выберите пункт New > Class. В открывшемся окне создания класса в поле Name укажите имя класса SnippetFileProvider. Нажмите кнопку Add для добавления интерфейса. В открывшемся окне Choose interfaces введите интерфейс SnippetFileProvider и нажмите OK.

Среда разработки Eclipse создаст новый класс, который реализует интерфейс SnippetProvider. Новый класс корректно определяет все методы интерфейса и их возвращаемые значения, так что проект компилируется без ошибок.

Новый класс загружает фрагменты кода из файловой системы, используя дерево директорий в качестве каталога фрагментов, расположенных по языкам и категориям. Класс считывает информацию о фрагменте кода из XML файла snippets.info. Этот файл является собой XML-представлением массива элементов SnippetInfo, преобразованного с использованием класса XMLEncoder.

Приведенный ниже метод configure() предоставляет возможность конфигурации адаптера в зависимости от передаваемого объекта Properties. Для класса SnippetFileProvider определен только один объект Properties - универсальный идентификатор ресурсов (URI), путь к директории, содержащей фрагмент кода, относительно базового каталога хранилища фрагментов.

Листинг 10. Метод configure() класса SnippetFileProvider

                
    public void configure(Properties props)
            throws SnippetProviderConfigurationException {
        this.properties = props;
        String uriPath = "";
        if (baseUri == null) {
            try {
                uriPath = properties
                        .getProperty("snippetFileProvider.base.directory");

                if (uriPath == null // uriPath.length() == 0) {
                    throw new SnippetProviderConfigurationException(
                            "Please supply a value for property " +
                            "'snippetFileProvider.base.directory'");
                }

                baseUri = new URI(uriPath);

            } catch (URISyntaxException urie) {
                throw new SnippetProviderConfigurationException("URI '"
                        + uriPath + "' incorrectly formatted.", urie);
            }
        }
    }

Метод getLanguages() возвращает имена директорий верхнего уровня внутри базового каталога. Имена директорий верхнего уровня соответствуют языкам, на которых написаны фрагменты кода - Java, XML и HTML. Метод getLanguages(), определенный в классе SnippetFileProvider, приведен в листинге 11.

Листинг 11. Метод getLanguages() класса SnippetFileProvider

                
    public String[] getLanguages() throws SnippetProviderException {
        /*
         * Языки фрагментов кода определяются по названиям
         * директорий верхнего уровня внутри базового каталога хранилища.
         */
        if (languages == null) {

            languages = getFormattedNamesFromLocation(getBaseUri());
        }
        return languages;
    }

Метод getLanguages() вызывает метод getFormattedNamesFromLocation, который определен как private static. Этот метод возвращает массив элементов типа String, содержащий правильно отформатированные имена всех элементов верхнего уровня внутри базовой директории. Каждая директория верхнего уровня, соответствующая какому-либо языку, включает в себя набор директорий для распределения фрагментов кода по категориям. Например, для языка Java могут использоваться категории фрагментов для обработки событий или для регистрации. Метод, возвращающий список категорий для конкретного языка программирования, приведен в листинге 12.

Листинг 12. Метод getCategories() класса SnippetFileProvider

                
    public String[] getCategories(String language)
            throws SnippetProviderException {
        try {
            return getFormattedNamesFromLocation(new URI(getBaseUri().getPath()
                    + "/" + language));
        } catch (URISyntaxException e) {
            throw new SnippetProviderException(
                    "Error while loading the categories", e);
        }
    }

Метод getSnippetInfo() в зависимости от передаваемых ему в качестве параметров языка и категории фрагмента кода извлекает массив объектов SnippetInfo из файла snippets.info, найденного в папке соответствующей категории.

Листинг 13. Методы getSnippetInfo() и getSnippet() класса SnippetFileProvider

                
    public SnippetInfo[] getSnippetInfo(String language, String category)
            throws SnippetProviderException {
        /* Извлекаем информацию о фрагменте кода из файловой системы */
        XMLDecoder decoder = null;
        SnippetInfo[] snippetInfo = null;

        try {
            decoder = new XMLDecoder(new BufferedInputStream(
                    new FileInputStream(buildSnippetInfoPath(getBaseUri(),
                            language, category))));
            snippetInfo = (SnippetInfo[]) decoder.readObject();

        } catch (FileNotFoundException e) {
            throw new SnippetProviderException(
                    "Could not load the snippet info index.", e);
        } finally {
            if (decoder != null) {
                decoder.close();
            }
            decoder = null;
        }

        return snippetInfo;
    }
    
    public Snippet getSnippet(SnippetInfo info) {
        Snippet snippet = null;

        String snippetPath = buildSnippetPath(getBaseUri(), info);

        /* Загружаем фрагмент кода из файла */
        BufferedInputStream stream = null;
        BufferedReader reader = null;
        try {
            stream = new BufferedInputStream(new FileInputStream(snippetPath));
            reader = new BufferedReader(new InputStreamReader(
                    stream));
            String line;
            StringBuffer sb = new StringBuffer();
            while ((line = reader.readLine()) != null) {
                sb.append(line);
                sb.append("\n");
            }
            
            snippet = new Snippet();
            snippet.setContent(sb.toString());
            
            sb = null;
            
        } catch (FileNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        } catch (IOException ioe) {
            // TODO Auto-generated catch block
            ioe.printStackTrace();
        }
        finally
        {
            try {
                if (reader != null) {
                    reader.close();
                }
                if (stream != null) {
                    stream.close();
                }
            } catch (IOException ioe) {
            }
        }

        return snippet;
    }

В заключение приведенный выше метод getSnippet() загружает фрагмент кода из файловой системы и возвращает его в качестве объекта Snippet. Остальные методы классса являются внутренними и определены как private. Они используются для поддержки ранее описанных внешних (public) методов. Вы можете подробно изучить их, загрузив весь исходный код, предлагаемый в дополнение к этой статье.

Расширение функциональности вида

Теперь, располагая структурой для загрузки фрагментов кода из директорий файловой системы, мы можем приступить к дальнейшей разработке кода класса SnippetView для отображения информации о фрагментах кода в панели вида, добавленного ранее к вашему модулю. Чтобы показать информацию о фрагментах кода, мы модифицируем метод initialize() внутреннего класса ViewContentProvider, включенного в класс SnippetView.

Метод initialize() создает дерево объектов, выводимых на экран в панели вида. При создании первоначальной версии класса по шаблону он включает в себя набор фиктивных данных для демонстрации работы вида при запуске модуля. Если вы выполнили все описанные ранее шаги, то теперь вы можете изменить метод initialize() так, чтобы он использовал интерфейс SnippetProvider для загрузки информации о фрагментах кода.

Измененный метод показан в листинге 14.

Листинг 14. Модифицированный метод initialize() класса SnippetsView

                
  private void initialize() {

      invisibleRoot = new TreeParent("");

      /*
       * Получаем от провайдера названия элементов верхнего уровня базового
       * каталога, которые соответствуют языкам программирования.
       */
      String[] topLevelNodes;

      try {

          Properties properties = new Properties();

          InputStream is = null;

          is = SnippetProviderFactory.class
                  .getResourceAsStream("/SnippetProvider.properties");
          properties.load(is);

          if (is != null) {
              try {
                  is.close();
              } catch (IOException innerE) {
                  throw new SnippetProviderException(
                          "Could not close resource stream.", innerE);
              }
          }

          snippetProvider = SnippetProviderFactory.createInstance();
          snippetProvider.configure(properties);
          topLevelNodes = snippetProvider.getLanguages();

          for (int i = 0; i < topLevelNodes.length; i++) {
              TreeParent parent = new TreeParent(topLevelNodes[i]);

              /* Для каждого языка получаем список категорий */
              String[] categories = snippetProvider
                      .getCategories(topLevelNodes[i]);
              for (int j = 0; j < categories.length; j++) {
                  TreeParent categoryParent = new TreeParent(
                          categories[j]);

                  /* Для каждой категории получаем список фрагментов кода */
                  SnippetInfo[] info = snippetProvider.getSnippetInfo(
                          topLevelNodes[i], categories[j]);

                  for (int k = 0; k < info.length; k++) {
                      TreeObject leaf = new TreeObject(info[k]);
                      categoryParent.addChild(leaf);
                  }

                  parent.addChild(categoryParent);
              }

              invisibleRoot.addChild(parent);
          }

      } catch (SnippetProviderConfigurationException spce) {
          topLevelNodes = new String[] { "Configuration error:  "
                  + spce.getLocalizedMessage() };
      } catch (SnippetProviderException spe) {
          topLevelNodes = new String[] { "Error while loading snippets" };
      } catch (IOException ioe) {
          topLevelNodes = new String[] { "Error loading configuration properties:"
                  + ioe.getLocalizedMessage() };
      }

  }

Модифицированный метод динамически загружает класс, используя фабрику SnippetProviderFactory, и тут же преобразует результат в соответствии с интерфейсом SnippetProvider. Это позволяет виду не зависеть от источника фрагментов кода. При обращении к интерфейсу провайдера конфигурация провайдера позволяет определить источник фрагментов кода. После этого в исходное дерево добавляется по одному узлу верхнего уровня для каждого языка.

 
Слишком много операций?

Просмотрев реализацию SnippetProvider, вы можете вполне обоснованно раскритиковать ее за излишнее количество действий. Код выполняет повторяющиеся однотипные операции, проходя по директориям, соответствующим языкам и категориям, и извлекая элементы, расположенные внутри этих директорий. Рациональнее было бы загрузить сразу всю файловую структуру. Такой код, выполняющий меньшее количество операций, больше подошел бы для написания Web-сервиса. Вы можете выбрать наиболее подходящую вам реализацию, исходя из ваших требований к быстродействию и масштабируемости.

Затем код проходит по языкам и итерационно для каждого языка обходит категории, добавляя их в качестве подчиненных узлов к узлу языка. Для каждой категории вид вызывает метод getSnippetInfo() для получения информации о фрагменте кода, входящего в категорию. Метод вызывается итерационо по числу фрагментов в каждой категории, создавая каждый раз новый экземпляр TreeObject для каждого элемента SnippetInfo.

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

Модификация класса TreeObject

Чтобы получить всю необходимую информацию о выбранном фрагменте кода, необходимо привязать информацию о фрагменте кода к объекту TreeObject. Для этого нужно слегка изменить класс TreeObject, а именно заменить внутреннее поле типа String на внутреннее поле типа SnippetInfo. Кроме того, необходимо изменить конструктор и метод getName().

Модифицированный класс TreeObject показан в листинге 15, изменения выделены жирным шрифтом.

Листинг 15. Модификация класса TreeObject

                
    class TreeObject implements IAdaptable {
        private SnippetInfo info;

        private TreeParent parent;

        public TreeObject(SnippetInfo info) {
            this.info = info;
        }

        public String getName() {
            return info.getName();
        }

        public void setParent(TreeParent parent) {
            this.parent = parent;
        }

        public TreeParent getParent() {
            return parent;
        }

        public String toString() {
            return getName();
        }

        public SnippetInfo getInfo() {
            return info;
        }

        public Object getAdapter(Class key) {
            return null;
        }
    }

Поскольку класс TreeParent является расширением TreeObject, необходимо внести аналогичные изменения и в TreeParent.

Листинг 16. Модифицированный класс TreeParent

                
    class TreeParent extends TreeObject {
        private ArrayList children;

        private String name;

        public TreeParent(String name) {
            super(null);
            this.name = name;
            children = new ArrayList();
        }

        @SuppressWarnings("unchecked")
        public void addChild(TreeObject child) {
            children.add(child);
            child.setParent(this);
        }

        public void removeChild(TreeObject child) {
            children.remove(child);
            child.setParent(null);
        }

        @SuppressWarnings("unchecked")
        public TreeObject[] getChildren() {
            return (TreeObject[]) children.toArray(new TreeObject[children
                    .size()]);
        }

        public boolean hasChildren() {
            return children.size() > 0;
        }

        @Override
        public String getName() {
            // TODO Auto-generated method stub
            return this.name;
        }

        @Override
        public String toString() {
            return this.getName();
        }
    }

Изменения включают в себя подмену функций базового класса getName() и toString(), и модификацию конструктора. Он не должен передавать пустое значение конструктору верхнего уровня, а также должен присваивать имя новой внутренней переменной типа String.

Подготовка структуры директорий

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

Сначала создайте одну или несколько директорий для языков программирования. Затем внутри каждой из таких директорий создайте одну или несколько директорий, соответствующих категориям фрагментов. Наконец, в каждой категории добавьте файл под названием snippets.info, содержимое которого показано в листинге 17.

Листинг 17. Образец файла snippets.info

                
<?xml version="1.0" encoding="UTF-8"?> 
<java version="1.5.0_07" class="java.beans.XMLDecoder"> 
 <array class="com.example.plugins.snippets.SnippetInfo" length="1"> 
  <void index="0"> 
   <object class="com.example.plugins.snippets.SnippetInfo"> 
    <void property="category"> 
     <string>Exception_Handling</string> 
    </void> 
    <void property="language"> 
     <string>Java</string> 
    </void> 
    <void property="name"> 
     <string>Exception class</string> 
    </void> 
    <void property="variables"> 
     <array class="java.lang.String" length="3"> 
      <void index="0"> 
       <string>exception.classname</string> 
      </void> 
      <void index="1"> 
       <string>author</string> 
      </void> 
      <void index="2"> 
       <string>package.name</string> 
      </void> 
     </array> 
    </void> 
   </object> 
  </void> 
 </array> 
</java> 

Приведенный выше пример snippets.info следует изменить в соответствии с созданной вами структурой языков и категорий. После добавления файла информации поместите в соотвествующие директории фрагменты кода. Пример окончательной структуры директорий показан в листинге 18.

Листинг 18. Структура директорий хранилища фрагментов кода

                
+- basedir
 +- Java
 / +- Exception_Handling
 /   +- snippets.info
 /   +- Exception_Class.snippet
 +- XML
   +- Ant_Build_Files
     +- snippets.info
     +- Simple_File.snippet

Просмотр изменений

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

Рисунок 6. Вид, отображающий дерево фрагментов
Вид, отображающий дерево фрагментов

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

Создание класса InsertSnippetAction

Теперь, когда провайдер SnippetFileProvider загружает информацию о фрагментах кода в вид, можно переходить к разработке кода, который, используя информацию о фрагменте, получает фрагмент от провайдера и вставляет его в открытое окно редактора. Информация о фрагменте хранится в экземпляре SnippetInfo, который доступен при выборе соответствующего элемента в панели Example.com Snippets View.

Начнем с создания внутреннего класса InsertSnippetAction, который содержит код для получения фрагмента кода и вставки его в редактор. Этот класс является расширением класса Action, который может использоваться другими компонентами модуля. Класс InsertSnippetAction является внутренним классом класса SnippetsView, так как он используется только внутри вида и предоставляет быстрый доступ к объектам в панели вида.

Код нового класса InsertSnippetAction показан в листинге 19. Он определяет метод run() для вывода текстового сообщения. Такое сообщение необходимо для проверки того, что класс корректно написан и правильно загружается.

Листинг 19. Класс InsertSnippetAction с тестовым вариантом метода run()

                
class InsertSnippetAction extends Action {

    @Override
    public void run() {
        MessageDialog.openInformation(
            window.getShell(),
            "SnippetsSample Plug-in",
            "Running Insert Action Now!");
    }
}

Модификация SnippetView

Исходная версия класса SnippetView, созданная по шаблону, включает набор примеров действий, доступных через основное и контекстное меню вида. Эти действия являются внутренними и не определены в файле манифеста plugin.xml как точки расширения. Такая реализация является корректной, так как действия определены и используются только внутри вида и не являются доступными другим компонентам IDE.

После добавления класса InsertSnippetAction откройте класс SnippetsView и удалите действия action2 и doubleClickAction. Сохраните action1 - используя механизм переименования Eclipse, вы легко сможете переименовать его в insertAction и изменить его тип с Action на InsertAction. Так как InsertAction является расширением класса Action, больше ничего менять не надо. Модифицированный класс SnippetsView показан в листинге 20, изменения выделены жирным шрифтом.

Листинг 20. Класс SnippetsView

                
public class SnippetsView extends ViewPart implements ISelectionListener {
    private TreeViewer viewer;

    private DrillDownAdapter drillDownAdapter;

    private InsertSnippetAction insertAction;

    private SnippetProvider snippetProvider;

    class InsertSnippetAction extends Action {
        // Код приведен в предыдущих листингах.
    }

    class TreeObject implements IAdaptable {
        // Код приведен в предыдущих листингах.
    }

    class TreeParent extends TreeObject {
        // Код приведен в предыдущих листингах.
    }

    class ViewContentProvider implements IStructuredContentProvider,
            ITreeContentProvider {
        private TreeParent invisibleRoot;

        public void inputChanged(Viewer v, Object oldInput, Object newInput) {
        }

        public void dispose() {
        }

        public Object[] getElements(Object parent) {
            if (parent.equals(getViewSite())) {
                if (invisibleRoot == null)
                    initialize();
                return getChildren(invisibleRoot);
            }
            return getChildren(parent);
        }

        public Object getParent(Object child) {
            if (child instanceof TreeObject) {
                return ((TreeObject) child).getParent();
            }
            return null;
        }

        public Object[] getChildren(Object parent) {
            if (parent instanceof TreeParent) {
                return ((TreeParent) parent).getChildren();
            }
            return new Object[0];
        }

        public boolean hasChildren(Object parent) {
            if (parent instanceof TreeParent)
                return ((TreeParent) parent).hasChildren();
            return false;
        }

        private void initialize() {
            // Код приведен в предыдущих листингах.
        }

    }

    class ViewLabelProvider extends LabelProvider {

        public String getText(Object obj) {
            return obj.toString();
        }

        public Image getImage(Object obj) {
            String imageKey = ISharedImages.IMG_OBJ_ELEMENT;
            if (obj instanceof TreeParent)
                imageKey = ISharedImages.IMG_OBJ_FOLDER;
            return PlatformUI.getWorkbench().getSharedImages().getImage(
                    imageKey);
        }
    }

    class NameSorter extends ViewerSorter {
    }

    /**
     * Конструктор.
     */
    public SnippetsView() {
    }

    /**
     * Обратный вызов для создания и инициализации просмотрщика вида.
     */
    public void createPartControl(Composite parent) {
            viewer = new TreeViewer(parent, SWT.MULTI / SWT.H_SCROLL / SWT.V_SCROLL);
        drillDownAdapter = new DrillDownAdapter(viewer);
        viewer.setContentProvider(new ViewContentProvider());
        viewer.setLabelProvider(new ViewLabelProvider());
        viewer.setSorter(new NameSorter());
        viewer.setInput(getViewSite());
        // Определяем себя в качестве "слушателя" выделенного элемента.
        getSite().getPage().addSelectionListener(this);

        // Определяем действия для выделенного элемента.
        selectionChanged(null, getSite().getPage().getSelection());
        makeActions();
        hookContextMenu();
        hookDoubleClickAction();
        contributeToActionBars();

    }

    private void hookContextMenu() {
        MenuManager menuMgr = new MenuManager("#PopupMenu");
        menuMgr.setRemoveAllWhenShown(true);
        menuMgr.addMenuListener(new IMenuListener() {
            public void menuAboutToShow(IMenuManager manager) {
                SnippetsView.this.fillContextMenu(manager);
            }
        });
        Menu menu = menuMgr.createContextMenu(viewer.getControl());
        viewer.getControl().setMenu(menu);
        getSite().registerContextMenu(menuMgr, viewer);
    }

    private void contributeToActionBars() {
        IActionBars bars = getViewSite().getActionBars();
        fillLocalPullDown(bars.getMenuManager());
        fillLocalToolBar(bars.getToolBarManager());
    }

    private void fillLocalPullDown(IMenuManager manager) {
        manager.add(insertAction);
        manager.add(new Separator());
    }

    private void fillContextMenu(IMenuManager manager) {
        manager.add(insertAction);
        manager.add(new Separator());
        drillDownAdapter.addNavigationActions(manager);
        // Другие подключаемые модули могут здесь добавить свои действия.
        manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS));
    }

    private void fillLocalToolBar(IToolBarManager manager) {
        manager.add(insertAction);
        manager.add(new Separator());
        drillDownAdapter.addNavigationActions(manager);
    }

    private void makeActions() {
        
        insertAction = new InsertSnippetAction();
        insertAction.setText("Insert Snippet");
        insertAction.setToolTipText("Inserts the selected snippet");
        insertAction.setImageDescriptor(PlatformUI.getWorkbench()
                .getSharedImages().getImageDescriptor(
                        ISharedImages.IMG_OBJS_INFO_TSK));
    }

    private void hookDoubleClickAction() {
        viewer.addDoubleClickListener(new IDoubleClickListener() {
            public void doubleClick(DoubleClickEvent event) {
                insertAction.run();
            }
        });
    }

    /**
     * Передача фокуса соответствующему элементу управления.
     */
    public void setFocus() {
        viewer.getControl().setFocus();
    }

    public void selectionChanged(IWorkbenchPart part, ISelection selection) {
    }
    
}

При тестировании модуля убедитесь, что двойной щелчок мышкой на элементе дерева выводит сообщение "Running insert action on XXX", где XXX - соответствующий элемент дерева в виде Example.com Snippets View. То же самое действие должно выполниться, если вы нажмете правую кнопку мышки и выберете пункт Action 1 в контекстном меню, или если вы нажмете соответствующую кнопку в меню вида.

Окончательный код InsertAction

Если двойной щелчок мышки на элементе и контекстное меню вида работают правильно, можно переходить к фактической реализации класса InsertSnippetAction. Новый усовершенствованный метод run() показан ниже.

Листинг 21. Окончательный вариант метода run() класса InsertSnippetAction

                
class InsertSnippetAction extends Action {

    @Override
    public void run() {

        IEditorPart target = getViewSite().getWorkbenchWindow()
                .getActivePage().getActiveEditor();
        if (target != null) {
            ITextEditor textEditor = null;
            if (target instanceof ITextEditor) {
                textEditor = (ITextEditor) target;
                ISelectionProvider sp = textEditor.getSelectionProvider();
                ITextSelection sel = (ITextSelection) sp.getSelection();

                IDocument doc = textEditor.getDocumentProvider()
                        .getDocument(textEditor.getEditorInput());
                try {
                    String text = getMergedSnippetContent();
                    doc.replace(sel.getOffset(), sel.getLength(), text);
                } catch (BadLocationException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }
}

Одна из функций метода run() - вызов метода getMergedSnippetContent(), код которого приведен в листинге 22. Остальной код run() предназначен для получения доступа к текущему редактору, определения выделенного в окне редактора текста и замены этого текста на другой, возвращаемый методом getMergedSnippetContent().

Листинг 22. Метод getMergedSnippetContent()

                
public String getMergedSnippetContent()
{
    String result = "";
    ISelection selection = viewer.getSelection();
    Object obj = ((IStructuredSelection) selection).getFirstElement();

    if (obj instanceof TreeObject) {
        SnippetInfo info = ((TreeObject) obj).getInfo();
        Snippet s = snippetProvider.getSnippet(info);

        SnippetVariablesWizardPage page = new SnippetVariablesWizardPage(
                "", info, s);
        page.setTitle("Snippet Variables");
        page.setDescription("Input the values to replace the template "
                + "variables found in this snippet.");

        // Создаем новый экземпляр мастера ввода значений переменных
        SnippetVariablesWizard wizard = new SnippetVariablesWizard(page);

        // Создаем и открываем экземпляр страницы мастера для ввода
		// значений переменных
        WizardDialog dialog = new WizardDialog(viewer.getControl()
                .getShell(), wizard);
        dialog.open();

        result = s.getMergedContent();
    }
    return result;
}

Метод getMergedSnippetContent() использует мастер и страницу мастера для ввода значений переменных фрагмента. Компиляция проекта на данном этапе покажет ошибки, так как мы еще не создали классы SnippetVariablesWizardPage и SnippetVariablesWizard. Эти классы будут созданы в следующем разделе.

Добавление ввода значений пользователем

Создание механизма, позволяющего пользователю вводить значения для переменных фрагмента кода, является на удивление простой задачей, хотя и потребует довольно большого количества деталей. Эти детали представляют собой элементы управления, предоставляемые стандартным набором Standard Widget Toolkit. До сих пор нам не приходилось о них думать, так как всю "грязную" работу за нас выполняли шаблоны. Однако при правильном порядке построения все эти элементы достаточно легко соединяются друг с другом. В этом разделе мы создадим страницу мастера, содержащую таблицу для ввода значений переменных пользователем.

Класс SnippetVariablesWizardPage

Класс SnippetVariablesWizardPage является расширением класса WizardPage, который, в свою очередь, является частью Eclipse Platform API и предоставляет базовую реализацию страницы мастера.

Существует несколько готовых реализаций страницы мастера, основанных на WizardPage, например страница мастера для создания файла WizardNewFileCreationPage. Но поскольку мы начинаем разработку собственного мастера "с чистого листа", то нам вполне подойдет исходный класс WizardPage. Полный код класса SnippetVariablesWizardPage показан в листинге 23. Он содержит несколько внутренних классов, что слегка усложняет код, но практически весь код класса предназначен для реализации таблицы для ввода значений пользователем. Таблица является редактируемой, что объясняет достаточно большой объем кода, необходимого для ее создания. Если бы таблица предназначалась только для чтения, вы смогли бы легко создать ее, используя объект Table и добавляя в него нужное количество строк. Если вы просмотрите код клсса SnippetsView, то увидите множество внутренних классов, реализующих те же интерфейсы, что и страница мастера.

Листинг 23. Класс SnippetVariablesWizardPage

                
public class SnippetVariablesWizardPage extends WizardPage {

    // Use the Table widget
    // http://www.eclipse.org/swt/widgets/
    // http://help.eclipse.org/help31/nftopic/org.eclipse.platform.doc.isv
/reference/api/org/eclipse/swt/widgets/Table.html
    private Table propertyTable;
    private TableViewer tableViewer;
    private SnippetInfo snippetInfo;
    private Snippet snippet;
    private static final String[] columnNames = new String[]{"property", "value"};
    private SnippetVariableValue[] variables;

    class SnippetVariableValue
    {
  		// Пропущенный здесь код приведен в Листинге 24
    }
    
    class SnippetVariableContentProvider implements IStructuredContentProvider {
		// Пропущенный здесь код приведен в Листинге 25
    }
    
    class SnippetVariableLabelProvider extends LabelProvider implements
            ITableLabelProvider {
 		// Пропущенный здесь код приведен в Листинге 26
    }
    
    class SnippetVariableCellModifier implements ICellModifier {
		// Пропущенный здесь код приведен в Листинге 27
    }
    
    private SnippetVariableValue[] initializeData(SnippetInfo info)
    {
     SnippetVariableValue[] result = new SnippetVariableValue[info.getVariables().length];
        for (int i = 0; i < info.getVariables().length; i++) {
            result[i] = new SnippetVariablesWizardPage.SnippetVariableValue(
                info.getVariables()[i]);
        }
        return result;
    }

    public SnippetVariablesWizardPage(String pageName, 
        SnippetInfo info, Snippet snippet) {
        super(pageName);
        this.snippetInfo = info;
        this.snippet = snippet;
        this.variables = initializeData(this.snippetInfo);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.jface.dialogs.IDialogPage#createControl(
     * org.eclipse.swt.widgets.Composite)
     */
    public void createControl(Composite parent) {
        // Создаем таблицу и определяем ее размер
        Composite container = new Composite(parent, SWT.NONE);
        container.setLayout(new FillLayout());
        container.setLayoutData(new GridData(GridData.FILL_BOTH));

        // titleText = new Text(mainComposite, SWT.BORDER / SWT.SINGLE);
        propertyTable = new Table(container, SWT.BORDER / SWT.SINGLE);
        propertyTable.setHeaderVisible(true);
        propertyTable.setLinesVisible(true);
        int colWidth = 160;
        TableColumn column = new TableColumn(propertyTable, SWT.LEFT, 0);
        column.setText("Property Name");
        column.setWidth(colWidth);
        // Второй столбец
        column = new TableColumn(propertyTable, SWT.LEFT, 1);
        column.setText("Value");
        column.setWidth(colWidth);
        
        // Проходим по всем переменным, указанным в информации о фрагменте
        // кода, и добавляем их в таблицу.
        tableViewer = new TableViewer(propertyTable);
        tableViewer.setUseHashlookup(true);
        tableViewer.setColumnProperties(columnNames);
        CellEditor[] editors = new CellEditor[columnNames.length];
        
        TextCellEditor textEditor = new TextCellEditor(propertyTable);
        ((Text) textEditor.getControl()).setTextLimit(60);
        editors[0] = textEditor;
        
        textEditor = new TextCellEditor(propertyTable);
        ((Text) textEditor.getControl()).setTextLimit(60);
        editors[1] = textEditor;
        
        tableViewer.setCellEditors(editors);
        tableViewer.setCellModifier(new SnippetVariableCellModifier());
        
        tableViewer.setContentProvider(new SnippetVariableContentProvider());
        tableViewer.setLabelProvider(new SnippetVariableLabelProvider());
        tableViewer.setInput(this.variables);

        // Настройки страницы
        setControl(container);
        setPageComplete(true);

    }
    
    public void updateSnippet()
    {
        /* Получаем данные из таблицы */
        HashMap map = createMap(variables);
        snippet.mergeContent(map);
    }
    
    @SuppressWarnings("unchecked")
    private HashMap createMap(SnippetVariableValue[] variables)
    {
        HashMap map = new HashMap();
        
        for (int i = 0; i < variables.length; i++)
        {
            map.put(variables[i].property, variables[i].value);
        }
        
        return map;
    }

}

Метод createControl() создает объекты TableViewer и Table и определяет их параметры. Сама таблица используется для представления информации пользователю, однако класс TableViewer позволяет легко добавить различные типы редакторов для разных столбцов таблицы. Кроме того, TableViewer дает возможность вводить данные в таблицу с помощью метода setInput(). Единственным недостатком использования этого метода является необходимость предварительной реализации провайдера значений переменных SnippetVariableContentProvider. Преимущество же метода - то, что вы можете расширить и дополнить таблицу без итерационного прохода по всем переменным и назначения им значений вручную.

Класс SnippetVariableValue

Это простой внутренний класс для хранения данных, вывода их в виде таблицы и редактирования данных пользователем. Класс определяет два внешних (public) поля: одно соответствует названию параметра или переменной, в другом хранится значение этого параметра или переменной, введенное пользователем. Массив таких объектов и используется для заполнения таблицы мастером, по одному объекту класса SnippetVariableValue для каждой переменной фрагмента кода.

Листинг 24. Класс SnippetVariableValue

                
class SnippetVariableValue
{
    public String property;
    public String value;
    
    public SnippetVariableValue(String property)
    {
        this.property = property;
        this.value = "";
    }
}

Класс SnippetVariableContentProvider

Провайдер значений переменных - внутренний класс, реализующий интерфейс IStructuredContentProvider. Код класса показан в листинге 25.

Листинг 25. Класс SnippetVariableContentProvider

                
class SnippetVariableContentProvider implements IStructuredContentProvider {
    public void dispose() {
    }

    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
    }

    public Object[] getElements(Object parent) {
        return variables;
    }
}

В рамках данной статьи класс SnippetVariableContentProvider реализует единственный метод getElements(), который возвращает значение переменной, инициализированной конструктором страницы мастера.

Класс SnippetVariableLabelProvider

Класс провайдера меток реализует интерфейс ITableLabelProvider и является расширением класса LabelProvider. Интерфейс ITableLabelProvider и класс LabelProvider являются элементами Eclipse Platform API. Код провайдера приведен в листинге 26.

Листинг 26. Класс SnippetVariableLabelProvider

                
class SnippetVariableLabelProvider extends LabelProvider implements
        ITableLabelProvider {
    public Image getColumnImage(Object element, int columnIndex) {
        return null;
    }

    public String getColumnText(Object element, int columnIndex) {
        SnippetVariableValue variable = (SnippetVariableValue) element;
        if (columnIndex == 0) {
            return variable.property;
        } else if (columnIndex == 1) {
            return (variable.value != null) ? variable.value : "Type value";
        } else {
            return "";
        }
    }
}

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

Класс SnippetVariableCellModifier

Класс SnippetVariableCellModifier реализует интерфейс ICellModifier, включенный в Eclipse Platform API. Интерфейс предоставляет метод, с помощью которого вы разрешаете или не разрешаете пользователю изменять значение ячейки таблицы, а также позволяет извлечь значение из ячейки и редактировать его. Код показан в листинге 27.

Листинг 27. Класс SnippetVariableCellModifier

                
class SnippetVariableCellModifier implements ICellModifier {

    public boolean canModify(Object element, String property) {
        return (!property.equals("property"));
    }

    public Object getValue(Object element, String property) {
        Object result;
        SnippetVariableValue variable = (SnippetVariableValue) element;

        if (property.equals("property")) {
            result = variable.property;
        } else if (property.equals("value")) {
            result = variable.value;
        } else {
            result = "";
        }

        return result;
    }

    public void modify(Object element, String property, Object value) {
        TableItem item = (TableItem) element;
        SnippetVariableValue variable = (SnippetVariableValue) item
                .getData();
        if (property.equals("value")) {
            variable.value = (value != null) ? value.toString() : "";
        }
        tableViewer.update(variable, null);
    }
    }

 
Не видите своих изменений?

Метод modify() включает один очень важный шаг: вызов update() для сохранения изменений в таблице. Если вы пропустили этот вызов, то измененные значения переменных в таблице не сохраняются.

Пользователь не должен иметь возможность изменять имя переменной в шаблоне фрагмента кода. Это требование довольно легко реализовать. Модифицируйте метод canModify() так, чтобы он возвращал значение false для ячеек в столбце property. Если же вы хотите создать таблицу, в которой пользователь может менять значения во всех столбцах, вы можете изменить метод canModify() так, чтобы он возвращал значение true для всех ячеек.

По сравнению с классом SnippetVariablesWizardPage класс SnippetVariablesWizard выглядит несколько легковесно. Полный код класса SnippetVariablesWizard показан в листинге 28. Этот класс является расширением класса Wizard, входящего в Eclipse Platform API.

Листинг 28. Класс SnippetVariablesWizard class

                
public class SnippetVariablesWizard extends Wizard {
    
    public SnippetVariablesWizard(SnippetVariablesWizardPage page)
    {
        addPage(page);
    }

    @Override
    public boolean performFinish() {
                      SnippetVariablesWizardPage page = 
                          (SnippetVariablesWizardPage)getPages()[0];
        page.updateSnippet();
        return true;
    }

}

Страница мастера добавляется в мастер методом addPage(). Мы можем не беспокоиться о том, как работает этот метод, так как он является частью базового класса Wizard. Метод performFinish() выполняется при нажатии на кнопку Finish в окне мастера. Этот метод должен возвращать значение true для закрытия страницы. Если возвращаемое значение false, страница мастера остается открытой. В тех случаях, когда применение и сохранение изменений требует большого количества времени, рекомендуется запускать процесс, реализующий интерфейс IRunnableWithProgress.

Открытая в Eclipse страница мастера ввода значений переменных выглядит так, как показано на рисунке 7.

Рисунок 7. Страница мастера ввода значений переменных
Страница мастера ввода значений переменных

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

Обработка новых событий

Теперь, когда вы добавили мастер ввода значений переменных и страницу мастера, ваш модуль умеет загружать фрагменты кода, используя провайдер, и изменять переменные кода, используя мастер ввода значений. Кроме того, модуль умеет реагировать на двойной щелчок мышкой и на нажатие правой кнопки мышки на элементе в панели вида. Расширим функциональность модуля, добавив возможность копирования путем перетаскивания (drag-and-drop). Eclipse Platform API включает классы, которые помогут реализовать "drag-and-drop"-копирование из вида Example.com Snippets View в редактор. Новая функциональность будет работать следующим образом: при выделении мышкой элемента дерева фрагментов и перетаскивании его в окно редактора запустится мастер ввода значений переменных, а затем результирующий фрагмент кода будет вставлен в редактор.

Класс SnippetDragDropListener

Для добавления поддержки drag-and-drop создадим новый внутренний класс. Он реализует интерфейс TransferDragSourceListener. Интерфейс включает в себя три метода, два из них мы модифицируем в рамках этой статьи. Код нового класса показан в листинге 29.

Листинг 29. Класс SnippetDragDropListener

                
class SnippetDragDropListener implements TransferDragSourceListener {

    public Transfer getTransfer() {
        return TextTransfer.getInstance();
    }

    public void dragFinished(DragSourceEvent event) {
        // Здесь не требуется никаких действий.
    }

    public void dragSetData(DragSourceEvent event) {

        ISelection selection = viewer.getSelection();
                      Object obj = ((IStructuredSelection) selection).getFirstElement();

        if (obj instanceof TreeObject) {
            event.data = getMergedSnippetContent();
        }
    }

    public void dragStart(DragSourceEvent event) {
        // Всегда активен, так что не требуется никаких действий 
		// для активации.
    }

}

Метод getTransfer() возвращает новый экземпляр TextTransfer. Это позволяет Eclipse определить, как выполняется перенос объекта.

Метод dragSetData() вызывает тот же внутренний метод, который используется в классе InsertSnippetAction (см. раздел Создание класса InsertSnippetAction). Однако вместо добавления фрагмента кода в редактор метод назначает значение переменной event.data. Обо всем остальном позаботится IDE, так как среда разработки знает, как именно перенести данные из одной части в другую: как текст.

Добавление функциональности drag-and-drop в модуль

Прежде чем использовать функциональность drag-and-drop, нужно вызвать метод для добавления поддержки drag-and-drop к компоненту TreeViewer вида Example.com Snippets view. Код, который выполняет это действие, показан в листинге 30.

Листинг 30. Добавление поддержки drag-and-drop

                
    public void createPartControl(Composite parent) {
        viewer = new TreeViewer(parent, SWT.MULTI / 
            SWT.H_SCROLL / SWT.V_SCROLL);
        drillDownAdapter = new DrillDownAdapter(viewer);
        viewer.setContentProvider(new ViewContentProvider());
        viewer.setLabelProvider(new ViewLabelProvider());
        viewer.setSorter(new NameSorter());
        viewer.setInput(getViewSite());
        // Определяем себя в качестве "слушателя" выделенного элемента.
        getSite().getPage().addSelectionListener(this);

        // Определяем действия для выделенного элемента.
        selectionChanged(null, getSite().getPage().getSelection());
        makeActions();
        hookContextMenu();
        hookDoubleClickAction();
        contributeToActionBars();

        DelegatingDragAdapter dragAdapter = new DelegatingDragAdapter();
        SnippetDragDropListener dragDropListener = 
            new SnippetDragDropListener();
        dragAdapter.addDragSourceListener(
            (TransferDragSourceListener) dragDropListener);
        viewer.addDragSupport(DND.DROP_COPY / DND.DROP_MOVE, 
            dragAdapter.getTransfers(), dragAdapter);

    }

Пользовательские настройки модуля

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

Модификация класса SnippetsPreferencePage

Откройте классы SnippetsPreferencePage и PreferenceConstants и внесите необходимые изменения в код. Вы можете удалить все поля, включенные в шаблон по умолчанию, кроме поля StringFieldEditor. После удаления ненужных полей следует внести небольшие изменения в метку редактора полей и добавить меню выбора базовой директории.

Окончательный код метода настроек модуля createFieldEditors() показан в листинге 31.

Листинг 31. Метод createFieldEditors() класса SnippetsPreferencePage

                
public void createFieldEditors() {
    addField(new StringFieldEditor(PreferenceConstants.P_CLASS,
            "Snippet &Implementation Class:", getFieldEditorParent()));
    addField(new StringFieldEditor(PreferenceConstants.P_SNIPPET_REPOS_LOC,
            "&Snippet Repository Path:", getFieldEditorParent()));
}

Не забудьте изменить имена констант в классе PreferenceConstants так, чтобы они отражали содержимое. Кроме того, модифицируйте метод initializeDefaultPreferences() класса PreferenceInitializer для задания подходящих значений по умолчанию. Например, назначьте имя класса провайдера фрагментов кода, соответствующее имени настоящего класса, и установите значение базовой директории, соответствующее реальному пути к хранилищу фрагментов кода.

Заключение

Используя интерфейсы и классы, включенные в Eclipse Platform API, можно существенно расширить функциональность Eclipse, добавляя к ней свои собственные модули. Процесс создания модулей значительно упрощается благодаря использованию готовых шаблонов. Вы можете добавить в Eclipse необходимые действия и операции, реализуя интерфейсы и расширяя классы, такие, например, как класс Action. Используя определенные вами действия, вы можете создавать новые функциональности, доступные через контекстное меню, кнопки основного меню или по двойному щелчку мышки. Шаблон вида поможет добавить в подключаемый модуль новые вид и вывести необходимую информацию в его панели.

Расширения класса TransferDragSourceListener могут использоваться для добавления "drag-and-drop" переноса данных из созданных вами видов в другие части среды разработки Eclipse IDE. Расширения классов Wizard и WizardPage позволяют реализовать новые мастера и страницы мастеров. Используя добавленный мастер, вы можете получать информацию от пользователя и запускать свои процессы, базируясь на этой информации. И наконец, подключаемый модуль можно легко протестировать, запустив его как новый экземпляр Eclipse. Вы можете проверить, как выглядит и работает модуль сразу же по завершении тех или иных модификаций.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=17323