(七)IntelliJ 插件开发—— File and Class Choosers(文件和类选择器)

官方文档

https://www.jetbrains.org/intellij/sdk/docs/user_interface_components/file_and_class_choosers.html

Github

https://github.com/kungyutucheng/my_gradle_plugin

运行环境

macOS 10.14.5
IntelliJ idea 2019.2.4

1、FileChooser#chooseFile

效果

入口

文件对话框

选中后效果

思路

1、创建FileChooserAction并注册到ToolsMenu中,作为触发入口
2、实现FileChooserAction#actionPerformed,在其中创建一个FileChooserDescriptor对象
3、将步骤2当中创建的FileChooserDescriptor对象作为入参,实现FileChooser.chooseFile,并获取到该方法返回到文件对象VirtualFile(VirtualFile传送门)

源码

FileChooserDescriptor

/**
   * Creates new instance. Use methods from {@link FileChooserDescriptorFactory} for most used descriptors.
   *
   * @param chooseFiles       controls whether files can be chosen
   * @param chooseFolders     controls whether folders can be chosen
   * @param chooseJars        controls whether .jar files can be chosen
   * @param chooseJarsAsFiles controls whether .jar files will be returned as files or as folders
   * @param chooseJarContents controls whether .jar file contents can be chosen
   * @param chooseMultiple    controls how many files can be chosen
   */
  public FileChooserDescriptor(boolean chooseFiles,
                               boolean chooseFolders,
                               boolean chooseJars,
                               boolean chooseJarsAsFiles,
                               boolean chooseJarContents,
                               boolean chooseMultiple) {
    myChooseFiles = chooseFiles;
    myChooseFolders = chooseFolders;
    myChooseJars = chooseJars;
    myChooseJarsAsFiles = chooseJarsAsFiles;
    myChooseJarContents = chooseJarContents;
    myChooseMultiple = chooseMultiple;
  }

该构造方法源码到注释已经很清楚了,就不废话多解释了,如果想要支持更多类型的文件,可以重写FileChooserDescriptor#isFileSelectable

FileChooser#chooseFile

@Nullable
  public static VirtualFile chooseFile(@NotNull final FileChooserDescriptor descriptor,
                                       @Nullable final Project project,
                                       @Nullable final VirtualFile toSelect) {
    return chooseFile(descriptor, null, project, toSelect);
  }

  @Nullable
  public static VirtualFile chooseFile(@NotNull final FileChooserDescriptor descriptor,
                                       @Nullable final Component parent,
                                       @Nullable final Project project,
                                       @Nullable final VirtualFile toSelect) {
    Component parentComponent = parent == null ? KeyboardFocusManager.getCurrentKeyboardFocusManager().getActiveWindow() : parent;
    LOG.assertTrue(!descriptor.isChooseMultiple());
    return ArrayUtil.getFirstElement(chooseFiles(descriptor, parentComponent, project, toSelect));
  }
  • project:当前打开的工程对象,从源码可以看到,为null的时候project设置为当前激活的窗口
  • toSelect:默认打开的对象或者路径

Demo

FileChooserAction

package com.kungyu.file.chooser;

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;

/**
 * @author wengyongcheng
 * @since 2020/3/5 11:23 下午
 */
public class FileChooserAction extends AnAction {

    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
        FileChooserDescriptor chooserDescriptor = new FileChooserDescriptor(true,true,true,true,true,true);
        VirtualFile virtualFile = FileChooser.chooseFile(chooserDescriptor, e.getProject(), null);
        if(virtualFile != null) {
            Messages.showMessageDialog(virtualFile.getName(), "获取到的文件名称", Messages.getInformationIcon());
        } else {
            Messages.showMessageDialog("文件名称为空", "文件名称为空", Messages.getInformationIcon());
        }
    }
}

注册action

<action id="com.kungyu.file.chooser.FileChooserAction" class="com.kungyu.file.chooser.FileChooserAction" text="FileChooserAction" description="FileChooserAction">
     <add-to-group group-id="ToolsMenu" anchor="after" relative-to-action="com.kungyu.notification.HintNotificationAction"/>
</action>

然而,按照上述代码运行,我们会发现报错了,异常栈如下:

