Brief description: due to personal reasons, there has been a very long period of time did not write an article, there is a sentence is so said that as long as the start will not be too late, so we started “with Kotlin yank a picture compression plug-in” series of articles the last practical article. I’ve actually posted the source code to GitHub and it’s pretty simple. Building on the foundation of the previous two articles, this article will use Kotlin to take you through an image compression plugin from scratch.

I. Preliminary preparation for development

  • 1. Visit the official website of TinyPng to register a TinyPng developer account and get the TinyPng ApiKey. The whole process is simple registration and verification.

  • 2. The image compression framework of this project is based on TinyPng’s image compression API, so you need to provide the Develop library on the Official website of TinyPng, where you can find the corresponding Java JAR. To facilitate the download, here is the address :TinyPng dependency package download

  • 3. Since the picture plug-in uses GUI, the plug-in GUI is built by the Swing framework in Java. Specifically, you can go to review relevant Swing knowledge points, of course, only need to roughly understand, after all, this is not the key point.

  • 4, need to master the basic knowledge of plug-in development, because this article is the actual combat will not go to detail about the basic knowledge of plug-in, specific details can refer to the second article in the series with Kotlin masturbate a picture compression plug-in – plug-in base (two)

  • 5. Basic development knowledge of Kotlin, such as Kotlin extension function encapsulation, Lambda expressions, functional API, IO stream API usage

Two, picture compression plug-in basic function points

The image compression plug-in mainly supports the following two functions:

  • 1, support the specified image source input directory batch compression to a specified output directory.

  • 2, support in the AndroidStudio project directly select the specified one or more pictures, right click directly compressed.

Three, to achieve the analysis of ideas

The overall idea of implementation: first of all, we need to find the key point of implementation, and then from the key point step by step to extend the extension, so the key point of the plug-in to achieve picture compression, there is no doubt that the picture compression API, that is, TinyPng API function call implementation.

Tinify.fromFile(inputFile).toFile(inputFile)
Copy the code

Using the TinyPng API above to find the key points, one is an input file and the other is an output file, so all the implementation of our image compression plug-in is around specifying an input file or directory and an output file or directory in a simple way.

Yes, it is so simple, so let’s analyze the above two functions to realize the idea is actually very simple:

  • Function point 1: is through the Swing framework JFileChooser component, open and specify a picture input file or directory and a picture compressed output file or directory.

  • Function Point 2: Datakeys.virtual_file_array.getdata (this) from the Intellij Idea Open API to retrieve the currently selected Virtual Files. Then the compressed image file directly output to the source file.

Note: Because tiny.fromfile ().tofile () internal source code actually sends network requests for image compression through OkHttp, and the internal way is synchronous request, but in the development of IDEA Plugin main thread is not able to perform time-consuming tasks, so it is necessary to put the API method call in asynchronous tasks

Four, code structure and implementation

  • The action package defines the two actions in the plug-in. We all know that the action is the entry point of the function execution in the plug-in development. ImageSlimmingAction is the first function point that specifies the input and output directory. RightSelectedAction is the second function point mentioned earlier that is compressed directly by right-clicking the file in the project selection diagram. The last two actions need to be registered in plugin.xml.
  <actions>
        <action class="com.mikyou.plugins.image.slimming.action.ImageSlimmingAction" text="ImageSlimming"
                id="com.mikyou.plugins.image.slimming.action.ImageSlimmingAction"
                description="compress picture plugin" icon="/img/icon_image_slimming.png">
            <add-to-group group-id="MainToolBar" anchor="after" relative-to-action="Android.MainToolBarSdkGroup"/>
        </action>

        <action id="com.mikyou.plugins.image.action.rightselectedaction"
                class="com.mikyou.plugins.image.slimming.action.RightSelectedAction" text="Quick Slim Images"
                description="Quick Slim Images">
            <add-to-group group-id="ProjectViewPopupMenu" anchor="after" relative-to-action="ReplaceInPath"/>
        </action>
    </actions>
Copy the code
  • The extension package defines the Kotlin extension functions. One is a Boolean extension that replaces if-else judgments with chain-like calls, and the other is an extension used by Dialog
/ / a Boolean extension
sealed class BooleanExt<out T>

object Otherwise : BooleanExt<Nothing> ()//Nothing is a subclass of all classes

class TransferData<T>(val data: T) : BooleanExt<T>()

inline fun <T> Boolean.yes(block: () -> T): BooleanExt<T> = when {
    this -> TransferData(block.invoke())
    else -> Otherwise
}

