aboutsummaryrefslogtreecommitdiff
path: root/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch
diff options
context:
space:
mode:
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch')
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/Draw9PatchEditor.java233
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/GraphicsUtilities.java169
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java882
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageEditorPanel.java97
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageViewer.java774
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/MainFrame.java79
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StatusPanel.java357
-rw-r--r--eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StretchesViewer.java267
8 files changed, 2858 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/Draw9PatchEditor.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/Draw9PatchEditor.java
new file mode 100644
index 000000000..48ef7c366
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/Draw9PatchEditor.java
@@ -0,0 +1,233 @@
+/*
+ * 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;
+
+import static com.android.SdkConstants.DOT_9PNG;
+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.ui.ImageViewer;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.ui.MainFrame;
+
+import org.eclipse.core.resources.IFile;
+import org.eclipse.core.resources.IProject;
+import org.eclipse.core.resources.ResourcesPlugin;
+import org.eclipse.core.runtime.CoreException;
+import org.eclipse.core.runtime.IPath;
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.jface.window.Window;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.ImageLoader;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.ui.IEditorInput;
+import org.eclipse.ui.IEditorSite;
+import org.eclipse.ui.PartInitException;
+import org.eclipse.ui.dialogs.SaveAsDialog;
+import org.eclipse.ui.part.EditorPart;
+import org.eclipse.ui.part.FileEditorInput;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+
+/**
+ * Draw9Patch editor part.
+ */
+public class Draw9PatchEditor extends EditorPart implements ImageViewer.UpdateListener {
+
+ private IProject mProject = null;
+
+ private FileEditorInput mFileEditorInput = null;
+
+ private String mFileName = null;
+
+ private NinePatchedImage mNinePatchedImage = null;
+
+ private MainFrame mMainFrame = null;
+
+ @Override
+ public void init(IEditorSite site, IEditorInput input) throws PartInitException {
+ setSite(site);
+ setInput(input);
+ setPartName(input.getName());
+
+ // The contract of init() mentions we need to fail if we can't
+ // understand the input.
+ if (input instanceof FileEditorInput) {
+ // We try to open a file that is part of the current workspace
+ mFileEditorInput = (FileEditorInput) input;
+ mFileName = mFileEditorInput.getName();
+ mProject = mFileEditorInput.getFile().getProject();
+ } else {
+ throw new PartInitException("Input is not of type FileEditorInput " + //$NON-NLS-1$
+ "nor FileStoreEditorInput: " + //$NON-NLS-1$
+ input == null ? "null" : input.toString()); //$NON-NLS-1$
+ }
+
+ }
+
+ @Override
+ public boolean isSaveAsAllowed() {
+ return true;
+ }
+
+ @Override
+ public void doSaveAs() {
+ IPath relativePath = null;
+ if ((relativePath = showSaveAsDialog()) != null) {
+ mFileEditorInput = new FileEditorInput(ResourcesPlugin.getWorkspace().getRoot()
+ .getFile(relativePath));
+ mFileName = mFileEditorInput.getName();
+ setInput(mFileEditorInput);
+
+ doSave(new NullProgressMonitor());
+ }
+ }
+
+ @Override
+ public void doSave(final IProgressMonitor monitor) {
+ boolean hasNinePatchExtension = mFileName.endsWith(DOT_9PNG);
+ boolean doConvert = false;
+
+ if (!hasNinePatchExtension) {
+ String patchedName = NinePatchedImage.getNinePatchedFileName(mFileName);
+ doConvert = MessageDialog
+ .openQuestion(AdtPlugin.getDisplay().getActiveShell(),
+ "Warning",
+ String.format(
+ "The file \"%s\" doesn't seem to be a 9-patch file. \n"
+ + "Do you want to convert and save as \"%s\" ?",
+ mFileName, patchedName));
+
+ if (doConvert) {
+ IFile destFile = mProject.getFile(NinePatchedImage.getNinePatchedFileName(
+ mFileEditorInput.getFile().getProjectRelativePath().toOSString()));
+ if (!destFile.exists()) {
+ mFileEditorInput = new FileEditorInput(destFile);
+ mFileName = mFileEditorInput.getName();
+ } else {
+ IPath relativePath = null;
+ if ((relativePath = showSaveAsDialog()) != null) {
+ mFileEditorInput = new FileEditorInput(ResourcesPlugin.getWorkspace()
+ .getRoot().getFile(relativePath));
+ mFileName = mFileEditorInput.getName();
+ } else {
+ doConvert = false;
+ }
+ }
+ }
+ }
+
+ if (hasNinePatchExtension || doConvert) {
+ ImageLoader loader = new ImageLoader();
+ loader.data = new ImageData[] {
+ mNinePatchedImage.getRawImageData()
+ };
+
+ IFile file = mFileEditorInput.getFile();
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ loader.save(outputStream, SWT.IMAGE_PNG);
+ byte[] byteArray = outputStream.toByteArray();
+
+ try {
+ if (file.exists()) {
+ file.setContents(new ByteArrayInputStream(byteArray), true, false, monitor);
+ } else {
+ file.create(new ByteArrayInputStream(byteArray), true, monitor);
+ }
+
+ mNinePatchedImage.clearDirtyFlag();
+
+ AdtPlugin.getDisplay().asyncExec(new Runnable() {
+ @Override
+ public void run() {
+ setPartName(mFileName);
+ firePropertyChange(PROP_DIRTY);
+ }
+ });
+ } catch (CoreException e) {
+ AdtPlugin.log(e, null);
+ }
+ }
+ }
+
+ @Override
+ public void createPartControl(Composite parent) {
+ mMainFrame = new MainFrame(parent, SWT.NULL);
+
+ ImageViewer imageViewer = mMainFrame.getImageEditorPanel().getImageViewer();
+ imageViewer.addUpdateListener(this);
+
+ mNinePatchedImage = imageViewer.loadFile(mFileEditorInput.getPath().toOSString());
+ if (mNinePatchedImage.hasNinePatchExtension()) {
+ if (!mNinePatchedImage.ensure9Patch() && showConvertMessageBox(mFileName)) {
+ // Reload image
+ mNinePatchedImage = imageViewer.loadFile(mFileEditorInput.getPath().toOSString());
+ mNinePatchedImage.convertToNinePatch();
+ }
+ } else {
+ mNinePatchedImage.convertToNinePatch();
+ }
+
+ imageViewer.startDisplay();
+
+ parent.layout();
+ }
+
+ @Override
+ public void setFocus() {
+ mMainFrame.forceFocus();
+ }
+
+ @Override
+ public boolean isDirty() {
+ return mNinePatchedImage.isDirty();
+ }
+
+ @Override
+ public void update(NinePatchedImage image) {
+ if (image.isDirty()) {
+ firePropertyChange(PROP_DIRTY);
+ }
+ }
+
+ private IPath showSaveAsDialog() {
+ SaveAsDialog dialog = new SaveAsDialog(AdtPlugin.getDisplay().getActiveShell());
+
+ IFile dest = mProject.getFile(NinePatchedImage.getNinePatchedFileName(
+ mFileEditorInput.getFile().getProjectRelativePath().toOSString()));
+ dialog.setOriginalFile(dest);
+
+ dialog.create();
+
+ if (dialog.open() == Window.CANCEL) {
+ return null;
+ }
+
+ return dialog.getResult();
+ }
+
+ private static boolean showConvertMessageBox(String fileName) {
+ return MessageDialog.openQuestion(
+ AdtPlugin.getDisplay().getActiveShell(),
+ "Warning",
+ String.format("The file \"%s\" doesn't seem to be a 9-patch file. \n"
+ + "Do you want to convert?", fileName));
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/GraphicsUtilities.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/GraphicsUtilities.java
new file mode 100644
index 000000000..74c2f043e
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/GraphicsUtilities.java
@@ -0,0 +1,169 @@
+/*
+ * 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.graphics;
+
+import org.eclipse.swt.graphics.ImageData;
+
+/**
+ * The utility class for SWT Image and ImageData manipulation.
+ */
+public class GraphicsUtilities {
+
+ /**
+ * Convert normal image to 9-patched.
+ * @return Returns 9-patched ImageData object. If image is null, returns null.
+ */
+ public static ImageData convertToNinePatch(ImageData image) {
+ if (image == null) {
+ return null;
+ }
+ ImageData result = new ImageData(image.width + 2, image.height + 2, image.depth,
+ image.palette);
+
+ final int[] colors = new int[image.width];
+ final byte[] alpha = new byte[image.width];
+
+ for (int y = 0; y < image.height; y++) {
+
+ // Copy pixels
+ image.getPixels(0, y, image.width, colors, 0);
+ result.setPixels(1, y + 1, image.width, colors, 0);
+
+ // Copy alpha
+ image.getAlphas(0, y, image.width, alpha, 0);
+ result.setAlphas(1, y + 1, image.width, alpha, 0);
+ }
+
+ return result;
+ }
+
+ /**
+ * Wipe all color and alpha pixels.
+ */
+ public static void clearImageData(ImageData imageData) {
+ if (imageData == null) {
+ throw new IllegalArgumentException("image data must not be null");
+ }
+ int width = imageData.width;
+ int height = imageData.height;
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ imageData.setPixel(x, y, 0x00000000);
+ imageData.setAlpha(x, y, 0x00);
+ }
+ }
+ }
+
+ /**
+ * Duplicate the image data.
+ * @return If image is null, return null.
+ */
+ public static ImageData copy(ImageData image) {
+ if (image == null) {
+ return null;
+ }
+ ImageData result = new ImageData(image.width, image.height, image.depth,
+ image.palette);
+
+ final int[] colors = new int[image.width];
+ final byte[] alpha = new byte[image.width];
+
+ for (int y = 0; y < image.height; y++) {
+
+ // Copy pixels
+ image.getPixels(0, y, image.width, colors, 0);
+ result.setPixels(0, y, image.width, colors, 0);
+
+ // Copy alpha
+ image.getAlphas(0, y, image.width, alpha, 0);
+ result.setAlphas(0, y, image.width, alpha, 0);
+ }
+
+ return result;
+ }
+
+ /**
+ * Get column pixels.
+ * @return length of obtained pixels.
+ */
+ public static int getVerticalPixels(ImageData data, int x, int y, int height, int[] out) {
+ if (data == null) {
+ throw new IllegalArgumentException("data must not be null");
+ }
+ if (out == null) {
+ throw new IllegalArgumentException("out array must not be null");
+ }
+ if (height > out.length) {
+ throw new IllegalArgumentException("out array length must be > height");
+ }
+ if (data.height < (y + height)) {
+ throw new IllegalArgumentException("image height must be > (y + height)");
+ }
+ if (x < 0 || y < 0) {
+ throw new IllegalArgumentException("argument x, y must be >= 0");
+ }
+ if (x >= data.width) {
+ throw new IllegalArgumentException("argument x must be < data.width");
+ }
+ if (y >= data.height) {
+ throw new IllegalArgumentException("argument y must be < data.height");
+ }
+ if (height <= 0) {
+ throw new IllegalArgumentException("argument height must be > 0");
+ }
+
+ int idx = 0;
+ while (idx < height) {
+ data.getPixels(x, (y + idx), 1, out, idx);
+ idx++;
+ }
+ return idx;
+ }
+
+ /**
+ * Get row pixels.
+ */
+ public static void getHorizontalPixels(ImageData data, int x, int y, int width, int[] out) {
+ if (data == null) {
+ throw new IllegalArgumentException("data must not be null");
+ }
+ if (out == null) {
+ throw new IllegalArgumentException("out array must not be null");
+ }
+ if (width > out.length) {
+ throw new IllegalArgumentException("out array length must be > width");
+ }
+ if (data.width < (x + width)) {
+ throw new IllegalArgumentException("image height must be > (x + width)");
+ }
+ if (x < 0 || y < 0) {
+ throw new IllegalArgumentException("argument x, y must be >= 0");
+ }
+ if (x >= data.width) {
+ throw new IllegalArgumentException("argument x must be < data.width");
+ }
+ if (y >= data.height) {
+ throw new IllegalArgumentException("argument y must be < data.height");
+ }
+ if (width <= 0) {
+ throw new IllegalArgumentException("argument width must be > 0");
+ }
+
+ data.getPixels(x, y, width, out, 0);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java
new file mode 100644
index 000000000..f1022c3a2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/graphics/NinePatchedImage.java
@@ -0,0 +1,882 @@
+/*
+ * 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.graphics;
+
+import static com.android.SdkConstants.DOT_9PNG;
+import static com.android.SdkConstants.DOT_PNG;
+import com.android.ide.eclipse.adt.AdtPlugin;
+
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.Rectangle;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The model of 9-patched image.
+ */
+public class NinePatchedImage {
+ private static final boolean DEBUG = false;
+
+ /**
+ * Get 9-patched filename as like image.9.png .
+ */
+ public static String getNinePatchedFileName(String fileName) {
+ if (fileName.endsWith(DOT_9PNG)) {
+ return fileName;
+ }
+ return fileName.substring(0, fileName.lastIndexOf(DOT_PNG)) + DOT_9PNG;
+ }
+
+ // For stretch regions and padding
+ public static final int BLACK_TICK = 0xFF000000;
+ // For Layout Bounds
+ public static final int RED_TICK = 0xFFFF0000;
+ // Blank
+ public static final int TRANSPARENT_TICK = 0x00000000;
+
+ private ImageData mBaseImageData;
+
+ private Image mBaseImage = null;
+
+ private boolean mHasNinePatchExtension = false;
+
+ private boolean mDirtyFlag = false;
+
+ private int[] mHorizontalPatchPixels = null;
+ private int[] mVerticalPatchPixels = null;
+
+ private int[] mHorizontalContentPixels = null;
+ private int[] mVerticalContentPixels = null;
+
+ // for Prevent unexpected stretch in StretchsView
+ private boolean mRedTickOnlyInHorizontalFlag = false;
+ private boolean mRedTickOnlyInVerticalFlag = false;
+
+ private final List<Tick> mHorizontalPatches = new ArrayList<Tick>();
+ private final List<Tick> mVerticalPatches = new ArrayList<Tick>();
+
+ private final List<Tick> mHorizontalContents = new ArrayList<Tick>();
+ private final List<Tick> mVerticalContents = new ArrayList<Tick>();
+
+
+ private static final int CHUNK_BIN_SIZE = 100;
+ private final List<Chunk> mChunkBin = new ArrayList<Chunk>(CHUNK_BIN_SIZE);
+
+ private int mHorizontalFixedPatchSum = 0;
+ private int mVerticalFixedPatchSum = 0;
+
+ private static final int PROJECTION_BIN_SIZE = 100;
+ private final List<Projection> mProjectionBin = new ArrayList<Projection>(PROJECTION_BIN_SIZE);
+
+ private Chunk[][] mPatchChunks = null;
+
+ public ImageData getImageData() {
+ return mBaseImageData;
+ }
+
+ public int getWidth() {
+ return mBaseImageData.width;
+ }
+
+ public int getHeight() {
+ return mBaseImageData.height;
+ }
+
+ public Image getImage() {
+ if (mBaseImage == null) {
+ mBaseImage = new Image(AdtPlugin.getDisplay(), mBaseImageData);
+ }
+ return mBaseImage;
+ }
+
+ public boolean hasNinePatchExtension() {
+ return mHasNinePatchExtension;
+ }
+
+ /**
+ * Get the image has/hasn't been edited flag.
+ * @return If has been edited, return true
+ */
+ public boolean isDirty() {
+ return mDirtyFlag;
+ }
+
+ /**
+ * Clear dirty(edited) flag.
+ */
+ public void clearDirtyFlag() {
+ mDirtyFlag = false;
+ }
+
+ public NinePatchedImage(String fileName) {
+ boolean hasNinePatchExtension = fileName.endsWith(DOT_9PNG);
+ ImageData data = new ImageData(fileName);
+
+ initNinePatchedImage(data, hasNinePatchExtension);
+ }
+
+ public NinePatchedImage(InputStream inputStream, String fileName) {
+ boolean hasNinePatchExtension = fileName.endsWith(DOT_9PNG);
+ ImageData data = new ImageData(inputStream);
+
+ initNinePatchedImage(data, hasNinePatchExtension);
+ }
+
+ private Chunk getChunk() {
+ if (mChunkBin.size() > 0) {
+ Chunk chunk = mChunkBin.remove(0);
+ chunk.init();
+ return chunk;
+ }
+ return new Chunk();
+ }
+
+ private static final void recycleChunks(Chunk[][] patchChunks, List<Chunk> bin) {
+ int yLen = patchChunks.length;
+ int xLen = patchChunks[0].length;
+
+ for (int y = 0; y < yLen; y++) {
+ for (int x = 0; x < xLen; x++) {
+ if (bin.size() < CHUNK_BIN_SIZE) {
+ bin.add(patchChunks[y][x]);
+ }
+ patchChunks[y][x] = null;
+ }
+ }
+ }
+
+ private Projection getProjection() {
+ if (mProjectionBin.size() > 0) {
+ Projection projection = mProjectionBin.remove(0);
+ return projection;
+ }
+ return new Projection();
+ }
+
+ private static final void recycleProjections(Projection[][] projections, List<Projection> bin) {
+ int yLen = projections.length;
+ int xLen = 0;
+ if (yLen > 0) {
+ xLen = projections[0].length;
+ }
+
+ for (int y = 0; y < yLen; y++) {
+ for (int x = 0; x < xLen; x++) {
+ if (bin.size() < CHUNK_BIN_SIZE) {
+ bin.add(projections[y][x]);
+ }
+ projections[y][x] = null;
+ }
+ }
+ }
+
+ private static final int[] initArray(int[] array) {
+ int len = array.length;
+ for (int i = 0; i < len; i++) {
+ array[i] = TRANSPARENT_TICK;
+ }
+ return array;
+ }
+
+ /**
+ * Get one pixel with alpha from the image.
+ * @return packed integer value as ARGB8888
+ */
+ private static final int getPixel(ImageData image, int x, int y) {
+ return (image.getAlpha(x, y) << 24) + image.getPixel(x, y);
+ }
+
+ private static final boolean isTransparentPixel(ImageData image, int x, int y) {
+ return image.getAlpha(x, y) == 0x0;
+ }
+
+ private static final boolean isValidTickColor(int pixel) {
+ return (pixel == BLACK_TICK || pixel == RED_TICK);
+ }
+
+ private void initNinePatchedImage(ImageData imageData, boolean hasNinePatchExtension) {
+ mBaseImageData = imageData;
+ mHasNinePatchExtension = hasNinePatchExtension;
+ }
+
+ private boolean ensurePixel(int x, int y, int[] pixels, int index) {
+ boolean isValid = true;
+ int pixel = getPixel(mBaseImageData, x, y);
+ if (!isTransparentPixel(mBaseImageData, x, y)) {
+ if (index == 0 || index == pixels.length - 1) {
+ isValid = false;
+ }
+ if (isValidTickColor(pixel)) {
+ pixels[index] = pixel;
+ } else {
+ isValid = false;
+ }
+ // clear pixel
+ mBaseImageData.setAlpha(x, y, 0x0);
+ }
+ return isValid;
+ }
+
+ private boolean ensureHorizontalPixel(int x, int y, int[] pixels) {
+ return ensurePixel(x, y, pixels, x);
+ }
+
+ private boolean ensureVerticalPixel(int x, int y, int[] pixels) {
+ return ensurePixel(x, y, pixels, y);
+ }
+
+ /**
+ * Ensure that image data is 9-patch.
+ */
+ public boolean ensure9Patch() {
+ boolean isValid = true;
+
+ int width = mBaseImageData.width;
+ int height = mBaseImageData.height;
+
+ createPatchArray();
+ createContentArray();
+
+ // horizontal
+ for (int x = 0; x < width; x++) {
+ // top row
+ if (!ensureHorizontalPixel(x, 0, mHorizontalPatchPixels)) {
+ isValid = false;
+ }
+ // bottom row
+ if (!ensureHorizontalPixel(x, height - 1, mHorizontalContentPixels)) {
+ isValid = false;
+ }
+ }
+ // vertical
+ for (int y = 0; y < height; y++) {
+ // left column
+ if (!ensureVerticalPixel(0, y, mVerticalPatchPixels)) {
+ isValid = false;
+ }
+ // right column
+ if (!ensureVerticalPixel(width -1, y, mVerticalContentPixels)) {
+ isValid = false;
+ }
+ }
+ findPatches();
+ findContentsArea();
+
+ return isValid;
+ }
+
+ private void createPatchArray() {
+ mHorizontalPatchPixels = initArray(new int[mBaseImageData.width]);
+ mVerticalPatchPixels = initArray(new int[mBaseImageData.height]);
+ }
+
+ private void createContentArray() {
+ mHorizontalContentPixels = initArray(new int[mBaseImageData.width]);
+ mVerticalContentPixels = initArray(new int[mBaseImageData.height]);
+ }
+
+ /**
+ * Convert to 9-patch image.
+ * <p>
+ * This method doesn't consider that target image is already 9-patched or
+ * not.
+ * </p>
+ */
+ public void convertToNinePatch() {
+ mBaseImageData = GraphicsUtilities.convertToNinePatch(mBaseImageData);
+ mHasNinePatchExtension = true;
+
+ createPatchArray();
+ createContentArray();
+
+ findPatches();
+ findContentsArea();
+ }
+
+ public boolean isValid(int x, int y) {
+ return (x == 0) ^ (y == 0)
+ ^ (x == mBaseImageData.width - 1) ^ (y == mBaseImageData.height - 1);
+ }
+
+ /**
+ * Set patch or content.
+ */
+ public void setPatch(int x, int y, int color) {
+ if (isValid(x, y)) {
+ if (x == 0) {
+ mVerticalPatchPixels[y] = color;
+ } else if (y == 0) {
+ mHorizontalPatchPixels[x] = color;
+ } else if (x == mBaseImageData.width - 1) {
+ mVerticalContentPixels[y] = color;
+ } else if (y == mBaseImageData.height - 1) {
+ mHorizontalContentPixels[x] = color;
+ }
+
+ // Mark as dirty
+ mDirtyFlag = true;
+ }
+ }
+
+ /**
+ * Erase the pixel.
+ */
+ public void erase(int x, int y) {
+ if (isValid(x, y)) {
+ int color = TRANSPARENT_TICK;
+ if (x == 0) {
+ mVerticalPatchPixels[y] = color;
+ } else if (y == 0) {
+ mHorizontalPatchPixels[x] = color;
+ } else if (x == mBaseImageData.width - 1) {
+ mVerticalContentPixels[y] = color;
+ } else if (y == mBaseImageData.height - 1) {
+ mHorizontalContentPixels[x] = color;
+ }
+
+ // Mark as dirty
+ mDirtyFlag = true;
+ }
+ }
+
+ public List<Tick> getHorizontalPatches() {
+ return mHorizontalPatches;
+ }
+
+ public List<Tick> getVerticalPatches() {
+ return mVerticalPatches;
+ }
+
+ /**
+ * Find patches from pixels array.
+ * @param pixels Target of seeking ticks.
+ * @param out Add the found ticks.
+ * @return If BlackTick is not found but only RedTick is found, returns true
+ */
+ private static boolean findPatches(int[] pixels, List<Tick> out) {
+ boolean redTickOnly = true;
+ Tick patch = null;
+ int len = 0;
+
+ // find patches
+ out.clear();
+ len = pixels.length - 1;
+ for (int i = 1; i < len; i++) {
+ int pixel = pixels[i];
+
+ if (redTickOnly && pixel != TRANSPARENT_TICK && pixel != RED_TICK) {
+ redTickOnly = false;
+ }
+
+ if (patch != null) {
+ if (patch.color != pixel) {
+ patch.end = i;
+ out.add(patch);
+ patch = null;
+ }
+ }
+ if (patch == null) {
+ patch = new Tick(pixel);
+ patch.start = i;
+ }
+ }
+
+ if (patch != null) {
+ patch.end = len;
+ out.add(patch);
+ }
+ return redTickOnly;
+ }
+
+ public void findPatches() {
+
+ // find horizontal patches
+ mRedTickOnlyInHorizontalFlag = findPatches(mHorizontalPatchPixels, mHorizontalPatches);
+
+ // find vertical patches
+ mRedTickOnlyInVerticalFlag = findPatches(mVerticalPatchPixels, mVerticalPatches);
+ }
+
+ public Rectangle getContentArea() {
+ Tick horizontal = getContentArea(mHorizontalContents);
+ Tick vertical = getContentArea(mVerticalContents);
+
+ Rectangle rect = new Rectangle(0, 0, 0, 0);
+ rect.x = 1;
+ rect.width = mBaseImageData.width - 1;
+ rect.y = 1;
+ rect.height = mBaseImageData.height - 1;
+
+ if (horizontal != null) {
+ rect.x = horizontal.start;
+ rect.width = horizontal.getLength();
+ }
+ if (vertical != null) {
+ rect.y = vertical.start;
+ rect.height = vertical.getLength();
+ }
+
+ return rect;
+ }
+
+ private Tick getContentArea(List<Tick> list) {
+ int size = list.size();
+ if (size == 0) {
+ return null;
+ }
+ if (size == 1) {
+ return list.get(0);
+ }
+
+ Tick start = null;
+ Tick end = null;
+
+ for (int i = 0; i < size; i++) {
+ Tick t = list.get(i);
+ if (t.color == BLACK_TICK) {
+ if (start == null) {
+ start = t;
+ end = t;
+ } else {
+ end = t;
+ }
+ }
+ }
+
+ // red tick only
+ if (start == null) {
+ return null;
+ }
+
+ Tick result = new Tick(start.color);
+ result.start = start.start;
+ result.end = end.end;
+
+ return result;
+ }
+
+ /**
+ * This is for unit test use only.
+ * @see com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImageTest
+ */
+ public List<Tick> getHorizontalContents() {
+ return mHorizontalContents;
+ }
+
+ /**
+ * This is for unit test use only.
+ * @see com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImageTest
+ */
+ public List<Tick> getVerticalContents() {
+ return mVerticalContents;
+ }
+
+ private static void findContentArea(int[] pixels, List<Tick> out) {
+ Tick contents = null;
+ int len = 0;
+
+ // find horizontal contents area
+ out.clear();
+ len = pixels.length - 1;
+ for (int x = 1; x < len; x++) {
+ if (contents != null) {
+ if (contents.color != pixels[x]) {
+ contents.end = x;
+ out.add(contents);
+ contents = null;
+ }
+ }
+ if (contents == null) {
+ contents = new Tick(pixels[x]);
+ contents.start = x;
+ }
+ }
+
+ if (contents != null) {
+ contents.end = len;
+ out.add(contents);
+ }
+ }
+
+ public void findContentsArea() {
+
+ // find horizontal contents area
+ findContentArea(mHorizontalContentPixels, mHorizontalContents);
+
+ // find vertical contents area
+ findContentArea(mVerticalContentPixels, mVerticalContents);
+ }
+
+ /**
+ * Get raw image data.
+ * <p>
+ * The raw image data is applicable for save.
+ * </p>
+ */
+ public ImageData getRawImageData() {
+ ImageData image = GraphicsUtilities.copy(mBaseImageData);
+
+ final int width = image.width;
+ final int height = image.height;
+ int len = 0;
+
+ len = mHorizontalPatchPixels.length;
+ for (int x = 0; x < len; x++) {
+ int pixel = mHorizontalPatchPixels[x];
+ if (pixel != TRANSPARENT_TICK) {
+ image.setAlpha(x, 0, 0xFF);
+ image.setPixel(x, 0, pixel);
+ }
+ }
+
+ len = mVerticalPatchPixels.length;
+ for (int y = 0; y < len; y++) {
+ int pixel = mVerticalPatchPixels[y];
+ if (pixel != TRANSPARENT_TICK) {
+ image.setAlpha(0, y, 0xFF);
+ image.setPixel(0, y, pixel);
+ }
+ }
+
+ len = mHorizontalContentPixels.length;
+ for (int x = 0; x < len; x++) {
+ int pixel = mHorizontalContentPixels[x];
+ if (pixel != TRANSPARENT_TICK) {
+ image.setAlpha(x, height - 1, 0xFF);
+ image.setPixel(x, height - 1, pixel);
+ }
+ }
+
+ len = mVerticalContentPixels.length;
+ for (int y = 0; y < len; y++) {
+ int pixel = mVerticalContentPixels[y];
+ if (pixel != TRANSPARENT_TICK) {
+ image.setAlpha(width - 1, y, 0xFF);
+ image.setPixel(width - 1, y, pixel);
+ }
+ }
+
+ return image;
+ }
+
+ public Chunk[][] getChunks(Chunk[][] chunks) {
+ int lenY = mVerticalPatches.size();
+ int lenX = mHorizontalPatches.size();
+
+ if (lenY == 0 || lenX == 0) {
+ return null;
+ }
+
+ if (chunks == null) {
+ chunks = new Chunk[lenY][lenX];
+ } else {
+ int y = chunks.length;
+ int x = chunks[0].length;
+ if (lenY != y || lenX != x) {
+ recycleChunks(chunks, mChunkBin);
+ chunks = new Chunk[lenY][lenX];
+ }
+ }
+
+ // for calculate weights
+ float horizontalPatchSum = 0;
+ float verticalPatchSum = 0;
+
+ mVerticalFixedPatchSum = 0;
+ mHorizontalFixedPatchSum = 0;
+
+ for (int y = 0; y < lenY; y++) {
+ Tick yTick = mVerticalPatches.get(y);
+
+ for (int x = 0; x < lenX; x++) {
+ Tick xTick = mHorizontalPatches.get(x);
+ Chunk t = getChunk();
+ chunks[y][x] = t;
+
+ t.rect.x = xTick.start;
+ t.rect.width = xTick.getLength();
+ t.rect.y = yTick.start;
+ t.rect.height = yTick.getLength();
+
+ if (mRedTickOnlyInHorizontalFlag
+ || xTick.color == BLACK_TICK || lenX == 1) {
+ t.type += Chunk.TYPE_HORIZONTAL;
+ if (y == 0) {
+ horizontalPatchSum += t.rect.width;
+ }
+ }
+ if (mRedTickOnlyInVerticalFlag
+ || yTick.color == BLACK_TICK || lenY == 1) {
+ t.type += Chunk.TYPE_VERTICAL;
+ if (x == 0) {
+ verticalPatchSum += t.rect.height;
+ }
+ }
+
+ if ((t.type & Chunk.TYPE_HORIZONTAL) == 0 && lenX > 1 && y == 0) {
+ mHorizontalFixedPatchSum += t.rect.width;
+ }
+ if ((t.type & Chunk.TYPE_VERTICAL) == 0 && lenY > 1 && x == 0) {
+ mVerticalFixedPatchSum += t.rect.height;
+ }
+
+ }
+ }
+
+ // calc weights
+ for (int y = 0; y < lenY; y++) {
+ for (int x = 0; x < lenX; x++) {
+ Chunk chunk = chunks[y][x];
+ if ((chunk.type & Chunk.TYPE_HORIZONTAL) != 0) {
+ chunk.horizontalWeight = chunk.rect.width / horizontalPatchSum;
+ }
+ if ((chunk.type & Chunk.TYPE_VERTICAL) != 0) {
+ chunk.verticalWeight = chunk.rect.height / verticalPatchSum;
+
+ }
+ }
+ }
+
+ return chunks;
+ }
+
+ public Chunk[][] getCorruptedChunks(Chunk[][] chunks) {
+ chunks = getChunks(chunks);
+
+ if (chunks != null) {
+ int yLen = chunks.length;
+ int xLen = chunks[0].length;
+
+ for (int yPos = 0; yPos < yLen; yPos++) {
+ for (int xPos = 0; xPos < xLen; xPos++) {
+ Chunk c = chunks[yPos][xPos];
+ Rectangle r = c.rect;
+ if ((c.type & Chunk.TYPE_HORIZONTAL) != 0
+ && isHorizontalCorrupted(mBaseImageData, r)) {
+ c.type |= Chunk.TYPE_CORRUPT;
+ }
+ if ((c.type & Chunk.TYPE_VERTICAL) != 0
+ && isVerticalCorrupted(mBaseImageData, r)) {
+ c.type |= Chunk.TYPE_CORRUPT;
+ }
+ }
+ }
+ }
+ return chunks;
+ }
+
+ private static boolean isVerticalCorrupted(ImageData data, Rectangle r) {
+ int[] column = new int[r.width];
+ int[] sample = new int[r.width];
+
+ GraphicsUtilities.getHorizontalPixels(data, r.x, r.y, r.width, column);
+
+ int lenY = r.y + r.height;
+ for (int y = r.y; y < lenY; y++) {
+ GraphicsUtilities.getHorizontalPixels(data, r.x, y, r.width, sample);
+ if (!Arrays.equals(column, sample)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isHorizontalCorrupted(ImageData data, Rectangle r) {
+ int[] column = new int[r.height];
+ int[] sample = new int[r.height];
+ GraphicsUtilities.getVerticalPixels(data, r.x, r.y, r.height, column);
+
+ int lenX = r.x + r.width;
+ for (int x = r.x; x < lenX; x++) {
+ GraphicsUtilities.getVerticalPixels(data, x, r.y, r.height, sample);
+ if (!Arrays.equals(column, sample)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public Projection[][] getProjections(int width, int height, Projection[][] projections) {
+ mPatchChunks = getChunks(mPatchChunks);
+ if (mPatchChunks == null) {
+ return null;
+ }
+
+ if (DEBUG) {
+ System.out.println(String.format("width:%d, height:%d", width, height));
+ }
+
+ int lenY = mPatchChunks.length;
+ int lenX = mPatchChunks[0].length;
+
+ if (projections == null) {
+ projections = new Projection[lenY][lenX];
+ } else {
+ int y = projections.length;
+ int x = projections[0].length;
+ if (lenY != y || lenX != x) {
+ recycleProjections(projections, mProjectionBin);
+ projections = new Projection[lenY][lenX];
+ }
+ }
+
+ float xZoom = ((float) width / mBaseImageData.width);
+ float yZoom = ((float) height / mBaseImageData.height);
+
+ if (DEBUG) {
+ System.out.println(String.format("xZoom:%f, yZoom:%f", xZoom, yZoom));
+ }
+
+ int destX = 0;
+ int destY = 0;
+ int streatchableWidth = width - mHorizontalFixedPatchSum;
+ streatchableWidth = streatchableWidth > 0 ? streatchableWidth : 1;
+
+ int streatchableHeight = height - mVerticalFixedPatchSum;
+ streatchableHeight = streatchableHeight > 0 ? streatchableHeight : 1;
+
+ if (DEBUG) {
+ System.out.println(String.format("streatchable %d %d", streatchableWidth,
+ streatchableHeight));
+ }
+
+ for (int yPos = 0; yPos < lenY; yPos++) {
+ destX = 0;
+ Projection p = null;
+ for (int xPos = 0; xPos < lenX; xPos++) {
+ Chunk chunk = mPatchChunks[yPos][xPos];
+
+ if (DEBUG) {
+ System.out.println(String.format("Tile[%d, %d] = %s",
+ yPos, xPos, chunk.toString()));
+ }
+
+ p = getProjection();
+ projections[yPos][xPos] = p;
+
+ p.chunk = chunk;
+ p.src = chunk.rect;
+ p.dest.x = destX;
+ p.dest.y = destY;
+
+ // fixed size
+ p.dest.width = chunk.rect.width;
+ p.dest.height = chunk.rect.height;
+
+ // horizontal stretch
+ if ((chunk.type & Chunk.TYPE_HORIZONTAL) != 0) {
+ p.dest.width = Math.round(streatchableWidth * chunk.horizontalWeight);
+ }
+ // vertical stretch
+ if ((chunk.type & Chunk.TYPE_VERTICAL) != 0) {
+ p.dest.height = Math.round(streatchableHeight * chunk.verticalWeight);
+ }
+
+ destX += p.dest.width;
+ }
+ destY += p.dest.height;
+ }
+ return projections;
+ }
+
+ /**
+ * Projection class for make relation between chunked image and resized image.
+ */
+ public static class Projection {
+ public Chunk chunk = null;
+ public Rectangle src = null;
+ public final Rectangle dest = new Rectangle(0, 0, 0, 0);
+
+ @Override
+ public String toString() {
+ return String.format("src[%d, %d, %d, %d] => dest[%d, %d, %d, %d]",
+ src.x, src.y, src.width, src.height,
+ dest.x, dest.y, dest.width, dest.height);
+ }
+ }
+
+ public static class Chunk {
+ public static final int TYPE_FIXED = 0x0;
+ public static final int TYPE_HORIZONTAL = 0x1;
+ public static final int TYPE_VERTICAL = 0x2;
+ public static final int TYPE_CORRUPT = 0x80000000;
+
+ public int type = TYPE_FIXED;
+
+ public Rectangle rect = new Rectangle(0, 0, 0, 0);
+
+ public float horizontalWeight = 0.0f;
+ public float verticalWeight = 0.0f;
+
+ void init() {
+ type = Chunk.TYPE_FIXED;
+ horizontalWeight = 0.0f;
+ verticalWeight = 0.0f;
+ rect.x = 0;
+ rect.y = 0;
+ rect.width = 0;
+ rect.height = 0;
+ }
+
+ private String typeToString() {
+ switch (type) {
+ case TYPE_FIXED:
+ return "FIXED";
+ case TYPE_HORIZONTAL:
+ return "HORIZONTAL";
+ case TYPE_VERTICAL:
+ return "VERTICAL";
+ case TYPE_HORIZONTAL + TYPE_VERTICAL:
+ return "BOTH";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s %f/%f %s", typeToString(), horizontalWeight, verticalWeight,
+ rect.toString());
+ }
+ }
+
+ public static class Tick {
+ public int start;
+ public int end;
+ public int color;
+
+ /**
+ * Get the tick length.
+ */
+ public int getLength() {
+ return end - start;
+ }
+
+ public Tick(int tickColor) {
+ color = tickColor;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%d tick: %d to %d", color, start, end);
+ }
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageEditorPanel.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageEditorPanel.java
new file mode 100644
index 000000000..7c4523024
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageEditorPanel.java
@@ -0,0 +1,97 @@
+/*
+ * 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.internal.editors.draw9patch.graphics.NinePatchedImage;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.custom.SashForm;
+import org.eclipse.swt.dnd.DND;
+import org.eclipse.swt.dnd.DropTarget;
+import org.eclipse.swt.dnd.DropTargetListener;
+import org.eclipse.swt.dnd.FileTransfer;
+import org.eclipse.swt.dnd.Transfer;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Image editor pane.
+ */
+public class ImageEditorPanel extends Composite implements ImageViewer.UpdateListener,
+ StatusPanel.StatusChangedListener {
+
+ private static final int WEIGHT_VIEWER = 3;
+ private static final int WEIGHT_PREVIEW = 1;
+
+ private final ImageViewer mImageViewer;
+ private final StretchesViewer mStretchesViewer;
+
+ public ImageViewer getImageViewer() {
+ return mImageViewer;
+ }
+
+ public ImageEditorPanel(Composite parent, int style) {
+ super(parent, style);
+
+ setLayout(new FillLayout());
+ SashForm sashForm = new SashForm(this, SWT.HORIZONTAL);
+
+ mImageViewer = new ImageViewer(sashForm, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL);
+ mImageViewer.addUpdateListener(this);
+
+ mStretchesViewer = new StretchesViewer(sashForm, SWT.BORDER);
+
+ sashForm.setWeights(new int[] {
+ WEIGHT_VIEWER, WEIGHT_PREVIEW
+ });
+ }
+
+ @Override
+ public void zoomChanged(int zoom) {
+ mImageViewer.setZoom(zoom);
+ }
+
+ @Override
+ public void scaleChanged(int scale) {
+ mStretchesViewer.setScale(scale);
+ }
+
+ @Override
+ public void lockVisibilityChanged(boolean visible) {
+ mImageViewer.setShowLock(visible);
+ }
+
+ @Override
+ public void patchesVisibilityChanged(boolean visible) {
+ mImageViewer.setShowPatchesArea(visible);
+ }
+
+ @Override
+ public void badPatchesVisibilityChanged(boolean visible) {
+ mImageViewer.setShowBadPatchesArea(visible);
+ }
+
+ @Override
+ public void contentAreaVisibilityChanged(boolean visible) {
+ mStretchesViewer.setShowContentArea(visible);
+ }
+
+ @Override
+ public void update(NinePatchedImage image) {
+ mStretchesViewer.updatePreview(image);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageViewer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageViewer.java
new file mode 100644
index 000000000..2414a39d7
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/ImageViewer.java
@@ -0,0 +1,774 @@
+/*
+ * 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);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/MainFrame.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/MainFrame.java
new file mode 100644
index 000000000..71845c11c
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/MainFrame.java
@@ -0,0 +1,79 @@
+/*
+ * 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 org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Main frame.
+ */
+public class MainFrame extends Composite implements ImageViewer.StatusChangedListener {
+
+ private final StatusPanel mStatusPanel;
+ private final ImageEditorPanel mImageEditorPanel;
+
+ public StatusPanel getStatusPanel() {
+ return mStatusPanel;
+ }
+
+ public ImageEditorPanel getImageEditorPanel() {
+ return mImageEditorPanel;
+ }
+
+ public MainFrame(Composite parent, int style) {
+ super(parent, style);
+ setLayout(new FormLayout());
+
+ mStatusPanel = new StatusPanel(this, SWT.NULL);
+
+ FormData bottom = new FormData();
+ bottom.bottom = new FormAttachment(100, 0);
+ bottom.left = new FormAttachment(0, 0);
+ bottom.right = new FormAttachment(100, 0);
+ mStatusPanel.setLayoutData(bottom);
+
+ mImageEditorPanel = new ImageEditorPanel(this, SWT.NULL);
+ mImageEditorPanel.getImageViewer().setStatusChangedListener(this);
+
+ mStatusPanel.setStatusChangedListener(mImageEditorPanel);
+
+ FormData top = new FormData();
+ top.top = new FormAttachment(0);
+ top.left = new FormAttachment(0);
+ top.right = new FormAttachment(100);
+ top.bottom = new FormAttachment(mStatusPanel);
+ mImageEditorPanel.setLayoutData(top);
+
+ addKeyListener(mStatusPanel);
+ addKeyListener(mImageEditorPanel.getImageViewer());
+ }
+
+ @Override
+ public void cursorPositionChanged(int x, int y) {
+ mStatusPanel.setPosition(x, y);
+ }
+
+ @Override
+ public void helpTextChanged(String text) {
+ mStatusPanel.setHelpText(text);
+ }
+
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StatusPanel.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StatusPanel.java
new file mode 100644
index 000000000..6ad258ee2
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StatusPanel.java
@@ -0,0 +1,357 @@
+/*
+ * 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 org.eclipse.swt.SWT;
+import org.eclipse.swt.events.ControlEvent;
+import org.eclipse.swt.events.ControlListener;
+import org.eclipse.swt.events.KeyEvent;
+import org.eclipse.swt.events.KeyListener;
+import org.eclipse.swt.events.SelectionAdapter;
+import org.eclipse.swt.events.SelectionEvent;
+import org.eclipse.swt.graphics.Color;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.layout.FormLayout;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Scale;
+
+/**
+ * Status and control pane.
+ */
+public class StatusPanel extends Composite implements KeyListener {
+
+ public static final int SCALE_MIN = 2;
+ public static final int SCALE_MAX = 6;
+
+ public static final int ZOOM_MIN = 100;
+ public static final int ZOOM_MAX = 800;
+
+ public static final int PADDING_TOP = 12;
+ public static final int PADDING_RIGHT = 0;
+ public static final int PADDING_BOTTOM = 5;
+ public static final int PADDING_LEFT = 10;
+
+ public static final int MIN_WIDTH = 800;
+
+ private Button mShowLock = null;
+ private Button mShowPatches = null;
+ private Button mShowBadPatches = null;
+ private Button mShowContent = null;
+
+ private Label mHelpLabel = null;
+
+ private Label mXPosLabel = null;
+ private Label mYPosLabel = null;
+
+ private ZoomControl mZoomControl = null;
+ private ZoomControl mScaleControl = null;
+
+ private StatusChangedListener mListener = null;
+
+ public void setStatusChangedListener(StatusChangedListener l) {
+ mListener = l;
+ }
+
+ public void setHelpText(String text) {
+ Point size = getSize();
+ // check window width
+ if (MIN_WIDTH < size.x) {
+ mHelpLabel.setText(text);
+ mHelpLabel.setVisible(true);
+ } else {
+ mHelpLabel.setText("N/A");
+ mHelpLabel.setVisible(false);
+ }
+ }
+
+ /**
+ * Set mouse cursor position.
+ */
+ public void setPosition(int x, int y) {
+ mXPosLabel.setText(String.format("X: %4d px", x));
+ mYPosLabel.setText(String.format("Y: %4d px", y));
+ }
+
+ public StatusPanel(Composite parent, int style) {
+ super(parent, style);
+ setLayout(new FormLayout());
+
+ final Composite container = new Composite(this, SWT.NULL);
+ container.setLayout(new FormLayout());
+
+ FormData innerForm = new FormData();
+ innerForm.left = new FormAttachment(0, PADDING_LEFT);
+ innerForm.top = new FormAttachment(0, PADDING_TOP);
+ innerForm.right = new FormAttachment(100, PADDING_RIGHT);
+ innerForm.bottom = new FormAttachment(100, -PADDING_BOTTOM);
+ container.setLayoutData(innerForm);
+
+ buildPosition(container);
+
+ Composite zoomPanels = new Composite(container, SWT.NULL);
+ zoomPanels.setLayout(new GridLayout(3, false));
+
+ buildZoomControl(zoomPanels);
+ buildScaleControl(zoomPanels);
+
+ Composite checkPanel = new Composite(container, SWT.NULL);
+ checkPanel.setLayout(new GridLayout(2, false));
+ FormData checkPanelForm = new FormData();
+ checkPanelForm.left = new FormAttachment(zoomPanels, 0);
+ checkPanelForm.bottom = new FormAttachment(100, -PADDING_BOTTOM);
+ checkPanel.setLayoutData(checkPanelForm);
+
+ buildCheckboxes(checkPanel);
+
+ mHelpLabel = new Label(container, SWT.BORDER_SOLID | SWT.BOLD | SWT.WRAP);
+ mHelpLabel.setBackground(new Color(AdtPlugin.getDisplay(), 0xFF, 0xFF, 0xFF));
+ FormData hintForm = new FormData();
+ hintForm.left = new FormAttachment(checkPanel, 5);
+ hintForm.right = new FormAttachment(mXPosLabel, -10);
+ hintForm.top = new FormAttachment(PADDING_TOP);
+ hintForm.bottom = new FormAttachment(100, -PADDING_BOTTOM);
+ mHelpLabel.setLayoutData(hintForm);
+
+ /*
+ * If the window width is not much, the "help label" will break the window.
+ * Because that is wrapped automatically.
+ *
+ * This listener catch resized events and reset help text.
+ *
+ * setHelpText method checks window width.
+ * If window is too narrow, help text will be set invisible.
+ */
+ container.addControlListener(new ControlListener() {
+ @Override
+ public void controlResized(ControlEvent event) {
+ // reset text
+ setHelpText(ImageViewer.HELP_MESSAGE_KEY_TIPS);
+ }
+ @Override
+ public void controlMoved(ControlEvent event) {
+ }
+ });
+
+ }
+
+ private void buildPosition(Composite parent) {
+ mXPosLabel = new Label(parent, SWT.NULL);
+ mYPosLabel = new Label(parent, SWT.NULL);
+
+ mXPosLabel.setText(String.format("X: %4d px", 1000));
+ mYPosLabel.setText(String.format("Y: %4d px", 1000));
+
+ FormData bottomRight = new FormData();
+ bottomRight.bottom = new FormAttachment(100, 0);
+ bottomRight.right = new FormAttachment(100, 0);
+ mYPosLabel.setLayoutData(bottomRight);
+
+ FormData aboveYPosLabel = new FormData();
+ aboveYPosLabel.bottom = new FormAttachment(mYPosLabel);
+ aboveYPosLabel.right = new FormAttachment(100, 0);
+ mXPosLabel.setLayoutData(aboveYPosLabel);
+ }
+
+ private void buildScaleControl(Composite parent) {
+ mScaleControl = new ZoomControl(parent);
+ mScaleControl.maxLabel.setText("6x");
+ mScaleControl.minLabel.setText("2x");
+ mScaleControl.scale.setMinimum(SCALE_MIN);
+ mScaleControl.scale.setMaximum(SCALE_MAX);
+ mScaleControl.scale.setSelection(2);
+ mScaleControl.scale.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ Scale scale = (Scale) event.widget;
+ mListener.scaleChanged(scale.getSelection());
+ }
+ }
+ });
+ }
+
+ private void buildZoomControl(Composite parent) {
+ mZoomControl = new ZoomControl(parent);
+ mZoomControl.maxLabel.setText("800%");
+ mZoomControl.minLabel.setText("100%");
+ mZoomControl.scale.setMinimum(ZOOM_MIN);
+ mZoomControl.scale.setMaximum(ZOOM_MAX - ZOOM_MIN);
+ mZoomControl.scale.setSelection(400);
+ mZoomControl.scale.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ Scale scale = (Scale) event.widget;
+ mListener.zoomChanged(scale.getSelection() + ZOOM_MIN);
+ }
+ }
+ });
+
+ }
+
+ private void buildCheckboxes(Composite parent) {
+ // check lock
+ mShowLock = new Button(parent, SWT.CHECK);
+ mShowLock.setText("show Lock");
+ mShowLock.setSelection(true);
+ mShowLock.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ mListener.lockVisibilityChanged(mShowLock.getSelection());
+ }
+ }
+ });
+
+ // check patches
+ mShowPatches = new Button(parent, SWT.CHECK);
+ mShowPatches.setText("show Patches");
+ mShowPatches.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ mListener.patchesVisibilityChanged(mShowPatches.getSelection());
+ }
+ }
+ });
+
+ // check patches
+ mShowBadPatches = new Button(parent, SWT.CHECK);
+ mShowBadPatches.setText("show Bad patches");
+ mShowBadPatches.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ mListener.badPatchesVisibilityChanged(mShowBadPatches.getSelection());
+ }
+ }
+ });
+
+ // check contents(padding)
+ mShowContent = new Button(parent, SWT.CHECK);
+ mShowContent.setText("show Contents");
+ mShowContent.addSelectionListener(new SelectionAdapter() {
+ @Override
+ public void widgetSelected(SelectionEvent event) {
+ super.widgetSelected(event);
+ if (mListener != null) {
+ mListener.contentAreaVisibilityChanged(mShowContent.getSelection());
+ }
+ }
+ });
+ }
+
+ @Override
+ public void keyPressed(KeyEvent event) {
+ switch (event.character) {
+ case 'c':
+ mShowContent.setSelection(!mShowContent.getSelection());
+ if (mListener != null) {
+ mListener.contentAreaVisibilityChanged(mShowContent.getSelection());
+ }
+ break;
+ case 'l':
+ mShowLock.setSelection(!mShowLock.getSelection());
+ if (mListener != null) {
+ mListener.lockVisibilityChanged(mShowLock.getSelection());
+ }
+ break;
+ case 'p':
+ mShowPatches.setSelection(!mShowPatches.getSelection());
+ if (mListener != null) {
+ mListener.patchesVisibilityChanged(mShowPatches.getSelection());
+ }
+ break;
+ case 'b':
+ mShowBadPatches.setSelection(!mShowBadPatches.getSelection());
+ if (mListener != null) {
+ mListener.badPatchesVisibilityChanged(mShowBadPatches.getSelection());
+ }
+ break;
+ }
+ }
+
+ @Override
+ public void keyReleased(KeyEvent event) {
+ }
+
+ private static class ZoomControl {
+
+ private Label minLabel;
+ private Label maxLabel;
+ Scale scale;
+
+ public ZoomControl(Composite composite) {
+ minLabel = new Label(composite, SWT.RIGHT);
+ scale = new Scale(composite, SWT.HORIZONTAL);
+ maxLabel = new Label(composite, SWT.LEFT);
+ }
+ }
+
+ /**
+ * Status changed events listener.
+ */
+ public interface StatusChangedListener {
+ /**
+ * Zoom level has been changed.
+ * @param zoom
+ */
+ public void zoomChanged(int zoom);
+
+ /**
+ * Scale has been changed.
+ * @param scale
+ */
+ public void scaleChanged(int scale);
+
+ /**
+ * Lock visibility has been changed.
+ * @param visible
+ */
+ public void lockVisibilityChanged(boolean visible);
+
+ /**
+ * Patches visibility has been changed.
+ * @param visible
+ */
+ public void patchesVisibilityChanged(boolean visible);
+
+ /**
+ * BadPatches visibility has been changed.
+ * @param visible
+ */
+ public void badPatchesVisibilityChanged(boolean visible);
+
+ /**
+ * Content visibility has been changed.
+ * @param visible
+ */
+ public void contentAreaVisibilityChanged(boolean visible);
+ }
+}
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StretchesViewer.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StretchesViewer.java
new file mode 100644
index 000000000..353312c85
--- /dev/null
+++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/draw9patch/ui/StretchesViewer.java
@@ -0,0 +1,267 @@
+/*
+ * 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.GraphicsUtilities;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage;
+import com.android.ide.eclipse.adt.internal.editors.draw9patch.graphics.NinePatchedImage.Projection;
+
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.events.PaintEvent;
+import org.eclipse.swt.events.PaintListener;
+import org.eclipse.swt.graphics.GC;
+import org.eclipse.swt.graphics.Image;
+import org.eclipse.swt.graphics.ImageData;
+import org.eclipse.swt.graphics.PaletteData;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.graphics.RGB;
+import org.eclipse.swt.graphics.Rectangle;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Canvas;
+import org.eclipse.swt.widgets.Composite;
+
+/**
+ * Preview 9-patched image pane.
+ */
+public class StretchesViewer extends Composite {
+ private static final boolean DEBUG = false;
+
+ private static final int PADDING_COLOR = 0x0000CC;
+
+ private static final int PADDING_COLOR_ALPHA = 100;
+
+ private static final PaletteData PADDING_PALLET
+ = new PaletteData(new RGB[] {new RGB(0x00, 0x00, 0xCC)});
+
+ private static final String CHECKER_PNG_PATH = "/icons/checker.png";
+
+ private Image mBackgroundLayer = null;
+
+ private final StretchView mHorizontal;
+ private final StretchView mVertical;
+
+ private final StretchView mBoth;
+
+ private NinePatchedImage mNinePatchedImage = null;
+
+ private ImageData mContentAreaImageData = null;
+
+ private boolean mIsContentAreaShown = false;
+
+ private Image mContentAreaImage = null;
+
+ private int mScale = 2;
+
+ public StretchesViewer(Composite parent, int style) {
+ super(parent, style);
+
+ mBackgroundLayer = AdtPlugin.getImageDescriptor(CHECKER_PNG_PATH).createImage();
+
+ setLayout(new FillLayout(SWT.VERTICAL));
+
+ mHorizontal = new StretchView(this, SWT.NULL);
+ mVertical = new StretchView(this, SWT.NULL);
+ mBoth = new StretchView(this, SWT.NULL);
+ }
+
+ /**
+ * Set show/not show content area.
+ * @param If show, true
+ */
+ public void setShowContentArea(boolean show) {
+ mIsContentAreaShown = show;
+ redraw();
+ }
+
+ private static final boolean equalSize(ImageData image1, ImageData image2) {
+ return (image1.width == image2.width && image1.height == image2.height);
+ }
+
+ /**
+ * Update preview image.
+ */
+ public void updatePreview(NinePatchedImage image) {
+ mNinePatchedImage = image;
+ ImageData base = mNinePatchedImage.getImageData();
+
+ if (mContentAreaImageData == null
+ || (mContentAreaImageData != null && !equalSize(base, mContentAreaImageData))) {
+ mContentAreaImageData = new ImageData(
+ base.width,
+ base.height,
+ 1,
+ PADDING_PALLET);
+ } else {
+ GraphicsUtilities.clearImageData(mContentAreaImageData);
+ }
+
+ mHorizontal.setImage(mNinePatchedImage);
+ mVertical.setImage(mNinePatchedImage);
+ mBoth.setImage(mNinePatchedImage);
+
+ mContentAreaImage = buildContentAreaPreview();
+
+ setScale(mScale);
+ }
+
+ private Image buildContentAreaPreview() {
+ if (mContentAreaImage != null) {
+ mContentAreaImage.dispose();
+ }
+
+ Rectangle rect = mNinePatchedImage.getContentArea();
+
+ int yLen = rect.y + rect.height;
+ for (int y = rect.y; y < yLen; y++) {
+ int xLen = rect.x + rect.width;
+ for (int x = rect.x; x < xLen; x++) {
+ mContentAreaImageData.setPixel(x, y, PADDING_COLOR);
+ mContentAreaImageData.setAlpha(x, y, PADDING_COLOR_ALPHA);
+ }
+ }
+ return new Image(AdtPlugin.getDisplay(), mContentAreaImageData);
+ }
+
+ public void setScale(int scale) {
+ if (DEBUG) {
+ System.out.println("scale = " + scale);
+ }
+
+ mScale = scale;
+ int imageWidth = mNinePatchedImage.getWidth();
+ int imageHeight = mNinePatchedImage.getHeight();
+
+ mHorizontal.setSize(imageWidth * scale, imageHeight);
+ mVertical.setSize(imageWidth, imageHeight * scale);
+ mBoth.setSize(imageWidth * scale, imageHeight * scale);
+
+ redraw();
+ }
+
+ @Override
+ public void dispose() {
+ mBackgroundLayer.dispose();
+ super.dispose();
+ }
+
+ private class StretchView extends Canvas implements PaintListener {
+
+ private final Point mSize = new Point(0, 0);
+ private final Rectangle mPadding = new Rectangle(0, 0, 0, 0);
+ private Projection[][] mProjection = null;
+
+ public StretchView(Composite parent, int style) {
+ super(parent, style);
+ addPaintListener(this);
+ }
+
+ private void setImage(NinePatchedImage image) {
+ setSize(image.getWidth(), image.getHeight());
+ }
+
+ @Override
+ public void setSize(int width, int heigh) {
+ mSize.x = width;
+ mSize.y = heigh;
+ mProjection = mNinePatchedImage.getProjections(mSize.x, mSize.y, mProjection);
+ }
+
+ private synchronized void calcPaddings(int width, int height) {
+ Point canvasSize = getSize();
+
+ mPadding.x = (canvasSize.x - width) / 2;
+ mPadding.y = (canvasSize.y - height) / 2;
+
+ mPadding.width = width;
+ mPadding.height = height;
+ }
+
+ @Override
+ public void paintControl(PaintEvent pe) {
+ if (mNinePatchedImage == null || mProjection == null) {
+ return;
+ }
+
+ Point size = getSize();
+
+ // relative scaling
+ float ratio = 1.0f;
+ float wRatio = ((float) size.x / mSize.x);
+ ratio = Math.min(wRatio, ratio);
+ float hRatio = ((float) size.y / mSize.y);
+ ratio = Math.min(hRatio, ratio);
+
+ int width = Math.round(mSize.x * ratio);
+ int height = Math.round(mSize.y * ratio);
+
+ calcPaddings(width, height);
+
+ Rectangle dest = new Rectangle(0, 0, 0, 0);
+
+ GC gc = pe.gc;
+
+ int backgroundLayerWidth = mBackgroundLayer.getImageData().width;
+ int backgroundLayerHeight = mBackgroundLayer.getImageData().height;
+
+ int yCount = size.y / backgroundLayerHeight
+ + ((size.y % backgroundLayerHeight) > 0 ? 1 : 0);
+ int xCount = size.x / backgroundLayerWidth
+ + ((size.x % backgroundLayerWidth) > 0 ? 1 : 0);
+
+ // draw background layer
+ for (int y = 0; y < yCount; y++) {
+ for (int x = 0; x < xCount; x++) {
+ gc.drawImage(mBackgroundLayer,
+ x * backgroundLayerWidth,
+ y * backgroundLayerHeight);
+ }
+ }
+
+ // draw the border line
+ gc.setAlpha(0x88);
+ gc.drawRectangle(0, 0, size.x, size.y);
+ gc.setAlpha(0xFF);
+
+ int yLen = mProjection.length;
+ int xLen = mProjection[0].length;
+ for (int yPos = 0; yPos < yLen; yPos++) {
+ for (int xPos = 0; xPos < xLen; xPos++) {
+ Projection p = mProjection[yPos][xPos];
+
+ // consider the scale
+ dest.x = (int) Math.ceil(p.dest.x * ratio);
+ dest.y = (int) Math.ceil(p.dest.y * ratio);
+ dest.width = (int) Math.ceil(p.dest.width * ratio);
+ dest.height = (int) Math.ceil(p.dest.height * ratio);
+
+ gc.drawImage(mNinePatchedImage.getImage(), p.src.x, p.src.y,
+ p.src.width, p.src.height,
+ (mPadding.x + dest.x), (mPadding.y + dest.y),
+ dest.width, dest.height);
+
+ if (mIsContentAreaShown) {
+ gc.drawImage(mContentAreaImage, p.src.x, p.src.y,
+ p.src.width, p.src.height,
+ (mPadding.x + dest.x), (mPadding.y + dest.y),
+ dest.width, dest.height);
+ }
+ }
+ }
+ }
+ }
+}