为 Eclipse 构建片段扩展

来源:互联网 发布:光纤网络交换机 编辑:程序博客网 时间:2024/05/02 00:42
了解如何为 Eclipse 和 Rational Application Developer V7 构建一个插件。可以使用这个插件定义片段,从而轻松地添加符合企业标准的代码。这个插件与 Web Tools Project 提供的 Snippets 视图相似,允许开发人员将代码片段拖放到编辑器中。我们采用面向对象的最佳实践,所以可以从任何来源装载片段,比如数据库(例如 Apache Derby)、文件系统或 Web 服务。

本文不仅仅要为 Eclipse 构建一个新的插件,尽管这里要讨论创建插件的详细过程。在为 Eclipse 构建插件的过程中,您将学习如何使用 Eclipse 集成开发环境(IDE)的特性进行快速开发。我还将讲解如何扩展 Eclipse IDE,为插件提供丰富的功能。在阅读本文之后,您应该能够使用 Eclipse IDE 插件开发向导构建新的插件。您还要学习如何扩展类并实现接口,从而在插件中添加首选项页面、属性页面和拖放支持。

请访问 Eclipse 技术资源中心,获取 Eclipse 相关信息,包括大量 Eclipse 技术文章、教程、下载和相关技术资源。 RSS 订阅 Eclipse 相关文章和教程的 RSS 提要

您应该熟悉面向对象(OO)概念,比如实现接口和扩展类。还应该相当熟悉 Eclipse IDE,但是不需要了解如何为 Eclipse 创建插件。需要 Eclipse V3.2 或更高版本。需要运行嵌套的 Eclipse 实例来测试插件,所以您的计算机应该有足够的 RAM,可以同时运行两个 Eclipse 实例。

开始插件项目

Eclipse IDE 是在一个可扩展框架上构建的,所以可以为 IDE 编写自己的插件。但是,不止如此:Eclipse 还附带一些模板,可以使用这些模板快速开始构建插件项目。

为了让本文有意义,我选择了一个涉及许多活动的场景,以此演示如何使用 Eclipse 框架构建插件。我选择的场景是一个企业代码片段插件。这个插件让开发人员能够使用预定义的分类的代码片段,并将它们插入编辑器中。这些片段可以从 Eclipse 之外的来源获得。

这个代码片段插件 “Example.com” 具有以下特性:

  1. 一个树视图,可以用来按类别寻找片段
  2. 一个首选项页面,可以用来配置片段的来源位置
  3. 一个上下文敏感的菜单,可以用来将片段插入编辑器
  4. 片段,包括 ${variable} 形式的模板变量和一个用来为这些变量收集值的向导

创建项目

首先,创建一个用来开发插件的项目。幸运的是,有一个向导可以引导您创建项目。执行以下步骤:

  1. 选择 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

    下面的 “了解模板” 一节中将详细描述这些模板对项目的作用。

  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 IDE 完成处理时,就会出现一个新的插件项目,其中包含许多文件。前面步骤中选择和配置的模板会创建几个包和 Java™ 源文件。关于这些模板的更多信息,请参见 “了解模板” 一节。如果您已经掌握了模板,或者不愿意花时间研究细节,那么可以跳到 “测试插件” 一节。

回页首

了解模板

看到 Eclipse 创建了新项目之后,您可能想了解这个过程中发生的情况。如果您按照前面的步骤进行操作的话,项目会包含几个包含 Java 源文件的包,还有许多其他文件。如果您是刚接触插件开发的新手,那么可能不知道这些文件的作用以及为什么会出现在这些位置,这是使用向导的一个缺点。

本节讨论本文中使用的模板。讨论各个配置选项以及各个部分如何组合成插件。在阅读下一节之后,如果愿意的话,您可以手工构建这些组成部分。

在 Eclipse IDE 中,动作集(action set)是一组在逻辑上应该分组在一起的菜单项或按钮动作。插件可以有一个动作集,因为将针对一个插件的所有动作分组在一起是顺理成章的。

“Hello world” Action Set 模板在 snippetssample.action 包中添加 SnippetAction.java 文件。这个类的名称根据 Sample Action Set 配置中的 Action Class Name 设置,如下图所示。

图 1. 配置动作集配置动作集

