org.asciidoc.intellij.editor.AsciiDocPreviewEditor.java Source code

Java tutorial

Introduction

Here is the source code for org.asciidoc.intellij.editor.AsciiDocPreviewEditor.java

Source

/*
 * Copyright 2013 Julien Viet
 *
 * 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 org.asciidoc.intellij.editor;

import com.intellij.codeHighlighting.BackgroundEditorHighlighter;
import com.intellij.ide.structureView.StructureViewBuilder;
import com.intellij.notification.Notification;
import com.intellij.notification.NotificationDisplayType;
import com.intellij.notification.NotificationGroup;
import com.intellij.notification.NotificationType;
import com.intellij.notification.Notifications;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorLocation;
import com.intellij.openapi.fileEditor.FileEditorState;
import com.intellij.openapi.fileEditor.FileEditorStateLevel;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.UserDataHolderBase;
import com.intellij.util.Alarm;
import com.intellij.util.messages.MessageBusConnection;
import org.apache.commons.io.FileUtils;
import org.asciidoc.intellij.AsciiDoc;
import org.asciidoc.intellij.editor.javafx.JavaFxCouldBeEnabledNotificationProvider;
import org.asciidoc.intellij.settings.AsciiDocApplicationSettings;
import org.asciidoc.intellij.settings.AsciiDocPreviewSettings;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.*;
import java.awt.*;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

/** @author Julien Viet */
public class AsciiDocPreviewEditor extends UserDataHolderBase implements FileEditor {

    public static final NotificationGroup NOTIFICATION_GROUP = new NotificationGroup("asciidoctor",
            NotificationDisplayType.NONE, true);

    /** single threaded with one task queue (one for each editor window) */
    private final LazyApplicationPoolExecutor LAZY_EXECUTOR = new LazyApplicationPoolExecutor();

    /** Indicates whether the HTML preview is obsolete and should regenerated from the AsciiDoc {@link #document}. */
    private transient String currentContent = "";

    private transient int targetLineNo = 0;

    private transient int currentLineNo = 0;

    /** The {@link Document} previewed in this editor. */
    protected final Document document;

    /** The directory which holds the temporary images. */
    protected final Path tempImagesPath;

    @NotNull
    private final JPanel myHtmlPanelWrapper;

    @NotNull
    private volatile AsciiDocHtmlPanel myPanel;

