AndroidStudio插件開發(進階篇之Action機制)

轉載請注明出處:【huachao1001的簡書:http://www.lxweimin.com/users/0a7e42698e4b/latest_articles】

從上一篇《AndroidStudio插件開發(Hello World篇)》中我們已經大致了解了Action,這篇文章繼續深入探究IntelliJ IDEA插件開發中的Action機制。一個Action本質上來說就是一個Java類,并且這個類需要繼承AnAction。而一個Action對應于一個菜單項,每一次點擊這個菜單項就回調這個Action的actionPerformed(AnActionEvent event)函數,因此我們定義的Action在繼承AnAction時,需要重寫actionPerformed函數。定義好Action類后,我們需要注冊Action,即在plugin.xml文件中添加Action對應的標簽,在這個標簽中定義了Action應放置在界面的的哪個位置,作為哪個菜單項的子項等。接下來我們對Action機制進行深入。

1. 定義Action(繼承AnAction)

定義Action只需簡單地定義一個繼承AnAction的子類即可,子類中,最重要的就是actionPerformed函數和update函數。

1.1 重寫actionPerformed函數

我們知道,每次在菜單項中點擊我們自定義的Action時,對應會執行AnAction的actionPerformed函數。對應actionPerformed函數的理解,只需記住,當回調actionPerformed函數函數時,就意味著當前Action被點擊了一次。重寫actionPerformed函數非常簡單,這里簡單彈出一個Hello World。

package com.huachao.plugin;

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;

/**
 * Created by huachao on 2016/12/26.
 */
public class MyAction extends AnAction {
    @Override
    public void actionPerformed(AnActionEvent event) {
        Project project = event.getData(PlatformDataKeys.PROJECT);
        Messages.showMessageDialog(project, "Hello World!", "Information", Messages.getInformationIcon());
    }
}

1.2 重寫update函數

我們知道,為了響應用戶的點擊操作,我們重寫了actionPerformed函數。在actionPerformed函數中執行一些邏輯,比如彈出對話框,在打開文件中自動生成代碼等等操作。這些邏輯在actionPerformed函數中完成就好,但是,有時候我們定義的插件只在某些場景中使用。比如說,當我們編寫自動生成代碼的插件時,只有當有文件打開時才可以正常執行。因此,當我們不希望用戶點擊我們定義的插件時,我們可以將插件隱藏,讓用戶無法看到插件,只有當符合插件執行的環境時,才讓插件在菜單中顯示。

為了能在用戶點擊自定義插件對應的菜單項之前動態判斷是否將插件項顯示,只需重寫update函數。

update函數在Action狀態發生更新時被回調,當Action狀態刷新時,update函數被IDEA回調,并且傳遞AnActionEvent對象,AnAction對象中封裝了當前Action對應的環境。

如何理解上面這段話呢?我們知道,我們定義的每個Action都在菜單中對應一個子選項(為了方便描述,本文稱之為Action菜單項),當Action菜單項被點擊或者是Action的父菜單(包含Action菜單項的菜單)被點擊使得Action菜單項被顯示出來時,就會回調update函數。在update被回調時,傳入AnActionEvent對象,通過AnActionEvent對象我們可以判斷當前編輯框是否已經打開等實時IDEA環境狀況。

注意:先執行update函數,再執行actionPerformed函數。換言之,update發生在actionPerformed之前。

比如,我們想要實現:當編輯框被打開時顯示自定義的Action菜單項,否則,將Action菜單項設置為灰色。

@Override
public void update(AnActionEvent e) {
    Editor editor = e.getData(PlatformDataKeys.EDITOR);

    if (editor != null)
        e.getPresentation().setEnabled(true);
    else
        e.getPresentation().setEnabled(false);

}

代碼中,如果editor!=null即編輯框已打開,將Action菜單項設置為可用狀態(即正常顏色,黑色),否則設置為不可用狀態(即灰色)。當然了,你也可以通過e.getPresentation().setVisible(false);將Action菜單項設置為不可見,這樣Action菜單項就不會出現在菜單中。

另外,不要忘記在plugin.xml中將MyAction注冊,具體注冊方法可以參考上一篇文章《AndroidStudio插件開發(Hello World篇)》或者后一節的詳細介紹。

當編輯框被打開時(即有文件打開時),可以看到我們自定義的插件是正常。

這里寫圖片描述

當編輯框被關閉時(即沒有文件被打開時),可以看到我們自定義的插件是灰色。

這里寫圖片描述