在 Eclipse 中运行这个插件时,“Hello, world” Action Set 在菜单栏中显示菜单 Sample Menu。菜单标签使用的文本是在 plugin.xml 文件中定义的,如下所示:

清单 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>

如果在 Eclipse 中单击这个菜单,就会执行 SnippetAction 类中的 run 方法。下面的模板代码显示一个消息框。

清单 2. SnippetAction 的 run() 方法
                public void run(IAction action) {    MessageDialog.openInformation(        window.getShell(),        "SnippetsPlugin Plug-in",        "Hello, Eclipse world");}
回页首

弹出菜单

Popup Menu 模板提供在右键单击文件时出现的一个菜单项 — 有时候称为上下文菜单(context-sensitive menu)。在 Package Explorer 中右键单击文件时,会看到菜单 Snippet Submenu,其中只包含一个菜单项 New Action。Popup Menu 配置示例如下所示:

图 2. 配置弹出菜单配置弹出菜单

Target Object's Class 在默认情况下指向 org.eclipse.core.resources.IFile,这个设置告诉 Eclipse 只在用户右键单击文件时应用弹出菜单。可以使用的其他资源是 IProjectIFolder。如果使用 IProject,那么只在用户右键单击项目时显示菜单;如果使用 IFolder,那么只在用户右键单击文件夹时显示菜单。

Name Filter 表示只有在选择某些文件时才显示弹出菜单。例如,如果指定过滤器为 *.html,那么只有在用户右键单击文件名以 .html 结尾的文件时菜单才会出现。在向导创建项目时,这个值定义在 plugin.xml 中。

Submenu Name 是弹出菜单中显示子菜单的菜单项名称。在创建项目之后,可以在 plugin.xml 文件中修改这个值。

Action Label 是子菜单中显示的菜单项名称。这个值也写入 plugin.xml 文件中,可以在这个文件中进行修改。

Java Package Name 是包含新类的包的名称。类名由 Action Class 定义。与其他值一样,以后也可以修改这个名称,但是不太容易。修改包和类的名称要求使用 Eclipse 的重构特性,确保相应地更新 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 执行 SnippetPopupAction 类的 run() 方法中的代码(见清单 4)。在默认情况下,这些代码显示一个消息框。

清单 4. SnippetPopupAction 的 run() 方法
                public void run(IAction action) {    Shell shell = new Shell();    MessageDialog.openInformation(        shell,        "SnippetsPlugin Plug-in",        "New Action was executed.");}
回页首

首选项页面

Preference Page 模板提供一个首选项页面,可以在这个页面中为插件设置首选项。在 Eclipse 中,通过 Window > Preferences 菜单项调用这个页面。设置首选项页面模板的向导页面如下所示。

图 3. 设置首选项页面设置首选项页面

Java Package Name 指定一个包,其中包含用来显示首选项页面的页面,以及支持首选项页面的所有类。Page Class Name 是页面类的名称,Page Name 是首选项列表中显示的名称。可以在 plugin.xml 文件中修改首选项页面的名称(已经改为 Snippet Preferences)。

除了 SnippetPreferencePage 类之外,这个模板还生成另外两个支持类。它们在同一个包中。其中一个类 PreferenceConstants 包含用来识别首选项的常量。另一个类 PreferenceInitializer 将首选项初始化为默认值。

使用 Preference Page 模板的出色之处是,不需要为保存和获取首选项编写代码 — 这由首选项页面上的控件自动处理。

回页首

视图

View 模板创建一个视图,当测试这个插件时,可以在 Eclipse IDE 中看到这个视图(参见 “测试插件” 一节)。

下面显示的 Main View Settings 屏幕集合了视图扩展的配置。

图 4. 配置视图配置视图

Java Package Name 是包含新类的包的名称。

View Class Name 是用来显示视图的 Java 类的名称。

View Name 是视图的名称,这个名称会显示在 Window > Show View 菜单中。

View Category ID 识别类别,View Category Name 是类别的名称。类别是一个组,当使用 Window > Show View 选择视图时,视图显示在这个组下面。

查看器类型影响生成视图类(SnippetView)的方式。如果选择 Table viewer,那么视图只显示条目的列表。条目的列表由 getElements() 方法生成。

