简述: 前两天写了篇用Kotlin撸一个图片压缩插件-导学篇,现在迎来了插件基础篇,没错这篇文章就是教你如何一步一步从零开始写一个插件,包括插件项目构建,运行,调试到最后的上线发布整个流程。如果你是插件零基础的小白,那么这篇文章适合你,而且这篇文章也是下面实战篇的基础.
ImageSlimming图片压缩插件开发完成后,马上就把它推荐给团队内部人员使用,在周会上就有同事提出了一个需求,就是在AndroidStudio项目中,可以任意选中res目录下一张或多张图片,然后直接右键选择,就可以实现图片压缩。然后思考了一波,这个需求挺好的,心里大概想了下,今晚就去把它实现了。实现效果大概如下:
实现这个功能后,把V1.1版本的代码做了很大的结构上调整,抽离出一些公共的顶层函数和扩展函数,目前这个功能代码已经更新到GitHub上了,请认准feature-image-slimming-v1.2分支。
IDE插件利用jetBrains公司开源的IntelliJ Platform SDK(java语言)来开发一个独立功能可以安装在IDEA之类的编辑器的功能组件。 IDE插件是基于IntelliJ IDEA开发工具开发,里面集成了插件的项目的构建。采用的是Java语言开发和IntelliJ的SDK相结合开发。并且在开发出来的插件不仅在AndroidStudio上可以使用,可以通用于jetBrains的编辑器的全家桶工具。通过源码可以发现Intellij Idea内置了大量的插件,可以这么说Intellij Idea开发工具大部分功能是由插件组合而成的。
注意: 构建插件项目的方式主要有两种:
一种是直接创建IDEA内置的插件项目.
另一种则是先通过构建一个gradle项目,然后加入plugin.xml配置以及 加入IDEA ERP的依赖,然后来构建一个插件项目(整个开发过程就和开发一个Android项目一样),当然这个构建过程可参考官方给出的gradle-intellij-plugin项目来实现。不过在最新2018.1.1之后版本中,IDEA内部也提供了构建grale插件项目入口,具体可下载新版本Intellij Idea。
<idea-plugin>
<id>com.your.company.unique.plugin.idid>
<name>Plugin display name herename>
<version>1.0version>
<vendor email="[email protected]" url="http://www.yourcompany.com">YourCompanyvendor>
<description>
most HTML tags may be used
]]>description>
<change-notes>
most HTML tags may be used
]]>
change-notes>
<idea-version since-build="173.0"/>
<extensions defaultExtensionNs="com.intellij">
extensions>
<actions>
actions>
idea-plugin>
id标签: plugin插件项目的标识,和Android项目中的package功能类似。唯一标识一个插件项目。
name标签: 插件名字,发布到jetBrains plugin仓库中会用这个。
version标签: 插件版本号,这个用于标识插件版本,一般用于更新jetbrains plugins仓库中插件版本标识。
vendor标签: 开发者信息,邮箱和个人主页,公司名字或个人开发者姓名,用于插件仓库中插件信息介绍显示。
description标签: 插件的描述信息,主要是描述插件有什么功能。支持标签内部内嵌HTML标签。
changNote标签: 一般用于插件版本变更的信息。支持标签内部内嵌HTML标签。
idea-version标签: 这个版本标签需要注意下,它决定了该插件能够运行在最低版本的IDEA中,一旦配置不当,会导致插件安装不成功,有点类似Android中AndroidManifest.xml中配置最低兼容Android版本意思。
depends标签: 表示当前的插件项目依赖哪些内置或者外部的插件库依赖,例如你需要实现类似git功能插件,你就可以通过depends标签引入Git4Idea即可,
extension标签: 插件与其他插件或与IDE本身交互。(默认是IDEA)如果您希望插件扩展其他插件或IntelliJ Platform的功能,则必须声明一个或多个扩展名。
<extensions defaultExtensionNs="com.intellij">
<appStarter implementation="MyTestPackage.MyTestExtension1" />
<applicationConfigurable implementation="MyTestPackage.MyTestExtension2" />
extensions>
action标签: 这个标签非常重要,它决定了你的插件在IDE上显示的位置和顺序,以及这个插件的点击事件和插件项目Action实现类的绑定。
第一种:就是通过IDEA提供的一个入口,直接去创建Action,然后它自动帮你实现plugin.xml中的事件绑定的注册
注意点一: 定义的Action最好要加入到一个IDE中内置组中,这样才能容易在对应组中找到插件,并运行插件。可能会有人问了,列举出来那么多z在我哪知道对应运行起来IDEA哪个地方,有小技巧看下对应组中小括号中的描述内容,然后就是选中一个组,看看里面都有哪些组,大概就能猜到对应IDEA哪个地方,最笨办法就是测试运行下即可,建议把测试结果记录下来,后续就方便了。
注意点二: 除了把定义的action加入到内置的组中,还可以加入自定义组中,如何自定义组下面第二种方法会讲述,但是还是需要自定义组加入内置的组中,所以一般都是需要把action直接或间接加入到内置的组中。
注意点三: Action还可以配置icon,也就是常见点击icon图标就执行插件,如何配置图标在下面第二种方法会有介绍。
第二种:手动创建一个Action类,然后继承AnAction类或者DumbAwareAction类,然后在plugin.xml中的action标签去注册action类与事件绑定
创建Action类:
package com.mikyou.plugins.demo
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ui.Messages//注意import,是com.intellij.openapi包下
class DemoAction: AnAction() {
override fun actionPerformed(p0: AnActionEvent?) {
Messages.showInfoMessage("Just a Test ", "来自DemoAction提示")
}
}
在plugin.xml中注册action类的绑定
<actions>
<action id="com.mikyou.plugins.demo.DemoAction" class="com.mikyou.plugins.demo.DemoAction" text="DemoAction"
description="just a test demo">
<add-to-group group-id="ToolbarRunGroup" anchor="last"/>
action>
actions>
在plugin.xml中配置插件图标,先在插件项目中resource目录下创建一个image目录或者直接把图标拷贝目录下即可
然后action标签中指定icon属性
<actions>
<action id="com.mikyou.plugins.demo.DemoAction" class="com.mikyou.plugins.demo.DemoAction" text="DemoAction"
description="just a test demo" icon="/image/icon_pic_demo.png">
<add-to-group group-id="ToolbarRunGroup" anchor="last"/>
action>
actions>
在plugin.xml中配置自定义组,并把自定义的组加入内置的组中。
<group id="com.mikyou.plugins.group.demo" text="Demo" description="just a demo group">
<add-to-group group-id="MainMenu" anchor="last"/>
<action id="com.mikyou.plugins.demo.DemoAction" class="com.mikyou.plugins.demo.DemoAction" text="DemoAction" description="just a test demo" icon="/image/icon_pic_demo.png">
<add-to-group group-id="ToolbarRunGroup" anchor="last"/>
action>
group>
7、你可以打断点,点击debug,然后就可以断点调试代码。
8、最后一步,打包插件,并发布。选择顶部工具栏Build, 点击"Prepare Plugin Module ‘Demo’ For Deployment",就会在当前工作目录下生成一个jar或zip的包。然后发布插件,只需要在jetBrains Plugins Repository上传你的包,等待jetBrains官方的审核通过了,就能通过ide中的plugins仓库中搜索找到。
插件开发最为重要之一的就是Action类了,可以说它是插件功能的一个入口,编写一个Action类,一般会去继承AnAction类,AnAction是一个抽象类,必须要去实现actionPerformed方法,这个方法是在用户触发插件的点击事件后回调的,所以类似于打开对话框,执行某个功能的逻辑可以写在里面等等。单从插件开发角度(插件的生命周期除外)来说,可以把当它当做程序中的main函数。
首先创建一个DemoAction继承AnAction
class DemoAction: AnAction() {
override fun actionPerformed(p0: AnActionEvent?) {
Messages.showInfoMessage("Just a Test ", "来自DemoAction提示")
}
}
然后看下AnAction重载的第三个构造器,会去拿到Presentation类的对象,准确来说这个对象保存了插件是否可见、是否可用、插件的Icon以及插件显示在IDE中的外观控制信息,可以说是插件外观信息和控制的实体。
public AnAction() {
this.myShortcutSet = CustomShortcutSet.EMPTY;
this.myIsDefaultIcon = true;
}
public AnAction(Icon icon) {
this((String)null, (String)null, icon);
}
public AnAction(@Nullable String text) {
this(text, (String)null, (Icon)null);
}
public AnAction(@Nullable String text, @Nullable String description, @Nullable Icon icon) {
this.myShortcutSet = CustomShortcutSet.EMPTY;
this.myIsDefaultIcon = true;
Presentation presentation = this.getTemplatePresentation();
presentation.setText(text);//设置插件显示文本
presentation.setDescription(description);//设置插件描述文件信息
presentation.setIcon(icon);//设置插件的图标
}
构建好自定义Action实体,外部调用方会触发actionPerformed方法,请注意actionPerformed方法带了一个AnActionEvent对象,它有个getData方法可以拿到IDEA很多窗口对象,但是实际上内部通过委托它的dataContext成员对象的getData方式实现的,它很重要代表上下文环境,相当于Android开发中的Context,可以通过它内部的dataContext中的getData方法可以得到IDEA界面各个窗口对象以及各个窗口为实现某些特定功能的对象。例如Project对象,VirtualFile对象、Editor对象、PsiFile持久化文件对象等等,毫不夸张的说后续插件功能开发都是围绕它来展开的,下面会详细描述。
class DemoAction: AnAction() {
override fun actionPerformed(p0: AnActionEvent?) {
Messages.showInfoMessage("Just a Test ", "来自DemoAction提示")
}
override fun update(e: AnActionEvent?) {
super.update(e)
}
}
update方法是在Action状态发生变化的时被回调,当Action状态更新时,update函数被IDEA回调,并且传递AnActionEvent对象参数,AnAction对象中封装了当前Action对应的上下文环境。 也就是说我们前面所讲的需要把action加入到组,才有可能得到显示,因为在action组显示的时候,该组内部的所有action中的update方法都会被回调,所以一个插件的update方法会比actionPerformed先执行,而且是有可能多次执行,也就是一个插件最开始得先显示出来并且可操作,然后才是点击触发action事件。所以也就产生一个场景的应用就是细心小伙伴会发现有时候右侧菜单中item是灰色的点不动,有时候可以,有时候不显示,有时候又是可以显示的。这些判断的逻辑一般是在update方法中执行的。
AnActionEvent对象,actionPerformed和update方法都会携带一个AnActionEvent对象,可以说它是插件与IDEA交互通信的一个媒介,通过AnActionEvent内部的dataContext的getData方法,传入对应的DataKey对象获得相应的窗口对象
@Nullable
public <T> T getData(@NotNull DataKey<T> key) {
if (key == null) {
$$$reportNull$$$0(28);
}
return this.getDataContext().getData(key);//委托给DataContext对象getData方法实现
}
@Nullable
public Project getProject() {
return (Project)this.getData(CommonDataKeys.PROJECT);
}
可以看到是通过AnActionEvent.getData方法传入一个CommonDataKeys.PROJECT参数,拿到Project对象,那么CommonDataKeys是不是一个key的集合呢?接着看会发现有很多对象key,例如Editor、VirtualFile、PsiFile对象等等。
public class CommonDataKeys {
public static final DataKey<Project> PROJECT = DataKey.create("project");
public static final DataKey<Editor> EDITOR = DataKey.create("editor");
public static final DataKey<Editor> HOST_EDITOR = DataKey.create("host.editor");
public static final DataKey<Caret> CARET = DataKey.create("caret");
public static final DataKey<Editor> EDITOR_EVEN_IF_INACTIVE = DataKey.create("editor.even.if.inactive");
public static final DataKey<Navigatable> NAVIGATABLE = DataKey.create("Navigatable");
public static final DataKey<Navigatable[]> NAVIGATABLE_ARRAY = DataKey.create("NavigatableArray");
public static final DataKey<VirtualFile> VIRTUAL_FILE = DataKey.create("virtualFile");
public static final DataKey<VirtualFile[]> VIRTUAL_FILE_ARRAY = DataKey.create("virtualFileArray");
public static final DataKey<PsiElement> PSI_ELEMENT = DataKey.create("psi.Element");
public static final DataKey<PsiFile> PSI_FILE = DataKey.create("psi.File");
public static final DataKey<Boolean> EDITOR_VIRTUAL_SPACE = DataKey.create("editor.virtual.space");
public CommonDataKeys() {
}
}
通过以上图示操作,会发现CommonDataKeys还有个子类PlatformDataKeys,PlatformDataKeys又有个子类LangDataKeys,所以这里列举下获取相关对象的key,以后开发需要哪个对象,直接查阅也很方便。
public class PlatformDataKeys extends CommonDataKeys {
public static final DataKey<FileEditor> FILE_EDITOR = DataKey.create("fileEditor");
public static final DataKey<String> FILE_TEXT = DataKey.create("fileText");
public static final DataKey<Boolean> IS_MODAL_CONTEXT = DataKey.create("isModalContext");
public static final DataKey<DiffViewer> DIFF_VIEWER = DataKey.create("diffViewer");
public static final DataKey<DiffViewer> COMPOSITE_DIFF_VIEWER = DataKey.create("compositeDiffViewer");
public static final DataKey<String> HELP_ID = DataKey.create("helpId");
public static final DataKey<Project> PROJECT_CONTEXT = DataKey.create("context.Project");
public static final DataKey<Component> CONTEXT_COMPONENT = DataKey.create("contextComponent");
public static final DataKey<CopyProvider> COPY_PROVIDER = DataKey.create("copyProvider");
public static final DataKey<CutProvider> CUT_PROVIDER = DataKey.create("cutProvider");
public static final DataKey<PasteProvider> PASTE_PROVIDER = DataKey.create("pasteProvider");
public static final DataKey<DeleteProvider> DELETE_ELEMENT_PROVIDER = DataKey.create("deleteElementProvider");
public static final DataKey<Object> SELECTED_ITEM = DataKey.create("selectedItem");
public static final DataKey<Object[]> SELECTED_ITEMS = DataKey.create("selectedItems");
public static final DataKey<Rectangle> DOMINANT_HINT_AREA_RECTANGLE = DataKey.create("dominant.hint.rectangle");
public static final DataKey<ContentManager> CONTENT_MANAGER = DataKey.create("contentManager");
public static final DataKey<ToolWindow> TOOL_WINDOW = DataKey.create("TOOL_WINDOW");
public static final DataKey<TreeExpander> TREE_EXPANDER = DataKey.create("treeExpander");
public static final DataKey<ExporterToTextFile> EXPORTER_TO_TEXT_FILE = DataKey.create("exporterToTextFile");
public static final DataKey<VirtualFile> PROJECT_FILE_DIRECTORY = DataKey.create("context.ProjectFileDirectory");
public static final DataKey<Disposable> UI_DISPOSABLE = DataKey.create("ui.disposable");
public static final DataKey<ContentManager> NONEMPTY_CONTENT_MANAGER = DataKey.create("nonemptyContentManager");
public static final DataKey<ModalityState> MODALITY_STATE = DataKey.create("ModalityState");
public static final DataKey<Boolean> SOURCE_NAVIGATION_LOCKED = DataKey.create("sourceNavigationLocked");
public static final DataKey<String> PREDEFINED_TEXT = DataKey.create("predefined.text.value");
public static final DataKey<String> SEARCH_INPUT_TEXT = DataKey.create("search.input.text.value");
public static final DataKey<Object> SPEED_SEARCH_COMPONENT = DataKey.create("speed.search.component.value");
public static final DataKey<Point> CONTEXT_MENU_POINT = DataKey.create("contextMenuPoint");
@Deprecated
public static final DataKey<Comparator<? super AnAction>> ACTIONS_SORTER = DataKey.create("actionsSorter");
}
public class LangDataKeys extends PlatformDataKeys {
public static final DataKey<Module> MODULE = DataKey.create("module");
public static final DataKey<Module> MODULE_CONTEXT = DataKey.create("context.Module");
public static final DataKey<Module[]> MODULE_CONTEXT_ARRAY = DataKey.create("context.Module.Array");
public static final DataKey<ModifiableModuleModel> MODIFIABLE_MODULE_MODEL = DataKey.create("modifiable.module.model");
public static final DataKey<Language> LANGUAGE = DataKey.create("Language");
public static final DataKey<Language[]> CONTEXT_LANGUAGES = DataKey.create("context.Languages");
public static final DataKey<PsiElement[]> PSI_ELEMENT_ARRAY = DataKey.create("psi.Element.array");
public static final DataKey<IdeView> IDE_VIEW = DataKey.create("IDEView");
public static final DataKey<Boolean> NO_NEW_ACTION = DataKey.create("IDEview.no.create.element.action");
public static final DataKey<Condition<AnAction>> PRESELECT_NEW_ACTION_CONDITION = DataKey.create("newElementAction.preselect.id");
public static final DataKey<PsiElement> TARGET_PSI_ELEMENT = DataKey.create("psi.TargetElement");
public static final DataKey<Module> TARGET_MODULE = DataKey.create("module.TargetModule");
public static final DataKey<PsiElement> PASTE_TARGET_PSI_ELEMENT = DataKey.create("psi.pasteTargetElement");
public static final DataKey<ConsoleView> CONSOLE_VIEW = DataKey.create("consoleView");
public static final DataKey<JBPopup> POSITION_ADJUSTER_POPUP = DataKey.create("chooseByNameDropDown");
public static final DataKey<JBPopup> PARENT_POPUP = DataKey.create("chooseByNamePopup");
public static final DataKey<Library> LIBRARY = DataKey.create("project.model.library");
public static final DataKey<RunProfile> RUN_PROFILE = DataKey.create("runProfile");
public static final DataKey<ExecutionEnvironment> EXECUTION_ENVIRONMENT = DataKey.create("executionEnvironment");
public static final DataKey<RunContentDescriptor> RUN_CONTENT_DESCRIPTOR = DataKey.create("RUN_CONTENT_DESCRIPTOR");
}
最后到这里,插件开发基础篇就结束,下一篇就是本系列完结实战开发篇,欢迎继续关注~~~
欢迎关注Kotlin开发者联盟,这里有最新Kotlin技术文章,每周会不定期翻译一篇Kotlin国外技术文章。如果你也喜欢Kotlin,欢迎加入我们~~~
Kotlin邂逅设计模式系列:
数据结构与算法系列:
翻译系列:
原创系列:
Effective Kotlin翻译系列
实战系列: