package org.plantuml.idea.toolwindow; import com.intellij.openapi.Disposable; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.LowMemoryWatcher; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.wm.ToolWindow; import com.intellij.ui.components.JBScrollPane; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.plantuml.idea.action.NextPageAction; import org.plantuml.idea.action.SelectPageAction; import org.plantuml.idea.lang.settings.PlantUmlSettings; import org.plantuml.idea.rendering.*; import org.plantuml.idea.toolwindow.listener.PlantUmlAncestorListener; import org.plantuml.idea.util.UIUtils; import javax.swing.*; import javax.swing.event.AncestorListener; import java.awt.*; import java.awt.event.MouseEvent; import java.awt.event.MouseMotionListener; import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.io.File; import java.util.concurrent.atomic.AtomicInteger; /** * @author Eugene Steinberg */ public class PlantUmlToolWindow extends JPanel implements Disposable { private static Logger logger = Logger.getInstance(PlantUmlToolWindow.class); private ToolWindow toolWindow; private JPanel imagesPanel; private JScrollPane scrollPane; private int zoom = 100; private int selectedPage = -1; private RenderCache renderCache; private AncestorListener plantUmlAncestorListener; private final LazyApplicationPoolExecutor lazyExecutor; private Project project; private AtomicInteger sequence = new AtomicInteger(); public boolean renderUrlLinks; public ExecutionStatusPanel executionStatusPanel; private SelectedPagePersistentStateComponent selectedPagePersistentStateComponent; private FileEditorManager fileEditorManager; private FileDocumentManager fileDocumentManager; public PlantUmlToolWindow(Project project, final ToolWindow toolWindow) { super(new BorderLayout()); this.project = project; this.toolWindow = toolWindow; PlantUmlSettings settings = PlantUmlSettings.getInstance();// Make sure settings are loaded and applied before we start rendering. renderCache = new RenderCache(settings.getCacheSizeAsInt()); selectedPagePersistentStateComponent = ServiceManager.getService(SelectedPagePersistentStateComponent.class); plantUmlAncestorListener = new PlantUmlAncestorListener(this, project); fileEditorManager = FileEditorManager.getInstance(project); fileDocumentManager = FileDocumentManager.getInstance(); setupUI(); lazyExecutor = new LazyApplicationPoolExecutor(settings.getRenderDelayAsInt(), executionStatusPanel); LowMemoryWatcher.register(new Runnable() { @Override public void run() { renderCache.clear(); if (renderCache.getDisplayedItem() != null && !toolWindow.isVisible()) { renderCache.setDisplayedItem(null); imagesPanel.removeAll(); imagesPanel.add(new JLabel("Low memory detected, cache and images cleared. Go to PlantUML plugin settings and set lower cache size, or increase IDE heap size (-Xmx).")); imagesPanel.revalidate(); imagesPanel.repaint(); } } }, this); //must be last this.toolWindow.getComponent().addAncestorListener(plantUmlAncestorListener); } private void setupUI() { DefaultActionGroup newGroup = getActionGroup(); final ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.UNKNOWN, newGroup, true); actionToolbar.setTargetComponent(this); add(actionToolbar.getComponent(), BorderLayout.PAGE_START); imagesPanel = new JPanel(); imagesPanel.setLayout(new BoxLayout(imagesPanel, BoxLayout.Y_AXIS)); scrollPane = new JBScrollPane(imagesPanel); scrollPane.getVerticalScrollBar().setUnitIncrement(20); imagesPanel.add(new Usage("Usage:\n")); add(scrollPane, BorderLayout.CENTER); addScrollBarListeners(imagesPanel); } @NotNull private DefaultActionGroup getActionGroup() { DefaultActionGroup group = (DefaultActionGroup) ActionManager.getInstance().getAction("PlantUML.Toolbar"); DefaultActionGroup newGroup = new DefaultActionGroup(); AnAction[] childActionsOrStubs = group.getChildActionsOrStubs(); for (int i = 0; i < childActionsOrStubs.length; i++) { AnAction stub = childActionsOrStubs[i]; newGroup.add(stub); if (stub instanceof ActionStub) { if (((ActionStub) stub).getClassName().equals(NextPageAction.class.getName())) { newGroup.add(new SelectPageAction(this)); } } } executionStatusPanel = new ExecutionStatusPanel(); newGroup.add(executionStatusPanel); return newGroup; } private void addScrollBarListeners(JComponent panel) { panel.addMouseWheelListener(new MouseWheelListener() { @Override public void mouseWheelMoved(MouseWheelEvent e) { if (e.isControlDown()) { setZoom(Math.max(getZoom() - e.getWheelRotation() * 10, 1)); } else { e.setSource(scrollPane); scrollPane.dispatchEvent(e); } } }); panel.addMouseMotionListener(new MouseMotionListener() { private int x, y; @Override public void mouseDragged(MouseEvent e) { JScrollBar h = scrollPane.getHorizontalScrollBar(); JScrollBar v = scrollPane.getVerticalScrollBar(); int dx = x - e.getXOnScreen(); int dy = y - e.getYOnScreen(); h.setValue(h.getValue() + dx); v.setValue(v.getValue() + dy); x = e.getXOnScreen(); y = e.getYOnScreen(); } @Override public void mouseMoved(MouseEvent e) { x = e.getXOnScreen(); y = e.getYOnScreen(); } }); } @Override public void dispose() { logger.debug("dispose"); toolWindow.getComponent().removeAncestorListener(plantUmlAncestorListener); } public void renderLater(final LazyApplicationPoolExecutor.Delay delay, final RenderCommand.Reason reason) { logger.debug("renderLater ", project.getName(), " ", delay); ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { if (isProjectValid(project)) { final String source = UIUtils.getSelectedSourceWithCaret(fileEditorManager); if ("".equals(source)) { //is included file or some crap? logger.debug("empty source"); VirtualFile selectedFile = UIUtils.getSelectedFile(fileEditorManager, fileDocumentManager); RenderCacheItem last = renderCache.getDisplayedItem(); //todo check all items for included file? // if (last != null && reason == RenderCommand.Reason.FILE_SWITCHED) { // selectedPage = selectedPagePersistentStateComponent.getPage(last.getSourceFilePath()); // logger.debug("file switched, setting selected page ",selectedPage); // } if (last != null && reason == RenderCommand.Reason.REFRESH) { logger.debug("empty source, executing command, reason=", reason); lazyExecutor.execute(getCommand(RenderCommand.Reason.REFRESH, last.getSourceFilePath(), last.getSource(), last.getBaseDir(), selectedPage, zoom, null, delay)); } if (last != null && last.isIncludedFile(selectedFile)) { logger.debug("include file selected"); if (last.isIncludedFileChanged(selectedFile, fileDocumentManager)) { logger.debug("includes changed, executing command"); lazyExecutor.execute(getCommand(RenderCommand.Reason.INCLUDES, last.getSourceFilePath(), last.getSource(), last.getBaseDir(), selectedPage, zoom, last, delay)); } else if (last.renderRequired(selectedPage, zoom, fileEditorManager, fileDocumentManager)) { logger.debug("render required"); lazyExecutor.execute(getCommand(RenderCommand.Reason.SOURCE_PAGE_ZOOM, last.getSourceFilePath(), last.getSource(), last.getBaseDir(), selectedPage, zoom, last, delay)); } else { logger.debug("include file, not changed"); } } else if (last != null && !renderCache.isDisplayed(last, selectedPage)) { logger.debug("empty source, not include file, displaying cached item ", last); displayExistingDiagram(last); } else { logger.debug("nothing needed"); } return; } String sourceFilePath = UIUtils.getSelectedFile(fileEditorManager, fileDocumentManager).getPath(); selectedPage = selectedPagePersistentStateComponent.getPage(sourceFilePath); logger.debug("setting selected page from storage ", selectedPage); if (reason == RenderCommand.Reason.REFRESH) { logger.debug("executing command, reason=", reason); final File selectedDir = UIUtils.getSelectedDir(fileEditorManager, fileDocumentManager); lazyExecutor.execute(getCommand(RenderCommand.Reason.REFRESH, sourceFilePath, source, selectedDir, selectedPage, zoom, null, delay)); return; } RenderCacheItem cachedItem = renderCache.getCachedItem(sourceFilePath, source, selectedPage, zoom, fileEditorManager, fileDocumentManager); if (cachedItem == null || cachedItem.renderRequired(source, selectedPage, fileEditorManager, fileDocumentManager)) { logger.debug("render required"); final File selectedDir = UIUtils.getSelectedDir(fileEditorManager, fileDocumentManager); lazyExecutor.execute(getCommand(RenderCommand.Reason.SOURCE_PAGE_ZOOM, sourceFilePath, source, selectedDir, selectedPage, zoom, cachedItem, delay)); } else if (!renderCache.isDisplayed(cachedItem, selectedPage)) { logger.debug("render not required, displaying cached item ", cachedItem); displayExistingDiagram(cachedItem); } else { logger.debug("render not required, item already displayed ", cachedItem); if (reason != RenderCommand.Reason.CARET) { cachedItem.setVersion(sequence.incrementAndGet()); lazyExecutor.cancel(); executionStatusPanel.updateNow(cachedItem.getVersion(), ExecutionStatusPanel.State.DONE, "cached"); } } } } }); } public void displayExistingDiagram(RenderCacheItem last) { executionStatusPanel.updateNow(last.getVersion(), ExecutionStatusPanel.State.DONE, "cached"); last.setVersion(sequence.incrementAndGet()); last.setRequestedPage(selectedPage); displayDiagram(last); } @NotNull protected RenderCommand getCommand(RenderCommand.Reason reason, String selectedFile, final String source, @Nullable final File baseDir, final int page, final int zoom, RenderCacheItem cachedItem, LazyApplicationPoolExecutor.Delay delay) { logger.debug("#getCommand selectedFile='", selectedFile, "', baseDir=", baseDir, ", page=", page, ", zoom=", zoom); int version = sequence.incrementAndGet(); return new MyRenderCommand(reason, selectedFile, source, baseDir, page, zoom, cachedItem, version, delay, renderUrlLinks, executionStatusPanel); } private class MyRenderCommand extends RenderCommand { public MyRenderCommand(Reason reason, String selectedFile, String source, File baseDir, int page, int zoom, RenderCacheItem cachedItem, int version, LazyApplicationPoolExecutor.Delay delay, boolean renderUrlLinks, ExecutionStatusPanel label) { super(reason, selectedFile, source, baseDir, page, zoom, cachedItem, version, renderUrlLinks, delay, label); } @Override public void postRenderOnEDT(RenderCacheItem newItem, long total, RenderResult result) { if (reason == Reason.REFRESH) { if (cachedItem != null) { renderCache.removeFromCache(cachedItem); } } if (!newItem.getRenderResult().hasError()) { renderCache.addToCache(newItem); } logger.debug("displaying item ", newItem); if (displayDiagram(newItem)) { executionStatusPanel.updateNow(newItem.getVersion(), ExecutionStatusPanel.State.DONE, total, result); } } } public boolean displayDiagram(RenderCacheItem cacheItem) { if (renderCache.isOlderRequest(cacheItem)) { //ctrl+z with cached image vs older request in progress logger.debug("skipping displaying older result", cacheItem); return false; } renderCache.setDisplayedItem(cacheItem); ImageItem[] imagesWithData = cacheItem.getImageItems(); RenderResult imageResult = cacheItem.getRenderResult(); int requestedPage = cacheItem.getRequestedPage(); if (requestedPage >= imageResult.getPages()) { logger.debug("requestedPage >= imageResult.getPages()", requestedPage, ">=", imageResult.getPages()); requestedPage = -1; if (!imageResult.hasError()) { logger.debug("toolWindow.page=", requestedPage, " (previously page=", selectedPage, ")"); selectedPage = requestedPage; } } imagesPanel.removeAll(); if (requestedPage == -1) { logger.debug("displaying images ", requestedPage); for (int i = 0; i < imagesWithData.length; i++) { displayImage(cacheItem, i, imagesWithData[i]); } } else { logger.debug("displaying image ", requestedPage); displayImage(cacheItem, requestedPage, imagesWithData[requestedPage]); } imagesPanel.revalidate(); imagesPanel.repaint(); return true; } public void displayImage(RenderCacheItem cacheItem, int pageNumber, ImageItem imageWithData) { if (imageWithData == null) { throw new RuntimeException("trying to display null image. selectedPage=" + selectedPage + ", nullPage=" + pageNumber + ", cacheItem=" + cacheItem); } PlantUmlImageLabel label = new PlantUmlImageLabel(imageWithData, pageNumber, cacheItem.getRenderRequest()); addScrollBarListeners(label); if (pageNumber != 0 && imagesPanel.getComponentCount() > 0) { imagesPanel.add(separator()); } imagesPanel.add(label); } public void applyNewSettings(PlantUmlSettings plantUmlSettings) { lazyExecutor.setDelay(plantUmlSettings.getRenderDelayAsInt()); renderCache.setMaxCacheSize(plantUmlSettings.getCacheSizeAsInt()); renderUrlLinks = plantUmlSettings.isRenderUrlLinks(); } private JSeparator separator() { JSeparator separator = new JSeparator(SwingConstants.HORIZONTAL); Dimension size = new Dimension(separator.getPreferredSize().width, 10); separator.setVisible(true); separator.setMaximumSize(size); separator.setPreferredSize(size); return separator; } public int getZoom() { return zoom; } public void setZoom(int zoom) { this.zoom = zoom; renderLater(LazyApplicationPoolExecutor.Delay.POST_DELAY, RenderCommand.Reason.SOURCE_PAGE_ZOOM); } public void setSelectedPage(int selectedPage) { if (selectedPage >= -1 && selectedPage < getNumPages()) { logger.debug("page ", selectedPage, " selected"); this.selectedPage = selectedPage; selectedPagePersistentStateComponent.setPage(selectedPage, renderCache.getDisplayedItem()); renderLater(LazyApplicationPoolExecutor.Delay.POST_DELAY, RenderCommand.Reason.SOURCE_PAGE_ZOOM); } } public void nextPage() { setSelectedPage(this.selectedPage + 1); } public void prevPage() { setSelectedPage(this.selectedPage - 1); } public int getNumPages() { int pages = -1; RenderCacheItem last = renderCache.getDisplayedItem(); if (last != null) { RenderResult imageResult = last.getRenderResult(); if (imageResult != null) { pages = imageResult.getPages(); } } return pages; } public int getSelectedPage() { return selectedPage; } public RenderCacheItem getDisplayedItem() { return renderCache.getDisplayedItem(); } private boolean isProjectValid(Project project) { return project != null && !project.isDisposed(); } public JPanel getImagesPanel() { return imagesPanel; } }