如果查看器类型是 Tree viewer,那么会以不同的方式生成视图。getElements() 方法会返回一组 TreeObject 对象,而且修改它的内部类 ViewContentProvider 还实现 ITreeContentProvider 接口。

视图类

View 模板生成的 SnippetView 类是这个项目中最复杂的类。如果在 Eclipse 的 Outline 视图中查看这个类,就会看到它包含下面的内部类:

TreeObject
这个类表示条目树的叶节点(leaf node)。这些节点不包含其他条目。
TreeParent
这个类表示树中的条目可以包含其他条目。
ViewContentProvider
这个类实现 IStructuredProviderITreeContentProvider 接口,它获取要在视图中显示的内容。在本文后面,我们要修改这个类,从而向视图提供代码片段的细节。
ViewLabelProvider
这个类为树条目提供图标图像。
NameSorter
可以修改这个类来为视图中的条目实现定制的排序特性。在本文中,我们不修改这个类。关于这个类的更多信息,请参考 Eclipse Platform API 文档中关于 ViewerSorter 的内容(参阅 参考资料)。
回页首

测试插件

到目前为止,您选择的模板已经在项目中生成了许多 Java 类。在继续开发之前,应该测试这个插件,看看模板生成的代码有什么作用。为此,需要运行一个 Eclipse 实例。要想在 Eclipse 中运行嵌套的 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 实例,选择菜单项,就会看到它显示新的值。如果不喜欢菜单或菜单项的名称,那么可以在 plugin.xml 文件中进行修改,见下面的粗体代码。

清单 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 中的弹出菜单,启动嵌套的 Eclipse 实例。它启动之后,在 Package Explorer 视图中找到一个文件。如果没有文件存在,就创建一个包含一个示例文件的项目。当右键单击这个文件时,会看到 Snippet Submenu 菜单,其中包含菜单项 New Action。单击这个菜单项,Eclipse 会显示一个消息框,其中显示 “New Action was executed”。

Eclipse 中的首选项页面

为了看到 Eclipse 中的首选项页面,在嵌套的 Eclipse 实例中选择 Window > Preferences。应该会看到一个称为 Snippet Preferences 的首选项类别。从列表中选择这个类别,就会打开一个首选项页面,其中包含许多控件。这些控件是模板自动生成的。

以后我们会修改这些首选项,让它们对这个插件更有意义。目前,可以修改这个首选项页面上的任何值,并通过 ApplyOK 按钮应用它们,然后返回,就会看到所做的修改已经保存并重新装载。而这些都不需要编写定制的代码。

下面所示方法的 SnippetPreferencePage 类包含显示首选项页面的代码。

清单 7. SnippetPreferencePage 的 createFieldEditors() 方法
                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 Snippets 类别下面,这是在 View 配置中指定的(参见 图 4)并保存在 plugin.xml 中。选择 Snippet View 并单击 OK,就可以打开这个视图。

在默认情况下,这个视图包含几个条目。应该会在视图中看到图 5 这样的条目树。

图 5. 填充了条目的视图填充了条目的视图

如果双击视图中的任何条目,Eclipse 就会显示一个消息框,其中显示 “Double-click detected on XXX”,这里的 XXX 是条目的文本。视图中还添加了两个动作;它们显示在视图的菜单栏上,并在右键单击视图中的任何位置时出现。这些动作是在下面的 makeActions() 方法中动态地添加的。

清单 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,并将这个接口放在 com.example.plugins.snippets.providers 包中,实现类和工厂类也将放在这个包中。

SnippetProvider.java 的内容如下所示。

清单 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) 每种语言(Java 或 XML)都有一组类别,片段可以按照这些类别进行组织。 getLanguages() 获得语言的列表,这是对片段进行分类的高级方式。 getSnippet(SnippetInfo info) 返回特定的片段。 getSnippetInfo(String language, String category) 获得一组 SnippetInfo 对象,可以在视图中显示其中的信息。

SnippetFileProvider 实现类

编写了接口之后,就要开始编写使用这个接口的实现类。在本节中,我们要编写一个实现类,它用来从文件系统目录中获取代码片段。

但这不是全部……

