blob: 2414a39d7fff5f3dc6d6202bab842848fb44358b [file] [log] [blame]
/*
* Copyright (C) 2013 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.ide.eclipse.adt.internal.editors.draw9patch.ui;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage;
import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage.Chunk;
import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage.Tick;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.ScrollBar;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
/**
* View and edit Draw 9-patch image.
*/
public class ImageViewer extends Canvas implements PaintListener, KeyListener, MouseListener,
MouseMoveListener {
private static final boolean DEBUG = false;
public static final String HELP_MESSAGE_KEY_TIPS = "Press Shift to erase pixels."
+ " Press Control to draw layout bounds.";
public static final String HELP_MESSAGE_KEY_TIPS2 = "Release Shift to draw pixels.";
private static final Color BLACK_COLOR = AdtPlugin.getDisplay().getSystemColor(SWT.COLOR_BLACK);
private static final Color RED_COLOR = AdtPlugin.getDisplay().getSystemColor(SWT.COLOR_RED);
private static final Color BACK_COLOR
= new Color(AdtPlugin.getDisplay(), new RGB(0x00, 0xFF, 0x00));
private static final Color LOCK_COLOR
= new Color(AdtPlugin.getDisplay(), new RGB(0xFF, 0x00, 0x00));
private static final Color PATCH_COLOR
= new Color(AdtPlugin.getDisplay(), new RGB(0xFF, 0xFF, 0x00));
private static final Color PATCH_ONEWAY_COLOR
= new Color(AdtPlugin.getDisplay(), new RGB(0x00, 0x00, 0xFF));
private static final Color CORRUPTED_COLOR
= new Color(AdtPlugin.getDisplay(), new RGB(0xFF, 0x00, 0x00));
private static final int NONE_ALPHA = 0xFF;
private static final int LOCK_ALPHA = 50;
private static final int PATCH_ALPHA = 50;
private static final int GUIDE_ALPHA = 60;
private static final int MODE_NONE = 0x00;
private static final int MODE_BLACK_TICK = 0x01;
private static final int MODE_RED_TICK = 0x02;
private static final int MODE_ERASE = 0xFF;
private int mDrawMode = MODE_NONE;
private static final int MARGIN = 5;
private static final String CHECKER_PNG_PATH = "/icons/checker.png";
private Image mBackgroundLayer = null;
private NinePatchedImage mNinePatchedImage = null;
private Chunk[][] mChunks = null;
private Chunk[][] mBadChunks = null;
private boolean mIsLockShown = true;
private boolean mIsPatchesShown = false;
private boolean mIsBadPatchesShown = false;
private ScrollBar mHorizontalBar;
private ScrollBar mVerticalBar;
private int mZoom = 500;
private int mHorizontalScroll = 0;
private int mVerticalScroll = 0;
private final Rectangle mPadding = new Rectangle(0, 0, 0, 0);
// one pixel size that considered zoom
private int mZoomedPixelSize = 1;
private Image mBufferImage = null;
private boolean isCtrlPressed = false;
private boolean isShiftPressed = false;
private final List<UpdateListener> mUpdateListenerList
= new ArrayList<UpdateListener>();
private final Point mCursorPoint = new Point(0, 0);
private static final int DEFAULT_UPDATE_QUEUE_SIZE = 10;
private final ArrayBlockingQueue<NinePatchedImage> mUpdateQueue
= new ArrayBlockingQueue<NinePatchedImage>(DEFAULT_UPDATE_QUEUE_SIZE);
private final Runnable mUpdateRunnable = new Runnable() {
@Override
public void run() {
if (isDisposed()) {
return;
}
redraw();
fireUpdateEvent();
}
};
private final Thread mUpdateThread = new Thread() {
@Override
public void run() {
while (!isDisposed()) {
try {
mUpdateQueue.take();
mNinePatchedImage.findPatches();
mNinePatchedImage.findContentsArea();
mChunks = mNinePatchedImage.getChunks(mChunks);
mBadChunks = mNinePatchedImage.getCorruptedChunks(mBadChunks);
AdtPlugin.getDisplay().asyncExec(mUpdateRunnable);
} catch (InterruptedException e) {
}
}
}
};
private StatusChangedListener mStatusChangedListener = null;
public void addUpdateListener(UpdateListener l) {
mUpdateListenerList.add(l);
}
public void removeUpdateListener(UpdateListener l) {
mUpdateListenerList.remove(l);
}
private void fireUpdateEvent() {
int len = mUpdateListenerList.size();
for(int i=0; i < len; i++) {
mUpdateListenerList.get(i).update(mNinePatchedImage);
}
}
public void setStatusChangedListener(StatusChangedListener l) {
mStatusChangedListener = l;
if (mStatusChangedListener != null) {
mStatusChangedListener.helpTextChanged(HELP_MESSAGE_KEY_TIPS);
}
}
void setShowLock(boolean show) {
mIsLockShown = show;
redraw();
}
void setShowPatchesArea(boolean show) {
mIsPatchesShown = show;
redraw();
}
void setShowBadPatchesArea(boolean show) {
mIsBadPatchesShown = show;
redraw();
}
void setZoom(int zoom) {
mZoom = zoom;
mZoomedPixelSize = getZoomedPixelSize(1);
redraw();
}
public ImageViewer(Composite parent, int style) {
super(parent, style);
mUpdateThread.start();
mBackgroundLayer = AdtPlugin.getImageDescriptor(CHECKER_PNG_PATH).createImage();
addMouseListener(this);
addMouseMoveListener(this);
addPaintListener(this);
mHorizontalBar = getHorizontalBar();
mHorizontalBar.setThumb(1);
mHorizontalBar.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
super.widgetSelected(event);
ScrollBar bar = (ScrollBar) event.widget;
if (mHorizontalBar.isVisible()
&& mHorizontalScroll != bar.getSelection()) {
mHorizontalScroll = bar.getSelection();
redraw();
}
}
});
mVerticalBar = getVerticalBar();
mVerticalBar.setThumb(1);
mVerticalBar.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent event) {
super.widgetSelected(event);
ScrollBar bar = (ScrollBar) event.widget;
if (mVerticalBar.isVisible()
&& mVerticalScroll != bar.getSelection()) {
mVerticalScroll = bar.getSelection();
redraw();
}
}
});
}
/**
* Load the image file.
*
* @param fileName must be absolute path
*/
public NinePatchedImage loadFile(String fileName) {
mNinePatchedImage = new NinePatchedImage(fileName);
return mNinePatchedImage;
}
/**
* Start displaying the image.
*/
public void startDisplay() {
mZoomedPixelSize = getZoomedPixelSize(1);
scheduleUpdate();
}
private void draw(int x, int y, int drawMode) {
if (drawMode == MODE_ERASE) {
erase(x, y);
} else {
int color = (drawMode == MODE_RED_TICK) ? NinePatchedImage.RED_TICK
: NinePatchedImage.BLACK_TICK;
mNinePatchedImage.setPatch(x, y, color);
redraw();
scheduleUpdate();
}
}
private void erase(int x, int y) {
mNinePatchedImage.erase(x, y);
redraw();
scheduleUpdate();
}
private void scheduleUpdate() {
try {
mUpdateQueue.put(mNinePatchedImage);
} catch (InterruptedException e) {
}
}
@Override
public void mouseDown(MouseEvent event) {
if (event.button == 1 && !isShiftPressed) {
mDrawMode = !isCtrlPressed ? MODE_BLACK_TICK : MODE_RED_TICK;
draw(mCursorPoint.x, mCursorPoint.y, mDrawMode);
} else if (event.button == 3 || isShiftPressed) {
mDrawMode = MODE_ERASE;
erase(mCursorPoint.x, mCursorPoint.y);
}
}
@Override
public void mouseUp(MouseEvent event) {
mDrawMode = MODE_NONE;
}
@Override
public void mouseDoubleClick(MouseEvent event) {
}
private int getLogicalPositionX(int x) {
return Math.round((x - mPadding.x + mHorizontalScroll) / ((float) mZoom / 100));
}
private int getLogicalPositionY(int y) {
return Math.round((y - mPadding.y + mVerticalScroll) / ((float) mZoom / 100));
}
@Override
public void mouseMove(MouseEvent event) {
int posX = getLogicalPositionX(event.x);
int posY = getLogicalPositionY(event.y);
int width = mNinePatchedImage.getWidth();
int height = mNinePatchedImage.getHeight();
if (posX < 0) {
posX = 0;
}
if (posX >= width) {
posX = width - 1;
}
if (posY < 0) {
posY = 0;
}
if (posY >= height) {
posY = height - 1;
}
if (mDrawMode != MODE_NONE) {
int drawMode = mDrawMode;
if (isShiftPressed) {
drawMode = MODE_ERASE;
} else if (mDrawMode != MODE_ERASE) {
drawMode = !isCtrlPressed ? MODE_BLACK_TICK : MODE_RED_TICK;
}
/*
* Consider the previous cursor position because mouseMove events are
* scatter.
*/
int x = mCursorPoint.x;
int y = mCursorPoint.y;
for (; y != posY; y += (y > posY) ? -1 : 1) {
draw(x, y, drawMode);
}
x = mCursorPoint.x;
y = mCursorPoint.y;
for (; x != posX; x += (x > posX) ? -1 : 1) {
draw(x, y, drawMode);
}
}
mCursorPoint.x = posX;
mCursorPoint.y = posY;
redraw();
if (mStatusChangedListener != null) {
// Update position on status panel if position is in logical size.
if (posX >= 0 && posY >= 0
&& posX <= mNinePatchedImage.getWidth()
&& posY <= mNinePatchedImage.getHeight()) {
mStatusChangedListener.cursorPositionChanged(posX + 1, posY + 1);
}
}
}
private synchronized void calcPaddings(int width, int height) {
Point canvasSize = getSize();
mPadding.width = getZoomedPixelSize(width);
mPadding.height = getZoomedPixelSize(height);
int margin = getZoomedPixelSize(MARGIN);
if (mPadding.width < canvasSize.x) {
mPadding.x = (canvasSize.x - mPadding.width) / 2;
} else {
mPadding.x = margin;
}
if (mPadding.height < canvasSize.y) {
mPadding.y = (canvasSize.y - mPadding.height) / 2;
} else {
mPadding.y = margin;
}
}
private void calcScrollBarSettings() {
Point size = getSize();
int screenWidth = size.x;
int screenHeight = size.y;
int imageWidth = getZoomedPixelSize(mNinePatchedImage.getWidth() + 1);
int imageHeight = getZoomedPixelSize(mNinePatchedImage.getHeight() + 1);
// consider the scroll bar sizes
int verticalBarSize = mVerticalBar.getSize().x;
int horizontalBarSize = mHorizontalBar.getSize().y;
int horizontalScroll = imageWidth - (screenWidth - verticalBarSize);
int verticalScroll = imageHeight - (screenHeight - horizontalBarSize);
int margin = getZoomedPixelSize(MARGIN) * 2;
if (horizontalScroll > 0) {
mHorizontalBar.setVisible(true);
// horizontal maximum
int max = horizontalScroll + verticalBarSize + margin;
mHorizontalBar.setMaximum(max);
// set corrected scroll size
int value = mHorizontalBar.getSelection();
value = max < value ? max : value;
mHorizontalBar.setSelection(value);
mHorizontalScroll = value;
} else {
mHorizontalBar.setSelection(0);
mHorizontalBar.setMaximum(0);
mHorizontalBar.setVisible(false);
}
if (verticalScroll > 0) {
mVerticalBar.setVisible(true);
// vertical maximum
int max = verticalScroll + horizontalBarSize + margin;
mVerticalBar.setMaximum(max);
// set corrected scroll size
int value = mVerticalBar.getSelection();
value = max < value ? max : value;
mVerticalBar.setSelection(value);
mVerticalScroll = value;
} else {
mVerticalBar.setSelection(0);
mVerticalBar.setMaximum(0);
mVerticalBar.setVisible(false);
}
}
private int getZoomedPixelSize(int val) {
return Math.round(val * (float) mZoom / 100);
}
@Override
public void paintControl(PaintEvent pe) {
if (mNinePatchedImage == null) {
return;
}
// Use buffer
GC bufferGc = null;
if (mBufferImage == null) {
mBufferImage = new Image(AdtPlugin.getDisplay(), pe.width, pe.height);
} else {
int width = mBufferImage.getBounds().width;
int height = mBufferImage.getBounds().height;
if (width != pe.width || height != pe.height) {
mBufferImage = new Image(AdtPlugin.getDisplay(), pe.width, pe.height);
}
}
// Draw previous image once for prevent flicking
pe.gc.drawImage(mBufferImage, 0, 0);
bufferGc = new GC(mBufferImage);
bufferGc.setAdvanced(true);
// Make interpolation disable
bufferGc.setInterpolation(SWT.NONE);
// clear buffer
bufferGc.fillRectangle(0, 0, pe.width, pe.height);
calcScrollBarSettings();
// padding from current zoom
int width = mNinePatchedImage.getWidth();
int height = mNinePatchedImage.getHeight();
calcPaddings(width, height);
int baseX = mPadding.x - mHorizontalScroll;
int baseY = mPadding.y - mVerticalScroll;
// draw checker image
bufferGc.drawImage(mBackgroundLayer,
0, 0, mBackgroundLayer.getImageData().width,
mBackgroundLayer.getImageData().height,
baseX, baseY, mPadding.width, mPadding.height);
if (DEBUG) {
System.out.println(String.format("%d,%d %d,%d %d,%d",
width, height, baseX, baseY, mPadding.width, mPadding.height));
}
// draw image
/* TODO: Do not draw invisible area, for better performance. */
bufferGc.drawImage(mNinePatchedImage.getImage(), 0, 0, width, height, baseX, baseY,
mPadding.width, mPadding.height);
bufferGc.setBackground(BLACK_COLOR);
// draw patch ticks
drawHorizontalPatches(bufferGc, baseX, baseY);
drawVerticalPatches(bufferGc, baseX, baseY);
// draw content ticks
drawHorizontalContentArea(bufferGc, baseX, baseY);
drawVerticalContentArea(bufferGc, baseX, baseY);
if (mNinePatchedImage.isValid(mCursorPoint.x, mCursorPoint.y)) {
bufferGc.setForeground(BLACK_COLOR);
} else if (mIsLockShown) {
drawLockArea(bufferGc, baseX, baseY);
}
// Patches
if (mIsPatchesShown) {
drawPatchAreas(bufferGc, baseX, baseY);
}
// Bad patches
if (mIsBadPatchesShown) {
drawBadPatchAreas(bufferGc, baseX, baseY);
}
if (mNinePatchedImage.isValid(mCursorPoint.x, mCursorPoint.y)) {
bufferGc.setForeground(BLACK_COLOR);
} else {
bufferGc.setForeground(RED_COLOR);
}
drawGuideLine(bufferGc, baseX, baseY);
bufferGc.dispose();
pe.gc.drawImage(mBufferImage, 0, 0);
}
private static final Color getColor(int color) {
switch (color) {
case NinePatchedImage.RED_TICK:
return RED_COLOR;
default:
return BLACK_COLOR;
}
}
private void drawVerticalPatches(GC gc, int baseX, int baseY) {
List<Tick> verticalPatches = mNinePatchedImage.getVerticalPatches();
for (Tick t : verticalPatches) {
if (t.color != NinePatchedImage.TRANSPARENT_TICK) {
gc.setBackground(getColor(t.color));
gc.fillRectangle(
baseX,
baseY + getZoomedPixelSize(t.start),
mZoomedPixelSize,
getZoomedPixelSize(t.getLength()));
}
}
}
private void drawHorizontalPatches(GC gc, int baseX, int baseY) {
List<Tick> horizontalPatches = mNinePatchedImage.getHorizontalPatches();
for (Tick t : horizontalPatches) {
if (t.color != NinePatchedImage.TRANSPARENT_TICK) {
gc.setBackground(getColor(t.color));
gc.fillRectangle(
baseX + getZoomedPixelSize(t.start),
baseY,
getZoomedPixelSize(t.getLength()),
mZoomedPixelSize);
}
}
}
private void drawHorizontalContentArea(GC gc, int baseX, int baseY) {
List<Tick> horizontalContentArea = mNinePatchedImage.getHorizontalContents();
for (Tick t : horizontalContentArea) {
if (t.color != NinePatchedImage.TRANSPARENT_TICK) {
gc.setBackground(getColor(t.color));
gc.fillRectangle(
baseX + getZoomedPixelSize(t.start),
baseY + getZoomedPixelSize(mNinePatchedImage.getHeight() - 1),
getZoomedPixelSize(t.getLength()),
mZoomedPixelSize);
}
}
}
private void drawVerticalContentArea(GC gc, int baseX, int baseY) {
List<Tick> verticalContentArea = mNinePatchedImage.getVerticalContents();
for (Tick t : verticalContentArea) {
if (t.color != NinePatchedImage.TRANSPARENT_TICK) {
gc.setBackground(getColor(t.color));
gc.fillRectangle(
baseX + getZoomedPixelSize(mNinePatchedImage.getWidth() - 1),
baseY + getZoomedPixelSize(t.start),
mZoomedPixelSize,
getZoomedPixelSize(t.getLength()));
}
}
}
private void drawLockArea(GC gc, int baseX, int baseY) {
gc.setAlpha(LOCK_ALPHA);
gc.setForeground(LOCK_COLOR);
gc.setBackground(LOCK_COLOR);
gc.fillRectangle(
baseX + mZoomedPixelSize,
baseY + mZoomedPixelSize,
getZoomedPixelSize(mNinePatchedImage.getWidth() - 2),
getZoomedPixelSize(mNinePatchedImage.getHeight() - 2));
gc.setAlpha(NONE_ALPHA);
}
private void drawPatchAreas(GC gc, int baseX, int baseY) {
if (mChunks != null) {
int yLen = mChunks.length;
int xLen = mChunks[0].length;
gc.setAlpha(PATCH_ALPHA);
for (int yPos = 0; yPos < yLen; yPos++) {
for (int xPos = 0; xPos < xLen; xPos++) {
Chunk c = mChunks[yPos][xPos];
if (c.type == Chunk.TYPE_FIXED) {
gc.setBackground(BACK_COLOR);
} else if (c.type == Chunk.TYPE_HORIZONTAL) {
gc.setBackground(PATCH_ONEWAY_COLOR);
} else if (c.type == Chunk.TYPE_VERTICAL) {
gc.setBackground(PATCH_ONEWAY_COLOR);
} else if (c.type == Chunk.TYPE_HORIZONTAL + Chunk.TYPE_VERTICAL) {
gc.setBackground(PATCH_COLOR);
}
Rectangle r = c.rect;
gc.fillRectangle(
baseX + getZoomedPixelSize(r.x),
baseY + getZoomedPixelSize(r.y),
getZoomedPixelSize(r.width),
getZoomedPixelSize(r.height));
}
}
}
gc.setAlpha(NONE_ALPHA);
}
private void drawBadPatchAreas(GC gc, int baseX, int baseY) {
if (mBadChunks != null) {
int yLen = mBadChunks.length;
int xLen = mBadChunks[0].length;
gc.setAlpha(NONE_ALPHA);
gc.setForeground(CORRUPTED_COLOR);
gc.setBackground(CORRUPTED_COLOR);
for (int yPos = 0; yPos < yLen; yPos++) {
for (int xPos = 0; xPos < xLen; xPos++) {
Chunk c = mBadChunks[yPos][xPos];
if ((c.type & Chunk.TYPE_CORRUPT) != 0) {
Rectangle r = c.rect;
gc.drawRectangle(
baseX + getZoomedPixelSize(r.x),
baseY + getZoomedPixelSize(r.y),
getZoomedPixelSize(r.width),
getZoomedPixelSize(r.height));
}
}
}
}
}
private void drawGuideLine(GC gc, int baseX, int baseY) {
gc.setAntialias(SWT.ON);
gc.setInterpolation(SWT.HIGH);
int x = Math.round((mCursorPoint.x * ((float) mZoom / 100) + baseX)
+ ((float) mZoom / 100 / 2));
int y = Math.round((mCursorPoint.y * ((float) mZoom / 100) + baseY)
+ ((float) mZoom / 100 / 2));
gc.setAlpha(GUIDE_ALPHA);
Point size = getSize();
gc.drawLine(x, 0, x, size.y);
gc.drawLine(0, y, size.x, y);
gc.setAlpha(NONE_ALPHA);
}
@Override
public void keyPressed(KeyEvent event) {
int keycode = event.keyCode;
if (keycode == SWT.CTRL) {
isCtrlPressed = true;
}
if (keycode == SWT.SHIFT) {
isShiftPressed = true;
if (mStatusChangedListener != null) {
mStatusChangedListener.helpTextChanged(HELP_MESSAGE_KEY_TIPS2);
}
}
}
@Override
public void keyReleased(KeyEvent event) {
int keycode = event.keyCode;
if (keycode == SWT.CTRL) {
isCtrlPressed = false;
}
if (keycode == SWT.SHIFT) {
isShiftPressed = false;
if (mStatusChangedListener != null) {
mStatusChangedListener.helpTextChanged(HELP_MESSAGE_KEY_TIPS);
}
}
}
@Override
public void dispose() {
mBackgroundLayer.dispose();
super.dispose();
}
/**
* Listen image updated event.
*/
public interface UpdateListener {
/**
* 9-patched image has been updated.
*/
public void update(NinePatchedImage image);
}
/**
* Listen status changed event.
*/
public interface StatusChangedListener {
/**
* Mouse cursor position has been changed.
*/
public void cursorPositionChanged(int x, int y);
/**
* Help text has been changed.
*/
public void helpTextChanged(String text);
}
}