用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

極客熊貓發表於2018-07-01

簡述: 前兩天寫了篇用Kotlin擼一個圖片壓縮外掛-導學篇,現在迎來了外掛基礎篇,沒錯這篇文章就是教你如何一步一步從零開始寫一個外掛,包括外掛專案構建,執行,除錯到最後的上線釋出整個流程。如果你是外掛零基礎的小白,那麼這篇文章適合你,而且這篇文章也是下面實戰篇的基礎.

插播一條訊息(有人提需求了)

ImageSlimming圖片壓縮外掛開發完成後,馬上就把它推薦給團隊內部人員使用,在週會上就有同事提出了一個需求,就是在AndroidStudio專案中,可以任意選中res目錄下一張或多張圖片,然後直接右鍵選擇,就可以實現圖片壓縮。然後思考了一波,這個需求挺好的,心裡大概想了下,今晚就去把它實現了。實現效果大概如下:

用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

實現這個功能後,把V1.1版本的程式碼做了很大的結構上調整,抽離出一些公共的頂層函式和擴充套件函式,目前這個功能程式碼已經更新到GitHub上了,請認準feature-image-slimming-v1.2分支。

一、什麼是IDE(JetBrains全家桶)外掛

IDE外掛利用jetBrains公司開源的IntelliJ Platform SDK(java語言)來開發一個獨立功能可以安裝在IDEA之類的編輯器的功能元件。 IDE外掛是基於IntelliJ IDEA開發工具開發,裡面整合了外掛的專案的構建。採用的是Java語言開發和IntelliJ的SDK相結合開發。並且在開發出來的外掛不僅在AndroidStudio上可以使用,可以通用於jetBrains的編輯器的全家桶工具。通過原始碼可以發現Intellij Idea內建了大量的外掛,可以這麼說Intellij Idea開發工具大部分功能是由外掛組合而成的。

二、開始構建你的第一個外掛專案

注意: 構建外掛專案的方式主要有兩種:

一種是直接建立IDEA內建的外掛專案.

用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

另一種則是先通過構建一個gradle專案,然後加入plugin.xml配置以及 加入IDEA ERP的依賴,然後來構建一個外掛專案(整個開發過程就和開發一個Android專案一樣),當然這個構建過程可參考官方給出的gradle-intellij-plugin專案來實現。不過在最新2018.1.1之後版本中,IDEA內部也提供了構建grale外掛專案入口,具體可下載新版本Intellij Idea。

用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

  • 1、(這裡我們以第一種為例)開啟已經安裝好的IntelliJ IDEA,然後create New Project. 選擇一個IntelliJ Platform Plugin專案。注意需要引入IntelliJ IDEA的SDK

用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

  • 2、選擇好SDK後,然後只需要一步一步把專案建立完畢即可,建立好的專案結構如下:

用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

  • 3、正如你所看到,生成了一個plugin.xml,這個檔案是外掛專案的配置檔案,它記錄了外掛相關的版本擴充套件等基本資訊,還記錄了外掛事件與具體實現類繫結過程,下面就一一介紹每個標籤的含義。
<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倉庫中會用這個。

version標籤: 外掛版本號,這個用於標識外掛版本,一般用於更新jetbrains plugins倉庫中外掛版本標識。

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實現類的繫結。

  • 4、建立一個Action類,在IDEA外掛專案中,IDEA點選Item或者按鈕或者一個圖示對應是觸發了外掛中一個Action,建立Action主要有兩種方式:

第一種:就是通過IDEA提供的一個入口,直接去建立Action,然後它自動幫你實現plugin.xml中的事件繫結的註冊

用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

注意點一: 定義的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>
複製程式碼
  • 5、配置OK後,現在就可以執行外掛了,執行成功後會新啟動一個Intellij IDEA,這個IDE就是安裝了開發的外掛,然後就可以在裡面去除錯你的外掛功能。

用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

  • 6、點選執行,進行測試

用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

  • 7、你可以打斷點,點選debug,然後就可以斷點除錯程式碼。

  • 8、最後一步,打包外掛,併發布。選擇頂部工具欄Build, 點選"Prepare Plugin Module 'Demo' For Deployment",就會在當前工作目錄下生成一個jar或zip的包。然後釋出外掛,只需要在jetBrains Plugins Repository上傳你的包,等待jetBrains官方的稽核通過了,就能通過ide中的plugins倉庫中搜尋找到。

三、從原始碼分析外掛中AnAction

  • 1、外掛中的AnAction類

外掛開發最為重要之一的就是Action類了,可以說它是外掛功能的一個入口,編寫一個Action類,一般會去繼承AnAction類,AnAction是一個抽象類,必須要去實現actionPerformed方法,這個方法是在使用者觸發外掛的點選事件後回撥的,所以類似於開啟對話方塊,執行某個功能的邏輯可以寫在裡面等等。單從外掛開發角度(外掛的生命週期除外)來說,可以把當它當做程式中的main函式。

  • 2、外掛中的AnAction類中的actionPerformed方法

首先建立一個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持久化檔案物件等等,毫不誇張的說後續外掛功能開發都是圍繞它來展開的,下面會詳細描述。

  • 2、外掛中的AnAction類中的update方法
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方法中執行的。

  • 3、外掛中的AnAction類中的AnActionEvent

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方法實現
}
複製程式碼
  • 4、AnActionEvent獲得當前Project物件,引出CommonDataKeys
    @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() {
    }
}
複製程式碼
  • 5、繼續深入CommonDataKeys挖掘它是否有什麼子類,或許能夠發現更多key集合,拿到更多物件對應key,意味著你開發IDEA外掛使用的API會更廣,也會更快更好實現需求開發。這裡我會教你如何去使用upsource線上檢視IDEA的原始碼,去檢視CommonDataKeys的子類。

用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

通過以上圖示操作,會發現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");
}
複製程式碼

四、外掛開發一些建議

  • 1、建議多檢視官方API文件,儘管我認為官方文件寫得不是很好,但是這是一條深入學習外掛開發比較快的途徑。
  • 2、建議多檢視一下IDE內建外掛的原始碼,這是我認為深入學習外掛開發最好方法,例如Git4Idea內建的git外掛,深入它的原始碼,你會發現IDE中pull,push,checkout,branch每個功能具體實現是怎樣的。而且還有個好處,你會模仿使用一些內建外掛使用過的API,比如如何執行後臺的執行緒任務,如何操作檔案系統(外掛內部檔案)。
  • 3、最後一個,也是最重要的一點就是你的idea想法,外掛開發只是個工具,最關鍵是想法,如何把一個比較繁雜操作簡化成使用外掛來實現

五、外掛開發一些資源

最後到這裡,外掛開發基礎篇就結束,下一篇就是本系列完結實戰開發篇,歡迎繼續關注~~~

用Kotlin擼一個圖片壓縮外掛-外掛基礎篇(二)

歡迎關注Kotlin開發者聯盟,這裡有最新Kotlin技術文章,每週會不定期翻譯一篇Kotlin國外技術文章。如果你也喜歡Kotlin,歡迎加入我們~~~

相關文章