您可能会注意到,我们没有详细讨论两个类:SnippetInfoSnippet。在本文提供的 下载文件 中可以找到这两个类。还有几个异常类(比如 SnippetProviderException)、一个用来装载 SnippetProvider 实现的工厂类(SnippetProviderFactory)和其他几个类。这些支持类是有用的,但是在这里讨论构建它们的过程是没有意义的。

我们不需要在一个新类中手工添加实现接口所需的方法,而是使用 Eclipse 构建这个类的开始部分。为了让 IDE 替我们做尽可能多的工作,右键单击 com.example.plugins.snippets.providers 包并选择 New > Class。在 Name 中输入 SnippetFileProvider,并单击接口列表旁边的 Add 按钮。在 Choose interfaces 中,输入 SnippetFileProvider 并单击 OK

Eclipse IDE 会生成一个正确地实现了 SnippetProvider 接口的新的类文件。所需的所有方法以及返回语句都已经生成了,所以这个项目可以顺利地编译。

这个新的类要根据语言和类别从文件系统中装载代码片段。它从一个 XML 文件(snippets.info)中装载代码片段的信息。这个文件包含一组 SnippetInfo 条目的 XML 表示,这些条目是使用 XMLEncoder 串行化为 XML 的。

下面的 configure() 方法提供了配置带有 Properties 对象的适配器的方法。对于 SnippetFileProvider,惟一需要设置的属性是基统一资源标识符(Uniform Resource Identifier,URI),这是包含代码片段结构的目录的基路径。

清单 10. SnippetFileProvider 的 configure() 方法
                    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 — 这是对片段进行分类的高级方式。SnippetFileProvider 类中实现的这个方法如下所示。

清单 11. SnippetFileProvider 的 getLanguages() 方法
                    public String[] getLanguages() throws SnippetProviderException {        /*         * The languages will be the high-level directories right underneath the         * base directory.         */        if (languages == null) {            languages = getFormattedNamesFromLocation(getBaseUri());        }        return languages;    }

这个方法调用一个私有静态方法 getFormattedNamesFromLocation,并将基 URI 传递给它。这个静态方法返回一个 String 数组,其中包含目录中找到的条目的名称,并做了适当的格式化。每个语言目录包含一组文件夹,这些文件夹用来将片段分类到子类别中。例如,Java 语言目录可能包含一个用于异常处理的类别,或者用于日志记录的类别。装载语言的类别的方法如下所示。

清单 12. SnippetFileProvider 的 getCategories() 方法
                    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() 方法就会从在这个类别目录中找到的 snippets.info 文件装载一个 SnippetInfo 对象数组。这个 XML 文件包含这个类别中的代码片段的相关信息,比如名称、描述和变量。这个方法如下所示。

清单 13. SnippetFileProvider 的 getSnippetInfo() 和 getSnippet() 方法
                    public SnippetInfo[] getSnippetInfo(String language, String category)            throws SnippetProviderException {        /* Dehydrate the snippet info from the filesystem */        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);        /* Load the snippet from the file */        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 对象中返回它。这个类中的其他方法是用于支持公共方法的私有方法。可以在本文提供的源代码中仔细研究这些方法。

回页首

填充视图

构建了从文件系统目录获取代码片段的框架之后,就应该在 SnippetView 类中添加适当的代码,以便在插件的视图中以树的形式显示片段信息。为了在视图中显示片段,修改 SnippetView 中内部类 ViewContentProviderinitialize() 方法。

initialize() 方法构建在视图中显示的条目树。当模板最初创建这个类时,它包含一些伪数据,所以在用 Eclipse 运行这个插件时看到了一些显示效果。如果您已经执行了前面的步骤,现在就该修改这个方法,让它使用 SnippetProvider 装载片段信息。

新的方法如下所示。