inline fun <T> Boolean.no(block: () -> T): BooleanExt<T> = when {
    this -> Otherwise
    else -> TransferData(block.invoke())
}

inline fun <T> BooleanExt<T>.otherwise(block: () -> T): T = when (this) {
    is Otherwise ->
        block()
    is TransferData ->
        this.data
}


/ / Dialog extension
fun Dialog.showDialog(width: Int = 550, height: Int = 400, isInCenter: Boolean = true, isResizable: Boolean = false) {
    pack()
    this.isResizable = isResizable
    setSize(width, height)
    if (isInCenter) {
        setLocation(Toolkit.getDefaultToolkit().screenSize.width / 2 - width / 2, Toolkit.getDefaultToolkit().screenSize.height / 2 - height / 2)
    }
    isVisible = true
}

fun Project.showWarnDialog(icon: Icon = UIUtil.getWarningIcon(), title: String, msg: String, positiveText: String = "Sure", negativeText: String = "Cancel", positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null) {
    Messages.showDialog(this, msg, title, arrayOf(positiveText, negativeText), 0, icon, object : DialogWrapper.DoNotAskOption.Adapter() {
        override fun rememberChoice(p0: Boolean, p1: Int) {
            if (p1 == 0) { positiveAction? .invoke() }else if (p1 == 1) { negativeAction? .invoke() } } }) }Copy the code
  • The Helper package mainly uses file IO operations. Since both actions have image compression operations, the implementation of image compression API calls is extracted and encapsulated in ImageSlimmingHelper for reuse.

  • The UI package is basically the implementation and interaction of some GUI interfaces in the Swing framework.

Four, the implementation of key technical points

  • Key point 1: How do you perform an asynchronous task in plug-in development

IDEA Plugin development is similar to Android development in that some time-consuming tasks cannot be executed directly in the main thread. They need to be executed in a specific background thread, otherwise the main thread will block. There is a task. Backgroundable abstract class in the Intellij Open API that handles asynchronous tasks. Backgroundable inherited the Task class and realize the PerformInBackgroundOption interface. It is very simple to pass in two parameters: a Project object and an asynchronous hint text. There are four callback functions: RUN (Progress: ProgressIndicator), onSuccess, onThrowable, and onFinished. Finally, queue is used to join the asynchronous task queue. Encapsulate it as an extension function for easy calls.

