package com.intellij.flex.uiDesigner; import com.intellij.ProjectTopics; import com.intellij.flex.uiDesigner.mxml.ProjectComponentReferenceCounter; import com.intellij.flex.uiDesigner.preview.MxmlPreviewToolWindowManager; import com.intellij.javascript.flex.mxml.FlexCommonTypeNames; import com.intellij.javascript.flex.resolve.ActionScriptClassResolver; import com.intellij.lang.javascript.JavaScriptSupportLoader; import com.intellij.lang.javascript.flex.FlexUtils; import com.intellij.lang.javascript.flex.sdk.FlexSdkUtils; import com.intellij.lang.javascript.psi.ecmal4.JSClass; import com.intellij.lang.javascript.psi.ecmal4.XmlBackedJSClassFactory; import com.intellij.notification.Notification; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.PathManager; import com.intellij.openapi.application.WriteAction; import com.intellij.openapi.components.ServiceDescriptor; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.components.impl.ServiceManagerImpl; import com.intellij.openapi.editor.Document; import com.intellij.openapi.extensions.ExtensionPoint; import com.intellij.openapi.extensions.ExtensionPointName; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.help.HelpManager; import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.project.ModuleListener; import com.intellij.openapi.project.Project; import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.project.ProjectManagerListener; import com.intellij.openapi.projectRoots.Sdk; import com.intellij.openapi.projectRoots.SdkModificator; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.util.ActionCallback; import com.intellij.openapi.util.AsyncResult; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.PsiTreeChangeAdapter; import com.intellij.psi.PsiTreeChangeEvent; import com.intellij.psi.css.StylesheetFile; import com.intellij.psi.xml.XmlComment; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.ui.AppUIUtil; import com.intellij.util.Consumer; import com.intellij.util.concurrency.QueueProcessor; import com.intellij.util.messages.MessageBusConnection; import com.intellij.util.messages.Topic; import com.intellij.util.ui.update.MergingUpdateQueue; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; import javax.swing.event.HyperlinkEvent; import java.awt.image.BufferedImage; import java.io.File; import java.util.concurrent.atomic.AtomicBoolean; import static com.intellij.flex.uiDesigner.DocumentFactoryManager.DocumentInfo; import static com.intellij.flex.uiDesigner.LogMessageUtil.LOG; import static com.intellij.flex.uiDesigner.RenderActionQueue.RenderAction; public class DesignerApplicationManager { private static final ExtensionPointName<ServiceDescriptor> SERVICES = new ExtensionPointName<>("com.intellij.flex.uiDesigner.service"); public static final File APP_DIR = new File(PathManager.getSystemPath(), "flashUIDesigner"); public static final Topic<DocumentRenderedListener> MESSAGE_TOPIC = new Topic<>("Flash UI Designer document rendered event", DocumentRenderedListener.class); private DesignerApplication application; private final RenderActionQueue initialRenderQueue = new RenderActionQueue(); private ServiceManagerImpl serviceManager; private static class MyServiceManagerImpl extends ServiceManagerImpl { public MyServiceManagerImpl(@NotNull DesignerApplication newApp) { super(true); installEP(SERVICES, newApp); } } public static <T> T getService(@NotNull Class<T> serviceClass) { //noinspection unchecked return (T)getApplication().getPicoContainer().getComponentInstance(serviceClass.getName()); } public static DesignerApplicationManager getInstance() { return ServiceManager.getService(DesignerApplicationManager.class); } public static DesignerApplication getApplication() { return getInstance().application; } @TestOnly static ExtensionPoint<ServiceDescriptor> getExtensionPoint() { return Extensions.getArea(null).getExtensionPoint(SERVICES); } void disposeApplication() { LOG.assertTrue(application != null); final DesignerApplication disposedApp = application; application = null; AppUIUtil.invokeOnEdt(() -> { try { Disposer.dispose(disposedApp); } finally { Disposer.dispose(serviceManager); serviceManager = null; } }); } void setApplication(@NotNull DesignerApplication newApp) { LOG.assertTrue(application == null); LOG.assertTrue(serviceManager == null); Disposer.register(ApplicationManager.getApplication(), newApp); serviceManager = new MyServiceManagerImpl(newApp); application = newApp; } public boolean isInitialRendering() { return !initialRenderQueue.isEmpty(); } private static boolean checkFlexSdkVersion(final Sdk sdk) { String version = sdk.getVersionString(); if (StringUtil.isEmpty(version)) { LOG.warn("Flex SDK " + sdk.getName() + " version is empty, try to read flex-sdk-description.xml"); VirtualFile sdkHomeDirectory = sdk.getHomeDirectory(); if (sdkHomeDirectory == null) { LOG.warn("Flex SDK " + sdk.getName() + " home directory is null, cannot read flex-sdk-description.xml"); return false; } version = FlexSdkUtils.doReadFlexSdkVersion(sdkHomeDirectory); if (StringUtil.isEmpty(version)) { LOG.warn("Flex SDK " + sdk.getName() + " version is empty and result of read flex-sdk-description.xml is also empty"); return false; } setVersionString(sdk, version); } if (StringUtil.compareVersionNumbers(version, "4.5.1") >= 0) { return true; } if (version.length() < 5 || version.charAt(0) < '4') { return false; } if (version.charAt(0) == '4') { int build = FlexSdkUtils.getFlexSdkRevision(version); if (version.charAt(2) == '1') { return build == 16076; } else if (version.charAt(2) == '5' && version.charAt(4) == '0') { return build == 20967; } else { return version.charAt(2) >= '5'; } } return true; } private static void setVersionString(Sdk sdk, String version) { WriteAction.run(() -> { SdkModificator modificator = sdk.getSdkModificator(); modificator.setVersionString(version); modificator.commitChanges(); }); } public void renderIfNeed(@NotNull XmlFile psiFile, @Nullable Consumer<DocumentInfo> handler) { renderIfNeed(psiFile, handler, null, false); } public void renderIfNeed(@NotNull XmlFile psiFile, @Nullable final Consumer<DocumentInfo> handler, @Nullable ActionCallback renderRejectedCallback, final boolean debug) { boolean needInitialRender = isApplicationClosed(); DocumentInfo documentInfo = null; if (!needInitialRender) { Document[] unsavedDocuments = FileDocumentManager.getInstance().getUnsavedDocuments(); if (unsavedDocuments.length > 0) { renderDocumentsAndCheckLocalStyleModification(unsavedDocuments); } documentInfo = DocumentFactoryManager.getInstance().getNullableInfo(psiFile); needInitialRender = documentInfo == null; } if (!needInitialRender) { if (handler == null) { return; } Application app = ApplicationManager.getApplication(); if (app.isDispatchThread()) { final DocumentInfo finalDocumentInfo = documentInfo; app.executeOnPooledThread(() -> handler.consume(finalDocumentInfo)); } else { handler.consume(documentInfo); } return; } synchronized (initialRenderQueue) { AsyncResult<DocumentInfo> renderResult = initialRenderQueue.findResult(psiFile); if (renderResult == null) { renderResult = new AsyncResult<>(); if (renderRejectedCallback != null) { renderResult.notifyWhenRejected(renderRejectedCallback); } initialRenderQueue.add(new RenderAction<AsyncResult<DocumentInfo>>(psiFile.getProject(), psiFile.getViewProvider().getVirtualFile(), renderResult) { @Override protected boolean isNeedEdt() { // ProgressManager requires dispatch thread return true; } @Override protected void doRun() { assert project != null; if (project.isDisposed()) { return; } assert file != null; PsiFile psiFile = PsiManager.getInstance(project).findFile(file); if (!(psiFile instanceof XmlFile)) { return; } Module module = ModuleUtilCore.findModuleForFile(file, project); if (module != null) { renderDocument(module, (XmlFile)psiFile, debug, result); } } }); } if (handler != null) { renderResult.doWhenDone(handler); } renderResult.doWhenDone(createDocumentRenderedNotificationDoneHandler(false)); } } public static Consumer<DocumentInfo> createDocumentRenderedNotificationDoneHandler(final boolean syncTimestamp) { return info -> { Document document = FileDocumentManager.getInstance().getCachedDocument(info.getElement()); if (document != null) { if (syncTimestamp) { info.documentModificationStamp = document.getModificationStamp(); } ApplicationManager.getApplication().getMessageBus().syncPublisher(MESSAGE_TOPIC).documentRendered(info); } }; } @NotNull public AsyncResult<BufferedImage> getDocumentImage(@NotNull XmlFile psiFile) { final AsyncResult<BufferedImage> result = new AsyncResult<>(); renderIfNeed(psiFile, documentInfo -> Client.getInstance().getDocumentImage(documentInfo, result), result, false); return result; } public void openDocument(@NotNull final XmlFile psiFile, final boolean debug) { renderIfNeed(psiFile, documentInfo -> Client.getInstance().selectComponent(documentInfo.getId(), 0), null, debug); } @TestOnly public AsyncResult<DocumentInfo> renderDocument(@NotNull final Module module, @NotNull final XmlFile psiFile) { final AsyncResult<DocumentInfo> result = new AsyncResult<>(); renderDocument(module, psiFile, false, result); return result; } private void renderDocument(@NotNull final Module module, @NotNull final XmlFile psiFile, boolean debug, AsyncResult<DocumentInfo> result) { final boolean appClosed = isApplicationClosed(); boolean hasError = true; try { if (appClosed || !Client.getInstance().isModuleRegistered(module)) { final Sdk sdk = FlexUtils.getSdkForActiveBC(module); if (sdk == null || !checkFlexSdkVersion(sdk)) { reportInvalidFlexSdk(module, debug, sdk); result.setRejected(); return; } } hasError = false; } finally { if (hasError) { result.setRejected(); } } final RenderDocumentTask renderDocumentTask = new RenderDocumentTask(psiFile, result); ProgressManager.getInstance().run(appClosed ? new DesignerApplicationLauncher(module, renderDocumentTask, debug) : new DocumentTaskExecutor(module, renderDocumentTask)); } public boolean isApplicationClosed() { return application == null; } private static void reportInvalidFlexSdk(final Module module, boolean debug, @Nullable Sdk sdk) { String message = sdk == null ? FlashUIDesignerBundle.message("module.sdk.is.not.specified", module.getName()) : FlashUIDesignerBundle.message("module.sdk.is.not.compatible", sdk.getVersionString(), module.getName()); notifyUser(debug, message, module.getProject(), id -> { if ("edit".equals(id)) { FlexSdkUtils.openModuleConfigurable(module); } else { LOG.error("unexpected id: " + id); } }); } public void renderDocumentsAndCheckLocalStyleModification(Document[] documents) { renderDocumentsAndCheckLocalStyleModification(documents, false, false); } public void renderDocumentsAndCheckLocalStyleModification(final Document[] documents, final boolean onlyStyle, boolean reportProblems) { synchronized (initialRenderQueue) { final AtomicBoolean result = new AtomicBoolean(); if (!initialRenderQueue.isEmpty()) { initialRenderQueue.processActions(renderAction -> { if (renderAction.file == null) { ComplexRenderAction action = (ComplexRenderAction)renderAction; if (onlyStyle == action.onlyStyle) { action.merge(documents); result.set(true); return false; } } return true; }); } if (!result.get()) { initialRenderQueue.add(new ComplexRenderAction(documents, onlyStyle, reportProblems)); } } } public static String getOpenActionTitle(boolean debug) { return FlashUIDesignerBundle .message(debug ? "action.FlashUIDesigner.DebugDesignView.text" : "action.FlashUIDesigner.RunDesignView.text"); } public static void notifyUser(boolean debug, @NotNull String text, @NotNull Module module) { notifyUser(debug, text, module.getProject(), null); } static void notifyUser(boolean debug, @NotNull String text, @NotNull Project project, @Nullable final Consumer<String> handler) { Notification notification = new Notification(FlashUIDesignerBundle.message("plugin.name"), getOpenActionTitle(debug), text, NotificationType.ERROR, handler == null ? null : new NotificationListener() { @Override public void hyperlinkUpdate(@NotNull Notification notification, @NotNull HyperlinkEvent event) { if (event.getEventType() != HyperlinkEvent.EventType.ACTIVATED) { return; } notification.expire(); if ("help".equals(event.getDescription())) { HelpManager.getInstance().invokeHelp("flex.ui.designer.launch"); } else { handler.consume(event.getDescription()); } } }); notification.notify(project); } public static boolean isApplicable(Project project, PsiFile psiFile) { if (!dumbAwareIsApplicable(project, psiFile)) { return false; } final JSClass jsClass = XmlBackedJSClassFactory.getInstance().getXmlBackedClass(((XmlFile)psiFile).getRootTag()); return jsClass != null && ActionScriptClassResolver.isParentClass(jsClass, FlexCommonTypeNames.FLASH_DISPLAY_OBJECT_CONTAINER); } public static boolean dumbAwareIsApplicable(Project project, PsiFile psiFile) { final VirtualFile file = psiFile == null ? null : psiFile.getViewProvider().getVirtualFile(); if (file == null || !JavaScriptSupportLoader.isFlexMxmFile(file) || !ProjectRootManager.getInstance(project).getFileIndex().isInSourceContent(file)) { return false; } final XmlTag rootTag = ((XmlFile)psiFile).getRootTag(); return rootTag != null && rootTag.getPrefixByNamespace(JavaScriptSupportLoader.MXML_URI3) != null; } public void projectRegistered(Project project) { PsiManager.getInstance(project).addPsiTreeChangeListener(new MyPsiTreeChangeListener(project), project); } void attachProjectAndModuleListeners(Disposable parentDisposable) { MessageBusConnection connection = ApplicationManager.getApplication().getMessageBus().connect(parentDisposable); connection.subscribe(ProjectManager.TOPIC, new ProjectManagerListener() { @Override public void projectClosed(Project project) { if (isApplicationClosed()) { return; } Client client = Client.getInstance(); if (client.getRegisteredProjects().contains(project)) { client.closeProject(project); } } }); // unregistered module is more complicated - we cannot just remove all document factories which belong to project as in case of close project // we must remove all document factories belong to module and all dependents (dependent may be from another module, so, we process moduleRemoved synchronous // one by one) connection.subscribe(ProjectTopics.MODULES, new ModuleListener() { @Override public void moduleRemoved(@NotNull Project project, @NotNull Module module) { Client client = Client.getInstance(); if (!client.isModuleRegistered(module)) { return; } if (unregisterTaskQueueProcessor == null) { unregisterTaskQueueProcessor = new QueueProcessor<>(module1 -> { boolean hasError = true; final ActionCallback callback; initialRenderQueue.suspend(); try { callback = Client.getInstance().unregisterModule(module1); hasError = false; } finally { if (hasError) { initialRenderQueue.resume(); } } callback.doWhenProcessed(() -> initialRenderQueue.resume()); }); } unregisterTaskQueueProcessor.add(module); } }); } private QueueProcessor<Module> unregisterTaskQueueProcessor; private static class RenderDocumentTask extends DesignerApplicationLauncher.PostTask { private final XmlFile psiFile; private final AsyncResult<DocumentInfo> asyncResult; public RenderDocumentTask(@NotNull XmlFile psiFile, @Nullable AsyncResult<DocumentInfo> asyncResult) { this.psiFile = psiFile; this.asyncResult = asyncResult; } @Override public void dispose() { super.dispose(); if (asyncResult != null && !asyncResult.isProcessed()) { asyncResult.setRejected(); } } @Override public boolean run(Module module, @Nullable ProjectComponentReferenceCounter projectComponentReferenceCounter, ProgressIndicator indicator, ProblemsHolder problemsHolder) { indicator.setText(FlashUIDesignerBundle.message("rendering.document")); Client client = Client.getInstance(); if (!client.flush()) { return false; } if (projectComponentReferenceCounter != null && !client.registerDocumentReferences(projectComponentReferenceCounter.unregistered, module, problemsHolder)) { return false; } AsyncResult<DocumentInfo> renderResult = client.renderDocument(module, psiFile, problemsHolder); if (renderResult.isRejected()) { return false; } final AtomicBoolean processed = new AtomicBoolean(false); indicator.setText(FlashUIDesignerBundle.message("loading.libraries")); renderResult.doWhenDone((Consumer<DocumentInfo>)documentInfo -> { if (asyncResult != null) { asyncResult.setDone(documentInfo); } }); renderResult.doWhenProcessed(() -> processed.set(true)); while (!processed.get()) { try { //noinspection BusyWait Thread.sleep(5); } catch (InterruptedException ignored) { renderResult.setRejected(); return false; } if (indicator.isCanceled() || getInstance().isApplicationClosed()) { renderResult.setRejected(); return false; } } return true; } } private static class MyPsiTreeChangeListener extends PsiTreeChangeAdapter { private final MxmlPreviewToolWindowManager previewToolWindowManager; private final MergingUpdateQueue updateQueue; public MyPsiTreeChangeListener(Project project) { previewToolWindowManager = project.getComponent(MxmlPreviewToolWindowManager.class); updateQueue = new MergingUpdateQueue("FlashUIDesigner.update", 100, true, null); } @Override public void childRemoved(@NotNull PsiTreeChangeEvent event) { update(event); } @Override public void childAdded(@NotNull PsiTreeChangeEvent event) { update(event); } @Override public void childReplaced(@NotNull PsiTreeChangeEvent event) { update(event); } private void update(PsiTreeChangeEvent event) { PsiFile psiFile = event.getFile(); if (psiFile == null || event.getParent() instanceof XmlComment) { return; } if (getInstance().isApplicationClosed()) { if (psiFile.getViewProvider().getVirtualFile().equals(previewToolWindowManager.getServedFile())) { IncrementalDocumentSynchronizer.initialRender(getInstance(), (XmlFile)psiFile); } return; } if (psiFile instanceof XmlFile) { DocumentInfo info = DocumentFactoryManager.getInstance().getNullableInfo(psiFile); if (info == null && !psiFile.equals(previewToolWindowManager.getServedFile())) { return; } } else if (!(psiFile instanceof StylesheetFile)) { return; } updateQueue.queue(new IncrementalDocumentSynchronizer(event)); } } }