AndroidStudio插件开发(进阶篇之Action机制)

转载请注明出处:【huachao1001的简书:http://www.jianshu.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阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容