blob: 4942d31a2f6309ae48e0ddc40dfd204d46fa3e63 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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.android.tools.idea.uibuilder.surface;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.annotations.VisibleForTesting;
import com.android.sdklib.devices.Device;
import com.android.sdklib.devices.State;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.ddms.screenshot.DeviceArtPainter;
import com.android.tools.idea.rendering.RenderErrorPanel;
import com.android.tools.idea.rendering.RenderResult;
import com.android.tools.idea.uibuilder.actions.SelectAllAction;
import com.android.tools.idea.uibuilder.model.*;
import com.android.tools.idea.uibuilder.palette.ScalableDesignSurface;
import com.google.common.collect.Lists;
import com.intellij.ide.IdeTooltip;
import com.intellij.ide.IdeTooltipManager;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.wm.IdeGlassPane;
import com.intellij.ui.components.JBScrollBar;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.ui.components.Magnificator;
import com.intellij.util.Alarm;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.ui.AsyncProcessIcon;
import com.intellij.util.ui.ButtonlessScrollBarUI;
import com.intellij.util.ui.UIUtil;
import com.intellij.util.ui.update.MergingUpdateQueue;
import com.intellij.util.ui.update.Update;
import org.intellij.lang.annotations.JdkConstants;
import org.jetbrains.annotations.NonNls;
import javax.swing.*;
import javax.swing.plaf.ScrollBarUI;
import javax.swing.plaf.basic.BasicScrollBarUI;
import java.awt.*;
import java.awt.Insets;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static com.android.tools.idea.uibuilder.graphics.NlConstants.*;
/**
* The design surface in the layout editor, which contains the full background, rulers, one
* or more device renderings, etc
*/
public class DesignSurface extends JPanel implements Disposable, ScalableDesignSurface {
private static final Logger LOG = Logger.getInstance(DesignSurface.class);
public static final boolean SIZE_ERROR_PANEL_DYNAMICALLY = true;
private static final Integer LAYER_PROGRESS = JLayeredPane.POPUP_LAYER + 100;
private final Project myProject;
public enum ScreenMode {
SCREEN_ONLY, BLUEPRINT_ONLY, BOTH;
@NonNull
public ScreenMode next() {
ScreenMode[] values = values();
return values[(ordinal() + 1) % values.length];
}
}
@NonNull private ScreenMode myScreenMode = ScreenMode.SCREEN_ONLY;
@Nullable private ScreenView myScreenView;
@Nullable private ScreenView myBlueprintView;
@SwingCoordinate private int myScreenX = RULER_SIZE_PX + DEFAULT_SCREEN_OFFSET_X;
@SwingCoordinate private int myScreenY = RULER_SIZE_PX + DEFAULT_SCREEN_OFFSET_Y;
private double myScale = 1;
@NonNull private final JScrollPane myScrollPane;
private final MyLayeredPane myLayeredPane;
private boolean myDeviceFrames = false;
private final List<Layer> myLayers = Lists.newArrayList();
private final InteractionManager myInteractionManager;
private final GlassPane myGlassPane;
private final RenderErrorPanel myErrorPanel;
private int myErrorPanelHeight = -1;
private List<DesignSurfaceListener> myListeners;
private boolean myCentered;
public DesignSurface(@NonNull Project project) {
super(new BorderLayout());
myProject = project;
setOpaque(true);
setFocusable(true);
setRequestFocusEnabled(true);
setBackground(UIUtil.TRANSPARENT_COLOR);
myInteractionManager = new InteractionManager(this);
myLayeredPane = new MyLayeredPane();
myLayeredPane.setBounds(0, 0, 100, 100);
myGlassPane = new GlassPane();
myLayeredPane.add(myGlassPane, JLayeredPane.DRAG_LAYER);
myProgressPanel = new MyProgressPanel();
myLayeredPane.add(myProgressPanel, LAYER_PROGRESS);
myScrollPane = new MyScrollPane();
myScrollPane.setViewportView(myLayeredPane);
myScrollPane.setBorder(null);
myScrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
myScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
add(myScrollPane, BorderLayout.CENTER);
myErrorPanel = new RenderErrorPanel();
myErrorPanel.setVisible(false);
myLayeredPane.add(myErrorPanel, JLayeredPane.POPUP_LAYER);
// TODO: Do this as part of the layout/validate operation instead
addComponentListener(new ComponentListener() {
@Override
public void componentResized(ComponentEvent componentEvent) {
updateScrolledAreaSize();
if (isShowing() && getWidth() > 0 && getHeight() > 0) {
zoomToFit();
}
}
@Override
public void componentMoved(ComponentEvent componentEvent) {
}
@Override
public void componentShown(ComponentEvent componentEvent) {
}
@Override
public void componentHidden(ComponentEvent componentEvent) {
}
});
myInteractionManager.registerListeners();
AnAction selectAllAction = new SelectAllAction(this);
registerAction(selectAllAction, "$SelectAll");
Disposer.register(project, this);
}
public Project getProject() {
return myProject;
}
public boolean isCentered() {
return myCentered;
}
public void setCentered(boolean centered) {
myCentered = centered;
}
@NonNull
public ScreenMode getScreenMode() {
return myScreenMode;
}
public void setScreenMode(@NonNull ScreenMode screenMode) {
if (screenMode != myScreenMode) {
// If we're going from 1 screens to 2 or back from 2 to 1, must adjust the zoom
// to-fit the screen(s) in the surface
boolean adjustZoom = screenMode == ScreenMode.BOTH || myScreenMode == ScreenMode.BOTH;
myScreenMode = screenMode;
if (myScreenView != null) {
NlModel model = myScreenView.getModel();
setModel(null);
setModel(model);
if (adjustZoom) {
zoomToFit();
}
}
}
}
public void setModel(@Nullable NlModel model) {
if (model == null && myScreenView == null) {
return;
}
List<NlComponent> selectionBefore = Collections.emptyList();
List<NlComponent> selectionAfter = Collections.emptyList();
if (myScreenView != null) {
SelectionModel selectionModel = myScreenView.getSelectionModel();
selectionBefore = selectionModel.getSelection();
selectionModel.removeListener(mySelectionListener);
myScreenView = null;
}
myLayers.clear();
if (model != null) {
myScreenView = new ScreenView(this, model);
Dimension screenSize = myScreenView.getPreferredSize();
myLayeredPane.setPreferredSize(screenSize);
if (myScreenMode == ScreenMode.SCREEN_ONLY) {
myLayers.add(new ScreenViewLayer(myScreenView));
myLayers.add(new SelectionLayer(myScreenView));
myLayers.add(new WarningLayer(myScreenView));
} else if (myScreenMode == ScreenMode.BOTH) {
myBlueprintView = new ScreenView(this, model);
assert screenSize != null;
myBlueprintView.setLocation(myScreenX + screenSize.width + 10, myScreenY);
myLayers.add(new ScreenViewLayer(myScreenView));
myLayers.add(new SelectionLayer(myScreenView));
myLayers.add(new WarningLayer(myScreenView));
myLayers.add(new BlueprintLayer(myBlueprintView));
myLayers.add(new SelectionLayer(myBlueprintView));
} else if (myScreenMode == ScreenMode.BLUEPRINT_ONLY) {
myLayers.add(new BlueprintLayer(myScreenView));
myLayers.add(new SelectionLayer(myScreenView));
} else {
assert false : myScreenMode;
}
positionScreens();
SelectionModel selectionModel = model.getSelectionModel();
selectionModel.addListener(mySelectionListener);
selectionAfter = selectionModel.getSelection();
} else {
myScreenView = null;
myBlueprintView = null;
}
repaint();
if (!selectionBefore.equals(selectionAfter)) {
notifySelectionListeners(selectionAfter);
}
notifyScreenViewChanged();
}
@Override
public void dispose() {
}
private void registerAction(AnAction action, @NonNls String actionId) {
action.registerCustomShortcutSet(
ActionManager.getInstance().getAction(actionId).getShortcutSet(),
myLayeredPane
);
}
private void updateScrolledAreaSize() {
if (myScreenView == null) {
return;
}
Dimension size = myScreenView.getPreferredSize();
if (size != null) {
// TODO: Account for the size of the blueprint screen too? I should figure out if I can automatically make it jump
// to the side or below based on the form factor and the available size
int scaledWidth = (int)(myScale * size.width);
int scaledHeight = (int)(myScale * size.height);
Dimension dimension = new Dimension(scaledWidth + 2 * DEFAULT_SCREEN_OFFSET_X,
scaledHeight + 2 * DEFAULT_SCREEN_OFFSET_Y);
myLayeredPane.setBounds(0, 0, dimension.width, dimension.height);
myLayeredPane.setPreferredSize(dimension);
myScrollPane.revalidate();
myProgressPanel.setBounds(myScreenX, myScreenY, scaledWidth, scaledHeight);
} else {
myProgressPanel.setBounds(0, 0, getWidth(), getHeight());
}
}
public JComponent getPreferredFocusedComponent() {
return myGlassPane;
}
@Override
protected void paintChildren(Graphics graphics) {
super.paintChildren(graphics);
if (isFocusOwner()) {
graphics.setColor(UIUtil.getFocusedBoundsColor());
graphics.drawRect(getX(), getY(), getWidth() - 1, getHeight() - 1);
}
}
@Nullable
@Override
public ScreenView getCurrentScreenView() {
return myScreenView;
}
@Nullable
public ScreenView getScreenView(@SwingCoordinate int x, @SwingCoordinate int y) {
// Currently only a single screen view active in the canvas.
if (myBlueprintView != null && x >= myBlueprintView.getX() && y >= myBlueprintView.getY()) {
return myBlueprintView;
}
return myScreenView;
}
public void hover(@SwingCoordinate int x, @SwingCoordinate int y) {
if (myErrorPanel.isVisible() && HighlightSeverity.ERROR.equals(myErrorPanel.getSeverity())) {
// don't show any warnings on hover if there is already some errors that are being displayed
// TODO: we should really move this logic into the error panel itself
return;
}
// Currently, we use the hover action only to check whether we need to show a warning.
for (Layer layer : myLayers) {
String tooltip = layer.getTooltip(x, y);
if (tooltip != null) {
myErrorPanel.showWarning(tooltip);
if (!myErrorPanel.isVisible()) {
myErrorPanel.setVisible(true);
revalidate();
} else {
repaint();
}
break;
}
}
}
public void resetHover() {
// if we were showing some warnings, then close it.
// TODO: similar to hover() method above, this logic of warning/error should be inside the error panel itself
if (HighlightSeverity.WARNING.equals(myErrorPanel.getSeverity())) {
myErrorPanel.setVisible(false);
}
}
public void zoom(@NonNull ZoomType type) {
switch (type) {
case IN:
setScale(myScale * 1.1);
repaint();
break;
case OUT:
setScale(myScale * (1/1.1));
repaint();
break;
case ACTUAL:
if (SystemInfo.isMac && UIUtil.isRetina()) {
setScale(0.5);
} else {
setScale(1);
}
repaint();
break;
case FIT:
case FIT_INTO:
if (myScreenView == null) {
return;
}
// Fit to zoom
int availableWidth = myScrollPane.getWidth();
int availableHeight = myScrollPane.getHeight();
Dimension preferredSize = myScreenView.getPreferredSize();
if (preferredSize != null) {
int requiredWidth = preferredSize.width;
int requiredHeight = preferredSize.height;
availableWidth -= 2 * DEFAULT_SCREEN_OFFSET_X;
availableHeight -= 2 * DEFAULT_SCREEN_OFFSET_Y;
if (myScreenMode == ScreenMode.BOTH) {
if (isVerticalScreenConfig(availableWidth, availableHeight, preferredSize)) {
requiredHeight *= 2;
requiredHeight += SCREEN_DELTA;
} else {
requiredWidth *= 2;
requiredWidth += SCREEN_DELTA;
}
}
double scaleX = (double)availableWidth / requiredWidth;
double scaleY = (double)availableHeight / requiredHeight;
double scale = Math.min(scaleX, scaleY);
if (type == ZoomType.FIT_INTO) {
scale = Math.min(1.0, scale);
}
setScale(scale);
repaint();
}
break;
default:
case SCREEN:
throw new UnsupportedOperationException("Not yet implemented: " + type);
}
}
public void zoomActual() {
zoom(ZoomType.ACTUAL);
}
public void zoomIn() {
zoom(ZoomType.IN);
}
public void zoomOut() {
zoom(ZoomType.OUT);
}
public void zoomToFit() {
zoom(ZoomType.FIT);
}
/** Returns true if we want to arrange screens vertically instead of horizontally */
private static boolean isVerticalScreenConfig(int availableWidth, int availableHeight, @NonNull Dimension preferredSize) {
boolean stackVertically = preferredSize.width > preferredSize.height;
if (availableWidth > 10 && availableHeight > 3 * availableWidth / 2) {
stackVertically = true;
}
return stackVertically;
}
@Override
public double getScale() {
return myScale;
}
@Override
public Configuration getConfiguration() {
return myScreenView != null ? myScreenView.getConfiguration() : null;
}
private void setScale(double scale) {
if (Math.abs(scale - 1) < 0.0001) {
scale = 1;
} else if (scale < 0.01) {
scale = 0.01;
} else if (scale > 10) {
scale = 10;
}
myScale = scale;
positionScreens();
updateScrolledAreaSize();
}
private void positionScreens() {
if (myScreenView == null) {
return;
}
Dimension preferredSize = myScreenView.getPreferredSize();
if (preferredSize == null) {
return;
}
int scaledScreenWidth = (int)(myScale * preferredSize.width);
int scaledScreenHeight = (int)(myScale * preferredSize.height);
// Position primary screen
int availableWidth = myScrollPane.getWidth();
int availableHeight = myScrollPane.getHeight();
boolean stackVertically = isVerticalScreenConfig(availableWidth, availableHeight, preferredSize);
if (myCentered && availableWidth > 10 && availableHeight > 10) {
int requiredWidth = scaledScreenWidth;
if (myScreenMode == ScreenMode.BOTH && !stackVertically) {
requiredWidth += SCREEN_DELTA;
requiredWidth += scaledScreenWidth;
}
if (requiredWidth < availableWidth) {
myScreenX = (availableWidth - requiredWidth) / 2;
} else {
myScreenX = 0;
}
int requiredHeight = scaledScreenHeight;
if (myScreenMode == ScreenMode.BOTH && stackVertically) {
requiredHeight += SCREEN_DELTA;
requiredHeight += scaledScreenHeight;
}
if (requiredHeight < availableHeight) {
myScreenY = (availableHeight - requiredHeight) / 2;
} else {
myScreenY = 0;
}
} else {
if (myDeviceFrames) {
myScreenX = RULER_SIZE_PX + 2 * DEFAULT_SCREEN_OFFSET_X;
myScreenY = RULER_SIZE_PX + 2 * DEFAULT_SCREEN_OFFSET_Y;
} else {
myScreenX = RULER_SIZE_PX + DEFAULT_SCREEN_OFFSET_X;
myScreenY = RULER_SIZE_PX + DEFAULT_SCREEN_OFFSET_Y;
}
}
myScreenView.setLocation(myScreenX, myScreenY);
// Position blueprint view
if (myBlueprintView != null) {
if (stackVertically) {
// top/bottom stacking
myBlueprintView.setLocation(myScreenX, myScreenY + scaledScreenHeight + SCREEN_DELTA);
}
else {
// left/right ordering
myBlueprintView.setLocation(myScreenX + scaledScreenWidth + SCREEN_DELTA, myScreenY);
}
}
}
public void toggleDeviceFrames() {
myDeviceFrames = !myDeviceFrames;
positionScreens();
repaint();
}
@NonNull
public JComponent getLayeredPane() {
return myLayeredPane;
}
@VisibleForTesting
@NonNull
public InteractionManager getInteractionManager() {
return myInteractionManager;
}
private void notifySelectionListeners(@NonNull List<NlComponent> newSelection) {
if (myListeners != null) {
List<DesignSurfaceListener> listeners = Lists.newArrayList(myListeners);
for (DesignSurfaceListener listener : listeners) {
listener.componentSelectionChanged(this, newSelection);
}
}
}
private void notifyScreenViewChanged() {
ScreenView screenView = myScreenView;
NlModel model = myScreenView != null ? myScreenView.getModel() : null;
if (myListeners != null) {
List<DesignSurfaceListener> listeners = Lists.newArrayList(myListeners);
for (DesignSurfaceListener listener : listeners) {
listener.modelChanged(this, model);
listener.screenChanged(this, screenView);
}
}
}
public void addListener(@NonNull DesignSurfaceListener listener) {
if (myListeners == null) {
myListeners = Lists.newArrayList();
} else {
myListeners.remove(listener); // ensure single registration
}
myListeners.add(listener);
}
public void removeListener(@NonNull DesignSurfaceListener listener) {
if (myListeners != null) {
myListeners.remove(listener);
}
}
private final SelectionListener mySelectionListener = new SelectionListener() {
@Override
public void selectionChanged(@NonNull SelectionModel model, @NonNull List<NlComponent> selection) {
if (myScreenView != null) {
notifySelectionListeners(selection);
} else {
notifySelectionListeners(Collections.<NlComponent>emptyList());
}
}
};
/** The editor has been activated */
public void activate() {
if (myScreenView != null) {
myScreenView.getModel().activate();
}
}
public void deactivate() {
if (myScreenView != null) {
myScreenView.getModel().deactivate();
}
}
private void positionErrorPanel() {
if (!myErrorPanel.isVisible()) {
return;
}
int height = getHeight();
int width = getWidth();
int size;
if (SIZE_ERROR_PANEL_DYNAMICALLY) { // TODO: Only do this when the error panel is showing
boolean showingErrors = HighlightSeverity.ERROR.equals(myErrorPanel.getSeverity());
size = computeErrorPanelHeight(showingErrors, height, myErrorPanel.getPreferredHeight(width) + 16);
} else {
size = height / 2;
}
myErrorPanel.setSize(width, size);
myErrorPanel.setLocation(RULER_SIZE_PX, height - size);
}
private static int computeErrorPanelHeight(boolean showingErrors, int designerHeight, int preferredHeight) {
int maxSize = designerHeight * 3/4; // error panel can take up to 3/4th of the designer
int minSize = showingErrors ? designerHeight / 4 : 16; // but is at least 1/4th if errors are being shown
if (preferredHeight < maxSize) {
return Math.max(preferredHeight, minSize);
}
else {
return maxSize;
}
}
private static class MyScrollPane extends JBScrollPane {
private MyScrollPane() {
super(0);
setOpaque(true);
setBackground(UIUtil.TRANSPARENT_COLOR);
setupCorners();
}
@NonNull
@Override
public JScrollBar createVerticalScrollBar() {
return new MyScrollBar(Adjustable.VERTICAL);
}
@Override
public JScrollBar createHorizontalScrollBar() {
return new MyScrollBar(Adjustable.HORIZONTAL);
}
@Override
protected boolean isOverlaidScrollbar(@Nullable JScrollBar scrollbar) {
ScrollBarUI vsbUI = scrollbar == null ? null : scrollbar.getUI();
return vsbUI instanceof ButtonlessScrollBarUI && !((ButtonlessScrollBarUI)vsbUI).alwaysShowTrack();
}
}
private static final Field decrButtonField = ReflectionUtil.getDeclaredField(BasicScrollBarUI.class, "decrButton");
private static final Field incrButtonField = ReflectionUtil.getDeclaredField(BasicScrollBarUI.class, "incrButton");
private static class MyScrollBar extends JBScrollBar implements IdeGlassPane.TopComponent {
@NonNls private static final String APPLE_LAF_AQUA_SCROLL_BAR_UI_CLASS = "apple.laf.AquaScrollBarUI";
private ScrollBarUI myPersistentUI;
private MyScrollBar(@JdkConstants.AdjustableOrientation int orientation) {
super(orientation);
setOpaque(false);
}
void setPersistentUI(ScrollBarUI ui) {
myPersistentUI = ui;
setUI(ui);
}
@Override
public boolean canBePreprocessed(MouseEvent e) {
return JBScrollPane.canBePreprocessed(e, this);
}
@Override
public void setUI(ScrollBarUI ui) {
if (myPersistentUI == null) myPersistentUI = ui;
super.setUI(myPersistentUI);
setOpaque(false);
}
/**
* This is helper method. It returns h of the top (decrease) scroll bar
* button. Please note, that it's possible to return real h only if scroll bar
* is instance of BasicScrollBarUI. Otherwise it returns fake (but good enough :) )
* value.
*/
int getDecScrollButtonHeight() {
ScrollBarUI barUI = getUI();
Insets insets = getInsets();
int top = Math.max(0, insets.top);
if (barUI instanceof ButtonlessScrollBarUI) {
return top + ((ButtonlessScrollBarUI)barUI).getDecrementButtonHeight();
}
if (barUI instanceof BasicScrollBarUI) {
try {
JButton decrButtonValue = (JButton)decrButtonField.get(barUI);
LOG.assertTrue(decrButtonValue != null);
return top + decrButtonValue.getHeight();
}
catch (Exception exc) {
throw new IllegalStateException(exc);
}
}
return top + 15;
}
/**
* This is helper method. It returns h of the bottom (increase) scroll bar
* button. Please note, that it's possible to return real h only if scroll bar
* is instance of BasicScrollBarUI. Otherwise it returns fake (but good enough :) )
* value.
*/
int getIncScrollButtonHeight() {
ScrollBarUI barUI = getUI();
Insets insets = getInsets();
if (barUI instanceof ButtonlessScrollBarUI) {
return insets.top + ((ButtonlessScrollBarUI)barUI).getIncrementButtonHeight();
}
if (barUI instanceof BasicScrollBarUI) {
try {
JButton incrButtonValue = (JButton)incrButtonField.get(barUI);
LOG.assertTrue(incrButtonValue != null);
return insets.bottom + incrButtonValue.getHeight();
}
catch (Exception exc) {
throw new IllegalStateException(exc);
}
}
if (APPLE_LAF_AQUA_SCROLL_BAR_UI_CLASS.equals(barUI.getClass().getName())) {
return insets.bottom + 30;
}
return insets.bottom + 15;
}
@Override
public int getUnitIncrement(int direction) {
return 5;
}
@Override
public int getBlockIncrement(int direction) {
return 1;
}
}
private class MyLayeredPane extends JLayeredPane implements Magnificator {
public MyLayeredPane() {
setOpaque(true);
setBackground(UIUtil.TRANSPARENT_COLOR);
// Enable pinching to zoom
putClientProperty(Magnificator.CLIENT_PROPERTY_KEY, this);
}
// ---- Implements Magnificator ----
@Override
public Point magnify(double scale, Point at) {
// Handle screen zooming.
// Note: This only seems to work (be invoked) on Mac with the Apple JDK (1.6) currently
setScale(scale * myScale);
DesignSurface.this.repaint();
return new Point((int)(at.x * scale), (int)(at.y * scale));
}
@Override
protected void paintComponent(@NonNull Graphics graphics) {
super.paintComponent(graphics);
if (myScreenView == null) {
return;
}
Graphics2D g2d = (Graphics2D)graphics;
// (x,y) coordinates of the top left corner in the view port
int tlx = myScrollPane.getHorizontalScrollBar().getValue();
int tly = myScrollPane.getVerticalScrollBar().getValue();
paintBackground(g2d, tlx, tly);
Composite oldComposite = g2d.getComposite();
RenderResult result = myScreenView.getResult();
boolean paintedFrame = false;
if (myDeviceFrames && result != null && result.getRenderedImage() != null) {
Configuration configuration = myScreenView.getConfiguration();
Device device = configuration.getDevice();
State deviceState = configuration.getDeviceState();
DeviceArtPainter painter = DeviceArtPainter.getInstance();
if (device != null && painter.hasDeviceFrame(device) && deviceState != null) {
paintedFrame = true;
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f));
painter.paintFrame(g2d, device, deviceState.getOrientation(), true, myScreenX, myScreenY,
(int)(myScale * result.getRenderedImage().getHeight()));
}
}
if (paintedFrame) {
// Only use alpha on the ruler bar if overlaying the device art
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.9f));
} else {
// Only show bounds dashed lines when there's no device
paintBoundsRectangle(g2d);
}
g2d.setComposite(oldComposite);
for (Layer layer : myLayers) {
if (!layer.isHidden()) {
layer.paint(g2d);
}
}
// Temporary overlays:
List<Layer> layers = myInteractionManager.getLayers();
if (layers != null) {
for (Layer layer : layers) {
if (!layer.isHidden()) {
layer.paint(g2d);
}
}
}
}
private void paintBackground(@NonNull Graphics2D graphics, int lx, int ly) {
int width = myScrollPane.getWidth() - RULER_SIZE_PX;
int height = myScrollPane.getHeight() - RULER_SIZE_PX;
graphics.setColor(DESIGN_SURFACE_BG);
graphics.fillRect(RULER_SIZE_PX + lx, RULER_SIZE_PX + ly, width, height);
}
private void paintRulers(@NonNull Graphics2D g, int lx, int ly) {
final Graphics2D graphics = (Graphics2D)g.create();
try {
int width = myScrollPane.getWidth();
int height = myScrollPane.getHeight();
graphics.setColor(RULER_BG);
graphics.fillRect(lx, ly, width, RULER_SIZE_PX);
graphics.fillRect(lx, ly + RULER_SIZE_PX, RULER_SIZE_PX, height - RULER_SIZE_PX);
graphics.setColor(RULER_TICK_COLOR);
int x = myScreenX + lx - lx % 100;
int px2 = x + 10 - 100;
for (int i = 1; i < 10; i++, px2 += 10) {
if (px2 < myScreenX + lx - 100) {
continue;
}
graphics.drawLine(px2, ly, px2, ly + RULER_MINOR_TICK_PX);
}
// TODO: The rulers need to be updated to track the scale!!!
for (int px = 0; px < width; px += 100, x += 100) {
graphics.drawLine(x, ly, x, ly + RULER_MAJOR_TICK_PX);
px2 = x + 10;
for (int i = 1; i < 10; i++, px2 += 10) {
graphics.drawLine(px2, ly, px2, ly + RULER_MINOR_TICK_PX);
}
}
int y = myScreenY + ly - ly % 100;
int py2 = y + 10 - 100;
for (int i = 1; i < 10; i++, py2 += 10) {
if (py2 < myScreenY + ly - 100) {
continue;
}
graphics.drawLine(lx, py2, lx + RULER_MINOR_TICK_PX, py2);
}
for (int py = 0; py < height; py += 100, y += 100) {
graphics.drawLine(lx, y, lx + RULER_MAJOR_TICK_PX, y);
py2 = y + 10;
for (int i = 1; i < 10; i++, py2 += 10) {
graphics.drawLine(lx, py2, lx + RULER_MINOR_TICK_PX, py2);
}
}
graphics.setColor(RULER_TEXT_COLOR);
graphics.setFont(RULER_TEXT_FONT);
int xDelta = lx - lx % 100;
x = myScreenX + 2 + xDelta;
for (int px = 0; px < width; px += 100, x += 100) {
graphics.drawString(Integer.toString(px + xDelta), x, ly + RULER_MAJOR_TICK_PX);
}
graphics.rotate(-Math.PI / 2);
int yDelta = ly - ly % 100;
y = myScreenY - 2 + yDelta;
for (int py = 0; py < height; py += 100, y += 100) {
graphics.drawString(Integer.toString(py + yDelta), -y, lx + RULER_MAJOR_TICK_PX);
}
}
finally {
graphics.dispose();
}
}
private void paintBoundsRectangle(Graphics2D g2d) {
if (myScreenView == null) {
return;
}
g2d.setColor(BOUNDS_RECT_COLOR);
int x = myScreenX;
int y = myScreenY;
Dimension preferredSize = myScreenView.getPreferredSize();
if (preferredSize == null) {
return;
}
double scale = myScreenView.getScale();
int width = (int)(scale * preferredSize.width);
int height = (int)(scale * preferredSize.height);
Stroke prevStroke = g2d.getStroke();
g2d.setStroke(DASHED_STROKE);
g2d.drawLine(x - 1, y - BOUNDS_RECT_DELTA, x - 1, y + height + BOUNDS_RECT_DELTA);
g2d.drawLine(x - BOUNDS_RECT_DELTA, y - 1, x + width + BOUNDS_RECT_DELTA, y - 1);
g2d.drawLine(x + width, y - BOUNDS_RECT_DELTA, x + width, y + height + BOUNDS_RECT_DELTA);
g2d.drawLine(x - BOUNDS_RECT_DELTA, y + height, x + width + BOUNDS_RECT_DELTA, y + height);
g2d.setStroke(prevStroke);
}
@Override
protected void paintChildren(@NonNull Graphics graphics) {
super.paintChildren(graphics); // paints the screen
// Paint rulers on top of whatever is under the scroll panel
Graphics2D g2d = (Graphics2D)graphics;
// (x,y) coordinates of the top left corner in the view port
int tlx = myScrollPane.getHorizontalScrollBar().getValue();
int tly = myScrollPane.getVerticalScrollBar().getValue();
paintRulers(g2d, tlx, tly);
}
@Override
public void doLayout() {
super.doLayout();
positionErrorPanel();
}
}
/**
* Notifies the design surface that the given screenview (which must be showing in this design surface)
* has been rendered (possibly with errors)
*/
public void updateErrorDisplay(@NonNull ScreenView view, @Nullable final RenderResult result) {
if (view == myScreenView) {
getErrorQueue().cancelAllUpdates();
boolean hasProblems = result != null && result.getLogger().hasProblems();
if (hasProblems != myErrorPanel.isVisible()) {
if (hasProblems) {
myErrorPanelHeight = -1;
updateErrors(result);
myErrorPanel.showErrors(result);
} else {
myErrorPanel.setVisible(false);
repaint();
}
}
}
}
/** When we have render errors for a given result, kick off a background computation
* of the error panel HTML, which when done will update the UI thread */
private void updateErrors(@Nullable final RenderResult result) {
assert result != null && result.getLogger().hasProblems();
getErrorQueue().cancelAllUpdates();
getErrorQueue().queue(new Update("errors") {
@Override
public void run() {
// Look up *current* result; a newer one could be available
final RenderResult result = myScreenView != null ? myScreenView.getResult() : null;
boolean hasProblems = result != null && result.getLogger().hasProblems();
final String html = hasProblems ? myErrorPanel.generateHtml(result, result.getLogger().getLinkManager()) : null;
if (hasProblems) {
ApplicationManager.getApplication().invokeLater(new Runnable() {
@Override
public void run() {
myErrorPanel.showErrors(html, result, result.getLogger().getLinkManager());
if (!myErrorPanel.isVisible()) {
myErrorPanel.setVisible(true);
revalidate();
} else {
repaint();
}
}
});
}
}
@Override
public boolean canEat(Update update) {
return true;
}
});
}
@NonNull
private MergingUpdateQueue getErrorQueue() {
synchronized (myErrorQueueLock) {
if (myErrorQueue == null) {
myErrorQueue = new MergingUpdateQueue("android.error.computation", 200, true, null, myProject, null,
Alarm.ThreadToUse.POOLED_THREAD);
}
return myErrorQueue;
}
}
private final Object myErrorQueueLock = new Object();
private MergingUpdateQueue myErrorQueue;
private static class GlassPane extends JComponent {
private static final long EVENT_FLAGS = AWTEvent.KEY_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK;
public GlassPane() {
enableEvents(EVENT_FLAGS);
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
if (enabled) {
enableEvents(EVENT_FLAGS);
}
else {
disableEvents(EVENT_FLAGS);
}
}
@Override
protected void processKeyEvent(KeyEvent event) {
if (!event.isConsumed()) {
super.processKeyEvent(event);
}
}
@Override
protected void processMouseEvent(MouseEvent event) {
if (event.getID() == MouseEvent.MOUSE_PRESSED) {
requestFocusInWindow();
}
super.processMouseEvent(event);
}
}
private final List<ProgressIndicator> myProgressIndicators = new ArrayList<ProgressIndicator>();
@SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized")
private final MyProgressPanel myProgressPanel;
public synchronized void registerIndicator(@NonNull ProgressIndicator indicator) {
synchronized (myProgressIndicators) {
myProgressIndicators.add(indicator);
myProgressPanel.showProgressIcon();
}
}
public void unregisterIndicator(@NonNull ProgressIndicator indicator) {
synchronized (myProgressIndicators) {
myProgressIndicators.remove(indicator);
if (myProgressIndicators.size() == 0) {
myProgressPanel.hideProgressIcon();
}
}
}
/**
* Panel which displays the progress icon. The progress icon can either be a large icon in the
* center, when there is no rendering showing, or a small icon in the upper right corner when there
* is a rendering. This is necessary because even though the progress icon looks good on some
* renderings, depending on the layout theme colors it is invisible in other cases.
*/
private class MyProgressPanel extends JPanel {
private AsyncProcessIcon mySmallProgressIcon;
private AsyncProcessIcon myLargeProgressIcon;
private boolean mySmall;
private boolean myProgressVisible;
private MyProgressPanel() {
super(new BorderLayout());
setOpaque(false);
}
/** The "small" icon mode isn't just for the icon size; it's for the layout position too; see {@link #doLayout} */
private void setSmallIcon(boolean small) {
if (small != mySmall) {
if (myProgressVisible && getComponentCount() != 0) {
AsyncProcessIcon oldIcon = getProgressIcon();
oldIcon.suspend();
}
mySmall = true;
removeAll();
AsyncProcessIcon icon = getProgressIcon();
add(icon, BorderLayout.CENTER);
if (myProgressVisible) {
icon.setVisible(true);
icon.resume();
}
}
}
public void showProgressIcon() {
if (!myProgressVisible) {
boolean hasResult = myScreenView != null && myScreenView.getResult() != null;
setSmallIcon(hasResult);
myProgressVisible = true;
setVisible(true);
AsyncProcessIcon icon = getProgressIcon();
if (getComponentCount() == 0) { // First time: haven't added icon yet?
add(getProgressIcon(), BorderLayout.CENTER);
} else {
icon.setVisible(true);
}
icon.resume();
}
}
public void hideProgressIcon() {
if (myProgressVisible) {
myProgressVisible = false;
setVisible(false);
AsyncProcessIcon icon = getProgressIcon();
icon.setVisible(false);
icon.suspend();
}
}
@Override
public void doLayout() {
super.doLayout();
setBackground(Color.RED);
if (!myProgressVisible) {
return;
}
// Place the progress icon in the center if there's no rendering, and in the
// upper right corner if there's a rendering. The reason for this is that the icon color
// will depend on whether we're in a light or dark IDE theme, and depending on the rendering
// in the layout it will be invisible. For example, in Darcula the icon is white, and if the
// layout is rendering a white screen, the progress is invisible.
AsyncProcessIcon icon = getProgressIcon();
Dimension size = icon.getPreferredSize();
if (mySmall) {
icon.setBounds(getWidth() - size.width - 1, 1, size.width, size.height);
} else {
icon.setBounds(getWidth() / 2 - size.width / 2, getHeight() / 2 - size.height / 2, size.width, size.height);
}
}
@Override
public Dimension getPreferredSize() {
return getProgressIcon().getPreferredSize();
}
@NonNull
private AsyncProcessIcon getProgressIcon() {
return getProgressIcon(mySmall);
}
@NonNull
private AsyncProcessIcon getProgressIcon(boolean small) {
if (small) {
if (mySmallProgressIcon == null) {
mySmallProgressIcon = new AsyncProcessIcon("Android layout rendering");
Disposer.register(myProject, mySmallProgressIcon);
}
return mySmallProgressIcon;
}
else {
if (myLargeProgressIcon == null) {
myLargeProgressIcon = new AsyncProcessIcon.Big("Android layout rendering");
Disposer.register(myProject, myLargeProgressIcon);
}
return myLargeProgressIcon;
}
}
}
}