// asyncTask is an extension of Project that creates background asynchronous tasks
private fun Project.asyncTask(
        hintText: String,
        runAction: (ProgressIndicator) -> Unit,
        successAction: (() -> Unit)? = null,
        failAction: ((Throwable) -> Unit)? = null,
        finishAction: (() -> Unit)? = null
) {
    object : Task.Backgroundable(this, hintText) {
        override fun run(p0: ProgressIndicator) {
            runAction.invoke(p0)
        }

        override fun onSuccess(a){ successAction? .invoke() }override fun onThrowable(error: Throwable){ failAction? .invoke(error) }override fun onFinished(a){ finishAction? .invoke() } }.queue() }/ / the use of asyncTaskproject? .asyncTask(hintText ="Compressing.", runAction = {
        // Perform image compression
        outputSameFile.yes {
            // For right-click the image, directly compress the current directory to select the image, output directory including files is the originalinputFiles.forEach { inputFile -> Tinify.fromFile(inputFile.absolutePath).toFile(inputFile.absolutePath) } }.otherwise {  inputFiles.forEach { inputFile -> Tinify.fromFile(inputFile.absolutePath).toFile(getDestFilePath(model, inputFile.name)) } } }, successAction = { successAction?.invoke() }, failAction = { failAction?.invoke("TinyPng key is abnormal. Please enter it again.")})Copy the code
  • Key point 2: How do I get the currently selected file or directory during plug-in development

In plug-in development, how to get the currently selected file, in fact, the open API provides a similar DataContext DataContext, we need to get the file collection object first need to find the file management window object, Remember that the AnActionEvent object mentioned in the last blog is a medium of interaction and communication between the plug-in and IDEA. Through the getData method of dataContext inside AnActionEvent, the corresponding DataKey object is passed in to obtain the corresponding window object. There is a DataKey<VirtualFile[]> in CommonDataKey, and the currently selected set of file objects is obtained by passing in the dataContext object in the current event.

    private fun DataContext.getSelectedFiles(a): Array<VirtualFile>? {
        return DataKeys.VIRTUAL_FILE_ARRAY.getData(this)// Right-click to get selected multiple files, extend the function
    }
Copy the code
  • Key point 3: The use of JFileChooser components in Swing

The JFileChooser component is relatively simple to use, but I won’t go into the details here, and the code is very simple

  private void openFileAndSetPath(JComboBox<String> cBoxPath, int selectedMode, Boolean isSupportMultiSelect) {
        JFileChooser fileChooser = new JFileChooser();
        fileChooser.setFileSelectionMode(selectedMode);
        fileChooser.setMultiSelectionEnabled(isSupportMultiSelect);
        // Set the file extension filter
        if(selectedMode ! = JFileChooser.DIRECTORIES_ONLY) { fileChooser.addChoosableFileFilter(new FileNameExtensionFilter(".png"."png"));
            fileChooser.addChoosableFileFilter(new FileNameExtensionFilter(".jpg"."jpg"));
            fileChooser.addChoosableFileFilter(new FileNameExtensionFilter(".jpeg"."jpeg"));
        }

        fileChooser.showOpenDialog(null);


        if (selectedMode == JFileChooser.DIRECTORIES_ONLY) {// Select directory only, there is no multi-file selection
            File selectedDir = fileChooser.getSelectedFile();
            if(selectedDir ! =null) {
                cBoxPath.insertItemAt(selectedDir.getAbsolutePath(), 0);
                cBoxPath.setSelectedIndex(0); }}else {// Select only files and both files and directories.
            File[] selectedFiles = fileChooser.getSelectedFiles();
            if(selectedFiles ! =null && selectedFiles.length > 0) {
                cBoxPath.insertItemAt(getSelectedFilePath(selectedFiles), 0);
                cBoxPath.setSelectedIndex(0); }}}Copy the code
  • Key point four: API key verification and image compression implementation

If the first validation is valid, you need to store the ApiKey locally. The next compression will directly use the local key for compression. Once the local key is invalid, The TinyPng APIKey authentication dialog box needs to be displayed for re-authentication. Of course, it is important to note that the validation of the API key is also a synchronous network request, so it should also be placed on an asynchronous task.

fun checkApiKeyValid(
        project: Project? , apiKey:String,
        validAction: (() -> Unit)? = null,
        invalidAction: ((String) -> Unit)? = null
) {
    if(apiKey.isBlank()) { invalidAction? .invoke("TinyPng key is empty, please re-enter") } project? .asyncTask(hintText ="Checking whether key is valid", runAction = {
        try {
            Tinify.setKey(apiKey)
            Tinify.validate()
        } catch (exception: Exception) {
            throwexception } }, successAction = { validAction? .invoke() }, failAction = { println("Authentication Key failed!!${it.message}") invalidAction? .invoke("TinyPng key authentication failed, please re-enter")})}Copy the code

The next step is to compress images using asynchronous tasks.

fun slimImage(
        project: Project? , inputFiles:List<File>,
        model: ImageSlimmingModel = ImageSlimmingModel("".""."".""),
        successAction: (() -> Unit)? = null,
        outputSameFile: Boolean = false,
        failAction: ((String) -> Unit)? = null) { project? .asyncTask(hintText ="Compressing.", runAction = {
        // Perform image compression
        outputSameFile.yes {
            // For right-click the image, directly compress the current directory to select the image, output directory including files is the originalinputFiles.forEach { inputFile -> Tinify.fromFile(inputFile.absolutePath).toFile(inputFile.absolutePath) } }.otherwise {  inputFiles.forEach { inputFile -> Tinify.fromFile(inputFile.absolutePath).toFile(getDestFilePath(model, inputFile.name)) } } }, successAction = { successAction?.invoke() }, failAction = { failAction?.invoke("TinyPng key is abnormal. Please enter it again.")})}Copy the code

Five, the summary

The main key is to become more familiar with the Intellij Open API, and then use some of the syntax features of Kotlin. The rest is simple. In addition, I think it is very efficient to compress images into a plug-in, otherwise the traditional mode requires dragging images to the browser and downloading them one by one. Some people ask me, isn’t it just a script that can solve this problem? The script is not flexible enough to select one or more images in the project like a plug-in. If you have any questions, please leave a comment below. Thank you.

Plugin project source code address

Welcome to the Kotlin Developer Association, where the latest Kotlin technical articles are published, and a weekly Kotlin foreign technical article is translated from time to time. If you like Kotlin, welcome to join us ~~~