/* * Copyright 2000-2012 JetBrains s.r.o. * * 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 com.intellij.android.designer.designSurface; import com.android.annotations.VisibleForTesting; import com.android.ide.common.rendering.api.RenderSession; import com.android.ide.common.rendering.api.SessionParams; import com.android.ide.common.rendering.api.ViewInfo; import com.android.sdklib.SdkVersionInfo; import com.android.resources.Density; import com.android.sdklib.IAndroidTarget; import com.android.tools.idea.AndroidPsiUtils; import com.android.tools.idea.configurations.*; import com.android.tools.idea.rendering.*; import com.android.tools.idea.rendering.multi.RenderPreviewManager; import com.android.tools.idea.rendering.multi.RenderPreviewMode; import com.google.common.primitives.Ints; import com.intellij.android.designer.componentTree.AndroidTreeDecorator; import com.intellij.android.designer.designSurface.graphics.DrawingStyle; import com.intellij.android.designer.inspection.ErrorAnalyzer; import com.intellij.android.designer.model.*; import com.intellij.android.designer.model.layout.actions.ToggleRenderModeAction; import com.intellij.designer.DesignerEditor; import com.intellij.designer.DesignerToolWindow; import com.intellij.designer.DesignerToolWindowManager; import com.intellij.designer.actions.DesignerActionPanel; import com.intellij.designer.componentTree.TreeComponentDecorator; import com.intellij.designer.designSurface.*; import com.intellij.designer.designSurface.tools.ComponentCreationFactory; import com.intellij.designer.designSurface.tools.ComponentPasteFactory; import com.intellij.designer.designSurface.tools.InputTool; import com.intellij.designer.model.RadComponent; import com.intellij.designer.model.RadVisualComponent; import com.intellij.designer.model.WrapInProvider; import com.intellij.designer.palette.DefaultPaletteItem; import com.intellij.designer.palette.PaletteGroup; import com.intellij.designer.palette.PaletteItem; import com.intellij.designer.palette.PaletteToolWindowManager; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.application.Application; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.module.Module; import com.intellij.openapi.progress.EmptyProgressIndicator; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.ReadonlyStatusHandler; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.xml.XmlFile; import com.intellij.psi.xml.XmlTag; import com.intellij.ui.ScrollPaneFactory; import com.intellij.util.Alarm; import com.intellij.util.PsiNavigateUtil; import com.intellij.util.ThrowableConsumer; import com.intellij.util.ThrowableRunnable; import com.intellij.util.ui.UIUtil; import com.intellij.util.ui.update.MergingUpdateQueue; import com.intellij.util.ui.update.Update; import org.jetbrains.android.facet.AndroidFacet; import org.jetbrains.android.refactoring.AndroidExtractAsIncludeAction; import org.jetbrains.android.refactoring.AndroidExtractStyleAction; import org.jetbrains.android.refactoring.AndroidInlineIncludeAction; import org.jetbrains.android.refactoring.AndroidInlineStyleReferenceAction; import org.jetbrains.android.sdk.AndroidPlatform; import org.jetbrains.android.uipreview.RenderingException; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; import java.awt.*; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.MouseEvent; import java.awt.image.BufferedImage; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import static com.android.tools.idea.configurations.ConfigurationListener.MASK_ALL; import static com.android.tools.idea.configurations.ConfigurationListener.MASK_RENDERING; import static com.android.tools.idea.rendering.RenderErrorPanel.SIZE_ERROR_PANEL_DYNAMICALLY; import static com.intellij.designer.designSurface.ZoomType.FIT_INTO; import static org.jetbrains.android.facet.ResourceFolderManager.ResourceFolderListener; /** * @author Alexander Lobas */ public final class AndroidDesignerEditorPanel extends DesignerEditorPanel implements RenderContext, ResourceFolderListener, OverlayContainer { private static final int DEFAULT_HORIZONTAL_MARGIN = 30; private static final int DEFAULT_VERTICAL_MARGIN = 20; private static final Integer LAYER_ERRORS = LAYER_INPLACE_EDITING + 150; // Must be an Integer, not an int; see JLayeredPane.addImpl private static final Integer LAYER_PREVIEW = LAYER_INPLACE_EDITING + 170; // Must be an Integer, not an int; see JLayeredPane.addImpl private final TreeComponentDecorator myTreeDecorator; private final XmlFile myXmlFile; private final ExternalPSIChangeListener myPsiChangeListener; private final Alarm mySessionAlarm = new Alarm(); private final MergingUpdateQueue mySessionQueue; private final AndroidDesignerEditorPanel.LayoutConfigurationListener myConfigListener; private final AndroidFacet myFacet; private volatile RenderSession mySession; private volatile long mySessionId; private final Lock myRendererLock = new ReentrantLock(); private WrapInProvider myWrapInProvider; private RootView myRootView; private boolean myShowingRoot; private RenderPreviewTool myPreviewTool; private RenderResult myRenderResult; private PropertyParser myPropertyParser; private final IdManager myIdManager = new IdManager(); @Nullable private Configuration myConfiguration; private int myConfigurationDirty; private boolean myActive; private boolean myVariantChanged; /** Zoom level (1 = 100%). TODO: Persist this setting across IDE sessions (on a per file basis) */ private double myZoom = 1; private ZoomType myZoomMode = ZoomType.FIT_INTO; private RenderPreviewManager myPreviewManager; private final HoverOverlay myHover = new HoverOverlay(this); private final List<Overlay> myOverlays = Arrays.asList(myHover, new IncludeOverlay(this)); public AndroidDesignerEditorPanel(@NotNull DesignerEditor editor, @NotNull Project project, @NotNull Module module, @NotNull VirtualFile file) { super(editor, project, module, file); myTreeDecorator = new AndroidTreeDecorator(project); showProgress("Loading configuration..."); AndroidFacet facet = AndroidFacet.getInstance(getModule()); assert facet != null; myFacet = facet; if (facet.isGradleProject()) { // Ensure that the app resources have been initialized first, since // we want it to add its own variant listeners before ours (such that // when the variant changes, the project resources get notified and updated // before our own update listener attempts a re-render) ModuleResourceRepository.getModuleResources(facet, true /*createIfNecessary*/); myFacet.getResourceFolderManager().addListener(this); } myConfigListener = new LayoutConfigurationListener(); initializeConfiguration(); mySessionQueue = new MergingUpdateQueue("android.designer", 10, true, null, editor, null, Alarm.ThreadToUse.OWN_THREAD); myXmlFile = (XmlFile)AndroidPsiUtils.getPsiFileSafely(getProject(), myFile); assert myXmlFile != null : myFile; myPsiChangeListener = new ExternalPSIChangeListener(this, myXmlFile, 100, new Runnable() { @Override public void run() { reparseFile(); } }); addActions(); myActive = true; myPsiChangeListener.setInitialize(); myPsiChangeListener.activate(); myPsiChangeListener.addRequest(); } private void initializeConfiguration() { myConfiguration = myFacet.getConfigurationManager().getConfiguration(myFile); myConfiguration.addListener(myConfigListener); } private void addActions() { addConfigurationActions(); myActionPanel.getPopupGroup().addSeparator(); myActionPanel.getPopupGroup().add(buildRefactorActionGroup()); addGotoDeclarationAction(); } private void addGotoDeclarationAction() { AnAction gotoDeclaration = new AnAction("Go To Declaration") { @Override public void update(AnActionEvent e) { EditableArea area = e.getData(EditableArea.DATA_KEY); e.getPresentation().setEnabled(area != null && area.getSelection().size() == 1); } @Override public void actionPerformed(AnActionEvent e) { EditableArea area = e.getData(EditableArea.DATA_KEY); if (area != null) { RadViewComponent component = (RadViewComponent)area.getSelection().get(0); PsiNavigateUtil.navigate(component.getTag()); } } }; myActionPanel.registerAction(gotoDeclaration, IdeActions.ACTION_GOTO_DECLARATION); myActionPanel.getPopupGroup().add(gotoDeclaration); } private void addConfigurationActions() { DefaultActionGroup designerActionGroup = getActionPanel().getActionGroup(); ActionGroup group = ConfigurationToolBar.createActions(this); designerActionGroup.add(group); } @Override protected DesignerActionPanel createActionPanel() { return new AndroidDesignerActionPanel(this, myGlassLayer); } @Override protected CaptionPanel createCaptionPanel(boolean horizontal) { // No borders; not necessary since we have a different designer background than the caption area return new CaptionPanel(this, horizontal, false); } @Override protected JScrollPane createScrollPane(@NotNull JLayeredPane content) { // No background color JScrollPane scrollPane = ScrollPaneFactory.createScrollPane(content); scrollPane.setBackground(null); return scrollPane; } @NotNull private static ActionGroup buildRefactorActionGroup() { final DefaultActionGroup group = new DefaultActionGroup("_Refactor", true); final ActionManager manager = ActionManager.getInstance(); AnAction action = manager.getAction(AndroidExtractStyleAction.ACTION_ID); group.add(new AndroidRefactoringActionWrapper("_Extract Style...", action)); action = manager.getAction(AndroidInlineStyleReferenceAction.ACTION_ID); group.add(new AndroidRefactoringActionWrapper("_Inline Style...", action)); action = manager.getAction(AndroidExtractAsIncludeAction.ACTION_ID); group.add(new AndroidRefactoringActionWrapper("E_xtract Layout...", action)); action = manager.getAction(AndroidInlineIncludeAction.ACTION_ID); group.add(new AndroidRefactoringActionWrapper("I_nline Layout...", action)); return group; } private void reparseFile() { try { storeState(); showDesignerCard(); parseFile(new Runnable() { @Override public void run() { showDesignerCard(); myLayeredPane.revalidate(); restoreState(); } }); } catch (RuntimeException e) { myPsiChangeListener.clear(); showError("Parsing error", e.getCause() == null ? e : e.getCause()); } } private void parseFile(final Runnable runnable) { if (myConfiguration == null) { return; } createRenderer(new ThrowableConsumer<RenderResult, Throwable>() { @Override public void consume(RenderResult result) throws Throwable { RenderSession session = result.getSession(); if (session == null) { return; } updateDeviceFrameVisibility(result); if (!session.getResult().isSuccess()) { // This image may not have been fully rendered before some error caused // the render to abort, but a partial render is better. However, if the render // was due to some configuration change, we don't want to replace the image // since all the mouse regions and model setup will no longer match the pixels. if (myRootView != null && myRootView.getImage() != null && session.getImage() != null && session.getImage().getWidth() == myRootView.getImage().getWidth() && session.getImage().getHeight() == myRootView.getImage().getHeight()) { myRootView.setRenderedImage(result.getImage()); myRootView.repaint(); } return; } boolean insertPanel = !myShowingRoot; if (myRootView == null) { myRootView = new RootView(AndroidDesignerEditorPanel.this, 0, 0, result); myRootView.addComponentListener(new ComponentAdapter() { @Override public void componentResized(ComponentEvent componentEvent) { zoomToFitIfNecessary(); } }); insertPanel = true; } else { myRootView.setRenderedImage(result.getImage()); myRootView.updateBounds(true); } boolean firstRender = myRootComponent == null; try { myRootComponent = RadModelBuilder.update(AndroidDesignerEditorPanel.this, result, (RadViewComponent)myRootComponent, myRootView); } catch (Throwable e) { myRootComponent = null; throw e; } // Start out selecting the root layout rather than the device item; this will // show relevant layout actions immediately, will cause the component tree to // be properly expanded, etc if (firstRender) { RadViewComponent rootComponent = getLayoutRoot(); if (rootComponent != null) { mySurfaceArea.setSelection(Collections.<RadComponent>singletonList(rootComponent)); } } if (insertPanel) { // Use a custom layout manager which adjusts the margins/padding around the designer canvas // dynamically; it will try to use DEFAULT_HORIZONTAL_MARGIN * DEFAULT_VERTICAL_MARGIN, but // if there is not enough room, it will split the margins evenly in each dimension until // there is no room available without scrollbars. JPanel rootPanel = new JPanel(new LayoutManager() { @Override public void addLayoutComponent(String s, Component component) { } @Override public void removeLayoutComponent(Component component) { } @Override public Dimension preferredLayoutSize(Container container) { return new Dimension(0, 0); } @Override public Dimension minimumLayoutSize(Container container) { return new Dimension(0, 0); } @Override public void layoutContainer(Container container) { myRootView.updateBounds(false); int x = Math.max(2, Math.min(DEFAULT_HORIZONTAL_MARGIN, (container.getWidth() - myRootView.getWidth()) / 2)); int y = Math.max(2, Math.min(DEFAULT_VERTICAL_MARGIN, (container.getHeight() - myRootView.getHeight()) / 2)); // If we're squeezing the image to fit, and there's a drop shadow showing // shift *some* space away from the tail portion of the drop shadow over to // the left to make the image look more balanced if (myRootView.getShowDropShadow()) { if (x <= 2) { x += ShadowPainter.SHADOW_SIZE / 3; } if (y <= 2) { y += ShadowPainter.SHADOW_SIZE / 3; } } if (myMaxWidth > 0) { myRootView.setLocation(Math.max(0, (myMaxWidth - myRootView.getScaledWidth()) / 2), 2 + Math.max(0, (myMaxHeight - myRootView.getScaledHeight()) / 2)); } else { myRootView.setLocation(x, y); } } }); rootPanel.setBackground(DrawingStyle.DESIGNER_BACKGROUND_COLOR); rootPanel.setOpaque(true); rootPanel.add(myRootView); myLayeredPane.add(rootPanel, LAYER_COMPONENT); myShowingRoot = true; } zoomToFitIfNecessary(); loadInspections(new EmptyProgressIndicator()); updateInspections(); if (RenderPreviewMode.getCurrent() != RenderPreviewMode.NONE) { RenderPreviewManager previewManager = getPreviewManager(true); if (previewManager != null) { previewManager.renderPreviews(); } } runnable.run(); } }); } private void createRenderer(final ThrowableConsumer<RenderResult, Throwable> runnable) { disposeRenderer(); if (myConfiguration == null) { return; } mySessionAlarm.addRequest(new Runnable() { @Override public void run() { if (mySession == null) { showProgress(mySessionId <= 1 ? "Initializing Rendering Library..." : "Rendering... "); } } }, 500); final long sessionId = ++mySessionId; mySessionQueue.queue(new Update("render") { private void cancel() { mySessionAlarm.cancelAllRequests(); ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { if (!isProjectClosed()) { hideProgress(); } } }); } @Override public void run() { try { if (sessionId != mySessionId) { cancel(); return; } final Module module = getModule(); final RenderLogger logger = new RenderLogger(myFile.getName(), module); if (myConfiguration.getTarget() == null) { logger.error(null, "No render target selected", null); } else if (myConfiguration.getTheme() == null) { logger.error(null, "No theme selected", null); } if (logger.hasProblems()) { cancel(); RenderResult renderResult = RenderResult.createBlank(myXmlFile, logger); runnable.consume(renderResult); updateErrors(renderResult); return; } final RenderResult renderResult; RenderContext renderContext = AndroidDesignerEditorPanel.this; if (myRendererLock.tryLock()) { try { final RenderService service = RenderService.create(myFacet, module, myXmlFile, myConfiguration, logger, renderContext); if (service != null) { if (!ToggleRenderModeAction.isRenderViewPort()) { service.useDesignMode(myXmlFile); } renderResult = service.render(); service.dispose(); } else { renderResult = RenderResult.createBlank(myXmlFile, logger); } myRenderResult = renderResult; } finally { myRendererLock.unlock(); } } else { cancel(); return; } if (sessionId != mySessionId) { cancel(); return; } if (renderResult == null) { throw new RenderingException(); } mySessionAlarm.cancelAllRequests(); Runnable uiRunnable = new Runnable() { @Override public void run() { try { if (!isProjectClosed()) { hideProgress(); if (sessionId == mySessionId) { runnable.consume(renderResult); updateErrors(renderResult); } } } catch (Throwable e) { myPsiChangeListener.clear(); showError("Parsing error", e); } } }; if (ApplicationManager.getApplication().isUnitTestMode()) { ApplicationManager.getApplication().invokeAndWait(uiRunnable, ModalityState.defaultModalityState()); } else { ApplicationManager.getApplication().invokeLater(uiRunnable); } } catch (final Throwable e) { myPsiChangeListener.clear(); mySessionAlarm.cancelAllRequests(); ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { myPsiChangeListener.clear(); showError("Render error", e); } }); } } }); } public int getDpi() { return myConfiguration != null ? myConfiguration.getDensity().getDpiValue() : Density.DEFAULT_DENSITY; } private MyRenderPanelWrapper myErrorPanelWrapper; private void updateErrors(@NotNull final RenderResult result) { Application application = ApplicationManager.getApplication(); if (!application.isDispatchThread()) { application.invokeLater(new Runnable() { @Override public void run() { updateErrors(result); } }); return; } RenderLogger logger = result.getLogger(); if (!logger.hasProblems()) { if (myErrorPanelWrapper == null) { return; } myLayeredPane.remove(myErrorPanelWrapper); myErrorPanelWrapper = null; myLayeredPane.repaint(); } else { if (myErrorPanelWrapper == null) { myErrorPanelWrapper = new MyRenderPanelWrapper(new RenderErrorPanel()); } myErrorPanelWrapper.getErrorPanel().showErrors(result); myLayeredPane.add(myErrorPanelWrapper, LAYER_ERRORS); myLayeredPane.repaint(); } } private void disposeRenderer() { if (mySession != null) { mySession.dispose(); mySession = null; } } private void updateRenderer(final boolean updateProperties) { if (myConfiguration == null) { return; } if (myRootComponent == null) { reparseFile(); return; } createRenderer(new ThrowableConsumer<RenderResult, Throwable>() { @Override public void consume(RenderResult result) throws Throwable { RenderSession session = result.getSession(); if (session == null || session.getImage() == null) { return; } updateDeviceFrameVisibility(result); myRootComponent = RadModelBuilder.update(AndroidDesignerEditorPanel.this, result, (RadViewComponent)myRootComponent, myRootView); myRootView.setRenderedImage(result.getImage()); zoomToFitIfNecessary(); myLayeredPane.revalidate(); myHorizontalCaption.update(); myVerticalCaption.update(); DesignerToolWindow toolWindow = getToolWindow(); if (toolWindow != null) { toolWindow.refresh(updateProperties); } if (RenderPreviewMode.getCurrent() != RenderPreviewMode.NONE) { RenderPreviewManager previewManager = getPreviewManager(true); if (previewManager != null) { previewManager.renderPreviews(); } } } }); } @Nullable private DesignerToolWindow getToolWindow() { try { // This method sometimes returns null. We don't want to bother the user with that; the worst that // can happen is that the property view is not updated. return DesignerToolWindowManager.getInstance(this); } catch (Exception ex) { return null; } } @Override protected void restoreState() { // Work around NPE in ui-designer-core // java.lang.NullPointerException // at com.intellij.designer.AbstractToolWindowManager.getContent(AbstractToolWindowManager.java:232) // at com.intellij.designer.DesignerToolWindowManager.getInstance(DesignerToolWindowManager.java:52) // at com.intellij.designer.designSurface.DesignerEditorPanel.restoreState(DesignerEditorPanel.java:606) // Only restore state, if we can find the tool window DesignerToolWindow toolWindow = getToolWindow(); if (toolWindow != null) { super.restoreState(); } } /** * Auto fits the scene, if requested. This will be the case the first time * the layout is opened, and after orientation or device changes. */ synchronized void zoomToFitIfNecessary() { if (isZoomToFit()) { if (myZoomInProgress) { // Prevent nested zooming when (e.g. oscillating between two values) return; } try { myZoomInProgress = true; updateZoom(); } finally { myZoomInProgress = false; } } } private boolean myZoomInProgress; private void removeNativeRoot() { if (myRootComponent != null) { Component component = ((RadVisualComponent)myRootComponent).getNativeComponent(); if (component != null) { myLayeredPane.remove(component.getParent()); myShowingRoot = false; } } } @Override protected void configureError(@NotNull ErrorInfo info) { // Error messages for the user (broken custom views, missing resources, etc) are already // trapped during rendering and shown in the error panel. These errors are internal errors // in the layout editor and should instead be redirected to the log. info.myShowMessage = false; info.myShowLog = true; StringBuilder builder = new StringBuilder(); builder.append("ActiveTool: ").append(myToolProvider.getActiveTool()); builder.append("\nSDK: "); try { AndroidPlatform platform = AndroidPlatform.getInstance(getModule()); if (platform != null) { IAndroidTarget target = platform.getTarget(); builder.append(target.getFullName()).append(" - ").append(target.getVersion()); } } catch (Throwable e) { builder.append("<unknown>"); } if (info.myThrowable instanceof IndexOutOfBoundsException && myRootComponent != null && mySession != null) { builder.append("\n-------- RadTree --------\n"); RadComponentOperations.printTree(builder, myRootComponent, 0); builder.append("\n-------- ViewTree(").append(mySession.getRootViews().size()).append(") --------\n"); for (ViewInfo viewInfo : mySession.getRootViews()) { RadComponentOperations.printTree(builder, viewInfo, 0); } } info.myMessage = builder.toString(); } @Override protected void showErrorPage(ErrorInfo info) { myPsiChangeListener.clear(); mySessionAlarm.cancelAllRequests(); removeNativeRoot(); super.showErrorPage(info); } @Override public void activate() { myActive = true; myPsiChangeListener.activate(); if (myVariantChanged || myPsiChangeListener.isUpdateRenderer() || ((myConfigurationDirty & MASK_RENDERING) != 0)) { myVariantChanged = false; updateRenderer(true); } else if (myRootComponent != null && myRootView != null) { if (RenderPreviewMode.getCurrent() != RenderPreviewMode.NONE) { RenderPreviewManager previewManager = getPreviewManager(true); if (previewManager != null) { previewManager.renderPreviews(); } } } myConfigurationDirty = 0; } @Override public void deactivate() { myActive = false; myPsiChangeListener.deactivate(); } public void buildProject() { if (myPsiChangeListener.ensureUpdateRenderer()) { updateRenderer(true); } } @Override public void dispose() { myPsiChangeListener.dispose(); if (myConfiguration != null) { myConfiguration.removeListener(myConfigListener); } super.dispose(); disposeRenderer(); if (myPreviewManager != null) { myPreviewManager.dispose(); myPreviewManager = null; } myFacet.getResourceFolderManager().removeListener(this); } @Override @Nullable protected Module findModule(Project project, VirtualFile file) { Module module = super.findModule(project, file); if (module == null) { module = AndroidPsiUtils.getModuleSafely(myXmlFile); } return module; } @Override public String getPlatformTarget() { return "android"; } @Override public TreeComponentDecorator getTreeDecorator() { return myTreeDecorator; } @Override public WrapInProvider getWrapInProvider() { if (myWrapInProvider == null) { myWrapInProvider = new AndroidWrapInProvider(getProject()); } return myWrapInProvider; } @Override protected ComponentDecorator getRootSelectionDecorator() { return EmptyComponentDecorator.INSTANCE; } @Override public List<PaletteGroup> getPaletteGroups() { return ViewsMetaManager.getInstance(getProject()).getPaletteGroups(); } @NotNull @Override public String getVersionLabel(@Nullable String version) { if (StringUtil.isEmpty(version)) { return ""; } // Android versions are recorded as API integers Integer api = Ints.tryParse(version); assert api != null : version; int since = api.intValue(); if (since <= 1) { return ""; } String name = SdkVersionInfo.getAndroidName(since); if (name == null) { name = String.format("API %1$d", since); } return name; } @Override public boolean isDeprecated(@Nullable String deprecatedIn) { if (deprecatedIn == null) { return false; } IAndroidTarget target = myConfiguration != null ? myConfiguration.getTarget() : null; if (target == null) { return super.isDeprecated(deprecatedIn); } if (StringUtil.isEmpty(deprecatedIn)) { return false; } Integer api = Ints.tryParse(deprecatedIn); assert api != null : deprecatedIn; return api.intValue() <= target.getVersion().getApiLevel(); } public PropertyParser getPropertyParser(@Nullable RenderResult result) { if (myPropertyParser == null) { myPropertyParser = new PropertyParser(result != null ? result : myRenderResult); } return myPropertyParser; } @Nullable public RadViewComponent getRootViewComponent() { return (RadViewComponent)myRootComponent; } @VisibleForTesting public RenderResult getLastRenderResult() { return myRenderResult; } @Override @NotNull protected ComponentCreationFactory createCreationFactory(final PaletteItem paletteItem) { return new ComponentCreationFactory() { @NotNull @Override public RadComponent create() throws Exception { RadViewComponent component = RadComponentOperations.createComponent(null, paletteItem.getMetaModel()); component.setInitialPaletteItem(paletteItem); if (component instanceof IConfigurableComponent) { ((IConfigurableComponent)component).configure(myRootComponent); } return component; } }; } @Override public ComponentPasteFactory createPasteFactory(String xmlComponents) { if (myConfiguration != null) { IAndroidTarget target = myConfiguration.getTarget(); if (target != null) { return new AndroidPasteFactory(getModule(), target, xmlComponents); } } return null; } private void updatePalette(IAndroidTarget target) { try { for (PaletteGroup group : getPaletteGroups()) { for (PaletteItem item : group.getItems()) { String version = item.getVersion(); if (version != null) { Integer api = Ints.tryParse(version); assert api != null : version; DefaultPaletteItem paletteItem = (DefaultPaletteItem)item; paletteItem.setEnabled(api.intValue() <= target.getVersion().getApiLevel()); } } } PaletteItem item = getActivePaletteItem(); if (item != null && !item.isEnabled()) { activatePaletteItem(null); } PaletteToolWindowManager.getInstance(this).refresh(); } catch (Throwable e) { // Pass } } @Override public String getEditorText() { return ApplicationManager.getApplication().runReadAction(new Computable<String>() { @Override public String compute() { return myXmlFile.getText(); } }); } @Override protected boolean execute(ThrowableRunnable<Exception> operation, final boolean updateProperties) { if (!ReadonlyStatusHandler.ensureFilesWritable(getProject(), myFile)) { return false; } try { myPsiChangeListener.stop(); operation.run(); ApplicationManager.getApplication().invokeLater(new Runnable() { @Override public void run() { boolean active = myPsiChangeListener.isActive(); if (active) { myPsiChangeListener.stop(); } updateRenderer(updateProperties); if (active) { myPsiChangeListener.start(); } } }); return true; } catch (Throwable e) { showError("Execute command", e); return false; } finally { myPsiChangeListener.start(); } } @Override protected void executeWithReparse(ThrowableRunnable<Exception> operation) { if (!ReadonlyStatusHandler.ensureFilesWritable(getProject(), myFile)) { return; } try { myPsiChangeListener.stop(); operation.run(); myPsiChangeListener.start(); reparseFile(); } catch (Throwable e) { showError("Execute command", e); myPsiChangeListener.start(); } } @Override protected void execute(List<EditOperation> operations) { if (!ReadonlyStatusHandler.ensureFilesWritable(getProject(), myFile)) { return; } try { myPsiChangeListener.stop(); for (EditOperation operation : operations) { operation.execute(); } updateRenderer(true); } catch (Throwable e) { showError("Execute command", e); } finally { myPsiChangeListener.start(); } } ////////////////////////////////////////////////////////////////////////////////////////// // // Inspections // ////////////////////////////////////////////////////////////////////////////////////////// @Override public void loadInspections(ProgressIndicator progress) { if (myRootComponent != null) { ErrorAnalyzer.load(getProject(), myXmlFile, myRootComponent, progress); } } ////////////////////////////////////////////////////////////////////////////////////////// // // Zooming // ////////////////////////////////////////////////////////////////////////////////////////// private static final double ZOOM_FACTOR = 1.2; public boolean isZoomToFit() { return myZoomMode == ZoomType.FIT || myZoomMode == ZoomType.FIT_INTO; } @Override public boolean isZoomSupported() { return true; } /** Sets the zoom level. Note that this should be 1, not 100 (percent), for an image at its actual size */ @Override public void setZoom(double zoom) { if (myMaxWidth > 0 && myRootComponent != null) { // If we have a fixed size, ignore scale factor assert myMaxHeight > 0; Rectangle bounds = myRootComponent.getBounds(); double imageWidth = bounds.getWidth(); double imageHeight = bounds.getHeight(); if (imageHeight > 0) { zoom = Math.min(myMaxWidth / imageWidth, myMaxHeight / imageHeight); if (myZoomMode == ZoomType.FIT_INTO && zoom > 1) { zoom = 1; } } } if (zoom != myZoom) { myZoom = zoom; normalizeScale(); viewZoomed(); mySurfaceArea.scrollToSelection(); repaint(); } } private void normalizeScale() { // Some operations are faster if the zoom is EXACTLY 1.0 rather than ALMOST 1.0. // (This is because there is a fast-path when image copying and the scale is 1.0; // in that case it does not have to do any scaling). // // If you zoom out 10 times and then back in 10 times, small rounding errors mean // that you end up with a scale=1.0000000000000004. In the cases, when you get close // to 1.0, just make the zoom an exact 1.0. if (Math.abs(myZoom - 1.0) < 0.01) { myZoom = 1.0; } } /** Returns the current zoom level. Note that this is 1, not 100 (percent) for an image at its actual size */ @Override public double getZoom() { return myZoom; } private int myMaxWidth; private int myMaxHeight; private boolean myUseLargeShadows = true; public boolean isUseLargeShadows() { return myUseLargeShadows; } /** Zooms the designer view */ @Override public void zoom(@NotNull ZoomType type) { myZoomMode = type; updateZoom(); } private void updateZoom() { switch (myZoomMode) { case IN: setZoom(myZoom * ZOOM_FACTOR); break; case OUT: setZoom(myZoom / ZOOM_FACTOR); break; case ACTUAL: if (SystemInfo.isMac && UIUtil.isRetina()) { setZoom(0.5); } else { setZoom(1); } break; case FIT_INTO: case FIT: { if (myRootComponent == null) { return; } Dimension sceneSize = myRootComponent.getBounds().getSize(); Dimension screenSize = getDesignerViewSize(); if (screenSize.width > 0 && screenSize.height > 0) { int sceneWidth = sceneSize.width; int sceneHeight = sceneSize.height; if (sceneWidth > 0 && sceneHeight > 0) { int viewWidth = screenSize.width; int viewHeight = screenSize.height; // Reduce the margins if necessary int hDelta = viewWidth - sceneWidth; int xMargin = 0; if (hDelta > 2 * DEFAULT_HORIZONTAL_MARGIN) { xMargin = DEFAULT_HORIZONTAL_MARGIN; } else if (hDelta > 0) { xMargin = hDelta / 2; } int vDelta = viewHeight - sceneHeight; int yMargin = 0; if (vDelta > 2 * DEFAULT_VERTICAL_MARGIN) { yMargin = DEFAULT_VERTICAL_MARGIN; } else if (vDelta > 0) { yMargin = vDelta / 2; } double hScale = (viewWidth - 2 * xMargin) / (double) sceneWidth; double vScale = (viewHeight - 2 * yMargin) / (double) sceneHeight; double scale = Math.min(hScale, vScale); if (myZoomMode == FIT_INTO) { scale = Math.min(1.0, scale); } setZoom(scale); } } break; } case SCREEN: default: throw new UnsupportedOperationException("Not yet implemented: " + myZoomMode); } } @NotNull private Dimension getDesignerViewSize() { Dimension size = myScrollPane.getSize(); size.width -= 2; size.height -= 2; RootView rootView = getRootView(); if (rootView != null) { if (rootView.getShowDropShadow()) { size.width -= ShadowPainter.SHADOW_SIZE; size.height -= ShadowPainter.SHADOW_SIZE; } final int MIN_SIZE = 200; if (myPreviewManager != null && size.width > MIN_SIZE) { int previewWidth = myPreviewManager.computePreviewWidth(); size.width = Math.max(MIN_SIZE, size.width - previewWidth); } } return size; } @Override @NotNull protected Dimension getSceneSize(@NotNull Component target) { int width = 0; int height = 0; if (myRootComponent != null) { Rectangle bounds = myRootComponent.getBounds(target); width = Math.max(width, (int)bounds.getMaxX()); height = Math.max(height, (int)bounds.getMaxY()); width += 1; height += 1; return new Dimension(width, height); } return super.getSceneSize(target); } @Override protected void viewZoomed() { RootView rootView = getRootView(); if (rootView != null) { rootView.updateSize(); } revalidate(); super.viewZoomed(); } @VisibleForTesting public RootView getCurrentRootView() { assert myRootView == getRootView(); return myRootView; } @Nullable private RootView getRootView() { // TODO: Why isn't this just using myRootView ? if (myRootComponent instanceof RadViewComponent) { Component nativeComponent = ((RadViewComponent)myRootComponent).getNativeComponent(); if (nativeComponent instanceof RootView) { return (RootView)nativeComponent; } } return null; } @Nullable private RadViewComponent getLayoutRoot() { if (myRootComponent != null && myRootComponent.getChildren().size() == 1) { RadComponent component = myRootComponent.getChildren().get(0); if (component.isBackground() && component instanceof RadViewComponent) { return (RadViewComponent)component; } } return null; } @Nullable @Override protected RadComponent findTarget(int x, int y, @Nullable ComponentTargetFilter filter) { RadComponent target = super.findTarget(x, y, filter); // If you click/drag outside the root, select the root if (target == null) { RadComponent rootTarget = getLayoutRoot(); if (rootTarget != null && filter != null && filter.preFilter(myRootComponent) && filter.resultFilter(rootTarget)) { return rootTarget; } } return target; } public IdManager getIdManager() { return myIdManager; } /** * Layered pane which shows the rendered image, as well as (if applicable) an error message panel on top of the rendering * near the bottom */ private static class MyRenderPanelWrapper extends JPanel { private final RenderErrorPanel myErrorPanel; private int myErrorPanelHeight = -1; public MyRenderPanelWrapper(@NotNull RenderErrorPanel errorPanel) { super(new BorderLayout()); myErrorPanel = errorPanel; setBackground(null); setOpaque(false); add(errorPanel); } private RenderErrorPanel getErrorPanel() { return myErrorPanel; } @Override public void doLayout() { super.doLayout(); positionErrorPanel(); } private void positionErrorPanel() { int height = getHeight(); int width = getWidth(); int size; if (SIZE_ERROR_PANEL_DYNAMICALLY) { if (myErrorPanelHeight == -1) { // Make the layout take up to 3/4ths of the height, and at least 1/4th, but // anywhere in between based on what the actual text requires size = height * 3 / 4; int preferredHeight = myErrorPanel.getPreferredHeight(width) + 8; if (preferredHeight < size) { size = Math.max(preferredHeight, Math.min(height / 4, size)); myErrorPanelHeight = size; } } else { size = myErrorPanelHeight; } } else { size = height / 2; } myErrorPanel.setSize(width, size); myErrorPanel.setLocation(0, height - size); } } private void saveState() { if (myConfiguration != null) { myConfiguration.save(); } } // ---- Implements RenderContext ---- @Override @Nullable public Configuration getConfiguration() { return myConfiguration; } @Override public void setConfiguration(@NotNull Configuration configuration) { if (configuration != myConfiguration) { if (myConfiguration != null) { myConfiguration.removeListener(myConfigListener); } myConfiguration = configuration; myConfiguration.addListener(myConfigListener); myConfigListener.changed(MASK_ALL); // TODO: Cause immediate toolbar updates? } } @Override public void requestRender() { updateRenderer(false); mySessionQueue.sendFlush(); } @VisibleForTesting public void requestImmediateRender() { updateRenderer(true); mySessionQueue.sendFlush(); } @VisibleForTesting public boolean isRenderPending() { return !mySessionQueue.isEmpty() && !mySessionQueue.isFlushing(); } @Override @NotNull public UsageType getType() { return UsageType.LAYOUT_EDITOR; } @NotNull @Override public XmlFile getXmlFile() { return myXmlFile; } @Nullable @Override public VirtualFile getVirtualFile() { return myFile; } @Override public boolean hasAlphaChannel() { return !myRootView.getShowDropShadow(); } @Override @NotNull public Component getComponent() { return myLayeredPane; } @Override public void updateLayout() { zoom(ZoomType.FIT_INTO); Component component = getComponent(); if (component instanceof JComponent) { JComponent jc = (JComponent)component; jc.revalidate(); } else { component.validate(); } layoutParent(); component.repaint(); } protected void layoutParent() { if (myRootView != null) { ((JComponent)myRootView.getParent()).revalidate(); } } private boolean myShowDeviceFrames = true; @Override public void setDeviceFramesEnabled(boolean on) { myShowDeviceFrames = on; if (myRootView != null) { RenderedImage image = myRootView.getRenderedImage(); if (image != null) { image.setDeviceFrameEnabled(on); } } } private void updateDeviceFrameVisibility(@Nullable RenderResult result) { if (result != null) { RenderedImage image = result.getImage(); if (image != null) { RenderService renderService = result.getRenderService(); image.setDeviceFrameEnabled(myShowDeviceFrames && renderService != null && renderService.getRenderingMode() == SessionParams.RenderingMode.NORMAL && renderService.getShowDecorations()); } } } @Nullable @Override public BufferedImage getRenderedImage() { return myRootView != null ? myRootView.getImage() : null; } @Nullable @Override public RenderResult getLastResult() { return myRenderResult; } @Override @Nullable public RenderedViewHierarchy getViewHierarchy() { return myRenderResult != null ? myRenderResult.getHierarchy() : null; } @Override @NotNull public Dimension getFullImageSize() { if (myRootView != null) { BufferedImage image = myRootView.getImage(); if (image != null) { return new Dimension(image.getWidth(), image.getHeight()); } } return NO_SIZE; } @Override @NotNull public Dimension getScaledImageSize() { if (myRootView != null) { BufferedImage image = myRootView.getImage(); if (image != null) { return new Dimension((int)(myZoom * image.getWidth()), (int)(myZoom * image.getHeight())); } } return NO_SIZE; } @Override public void setMaxSize(int width, int height) { myMaxWidth = width; myMaxHeight = height; myUseLargeShadows = width <= 0; layoutParent(); } @Override public void zoomFit(boolean onlyZoomOut, boolean allowZoomIn) { zoom(allowZoomIn ? ZoomType.FIT : ZoomType.FIT_INTO); } @Override @NotNull public Rectangle getClientArea() { return myScrollPane.getViewport().getViewRect(); } @Override public boolean supportsPreviews() { return true; } @Nullable @Override public RenderPreviewManager getPreviewManager(boolean createIfNecessary) { if (myPreviewManager == null && createIfNecessary) { myPreviewManager = new RenderPreviewManager(this); RenderPreviewPanel panel = new RenderPreviewPanel(); myLayeredPane.add(panel, LAYER_PREVIEW); myLayeredPane.revalidate(); myLayeredPane.repaint(); } return myPreviewManager; } // ---- Implements OverlayContainer ---- @NotNull @Override public Rectangle fromModel(@NotNull Component target, @NotNull Rectangle rectangle) { Rectangle r = myRootComponent.fromModel(target, rectangle); if (target == myRootView) { double scale = myRootView.getScale(); r.x *= scale; r.y *= scale; r.width *= scale; r.height *= scale; } RenderedImage renderedImage = myRootView.getRenderedImage(); if (renderedImage != null) { Rectangle imageBounds = renderedImage.getImageBounds(); if (imageBounds != null) { r.x += imageBounds.x; r.y += imageBounds.y; } } return r; } @NotNull @Override public Rectangle toModel(@NotNull Component source, @NotNull Rectangle rectangle) { RenderedImage renderedImage = myRootView.getRenderedImage(); if (renderedImage != null) { Rectangle imageBounds = renderedImage.getImageBounds(); if (imageBounds != null && imageBounds.x != 0 && imageBounds.y != 0) { rectangle = new Rectangle(rectangle); rectangle.x -= imageBounds.x; rectangle.y -= imageBounds.y; } } Rectangle r = myRootComponent.toModel(source, rectangle); if (source == myRootView) { double scale = myRootView.getScale(); r.x /= scale; r.y /= scale; r.width /= scale; r.height /= scale; } return r; } @Override @Nullable public List<Overlay> getOverlays() { return myOverlays; } @Override public boolean isSelected(@NotNull XmlTag tag) { for (RadComponent component : getSurfaceArea().getSelection()) { if (component instanceof RadViewComponent) { RadViewComponent rv = (RadViewComponent)component; if (tag == rv.getTag()) { return true; } } } return false; } // ---- Implements ResourceFolderManager.ResourceFolderListener ---- @Override public void resourceFoldersChanged(@NotNull AndroidFacet facet, @NotNull List<VirtualFile> folders, @NotNull Collection<VirtualFile> added, @NotNull Collection<VirtualFile> removed) { if (facet == myFacet) { if (myActive) { // The app resources should already have been refreshed by their own variant listener updateRenderer(true); } else { myVariantChanged = true; } } } private class LayoutConfigurationListener implements ConfigurationListener { @Override public boolean changed(int flags) { if (isProjectClosed()) { return true; } if (myActive) { updateRenderer(false); if ((flags & CFG_TARGET) != 0) { IAndroidTarget target = myConfiguration != null ? myConfiguration.getTarget() : null; if (target != null) { updatePalette(target); } } saveState(); } else { myConfigurationDirty |= flags; } return true; } } private class RenderPreviewPanel extends JComponent { RenderPreviewPanel() { //super(new BorderLayout()); setBackground(null); setOpaque(false); } @Override protected void paintComponent(Graphics g) { if (myPreviewManager != null) { myPreviewManager.paint((Graphics2D)g); } } } @Override protected DesignerEditableArea createEditableArea() { return new AndroidEditableArea(); } private class AndroidEditableArea extends DesignerEditableArea { @Override public InputTool findTargetTool(int x, int y) { if (myPreviewManager != null && myRootView != null) { if (myPreviewTool == null) { myPreviewTool = new RenderPreviewTool(); } if (x > (myRootView.getX() + myRootView.getWidth()) || y > (myRootView.getY() + myRootView.getHeight())) { return myPreviewTool; } } if (myRootComponent != null && myRenderResult != null) { RadComponent target = findTarget(x, y, null); RenderedView leaf = null; if (target instanceof RadViewComponent) { RadViewComponent rv = (RadViewComponent)target; RenderedViewHierarchy hierarchy = myRenderResult.getHierarchy(); if (hierarchy != null) { leaf = hierarchy.findViewByTag(rv.getTag()); } } if (myHover.setHoveredView(leaf)) { repaint(); } } return super.findTargetTool(x, y); } } private class RenderPreviewTool extends InputTool { @Override public void mouseMove(MouseEvent event, EditableArea area) throws Exception { if (myPreviewManager != null) { myPreviewManager.moved(event); } } @Override public void mouseUp(MouseEvent event, EditableArea area) throws Exception { super.mouseUp(event, area); if (myPreviewManager != null && event.getClickCount() > 0) { myPreviewManager.click(event); } } @Override public void mouseEntered(MouseEvent event, EditableArea area) throws Exception { super.mouseEntered(event, area); if (myPreviewManager != null) { myPreviewManager.enter(event); } } @Override public void mouseExited(MouseEvent event, EditableArea area) throws Exception { super.mouseExited(event, area); if (myPreviewManager != null) { myPreviewManager.exit(event); } } } }