java.lang.Throwable: Assertion failed
    at com.intellij.openapi.diagnostic.Logger.assertTrue(Logger.java:180)
    at com.intellij.openapi.diagnostic.Logger.assertTrue(Logger.java:189)
    at com.intellij.openapi.fileChooser.FileChooser.chooseFile(FileChooser.java:72)
    at com.intellij.openapi.fileChooser.FileChooser.chooseFile(FileChooser.java:63)
    at com.kungyu.file.chooser.FileChooserAction.actionPerformed(FileChooserAction.java:20)
    // 其余略

可以看到,这是我们的代码:

com.kungyu.file.chooser.FileChooserAction.actionPerformed(FileChooserAction.java:20)

于是,往上追溯:

at com.intellij.openapi.fileChooser.FileChooser.chooseFile(FileChooser.java:63)

FileChooser.java:72处可以看到:

LOG.assertTrue(!descriptor.isChooseMultiple());

显而易见,我们在创建FileChooserDescriptor对象的时候指定的chooseMultiple 属性为true,触发了断言,改之为false,不再出现异常

问题解决了,但总得知道此处源码为何要加这个断言呢,个人猜测,FileChooser#chooseFile可能是之前的版本实现,官网也并没有介绍该方法,而且在源码中我们还可以看到FileChooser#chooseFiles方法,而该方法顾名思义就知道它是默认支持多选的,所以可能官方已经不推荐使用FileChooser#chooseFile,但是为了兼容,依旧保留了该方法,毕竟,使用FileChooser#chooseFile还是可以正常实现对应的功能的,虽然idea会报错


2、Filechooser#chooseFiles

效果

chooseFiles

该方法有多个重载方法,官网推荐使用带有回调参数的方法:

The best method to use is the one which returns void and takes a callback receiving the list of selected files as a parameter. This is the only overload which will display a native file open dialog on macOS.

因此,本文以以下方法为例:

源码

/**
   * Shows file/folder open dialog, allows user to choose files/folders and then passes result to callback in EDT.
   * On MacOS Open Dialog will be shown with slide effect if Macish UI is turned on.
   *
   * @param descriptor file chooser descriptor
   * @param project    project
   * @param toSelect   file to preselect
   * @param callback   callback will be invoked after user have closed dialog and only if there are files selected
   * @see FileChooserConsumer
   */
  public static void chooseFiles(@NotNull final FileChooserDescriptor descriptor,
                                 @Nullable final Project project,
                                 @Nullable final VirtualFile toSelect,
                                 @NotNull final Consumer<? super List<VirtualFile>> callback) {
    chooseFiles(descriptor, project, null, toSelect, callback);
  }

Demo

FileChooserAction

FileChooserDescriptor chooserDescriptor = new FileChooserDescriptor(true,true,true,true,true,true);
VirtualFile toSelect = LocalFileSystem.getInstance().findFileByPath(File.separator + "Users" + File.separator + "wengyongcheng" + File.separator + "swagger-html" + File.separator);
FileChooser.chooseFiles(chooserDescriptor, null, toSelect, virtualFiles -> {
    if (CollectionUtils.isNotEmpty(virtualFiles)) {
        for (VirtualFile file : virtualFiles) {
            Messages.showMessageDialog(file.getPath(), file.getName(),Messages.getInformationIcon());
        }
   }
});

3、 TextFieldWithBrowseButton

实现一个右边带有浏览图标的文件选择器

效果

image.png

源码