    @NotNull
    private final Alarm mySwingAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, this);

    /** . */
    private FutureTask<AsciiDoc> asciidoc = new FutureTask<AsciiDoc>(new Callable<AsciiDoc>() {
        public AsciiDoc call() throws Exception {
            return new AsciiDoc(
                    new File(FileDocumentManager.getInstance().getFile(document).getParent().getCanonicalPath()),
                    tempImagesPath);
        }
    });

    private void render() {
        LAZY_EXECUTOR.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    if (!document.getText().equals(currentContent)) {
                        currentContent = document.getText();

                        String markup = asciidoc.get().render(currentContent);
                        if (markup != null) {
                            myPanel.setHtml(markup);
                        }
                    }
                    if (currentLineNo != targetLineNo) {
                        currentLineNo = targetLineNo;
                        myPanel.scrollToLine(targetLineNo, document.getLineCount());
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } catch (Exception ex) {
                    String message = "Error rendering asciidoctor: " + ex.getMessage();
                    Notification notification = NOTIFICATION_GROUP.createNotification("Error rendering asciidoctor",
                            message, NotificationType.ERROR, null);
                    // increase event log counter
                    notification.setImportant(true);
                    Notifications.Bus.notify(notification);
                }
            }
        });
    }

    public void renderIfVisible() {
        if (getComponent().isVisible()) {
            render();
        }
    }

    @Nullable("Null means leave current panel")
    private AsciiDocHtmlPanelProvider retrievePanelProvider(@NotNull AsciiDocApplicationSettings settings) {
        final AsciiDocHtmlPanelProvider.ProviderInfo providerInfo = settings.getAsciiDocPreviewSettings()
                .getHtmlPanelProviderInfo();

        AsciiDocHtmlPanelProvider provider = AsciiDocHtmlPanelProvider.createFromInfo(providerInfo);

        if (provider.isAvailable() != AsciiDocHtmlPanelProvider.AvailabilityInfo.AVAILABLE) {
            settings.setAsciiDocPreviewSettings(
                    new AsciiDocPreviewSettings(settings.getAsciiDocPreviewSettings().getSplitEditorLayout(),
                            AsciiDocPreviewSettings.DEFAULT.getHtmlPanelProviderInfo(),
                            settings.getAsciiDocPreviewSettings().getPreviewTheme()));

            /* the following will not work, IntellIJ will show the error "parent must be showing" when this is
               tiggered during startup. */
            /*
            Messages.showMessageDialog(
                myHtmlPanelWrapper,
                "Tried to use preview panel provider (" + providerInfo.getName() + "), but it is unavailable. Reverting to default.",
                CommonBundle.getErrorTitle(),
                Messages.getErrorIcon()
            );
            */

            provider = AsciiDocHtmlPanelProvider.getProviders()[0];
        }

        return provider;
    }

    public AsciiDocPreviewEditor(final Document document) {

        this.document = document;

        // create temp dir for images. Will be used by JavaFX only!
        Path tempImagesPath = null;

        try {
            tempImagesPath = Files.createTempDirectory("asciidoctor-intellij");
        } catch (IOException _ex) {
            String message = "Can't create temp folder to render images: " + _ex.getMessage();
            Notification notification = AsciiDocPreviewEditor.NOTIFICATION_GROUP
                    .createNotification("Error rendering asciidoctor", message, NotificationType.ERROR, null);
            // increase event log counter
            notification.setImportant(true);
            Notifications.Bus.notify(notification);
        }
        this.tempImagesPath = tempImagesPath;

        myHtmlPanelWrapper = new JPanel(new BorderLayout());

        final AsciiDocApplicationSettings settings = AsciiDocApplicationSettings.getInstance();

        myPanel = detachOldPanelAndCreateAndAttachNewOne(document, tempImagesPath, myHtmlPanelWrapper, null,
                retrievePanelProvider(settings));

        MessageBusConnection settingsConnection = ApplicationManager.getApplication().getMessageBus().connect(this);
        AsciiDocApplicationSettings.SettingsChangedListener settingsChangedListener = new MyUpdatePanelOnSettingsChangedListener();
        settingsConnection.subscribe(AsciiDocApplicationSettings.SettingsChangedListener.TOPIC,
                settingsChangedListener);

        // Get asciidoc asynchronously
        new Thread() {
            @Override
            public void run() {
                asciidoc.run();
            }
        }.start();

        // Listen to the document modifications.
        this.document.addDocumentListener(new DocumentAdapter() {
            @Override
            public void documentChanged(DocumentEvent e) {
                renderIfVisible();
            }
        }, this);
    }

    @Contract("_, null, null -> fail")
    @NotNull
    private static AsciiDocHtmlPanel detachOldPanelAndCreateAndAttachNewOne(Document document, Path imagesDir,
            @NotNull JPanel panelWrapper, @Nullable AsciiDocHtmlPanel oldPanel,
            @Nullable AsciiDocHtmlPanelProvider newPanelProvider) {
        ApplicationManager.getApplication().assertIsDispatchThread();
        if (oldPanel == null && newPanelProvider == null) {
            throw new IllegalArgumentException("Either create new one or leave the old");
        }
        if (newPanelProvider == null) {
            return oldPanel;
        }
        if (oldPanel != null) {
            panelWrapper.remove(oldPanel.getComponent());
            Disposer.dispose(oldPanel);
        }

        final AsciiDocHtmlPanel newPanel = newPanelProvider.createHtmlPanel(document, imagesDir);
        if (oldPanel != null) {
            newPanel.setEditor(oldPanel.getEditor());
        }
        panelWrapper.add(newPanel.getComponent(), BorderLayout.CENTER);
        panelWrapper.repaint();

        return newPanel;
    }

    /**
     * Get the {@link java.awt.Component} to display as this editor's UI.
     */
    @NotNull
    public JComponent getComponent() {
        return myHtmlPanelWrapper;
    }

    /**
     * Get the component to be focused when the editor is opened.
     */
    @Nullable
    public JComponent getPreferredFocusedComponent() {
        return myHtmlPanelWrapper;
    }

    /**
     * Get the editor displayable name.
     *
     * @return <code>AsciiDoc</code>
     */
    @NotNull
    @NonNls
    public String getName() {
        return "Preview";
    }

    /**
     * Get the state of the editor.
     * <p/>
     * Just returns {@link FileEditorState#INSTANCE} as {@link AsciiDocPreviewEditor} is stateless.
     *
     * @param level the level.
     * @return {@link FileEditorState#INSTANCE}
     * @see #setState(com.intellij.openapi.fileEditor.FileEditorState)
     */
    @NotNull
    public FileEditorState getState(@NotNull FileEditorStateLevel level) {
        return FileEditorState.INSTANCE;
    }

    /**
     * Set the state of the editor.
     * <p/>
     * Does not do anything as {@link AsciiDocPreviewEditor} is stateless.
     *
     * @param state the new state.
     * @see #getState(com.intellij.openapi.fileEditor.FileEditorStateLevel)
     */
    public void setState(@NotNull FileEditorState state) {
    }

    /**
     * Indicates whether the document content is modified compared to its file.
     *
     * @return {@code false} as {@link AsciiDocPreviewEditor} is read-only.
     */
    public boolean isModified() {
        return false;
    }

    /**
     * Indicates whether the editor is valid.
     *
     * @return {@code true} if {@link #document} content is readable.
     */
    public boolean isValid() {
        return document.getText() != null;
    }

    /**
     * Invoked when the editor is selected.
     * <p/>
     * Refresh view on select (as dependent elements might have changed).
     */
    public void selectNotify() {
        currentContent = "";
        renderIfVisible();
    }

    /**
     * Invoked when the editor is deselected (it does not mean that it is not visible).
     * <p/>
     * Does nothing.
     */
    public void deselectNotify() {
    }

    /**
     * Add specified listener.
     * <p/>
     * Does nothing.
     *
     * @param listener the listener.
     */
    public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) {
    }

    /**
     * Remove specified listener.
     * <p/>
     * Does nothing.
     *
     * @param listener the listener.
     */
    public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) {
    }

    /**
     * Get the background editor highlighter.
     *
     * @return {@code null} as {@link AsciiDocPreviewEditor} does not require highlighting.
     */
    @Nullable
    public BackgroundEditorHighlighter getBackgroundHighlighter() {
        return null;
    }

    /**
     * Get the current location.
     *
     * @return {@code null} as {@link AsciiDocPreviewEditor} is not navigable.
     */
    @Nullable
    public FileEditorLocation getCurrentLocation() {
        return null;
    }

    /**
     * Get the structure view builder.
     *
     * @return TODO {@code null} as parsing/PSI is not implemented.
     */
    @Nullable
    public StructureViewBuilder getStructureViewBuilder() {
        return null;
    }

    /** Dispose the editor. */
    public void dispose() {
        Disposer.dispose(this);
        if (tempImagesPath != null) {
            try {
                FileUtils.deleteDirectory(tempImagesPath.toFile());
            } catch (IOException _ex) {
                Logger.getInstance(AsciiDocPreviewEditor.class).warn("could not remove temp folder", _ex);
            }
        }
    }

    public void scrollToLine(int line) {
        targetLineNo = line;
        renderIfVisible();
    }

    private class MyUpdatePanelOnSettingsChangedListener
            implements AsciiDocApplicationSettings.SettingsChangedListener {
        @Override
        public void onSettingsChange(@NotNull AsciiDocApplicationSettings settings) {
            final AsciiDocHtmlPanelProvider newPanelProvider = retrievePanelProvider(settings);

            mySwingAlarm.addRequest(new Runnable() {
                @Override
                public void run() {
                    myPanel = detachOldPanelAndCreateAndAttachNewOne(document, tempImagesPath, myHtmlPanelWrapper,
                            myPanel, newPanelProvider);
                    currentContent = "";
                    render();
                }
            }, 0, ModalityState.stateForComponent(getComponent()));
        }
    }

    public Editor getEditor() {
        return myPanel.getEditor();
    }

    public void setEditor(Editor editor) {
        myPanel.setEditor(editor);
    }
}