注意:Action菜單項為灰色并不意味著被點擊時actionPerformed不會被調用,相反,只要Action菜單項被點擊,actionPerformed函數就會被調用。因此如果希望點擊Action菜單項時不做響應,需要在actionPerformed函數里面再次做具體判斷。

1.3 關于AnActionEvent

前面我們多次用到了AnActionEvent 對象,AnActionEvent 函數和update函數的形參都包含AnActionEvent對象。AnActionEvent對象是我們與IntelliJ IDEA交互的橋梁,我們可以通過AnActionEvent對象獲取當前IntelliJ IDEA的各個模塊對象,如編輯框窗口對象、項目窗口對象等,獲取到這些對象我們就可以做一些定制的效果。

1.3.1 getData函數

通過AnActionEvent對象的getData函數可以得到IDEA界面各個窗口對象以及各個窗口為實現某些特定功能的對象。getData函數需要傳入DataKey<T>對象,用于指明想要獲取的IDEA中的哪個對象。在CommonDataKeys已經定義好各個IDEA對象對應的DataKey<T>對象。

CommonDataKeys.java定義的DataKey<T>對象如下:

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");

不僅僅CommonDataKeys中定義了DataKey<T>對象,為了添加更多的DataKey<T>對象并且兼容等,又提供了PlatformDataKeys類,PlatformDataKeys類是CommonDataKeys子類,也就是說,只要是CommonDataKeys有的,PlatformDataKeys類都有。

PlatformDataKeys.java定義的DataKey<T>對象如下:

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");

1.3.2 Presentation對象

一個Presentation對象表示一個Action在菜單中的外觀,通過Presentation可以獲取Action菜單項的各種屬性,如顯示的文本、描述、圖標(Icon)等。并且可以設置當前Action菜單項的狀態、是否可見、顯示的文本等等。通過AnActionEvent對象的getPresentation()函數可以取得Presentation對象。

2. 注冊Action(修改plugin.xml)

注冊Action,我們可以手動直接修改plugin.xml文件,也可由IDEA直接自動幫我們生成,甚至是通過代碼動態注冊。其中,個人認為必須把手動注冊過程掌握透徹,這樣就能理解自動注冊與代碼注冊的原理。

2.1 手動注冊Action

2.1.1 單個Action

手動注冊即我們直接修改plugin.xml文件,在plugin.xml文件(resoutces/META-INF/plugin.xml)中找到<actions>標簽,并在<actions>標簽中添加<action>標簽。<action>標簽的屬性在上一篇文章中解釋過,這里再解釋一遍:

id:作為<action>標簽的唯一標識。一般以<項目名>.<類名>方式。
class:即我們自定義的AnAction類
text:顯示的文字,如我們自定義的插件放在菜單列表中,這個文字就是對應的菜單項
description:對這個AnAction的描述

<add-to-group>標簽用于描述當前Action放入到那個菜單組中,<add-to-group>標簽主要關注anchor屬性和relative-to-action屬性。anchor屬性用于描述位置,主要有四個選項:first、last、before、after。他們的含義如下:

first:放在所有子菜單的最前面
last:放在所有子菜單的最后
before:放在relative-to-action屬性指定的ID的子菜單的前面
after:放在relative-to-action屬性指定的ID的子菜單的后面

<keyboard-shortcut>標簽用于描述快捷鍵,主要關注2個屬性:keymap和first-keystroke。keymap使用默認值($default)就好,first-keystroke用于指定快捷鍵。

將Action菜單項放入到Help菜單的最前面,示例如下:

<actions>
    <!-- Add your actions here -->
    <action class="com.huachao.plugin.MyAction" id="StudyAction.MyAction" text="Hello Action">
        <add-to-group group-id="HelpMenu" anchor="first"/>
        <keyboard-shortcut keymap="$default" first-keystroke="ctrl alt Q"/>
    </action>
</actions>
Action菜單項

2.1.2 Action組(Action Group)

前面我們都是將一個Action放入到已有的菜單中作為子選項。現在我們定義一個跟Help同級的菜單,或者是定義包含多個子選項的菜單,這就是Action Group。使用Action Group非常簡單,就是在<actions>標簽中添加<group>子標簽,<group>標簽主要關注3個屬性:id、text、popup。id和text跟<action>標簽意義一樣,不再解釋,但需要注意,text中如果需要首字母加下劃線,則開頭下“_”即可。popup屬性用于描述是否有子菜單彈出,如果取值為true,則<group>標簽的內所有的<action>子標簽作為<group>菜單的子選項,否則,<group>標簽的內所有的<action>子標簽將替換<group>菜單項所在的位置,即沒有<group>這一層菜單。下面通過一個例子進行對比。

