簡述: 前兩天寫了篇用Kotlin擼一個圖片壓縮插件-導學篇,如今迎來了插件基礎篇,沒錯這篇文章就是教你如何一步一步從零開始寫一個插件,包括插件項目構建,運行,調試到最後的上線發佈整個流程。若是你是插件零基礎的小白,那麼這篇文章適合你,並且這篇文章也是下面實戰篇的基礎.html
ImageSlimming圖片壓縮插件開發完成後,立刻就把它推薦給團隊內部人員使用,在週會上就有同事提出了一個需求,就是在AndroidStudio項目中,能夠任意選中res目錄下一張或多張圖片,而後直接右鍵選擇,就能夠實現圖片壓縮。而後思考了一波,這個需求挺好的,內心大概想了下,今晚就去把它實現了。實現效果大概以下:java
實現這個功能後,把V1.1版本的代碼作了很大的結構上調整,抽離出一些公共的頂層函數和擴展函數,目前這個功能代碼已經更新到GitHub上了,請認準feature-image-slimming-v1.2分支。android
IDE插件利用jetBrains公司開源的IntelliJ Platform SDK(java語言)來開發一個獨立功能能夠安裝在IDEA之類的編輯器的功能組件。 IDE插件是基於IntelliJ IDEA開發工具開發,裏面集成了插件的項目的構建。採用的是Java語言開發和IntelliJ的SDK相結合開發。而且在開發出來的插件不只在AndroidStudio上可使用,能夠通用於jetBrains的編輯器的全家桶工具。經過源碼能夠發現Intellij Idea內置了大量的插件,能夠這麼說Intellij Idea開發工具大部分功能是由插件組合而成的。git
注意: 構建插件項目的方式主要有兩種:github
一種是直接建立IDEA內置的插件項目.api
另外一種則是先經過構建一個gradle項目,而後加入plugin.xml配置以及 加入IDEA ERP的依賴,而後來構建一個插件項目(整個開發過程就和開發一個Android項目同樣),固然這個構建過程可參考官方給出的gradle-intellij-plugin項目來實現。不過在最新2018.1.1以後版本中,IDEA內部也提供了構建grale插件項目入口,具體可下載新版本Intellij Idea。架構
<idea-plugin>
<id>com.your.company.unique.plugin.id</id>
<name>Plugin display name here</name>
<version>1.0</version>
<vendor email="support@yourcompany.com" url="http://www.yourcompany.com">YourCompany</vendor>
<description><![CDATA[
Enter short description for your plugin here.<br>
<em>most HTML tags may be used</em>
]]></description>
<change-notes><![CDATA[
Add change notes here.<br>
<em>most HTML tags may be used</em>
]]>
</change-notes>
<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/build_number_ranges.html for description -->
<idea-version since-build="173.0"/>
<!-- please see http://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html on how to target different products -->
<!-- uncomment to enable plugin in all products <depends>com.intellij.modules.lang</depends> -->
<extensions defaultExtensionNs="com.intellij">
<!-- Add your extensions here -->
</extensions>
<actions>
<!-- Add your actions here -->
</actions>
</idea-plugin>
複製代碼
id標籤: plugin插件項目的標識,和Android項目中的package功能相似。惟一標識一個插件項目。併發
name標籤: 插件名字,發佈到jetBrains plugin倉庫中會用這個。app
version標籤: 插件版本號,這個用於標識插件版本,通常用於更新jetbrains plugins倉庫中插件版本標識。dom
vendor標籤: 開發者信息,郵箱和我的主頁,公司名字或我的開發者姓名,用於插件倉庫中插件信息介紹顯示。
description標籤: 插件的描述信息,主要是描述插件有什麼功能。支持標籤內部內嵌HTML標籤。
changNote標籤: 通常用於插件版本變動的信息。支持標籤內部內嵌HTML標籤。
idea-version標籤: 這個版本標籤須要注意下,它決定了該插件可以運行在最低版本的IDEA中,一旦配置不當,會致使插件安裝不成功,有點相似Android中AndroidManifest.xml中配置最低兼容Android版本意思。
depends標籤: 表示當前的插件項目依賴哪些內置或者外部的插件庫依賴,例如你須要實現相似git功能插件,你就能夠經過depends標籤引入Git4Idea便可,<depends>Git4Idea</depends>,若是看過IDEA源碼的話,實際上內置GitHub插件就是經過depends依賴內部Git4Idea插件實現的,還有如今的碼雲git工具插件也是經過依賴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>
<!-- Add your actions here -->
<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"/><!--加入到ToolbarRunGroup內置組-->
</action>
</actions>
複製代碼
在plugin.xml中配置插件圖標,先在插件項目中resource目錄下建立一個image目錄或者直接把圖標拷貝目錄下便可 而後action標籤中指定icon屬性
<actions>
<!-- Add your actions here -->
<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"/><!--加入到ToolbarRunGroup內置組-->
</action>
</actions>
複製代碼
在plugin.xml中配置自定義組,並把自定義的組加入內置的組中。
<group id="com.mikyou.plugins.group.demo" text="Demo" description="just a demo group"><!--group標籤實現自定義組,id:組的惟一標識,text:組顯示名稱,description:組的描述名-->
<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"/><!--加入到ToolbarRunGroup內置組-->
</action>
</group>
複製代碼
七、你能夠打斷點,點擊debug,而後就能夠斷點調試代碼。
八、最後一步,打包插件,併發布。選擇頂部工具欄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,歡迎加入咱們~~~