| /* |
| * 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; |
| |
| @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.requiresAndroidModel()) { |
| // 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(); |
| } |
| |
| 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; |
| } |
| |
| RenderService renderService = RenderService.get(myFacet); |
| final RenderLogger logger = renderService.createLogger(); |
| |
| 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 RenderTask task = renderService.createTask(myXmlFile, myConfiguration, logger, renderContext); |
| if (task != null) { |
| if (!ToggleRenderModeAction.isRenderViewPort()) { |
| task.useDesignMode(myXmlFile); |
| } |
| renderResult = task.render(); |
| task.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(); |
| } |
| } |
| } |
| }); |
| } |
| |
| @VisibleForTesting |
| @Nullable |
| public 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(); |
| |
| myHorizontalCaption.update(); |
| myVerticalCaption.update(); |
| |
| 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; |
| } |
| |
| /** |
| * 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(); |
| } |
| |
| private 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) { |
| RenderTask renderTask = result.getRenderTask(); |
| image.setDeviceFrameEnabled(myShowDeviceFrames && renderTask != null && |
| renderTask.getRenderingMode() == SessionParams.RenderingMode.NORMAL && |
| renderTask.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); |
| } |
| } |
| } |
| } |