<actions>
    <group id="StudyAction.MyGroup" text="_MyGroup" popup="true">
        <add-to-group group-id="HelpMenu" anchor="first"/>
        <action class="com.huachao.plugin.MyAction" id="StudyAction.MyAction" text="Hello Action">
            <keyboard-shortcut keymap="$default" first-keystroke="ctrl alt Q"/>
        </action>
        <action id="StudyAction.SecondAction" class="com.huachao.plugin.SecondAction" text="SecondAction"/>
    </group>

</actions>

運行結果如下:


Action Group

將popup屬性改為false運行結果如下:

Action Group

注意到,我們將<group><add-to-group>子標簽的group-id屬性依然指定為Help菜單,現在我們換成與Help同級。將group-id屬性指定為MainMenu,運行如下:

添加到MainMenu

可以看到,IDEA的所有的導航菜單都放在MainMenu中,我們指定了anchor="first",因此被加入第一個位置。接下來我們再看看將group加入到編輯框窗口右鍵菜單,只需將group-id屬性指定為EditorPopupMenu,運行如下:

加入編輯框窗口右鍵菜單

修改為項目窗口右鍵菜單,修改group-id為:ProjectViewPopupMenu。運行如下:

加入項目窗口右鍵菜單

2.2 IDEA自動注冊Action

在我們熟悉了手動修改plugin.xml后,使用IDEA的方式就更簡單了。直接點擊在包目錄上右擊>New>Action。彈出框對應填寫屬性即可,這樣在自動創建Action的同時,完成了Action的注冊。

IDEA自動注冊

2.3 代碼動態注冊Action

代碼動態注冊Action主要是以Action Group動態添加和移除Action。前面我們在使用<group>標簽時,沒有使用到class屬性,即我們沒有定義自己的Action Group,而是使用默認的Action Group(DefaultActionGroup)。為了定制自己的Action Group,我們定義MyGroup類,使之繼承ActionGroup類,并在<group>標簽的class屬性中指定com.huachao.plugin.MyGroup

package com.huachao.plugin;

import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

/**
 * Created by huachao on 2016/12/26.
 */
public class MyGroup extends ActionGroup {


    @NotNull
    @Override
    public AnAction[] getChildren(@Nullable AnActionEvent anActionEvent) {
        return new AnAction[]{new CustomAction("first"),new CustomAction("second")};
    }


    class CustomAction extends AnAction {
        public CustomAction(String text) {
            super(text);
        }
        @Override
        public void actionPerformed(@NotNull AnActionEvent anActionEvent) {
        }
    }
}

plugin.xml文件中對應的<actions>標簽如下:

<actions>
    <group id="StudyAction.MyGroup" class="com.huachao.plugin.MyGroup" text="_MyGroup" popup="true">
        <add-to-group group-id="MainMenu" anchor="last"/> 
    </group> 
</actions>

運行結果如下:

代碼注冊

如果我們想在plugin.xml中注冊Action,并且想修改Group的菜單屬性。我們只需重寫DefaultActionGroup的update函數,DefaultActionGroup的update函數與AnAction的update函數意義差不多,前面解釋過AnAction的update函數,這里就不再解釋。例如,我們將Group菜單添加一個圖標,代碼如下:

package com.huachao.plugin;

import com.intellij.icons.AllIcons;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.editor.Editor;

/**
 * Created by huachao on 2016/12/26.
 */
public class MyGroup extends DefaultActionGroup {

    @Override
    public void update(AnActionEvent e) {
        Editor editor = e.getData(CommonDataKeys.EDITOR);
        e.getPresentation().setVisible(true);
        e.getPresentation().setEnabled(editor != null);
        e.getPresentation().setIcon(AllIcons.General.Error);
    }
}


運行結果如下:

修改Action Group菜單項

3. 總結

定義一個AndroidStudio插件只需簡單的2步:

  1. 定義Action
  • actionPerformed()
  • update()
  • AnActionEvent對象
  1. 注冊Action
  • 手動修改plugin.xml
  • Action Group
  • IDEA自動生成(New>Action>...)
  • 代碼注冊(通過Acton Group動態添加)

相比上一篇文章,在本文中,我們知其然更知其所以然。為后面定制AndroidStudio打下基礎。

參考資料

Action System相關類源碼(Github):《intellij-community》

官網資料:http://www.jetbrains.org/intellij/sdk/docs/tutorials/action_system.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容