/*
 * Copyright 2000-2013 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.openapi.ui;

import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;

public class TextBrowseFolderListener extends ComponentWithBrowseButton.BrowseFolderActionListener<JTextField> {
  public TextBrowseFolderListener(@NotNull FileChooserDescriptor fileChooserDescriptor) {
    this(fileChooserDescriptor, null);
  }

  public TextBrowseFolderListener(@NotNull FileChooserDescriptor fileChooserDescriptor, @Nullable Project project) {
    super(null, null, null, project, fileChooserDescriptor, TextComponentAccessor.TEXT_FIELD_WHOLE_TEXT);
  }

  void setOwnerComponent(@NotNull TextFieldWithBrowseButton component) {
    myTextComponent = component.getChildComponent();
  }

  FileChooserDescriptor getFileChooserDescriptor() {
    return myFileChooserDescriptor;
  }
}

追溯进super方法:

public static class BrowseFolderActionListener<T extends JComponent> extends BrowseFolderRunnable <T> implements ActionListener {
    public BrowseFolderActionListener(@Nullable @Nls(capitalization = Nls.Capitalization.Title) String title,
                                      @Nullable @Nls(capitalization = Nls.Capitalization.Sentence) String description,
                                      @Nullable ComponentWithBrowseButton<T> textField,
                                      @Nullable Project project,
                                      FileChooserDescriptor fileChooserDescriptor,
                                      TextComponentAccessor<? super T> accessor) {
    super(title, description, project, fileChooserDescriptor, textField != null ? textField.getChildComponent() : null, accessor);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
      run();
    }
}

可以看到实现了BrowseFolderRunnable接口,而在BrowseFolderRunnable#run方法中,可以看到底层依旧是使用了FileChooser#chooseFile:

@Override
public void run() {
    FileChooserDescriptor fileChooserDescriptor = myFileChooserDescriptor;
    if (myTitle != null || myDescription != null) {
        fileChooserDescriptor = (FileChooserDescriptor)myFileChooserDescriptor.clone();
        if (myTitle != null) {
            fileChooserDescriptor.setTitle(myTitle);
        }
        if (myDescription != null) {
            fileChooserDescriptor.setDescription(myDescription);
        }
    }

    FileChooser.chooseFile(fileChooserDescriptor, getProject(), myTextComponent, getInitialFile(), this::onFileChosen);
}

Demo

FileChooserDialogWrapper

package com.kungyu.file.chooser;

import com.intellij.ide.util.AbstractTreeClassChooserDialog;
import com.intellij.ide.util.BrowseFilesListener;
import com.intellij.ide.util.TreeFileChooserFactory;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.ui.*;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import org.apache.commons.collections.CollectionUtils;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.io.File;

/**
 * @author wengyongcheng
 * @since 2020/3/6 11:55 下午
 */
public class FileChooserDialogWrapper extends DialogWrapper {

    private TextFieldWithBrowseButton textFieldWithBrowseButton;

    private JTextField fileTextFiled;

    public FileChooserDialogWrapper(){
        super(true);
        init();
        setTitle("文件选择对话框");
    }

    @Nullable
    @Override
    protected JComponent createCenterPanel() {

        JPanel panel = new JPanel();
        textFieldWithBrowseButton = new TextFieldWithBrowseButton();
        fileTextFiled = new JTextField();
        FileChooserDescriptor chooserDescriptor = new FileChooserDescriptor(true,true,true,true,true,true);
        TextBrowseFolderListener listener = new TextBrowseFolderListener(chooserDescriptor);
        textFieldWithBrowseButton.addBrowseFolderListener(listener);
        textFieldWithBrowseButton.setText(File.separator + "Users" + File.separator + "wengyongcheng" + File.separator + "swagger-html" + File.separator);
        panel.setLayout(new BorderLayout());
        panel.setPreferredSize(new Dimension(400,40));
        panel.add(textFieldWithBrowseButton, BorderLayout.CENTER);


        return panel;
    }

    @Nullable
    @Override
    protected ValidationInfo doValidate() {
        String filePath = textFieldWithBrowseButton.getText();
        VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(filePath);
        if (virtualFile != null) {
            Messages.showMessageDialog(virtualFile.getPath(), virtualFile.getName(), Messages.getInformationIcon());
        }
        return null;
    }
}

FileChooserAction

new FileChooserDialogWrapper().showAndGet();

4、TreeFileChooserFactory

可以打开树形文件选择框,且附带搜索功能

效果

未限制文件类型
限制Java文件类型

Demo

FileChooserAction

TreeFileChooserFactory instance = TreeFileChooserFactory.getInstance(e.getProject());
TreeFileChooser.PsiFileFilter fileFilter = file -> file.getName().endsWith(".java");
TreeFileChooser javaFileChooser = instance.createFileChooser("java文件选择器", null, null, fileFilter);
javaFileChooser.showDialog();
PsiFile selectedFile = javaFileChooser.getSelectedFile();
if (selectedFile != null) {
     Messages.showMessageDialog(selectedFile.getVirtualFile().getPath(),selectedFile.getVirtualFile().getName(), Messages.getInformationIcon());
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容