清单 14. 新的 SnippetsView initialize() 方法
                  private void initialize() {      invisibleRoot = new TreeParent("");      /*       * Get the high-level elements from the provider, which are the       * languages.       */      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]);              /* Get the categories for each one of the parents */              String[] categories = snippetProvider                      .getCategories(topLevelNodes[i]);              for (int j = 0; j < categories.length; j++) {                  TreeParent categoryParent = new TreeParent(                          categories[j]);                  /* Now get the snippet names for the categories */                  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() 方法获取类别中包含的代码片段的信息。它循环遍历数组并为每个 SnippetInfo 构建新的 TreeObject

关于新代码,一定要注意两点。首先,SnippetProviderFactory 装载一个默认的实现类。必须修改这个操作,插件才能够从不同的位置获得代码片段。第二,使用从一个文件装载的属性配置适配器。也必须修改这个操作,因为在发布这个插件时,用户需要能够修改属性。这两个设置是从首选项装载的。后面的 “添加用户首选项” 一节将讨论从首选项装载这些值的过程。

修改 TreeObject

为了获得在树视图中选择的片段的所有信息,需要将这些信息与 TreeObject 关联起来。这需要对 TreeObject 内部类做一些调整。需要将私有的 String 名称字段替换为一个包含 SnippetInfo 对象的私有字段。还需要修改构造器和 getName() 方法。

修改后的 TreeObject 类如下所示,修改的地方以粗体显示。

清单 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,所以也需要对它做两处修改。修改之处见下面的粗体代码。

清单 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() 方法以及修改构造器,让它不把 null 传递给超类构造器并将名称赋值给新的私有 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> 

需要根据语言和类别更新每个文件。在添加这个文件之后,添加包含每个片段的文件。下面是目录结构的一个示例。

清单 18. 基于片段文件的存储库结构
                +- basedir +- Java | +- Exception_Handling |   +- snippets.info |   +- Exception_Class.snippet +- XML   +- Ant_Build_Files     +- snippets.info     +- Simple_File.snippet

查看修改的效果

创建了所有支持类和基于文件的代码片段存储库之后,现在可以启动一个嵌套的 Eclipse 实例,看看新的片段效果如何。如果一切正常,应该会看到与图 6 相似的视图。

图 6. 显示片段的视图显示片段的视图

代码片段信息已经装载了,现在进行下一步:将 .snippet 文件的内容添加到编辑器中。

回页首

编写 InsertSnippetAction 类

既然 SnippetFileProvider 实现已经将片段信息装载进视图中,就可以使用片段信息从提供者获得代码片段,并将其插入编辑器中。片段信息存储在一个 SnippetInfo 实例中,这个实例是从 Example.com Snippets 视图中选择的条目获得的。

首先构建 InsertSnippetAction 内部类,这个类中的代码获取片段内容并执行插入。它扩展 Action 类,所以其他插件组件可以很容易地使用它。它是 SnippetsView 的内部类,因为它由视图私有地使用,并提供对视图中对象的访问。

新的 InsertSnippetAction 类如下所示,其中的 run() 方法显示一个消息框。这个消息框有助于确保新的 InsertSnippetAction 已经正确地构建并添加到适当的位置。

清单 19. InsertSnippetAction
                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 类并删除 action2doubleClickAction。目前保留 action1 — 可以使用 Eclipse 的重构工具将它重命名为 insertAction 并将类型从 Action 改为 InsertAction。因为 InsertAction 扩展 Action,所以不需要修改别的地方。清单 20 显示新的 SnippetsView 类,修改之处以粗体显示。

清单 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 {        // Snipped...    }    class TreeObject implements IAdaptable {        // Snipped...    }    class TreeParent extends TreeObject {        // Snipped...    }    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() {            // Snipped... see earlier Listing.        }    }    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 {    }    /**     * The constructor.     */    public SnippetsView() {    }    /**     * This is a callback that will allow us to create the viewer and initialize     * it.     */    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());        // add myself as a global selection listener        getSite().getPage().addSelectionListener(this);        // prime the selection        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);        // Other plug-ins can contribute there actions here        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();            }        });    }    /**     * Passing the focus request to the viewer's control.     */    public void setFocus() {        viewer.getControl().setFocus();    }    public void selectionChanged(IWorkbenchPart part, ISelection selection) {    }    }

在测试这个插件时,在 Eclipse 中双击一个树条目,就会显示消息 “Running insert action on XXX”,这里的 XXX 是 Example.com Snippets 视图中选择的条目的文本。如果右键单击并从上下文菜单中选择 Action 1,或者单击视图菜单栏中的按钮,那么也会执行同样的动作。

完成 InsertAction

如果所有双击操作和弹出菜单项都正常,就可以添加余下的代码,完成 InsertSnippetAction 类。改进后的 run() 方法如下所示。

清单 21. 完成后的 InsertSnippetAction run() 方法
                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() 方法。其余的代码获得当前的编辑器实例,获得编辑器中选择的文本,然后用 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.");        // create wizard        SnippetVariablesWizard wizard = new SnippetVariablesWizard(page);        // create wizard dialog & launch wizard dialog        WizardDialog dialog = new WizardDialog(viewer.getControl()                .getShell(), wizard);        dialog.open();        result = s.getMergedContent();    }    return result;}

这个方法使用一个向导和向导页面,从用户那里收集用于片段变量的输入。如果现在编译上面的代码,就会收到错误。这是因为还没有创建 SnippetVariablesWizardPageSnippetVariablesWizard。我们将在下一节创建它们。

回页首

添加用户输入

为代码片段模板变量建立收集用户输入的机制是一项非常简单的任务,但是它需要几个特殊的组成部分。这些部分采用 Standard Widget Toolkit(SWT)元素和 helper 的形式;到目前为止,我们没有考虑过这些,因为模板处理了细节。但是,如果以正确的次序构建的话,各个部分会配合得很好。本节讲解如何构建一个包含表格的向导页面,从而为片段变量收集用户输入。

SnippetVariablesWizardPage

SnippetVariablesWizardPage 类扩展 WizardPage 类。WizardPage 类是 Eclipse Platform API 的一部分,提供了向导页面的基本实现。请参阅 参考资料 中的 Eclipse Platform API 文档。

WizardPage 有一些更特殊化的扩展类,比如 WizardNewFileCreationPage,这个类用来收集创建新文件所需的输入。但是,在这个项目中情况比较简单,所以直接使用 WizardPage 就可以了。SnippetVariablesWizardPage 的完整清单如下所示。它有两个内部类,所以显得有点儿复杂,但是几乎所有代码都是用来显示属性表格的。这个表格是可编辑的,所以需要不少代码。如果表格是只读数据,那么就可以快速地循环遍历 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    {  // Snipped... see Listing 24    }        class SnippetVariableContentProvider implements IStructuredContentProvider {// Snipped... see Listing 25    }        class SnippetVariableLabelProvider extends LabelProvider implements            ITableLabelProvider { // Snipped... see Listing 26    }        class SnippetVariableCellModifier implements ICellModifier {// Snipped... see Listing 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) {        // create main composite & set layout        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);        // Second column        column = new TableColumn(propertyTable, SWT.LEFT, 1);        column.setText("Value");        column.setWidth(colWidth);                // Iterate through the variables in the snippet info and add them to the         // table.        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);        // page setting        setControl(container);        setPageComplete(true);    }        public void updateSnippet()    {        /* Get the values from the table */        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

这个简单的内部类包含表格中显示的数据,用户可以更新这些数据。它有两个公共字段:一个包含属性名,另一个包含用户输入的值。对于片段中的每个变量,都有一个 SnippetVariableValue 对象;这些对象的数组显示在向导中的表格中。

清单 24. SnippetVariableValue 类
                class SnippetVariableValue{    public String property;    public String value;        public SnippetVariableValue(String property)    {        this.property = property;        this.value = "";    }}

SnippetVariableContentProvider

内容提供者是一个实现 IStructuredContentProvider 接口的内部类。完整的内部类如下所示。

清单 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;    }}

对于本文,包含实现代码的惟一方法是 getElements(),它返回在向导页面的构造器中初始化的变量。

SnippetVariableLabelProvider

标签提供者实现 ITableLabelProvider 接口并扩展 LabelProvider 类。这个接口和这个类都是 Eclipse Platform API 的一部分。标签提供者的代码如下所示。

清单 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 实现 Eclipse Platform API 中的 ICellModifier 接口。它提供的方法可以用于判断一个单元格是否可以修改,以及获取和修改它的值。这个类如下所示。

清单 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() 方法中添加代码,如果列名是 property,就返回 false。如果要构建可编辑的表格,而且允许用户编辑任何列,那么这个方法总是返回 true 即可。

SnippetVariablesWizardPage 相比,SnippetVariablesWizard 是一个轻量级的类。完整的类如下所示。它扩展 Eclipse Platform API 中的 Wizard 类。

清单 28. SnippetVariablesWizard 类
                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 类负责处理,不需要为实现的方式操心。当用户单击向导中的 Finish 按钮时,运行 performFinish() 方法。如果它返回 true,向导就会关闭;如果返回 false,向导就继续显示。在许多情况下,需要长期运行的进程,这时可以初始化一个实现了 IRunnableWithProgress 接口的进程。

在执行这个向导时,它的外观与下图相似。

图 7. 向导向导

当向导退出时,已经更新了传递给向导页面的片段,包含片段文本和用户提供的值的合并结果。现在,在运行 InsertSnippetAction 时,向导出现,在左列中包含片段中的变量。在右列中添加值并单击 Finish 之后,合并的结果就被放入到活动的文本编辑器中。

回页首

响应新事件

添加向导页面和类之后,应该能够使用提供者装载片段并定制片段中的值。为了让这个插件更完善,可以在弹出菜单和双击事件之外,添加拖放功能。Eclipse Platform API 提供了在 Example.com Snippets 视图和编辑器之间实现拖放功能所需的类。如果选择一个树对象并将它拖到编辑器中,就会出现向导,提示您输入值。然后,合并的结果就会插入编辑器。

SnippetDragDropListener

为了在新视图中添加拖放支持,添加另一个内部类。它实现 TransferDragSourceListener 接口。这个接口有三个方法;在本文中,只需实现其中两个方法。这些方法在下面以粗体显示。

清单 29. SnippetDragDropListener 类
                class SnippetDragDropListener implements TransferDragSourceListener {    public Transfer getTransfer() {        return TextTransfer.getInstance();    }    public void dragFinished(DragSourceEvent event) {        // There is nothing to clean up or do.    }    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) {        // Always enabled, so don't do anything on the start.    }}

getTransfer() 方法返回 TextTransfer 的一个新实例。这让 Eclipse 知道如何处理文本传输。

dragSetData() 方法调用的私有方法与 InsertSnippetAction 类相同(参见 “编写 InsertSnippetAction 类”)。但是,这个方法不必在当前编辑器中插入代码,而是设置 event.data 值。IDE 负责其余的工作,因为它知道如何在这两个部分之间传输数据:以文本形式。

回页首

添加拖放支持

在使用拖放之前,必须调用一个方法,在 Example.com Snippets 视图中的 TreeViewer 组件上添加拖放支持。代码如下所示。

清单 30. 添加拖放支持
                    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());        // add myself as a global selection listener        getSite().getPage().addSelectionListener(this);        // prime the selection        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

打开并修改 SnippetsPreferencePagePreferenceConstants 类。可以删除除 StringFieldEditor 之外的所有默认模板字段。在删除不需要的字段之后,可以修改留下的字段的标签,并添加一个指定片段存储库位置的字段。

首选项页面 createFieldEditors() 方法最终的代码如下所示。

清单 31. SnippetsPreferencePage 的 createFieldEditors() 方法
                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 类中的常量名更新为更有意义的名称。还要更新 PreferenceInitializer 类中的 initializeDefaultPreferences() 方法,将首选项设置为适当的默认值。例如,将默认的片段提供者实现类名设置为一个真实有效的类,将存储库位置设置为有效的存储库路径。

回页首

结束语

分享这篇文章……

digg 将这篇文章提交到 Digg del.icio.us 发布到 del.icio.us Slashdot 提交到 Slashdot!

通过使用 Eclipse Platform API 提供的接口和类,可以在 Eclipse 中添加自己的插件。如果使用 Eclipse 提供的模板,构建自己的插件甚至会更容易。可以通过实现接口或扩展类(比如 Action 类),在 Eclipse 中添加定制的动作。可以利用这些动作将有关的定制行为与弹出菜单、工具栏按钮和双击事件连接起来。可以在 Eclipse 中添加新的定制视图并用自己的信息填充它们。

扩展 TransferDragSourceListener 的类可以在定制视图和 Eclipse IDE 的各个部分之间添加拖放支持。通过扩展 WizardWizardPage 类,可以添加包含一个或多个向导页面的定制向导。可以用这些向导收集用户输入,然后用这些信息运行定制进程。最后,在 Eclipse 中可以启动嵌套的 Eclipse 实例,可以在嵌套实例中快速测试插件。可以立即看到插件的外观和行为。

原创粉丝点击