Java tutorial
/* * 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); } }