• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2008 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.draw9patch.ui;
18 
19 import com.android.draw9patch.graphics.GraphicsUtilities;
20 
21 import javax.swing.JPanel;
22 import javax.swing.JLabel;
23 import javax.swing.BorderFactory;
24 import javax.swing.JSlider;
25 import javax.swing.JComponent;
26 import javax.swing.JScrollPane;
27 import javax.swing.JCheckBox;
28 import javax.swing.Box;
29 import javax.swing.JFileChooser;
30 import javax.swing.JSplitPane;
31 import javax.swing.JButton;
32 import javax.swing.border.EmptyBorder;
33 import javax.swing.event.AncestorEvent;
34 import javax.swing.event.AncestorListener;
35 import javax.swing.event.ChangeListener;
36 import javax.swing.event.ChangeEvent;
37 import java.awt.image.BufferedImage;
38 import java.awt.image.RenderedImage;
39 import java.awt.Graphics2D;
40 import java.awt.BorderLayout;
41 import java.awt.Color;
42 import java.awt.Graphics;
43 import java.awt.Dimension;
44 import java.awt.TexturePaint;
45 import java.awt.Shape;
46 import java.awt.BasicStroke;
47 import java.awt.RenderingHints;
48 import java.awt.Rectangle;
49 import java.awt.GridBagLayout;
50 import java.awt.GridBagConstraints;
51 import java.awt.Insets;
52 import java.awt.Toolkit;
53 import java.awt.AWTEvent;
54 import java.awt.event.MouseMotionAdapter;
55 import java.awt.event.MouseEvent;
56 import java.awt.event.MouseAdapter;
57 import java.awt.event.ActionListener;
58 import java.awt.event.ActionEvent;
59 import java.awt.event.KeyEvent;
60 import java.awt.event.AWTEventListener;
61 import java.awt.geom.Rectangle2D;
62 import java.awt.geom.Line2D;
63 import java.awt.geom.Area;
64 import java.awt.geom.RoundRectangle2D;
65 import java.io.IOException;
66 import java.io.File;
67 import java.net.URL;
68 import java.util.List;
69 import java.util.ArrayList;
70 import java.util.Arrays;
71 
72 class ImageEditorPanel extends JPanel {
73     private static final String EXTENSION_9PATCH = ".9.png";
74     private static final int DEFAULT_ZOOM = 8;
75     private static final float DEFAULT_SCALE = 2.0f;
76 
77     // For stretch regions and padding
78     private static final int BLACK_TICK = 0xFF000000;
79     // For Layout Bounds
80     private static final int RED_TICK = 0xFFFF0000;
81 
82     private String name;
83     private BufferedImage image;
84     private boolean is9Patch;
85 
86     private ImageViewer viewer;
87     private StretchesViewer stretchesViewer;
88     private JLabel xLabel;
89     private JLabel yLabel;
90 
91     private TexturePaint texture;
92 
93     private List<Rectangle> patches;
94     private List<Rectangle> horizontalPatches;
95     private List<Rectangle> verticalPatches;
96     private List<Rectangle> fixed;
97     private boolean verticalStartWithPatch;
98     private boolean horizontalStartWithPatch;
99 
100     private Pair<Integer> horizontalPadding;
101     private Pair<Integer> verticalPadding;
102 
ImageEditorPanel(MainFrame mainFrame, BufferedImage image, String name)103     ImageEditorPanel(MainFrame mainFrame, BufferedImage image, String name) {
104         this.image = image;
105         this.name = name;
106 
107         setTransferHandler(new ImageTransferHandler(mainFrame));
108 
109         checkImage();
110 
111         setOpaque(false);
112         setLayout(new BorderLayout());
113 
114         loadSupport();
115         buildImageViewer();
116         buildStatusPanel();
117     }
118 
loadSupport()119     private void loadSupport() {
120         try {
121             URL resource = getClass().getResource("/images/checker.png");
122             BufferedImage checker = GraphicsUtilities.loadCompatibleImage(resource);
123             texture = new TexturePaint(checker, new Rectangle2D.Double(0, 0,
124                     checker.getWidth(), checker.getHeight()));
125         } catch (IOException e) {
126             e.printStackTrace();
127         }
128     }
129 
buildImageViewer()130     private void buildImageViewer() {
131         viewer = new ImageViewer();
132 
133         JSplitPane splitter = new JSplitPane();
134         splitter.setContinuousLayout(true);
135         splitter.setResizeWeight(0.8);
136         splitter.setBorder(null);
137 
138         JScrollPane scroller = new JScrollPane(viewer);
139         scroller.setOpaque(false);
140         scroller.setBorder(null);
141         scroller.getViewport().setBorder(null);
142         scroller.getViewport().setOpaque(false);
143 
144         splitter.setLeftComponent(scroller);
145         splitter.setRightComponent(buildStretchesViewer());
146 
147         add(splitter);
148     }
149 
buildStretchesViewer()150     private JComponent buildStretchesViewer() {
151         stretchesViewer = new StretchesViewer();
152         JScrollPane scroller = new JScrollPane(stretchesViewer);
153         scroller.setBorder(null);
154         scroller.getViewport().setBorder(null);
155         scroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
156         scroller.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
157         return scroller;
158     }
159 
buildStatusPanel()160     private void buildStatusPanel() {
161         JPanel status = new JPanel(new GridBagLayout());
162         status.setOpaque(false);
163 
164         JLabel label = new JLabel();
165         label.setForeground(Color.WHITE);
166         label.setText("Zoom: ");
167         label.putClientProperty("JComponent.sizeVariant", "small");
168         status.add(label, new GridBagConstraints(0, 0, 1, 1, 0.0f, 0.0f,
169                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
170                 new Insets(0, 6, 0, 0), 0, 0));
171 
172         label = new JLabel();
173         label.setForeground(Color.WHITE);
174         label.setText("100%");
175         label.putClientProperty("JComponent.sizeVariant", "small");
176         status.add(label, new GridBagConstraints(1, 0, 1, 1, 0.0f, 0.0f,
177                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
178                 new Insets(0, 0, 0, 0), 0, 0));
179 
180         JSlider zoomSlider = new JSlider(1, 16, DEFAULT_ZOOM);
181         zoomSlider.setSnapToTicks(true);
182         zoomSlider.putClientProperty("JComponent.sizeVariant", "small");
183         zoomSlider.addChangeListener(new ChangeListener() {
184             public void stateChanged(ChangeEvent evt) {
185                 viewer.setZoom(((JSlider) evt.getSource()).getValue());
186             }
187         });
188         status.add(zoomSlider, new GridBagConstraints(2, 0, 1, 1, 0.0f, 0.0f,
189                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
190                 new Insets(0, 0, 0, 0), 0, 0));
191 
192         JLabel maxZoomLabel = new JLabel();
193         maxZoomLabel.setForeground(Color.WHITE);
194         maxZoomLabel.putClientProperty("JComponent.sizeVariant", "small");
195         maxZoomLabel.setText("800%");
196         status.add(maxZoomLabel, new GridBagConstraints(3, 0, 1, 1, 0.0f, 0.0f,
197                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
198                 new Insets(0, 0, 0, 0), 0, 0));
199 
200         label = new JLabel();
201         label.setForeground(Color.WHITE);
202         label.setText("Patch scale: ");
203         label.putClientProperty("JComponent.sizeVariant", "small");
204         status.add(label, new GridBagConstraints(0, 1, 1, 1, 0.0f, 0.0f,
205                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
206                 new Insets(0, 6, 0, 0), 0, 0));
207 
208         label = new JLabel();
209         label.setForeground(Color.WHITE);
210         label.setText("2x");
211         label.putClientProperty("JComponent.sizeVariant", "small");
212         status.add(label, new GridBagConstraints(1, 1, 1, 1, 0.0f, 0.0f,
213                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
214                 new Insets(0, 0, 0, 0), 0, 0));
215 
216         zoomSlider = new JSlider(200, 600, (int) (DEFAULT_SCALE * 100.0f));
217         zoomSlider.setSnapToTicks(true);
218         zoomSlider.putClientProperty("JComponent.sizeVariant", "small");
219         zoomSlider.addChangeListener(new ChangeListener() {
220             public void stateChanged(ChangeEvent evt) {
221                 stretchesViewer.setScale(((JSlider) evt.getSource()).getValue() / 100.0f);
222             }
223         });
224         status.add(zoomSlider, new GridBagConstraints(2, 1, 1, 1, 0.0f, 0.0f,
225                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
226                 new Insets(0, 0, 0, 0), 0, 0));
227 
228         maxZoomLabel = new JLabel();
229         maxZoomLabel.setForeground(Color.WHITE);
230         maxZoomLabel.putClientProperty("JComponent.sizeVariant", "small");
231         maxZoomLabel.setText("6x");
232         status.add(maxZoomLabel, new GridBagConstraints(3, 1, 1, 1, 0.0f, 0.0f,
233                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
234                 new Insets(0, 0, 0, 0), 0, 0));
235 
236         JCheckBox showLock = new JCheckBox("Show lock");
237         showLock.setOpaque(false);
238         showLock.setForeground(Color.WHITE);
239         showLock.setSelected(true);
240         showLock.putClientProperty("JComponent.sizeVariant", "small");
241         showLock.addActionListener(new ActionListener() {
242             public void actionPerformed(ActionEvent event) {
243                 viewer.setLockVisible(((JCheckBox) event.getSource()).isSelected());
244             }
245         });
246         status.add(showLock, new GridBagConstraints(4, 0, 1, 1, 0.0f, 0.0f,
247                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
248                 new Insets(0, 12, 0, 0), 0, 0));
249 
250         JCheckBox showPatches = new JCheckBox("Show patches");
251         showPatches.setOpaque(false);
252         showPatches.setForeground(Color.WHITE);
253         showPatches.putClientProperty("JComponent.sizeVariant", "small");
254         showPatches.addActionListener(new ActionListener() {
255             public void actionPerformed(ActionEvent event) {
256                 viewer.setPatchesVisible(((JCheckBox) event.getSource()).isSelected());
257             }
258         });
259         status.add(showPatches, new GridBagConstraints(4, 1, 1, 1, 0.0f, 0.0f,
260                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
261                 new Insets(0, 12, 0, 0), 0, 0));
262 
263         JCheckBox showPadding = new JCheckBox("Show content");
264         showPadding.setOpaque(false);
265         showPadding.setForeground(Color.WHITE);
266         showPadding.putClientProperty("JComponent.sizeVariant", "small");
267         showPadding.addActionListener(new ActionListener() {
268             public void actionPerformed(ActionEvent event) {
269                 stretchesViewer.setPaddingVisible(((JCheckBox) event.getSource()).isSelected());
270             }
271         });
272         status.add(showPadding, new GridBagConstraints(5, 0, 1, 1, 0.0f, 0.0f,
273                 GridBagConstraints.LINE_START, GridBagConstraints.NONE,
274                 new Insets(0, 12, 0, 0), 0, 0));
275 
276         status.add(Box.createHorizontalGlue(), new GridBagConstraints(6, 0, 1, 1, 1.0f, 1.0f,
277                 GridBagConstraints.LINE_START, GridBagConstraints.BOTH,
278                 new Insets(0, 0, 0, 0), 0, 0));
279 
280         label = new JLabel("X: ");
281         label.setForeground(Color.WHITE);
282         label.putClientProperty("JComponent.sizeVariant", "small");
283         status.add(label, new GridBagConstraints(7, 0, 1, 1, 0.0f, 0.0f,
284                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
285                 new Insets(0, 0, 0, 0), 0, 0));
286 
287         xLabel = new JLabel("0px");
288         xLabel.setForeground(Color.WHITE);
289         xLabel.putClientProperty("JComponent.sizeVariant", "small");
290         status.add(xLabel, new GridBagConstraints(8, 0, 1, 1, 0.0f, 0.0f,
291                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
292                 new Insets(0, 0, 0, 6), 0, 0));
293 
294         label = new JLabel("Y: ");
295         label.setForeground(Color.WHITE);
296         label.putClientProperty("JComponent.sizeVariant", "small");
297         status.add(label, new GridBagConstraints(7, 1, 1, 1, 0.0f, 0.0f,
298                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
299                 new Insets(0, 0, 0, 0), 0, 0));
300 
301         yLabel = new JLabel("0px");
302         yLabel.setForeground(Color.WHITE);
303         yLabel.putClientProperty("JComponent.sizeVariant", "small");
304         status.add(yLabel, new GridBagConstraints(8, 1, 1, 1, 0.0f, 0.0f,
305                 GridBagConstraints.LINE_END, GridBagConstraints.NONE,
306                 new Insets(0, 0, 0, 6), 0, 0));
307 
308         add(status, BorderLayout.SOUTH);
309     }
310 
checkImage()311     private void checkImage() {
312         is9Patch = name.endsWith(EXTENSION_9PATCH);
313         if (!is9Patch) {
314             convertTo9Patch();
315         } else {
316             ensure9Patch();
317         }
318     }
319 
ensure9Patch()320     private void ensure9Patch() {
321         int width = image.getWidth();
322         int height = image.getHeight();
323         for (int i = 0; i < width; i++) {
324             int pixel = image.getRGB(i, 0);
325             if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
326                 image.setRGB(i, 0, 0);
327             }
328             pixel = image.getRGB(i, height - 1);
329             if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
330                 image.setRGB(i, height - 1, 0);
331             }
332         }
333         for (int i = 0; i < height; i++) {
334             int pixel = image.getRGB(0, i);
335             if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
336                 image.setRGB(0, i, 0);
337             }
338             pixel = image.getRGB(width - 1, i);
339             if (pixel != 0 && pixel != BLACK_TICK && pixel != RED_TICK) {
340                 image.setRGB(width - 1, i, 0);
341             }
342         }
343     }
344 
convertTo9Patch()345     private void convertTo9Patch() {
346         BufferedImage buffer = GraphicsUtilities.createTranslucentCompatibleImage(
347                 image.getWidth() + 2, image.getHeight() + 2);
348 
349         Graphics2D g2 = buffer.createGraphics();
350         g2.drawImage(image, 1, 1, null);
351         g2.dispose();
352 
353         image = buffer;
354         name = name.substring(0, name.lastIndexOf('.')) + ".9.png";
355     }
356 
chooseSaveFile()357     File chooseSaveFile() {
358         if (is9Patch) {
359             return new File(name);
360         } else {
361             JFileChooser chooser = new JFileChooser();
362             chooser.setFileFilter(new PngFileFilter());
363             int choice = chooser.showSaveDialog(this);
364             if (choice == JFileChooser.APPROVE_OPTION) {
365                 File file = chooser.getSelectedFile();
366                 if (!file.getAbsolutePath().endsWith(EXTENSION_9PATCH)) {
367                     String path = file.getAbsolutePath();
368                     if (path.endsWith(".png")) {
369                         path = path.substring(0, path.lastIndexOf(".png")) + EXTENSION_9PATCH;
370                     } else {
371                         path = path + EXTENSION_9PATCH;
372                     }
373                     name = path;
374                     is9Patch = true;
375                     return new File(path);
376                 }
377                 is9Patch = true;
378                 return file;
379             }
380         }
381         return null;
382     }
383 
getImage()384     RenderedImage getImage() {
385         return image;
386     }
387 
388     private class StretchesViewer extends JPanel {
389         private static final int MARGIN = 24;
390 
391         private StretchView horizontal;
392         private StretchView vertical;
393         private StretchView both;
394 
395         private Dimension size;
396 
397         private float horizontalPatchesSum;
398         private float verticalPatchesSum;
399 
400         private boolean showPadding;
401 
StretchesViewer()402         StretchesViewer() {
403             setOpaque(false);
404             setLayout(new GridBagLayout());
405             setBorder(BorderFactory.createEmptyBorder(MARGIN, MARGIN, MARGIN, MARGIN));
406 
407             horizontal = new StretchView();
408             vertical = new StretchView();
409             both = new StretchView();
410 
411             setScale(DEFAULT_SCALE);
412 
413             add(vertical, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
414                     GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
415             add(horizontal, new GridBagConstraints(0, 1, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
416                     GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
417             add(both, new GridBagConstraints(0, 2, 1, 1, 1.0, 1.0, GridBagConstraints.CENTER,
418                     GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0));
419         }
420 
421         @Override
paintComponent(Graphics g)422         protected void paintComponent(Graphics g) {
423             Graphics2D g2 = (Graphics2D) g.create();
424             g2.setPaint(texture);
425             g2.fillRect(0, 0, getWidth(), getHeight());
426             g2.dispose();
427         }
428 
setScale(float scale)429         void setScale(float scale) {
430             int patchWidth = image.getWidth() - 2;
431             int patchHeight = image.getHeight() - 2;
432 
433             int scaledWidth = (int) (patchWidth * scale);
434             int scaledHeight = (int) (patchHeight * scale);
435 
436             horizontal.scaledWidth = scaledWidth;
437             vertical.scaledHeight = scaledHeight;
438             both.scaledWidth = scaledWidth;
439             both.scaledHeight = scaledHeight;
440 
441             size = new Dimension(scaledWidth, scaledHeight);
442 
443             computePatches();
444         }
445 
computePatches()446         void computePatches() {
447             boolean measuredWidth = false;
448             boolean endRow = true;
449 
450             int remainderHorizontal = 0;
451             int remainderVertical = 0;
452 
453             if (fixed.size() > 0) {
454                 int start = fixed.get(0).y;
455                 for (Rectangle rect : fixed) {
456                     if (rect.y > start) {
457                         endRow = true;
458                         measuredWidth = true;
459                     }
460                     if (!measuredWidth) {
461                         remainderHorizontal += rect.width;
462                     }
463                     if (endRow) {
464                         remainderVertical += rect.height;
465                         endRow = false;
466                         start = rect.y;
467                     }
468                 }
469             }
470 
471             horizontal.remainderHorizontal = horizontal.scaledWidth - remainderHorizontal;
472             vertical.remainderHorizontal = vertical.scaledWidth - remainderHorizontal;
473             both.remainderHorizontal = both.scaledWidth - remainderHorizontal;
474 
475             horizontal.remainderVertical = horizontal.scaledHeight - remainderVertical;
476             vertical.remainderVertical = vertical.scaledHeight - remainderVertical;
477             both.remainderVertical = both.scaledHeight - remainderVertical;
478 
479             horizontalPatchesSum = 0;
480             if (horizontalPatches.size() > 0) {
481                 int start = -1;
482                 for (Rectangle rect : horizontalPatches) {
483                     if (rect.x > start) {
484                         horizontalPatchesSum += rect.width;
485                         start = rect.x;
486                     }
487                 }
488             } else {
489                 int start = -1;
490                 for (Rectangle rect : patches) {
491                     if (rect.x > start) {
492                         horizontalPatchesSum += rect.width;
493                         start = rect.x;
494                     }
495                 }
496             }
497 
498             verticalPatchesSum = 0;
499             if (verticalPatches.size() > 0) {
500                 int start = -1;
501                 for (Rectangle rect : verticalPatches) {
502                     if (rect.y > start) {
503                         verticalPatchesSum += rect.height;
504                         start = rect.y;
505                     }
506                 }
507             } else {
508                 int start = -1;
509                 for (Rectangle rect : patches) {
510                     if (rect.y > start) {
511                         verticalPatchesSum += rect.height;
512                         start = rect.y;
513                     }
514                 }
515             }
516 
517             setSize(size);
518             ImageEditorPanel.this.validate();
519             repaint();
520         }
521 
setPaddingVisible(boolean visible)522         void setPaddingVisible(boolean visible) {
523             showPadding = visible;
524             repaint();
525         }
526 
527         private class StretchView extends JComponent {
528             private final Color PADDING_COLOR = new Color(0.37f, 0.37f, 1.0f, 0.5f);
529 
530             int scaledWidth;
531             int scaledHeight;
532 
533             int remainderHorizontal;
534             int remainderVertical;
535 
StretchView()536             StretchView() {
537                 scaledWidth = image.getWidth();
538                 scaledHeight = image.getHeight();
539             }
540 
541             @Override
paintComponent(Graphics g)542             protected void paintComponent(Graphics g) {
543                 int x = (getWidth() - scaledWidth) / 2;
544                 int y = (getHeight() - scaledHeight) / 2;
545 
546                 Graphics2D g2 = (Graphics2D) g.create();
547                 g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
548                         RenderingHints.VALUE_INTERPOLATION_BILINEAR);
549                 g.translate(x, y);
550 
551                 x = 0;
552                 y = 0;
553 
554                 if (patches.size() == 0) {
555                     g.drawImage(image, 0, 0, scaledWidth, scaledHeight, null);
556                     g2.dispose();
557                     return;
558                 }
559 
560                 int fixedIndex = 0;
561                 int horizontalIndex = 0;
562                 int verticalIndex = 0;
563                 int patchIndex = 0;
564 
565                 boolean hStretch;
566                 boolean vStretch;
567 
568                 float vWeightSum = 1.0f;
569                 float vRemainder = remainderVertical;
570 
571                 vStretch = verticalStartWithPatch;
572                 while (y < scaledHeight - 1) {
573                     hStretch = horizontalStartWithPatch;
574 
575                     int height = 0;
576                     float vExtra = 0.0f;
577 
578                     float hWeightSum = 1.0f;
579                     float hRemainder = remainderHorizontal;
580 
581                     while (x < scaledWidth - 1) {
582                         Rectangle r;
583                         if (!vStretch) {
584                             if (hStretch) {
585                                 r = horizontalPatches.get(horizontalIndex++);
586                                 float extra = r.width / horizontalPatchesSum;
587                                 int width = (int) (extra * hRemainder / hWeightSum);
588                                 hWeightSum -= extra;
589                                 hRemainder -= width;
590                                 g.drawImage(image, x, y, x + width, y + r.height, r.x, r.y,
591                                         r.x + r.width, r.y + r.height, null);
592                                 x += width;
593                             } else {
594                                 r = fixed.get(fixedIndex++);
595                                 g.drawImage(image, x, y, x + r.width, y + r.height, r.x, r.y,
596                                         r.x + r.width, r.y + r.height, null);
597                                 x += r.width;
598                             }
599                             height = r.height;
600                         } else {
601                             if (hStretch) {
602                                 r = patches.get(patchIndex++);
603                                 vExtra = r.height / verticalPatchesSum;
604                                 height = (int) (vExtra * vRemainder / vWeightSum);
605                                 float extra = r.width / horizontalPatchesSum;
606                                 int width = (int) (extra * hRemainder / hWeightSum);
607                                 hWeightSum -= extra;
608                                 hRemainder -= width;
609                                 g.drawImage(image, x, y, x + width, y + height, r.x, r.y,
610                                         r.x + r.width, r.y + r.height, null);
611                                 x += width;
612                             } else {
613                                 r = verticalPatches.get(verticalIndex++);
614                                 vExtra = r.height / verticalPatchesSum;
615                                 height = (int) (vExtra * vRemainder / vWeightSum);
616                                 g.drawImage(image, x, y, x + r.width, y + height, r.x, r.y,
617                                         r.x + r.width, r.y + r.height, null);
618                                 x += r.width;
619                             }
620 
621                         }
622                         hStretch = !hStretch;
623                     }
624                     x = 0;
625                     y += height;
626                     if (vStretch) {
627                         vWeightSum -= vExtra;
628                         vRemainder -= height;
629                     }
630                     vStretch = !vStretch;
631                 }
632 
633                 if (showPadding) {
634                     g.setColor(PADDING_COLOR);
635                     g.fillRect(horizontalPadding.first, verticalPadding.first,
636                             scaledWidth - horizontalPadding.first - horizontalPadding.second,
637                             scaledHeight - verticalPadding.first - verticalPadding.second);
638                 }
639 
640                 g2.dispose();
641             }
642 
643             @Override
getPreferredSize()644             public Dimension getPreferredSize() {
645                 return size;
646             }
647         }
648     }
649 
650     private class ImageViewer extends JComponent {
651         private final Color CORRUPTED_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.7f);
652         private final Color LOCK_COLOR = new Color(0.0f, 0.0f, 0.0f, 0.7f);
653         private final Color STRIPES_COLOR = new Color(1.0f, 0.0f, 0.0f, 0.5f);
654         private final Color BACK_COLOR = new Color(0xc0c0c0);
655         private final Color HELP_COLOR = new Color(0xffffe1);
656         private final Color PATCH_COLOR = new Color(1.0f, 0.37f, 0.99f, 0.5f);
657         private final Color PATCH_ONEWAY_COLOR = new Color(0.37f, 1.0f, 0.37f, 0.5f);
658 
659         private static final float STRIPES_WIDTH = 4.0f;
660         private static final double STRIPES_SPACING = 6.0;
661         private static final int STRIPES_ANGLE = 45;
662 
663         private int zoom = DEFAULT_ZOOM;
664         private boolean showPatches;
665         private boolean showLock = true;
666 
667         private final Dimension size;
668 
669         private boolean locked;
670 
671         private int[] row;
672         private int[] column;
673 
674         private int lastPositionX;
675         private int lastPositionY;
676         private int currentButton;
677         private boolean showCursor;
678 
679         private JLabel helpLabel;
680         private boolean eraseMode;
681 
682         private JButton checkButton;
683         private List<Rectangle> corruptedPatches;
684         private boolean showBadPatches;
685 
686         private JPanel helpPanel;
687 
ImageViewer()688         ImageViewer() {
689             setLayout(new GridBagLayout());
690             helpPanel = new JPanel(new BorderLayout());
691             helpPanel.setBorder(new EmptyBorder(0, 6, 0, 6));
692             helpPanel.setBackground(HELP_COLOR);
693             helpLabel = new JLabel("Press Shift to erase pixels."
694                     + " Press Control to draw layout bounds");
695             helpLabel.putClientProperty("JComponent.sizeVariant", "small");
696             helpPanel.add(helpLabel, BorderLayout.WEST);
697             checkButton = new JButton("Show bad patches");
698             checkButton.putClientProperty("JComponent.sizeVariant", "small");
699             checkButton.putClientProperty("JButton.buttonType", "roundRect");
700             helpPanel.add(checkButton, BorderLayout.EAST);
701 
702             add(helpPanel, new GridBagConstraints(0, 0, 1, 1,
703                     1.0f, 1.0f, GridBagConstraints.FIRST_LINE_START, GridBagConstraints.HORIZONTAL,
704                     new Insets(0, 0, 0, 0), 0, 0));
705 
706             setOpaque(true);
707 
708             // Exact size will be set by setZoom() in AncestorListener#ancestorMoved.
709             size = new Dimension(0, 0);
710 
711             addAncestorListener(new AncestorListener() {
712                 @Override
713                 public void ancestorRemoved(AncestorEvent event) {
714                 }
715                 @Override
716                 public void ancestorMoved(AncestorEvent event) {
717                     // Set exactly size.
718                     viewer.setZoom(DEFAULT_ZOOM);
719                     viewer.removeAncestorListener(this);
720                 }
721                 @Override
722                 public void ancestorAdded(AncestorEvent event) {
723                 }
724             });
725 
726             findPatches();
727 
728             addMouseListener(new MouseAdapter() {
729                 @Override
730                 public void mousePressed(MouseEvent event) {
731                     // Store the button here instead of retrieving it again in MouseDragged
732                     // below, because on linux, calling MouseEvent.getButton() for the drag
733                     // event returns 0, which appears to be technically correct (no button
734                     // changed state).
735                     currentButton = event.isShiftDown() ? MouseEvent.BUTTON3 : event.getButton();
736                     currentButton = event.isControlDown() ? MouseEvent.BUTTON2 : currentButton;
737                     paint(event.getX(), event.getY(), currentButton);
738                 }
739             });
740             addMouseMotionListener(new MouseMotionAdapter() {
741                 @Override
742                 public void mouseDragged(MouseEvent event) {
743                     if (!checkLockedRegion(event.getX(), event.getY())) {
744                         // use the stored button, see note above
745                         paint(event.getX(), event.getY(),  currentButton);
746                     }
747                 }
748 
749                 @Override
750                 public void mouseMoved(MouseEvent event) {
751                     checkLockedRegion(event.getX(), event.getY());
752                 }
753             });
754             Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() {
755                 public void eventDispatched(AWTEvent event) {
756                     enableEraseMode((KeyEvent) event);
757                 }
758             }, AWTEvent.KEY_EVENT_MASK);
759 
760             checkButton.addActionListener(new ActionListener() {
761                 public void actionPerformed(ActionEvent event) {
762                     if (!showBadPatches) {
763                         findBadPatches();
764                         checkButton.setText("Hide bad patches");
765                     } else {
766                         checkButton.setText("Show bad patches");
767                         corruptedPatches = null;
768                     }
769                     repaint();
770                     showBadPatches = !showBadPatches;
771                 }
772             });
773         }
774 
findBadPatches()775         private void findBadPatches() {
776             corruptedPatches = new ArrayList<Rectangle>();
777 
778             for (Rectangle patch : patches) {
779                 if (corruptPatch(patch)) {
780                     corruptedPatches.add(patch);
781                 }
782             }
783 
784             for (Rectangle patch : horizontalPatches) {
785                 if (corruptHorizontalPatch(patch)) {
786                     corruptedPatches.add(patch);
787                 }
788             }
789 
790             for (Rectangle patch : verticalPatches) {
791                 if (corruptVerticalPatch(patch)) {
792                     corruptedPatches.add(patch);
793                 }
794             }
795         }
796 
corruptPatch(Rectangle patch)797         private boolean corruptPatch(Rectangle patch) {
798             int[] pixels = GraphicsUtilities.getPixels(image, patch.x, patch.y,
799                     patch.width, patch.height, null);
800 
801             if (pixels.length > 0) {
802                 int reference = pixels[0];
803                 for (int pixel : pixels) {
804                     if (pixel != reference) {
805                         return true;
806                     }
807                 }
808             }
809 
810             return false;
811         }
812 
corruptHorizontalPatch(Rectangle patch)813         private boolean corruptHorizontalPatch(Rectangle patch) {
814             int[] reference = new int[patch.height];
815             int[] column = new int[patch.height];
816             reference = GraphicsUtilities.getPixels(image, patch.x, patch.y,
817                     1, patch.height, reference);
818 
819             for (int i = 1; i < patch.width; i++) {
820                 column = GraphicsUtilities.getPixels(image, patch.x + i, patch.y,
821                         1, patch.height, column);
822                 if (!Arrays.equals(reference, column)) {
823                     return true;
824                 }
825             }
826 
827             return false;
828         }
829 
corruptVerticalPatch(Rectangle patch)830         private boolean corruptVerticalPatch(Rectangle patch) {
831             int[] reference = new int[patch.width];
832             int[] row = new int[patch.width];
833             reference = GraphicsUtilities.getPixels(image, patch.x, patch.y,
834                     patch.width, 1, reference);
835 
836             for (int i = 1; i < patch.height; i++) {
837                 row = GraphicsUtilities.getPixels(image, patch.x, patch.y + i, patch.width, 1, row);
838                 if (!Arrays.equals(reference, row)) {
839                     return true;
840                 }
841             }
842 
843             return false;
844         }
845 
enableEraseMode(KeyEvent event)846         private void enableEraseMode(KeyEvent event) {
847             boolean oldEraseMode = eraseMode;
848             eraseMode = event.isShiftDown();
849             if (eraseMode != oldEraseMode) {
850                 if (eraseMode) {
851                     helpLabel.setText("Release Shift to draw pixels");
852                 } else {
853                     helpLabel.setText("Press Shift to erase pixels."
854                                       + " Press Control to draw layout bounds");
855                 }
856             }
857         }
858 
paint(int x, int y, int button)859         private void paint(int x, int y, int button) {
860             int color;
861             switch (button) {
862                 case MouseEvent.BUTTON1:
863                     color = BLACK_TICK;
864                     break;
865                 case MouseEvent.BUTTON2:
866                     color = RED_TICK;
867                     break;
868                 case MouseEvent.BUTTON3:
869                     color = 0;
870                     break;
871                 default:
872                     return;
873             }
874 
875             int left = (getWidth() - size.width) / 2;
876             int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
877 
878             x = (x - left) / zoom;
879             y = (y - top) / zoom;
880 
881             int width = image.getWidth();
882             int height = image.getHeight();
883             if (((x == 0 || x == width - 1) && (y > 0 && y < height - 1)) ||
884                     ((x > 0 && x < width - 1) && (y == 0 || y == height - 1))) {
885                 image.setRGB(x, y, color);
886                 findPatches();
887                 stretchesViewer.computePatches();
888                 if (showBadPatches) {
889                     findBadPatches();
890                 }
891                 repaint();
892             }
893         }
894 
checkLockedRegion(int x, int y)895         private boolean checkLockedRegion(int x, int y) {
896             int oldX = lastPositionX;
897             int oldY = lastPositionY;
898             lastPositionX = x;
899             lastPositionY = y;
900 
901             int left = (getWidth() - size.width) / 2;
902             int top = helpPanel.getHeight() + (getHeight() - size.height) / 2;
903 
904             x = (x - left) / zoom;
905             y = (y - top) / zoom;
906 
907             int width = image.getWidth();
908             int height = image.getHeight();
909 
910             xLabel.setText(Math.max(0, Math.min(x, width - 1)) + " px");
911             yLabel.setText(Math.max(0, Math.min(y, height - 1)) + " px");
912 
913             boolean previousLock = locked;
914             locked = x > 0 && x < width - 1 && y > 0 && y < height - 1;
915 
916             boolean previousCursor = showCursor;
917             showCursor = ((x == 0 || x == width - 1) && (y > 0 && y < height - 1)) ||
918                     ((x > 0 && x < width - 1) && (y == 0 || y == height - 1));
919 
920             if (locked != previousLock) {
921                 repaint();
922             } else if (showCursor || (showCursor != previousCursor)) {
923                 Rectangle clip = new Rectangle(lastPositionX - 1 - zoom / 2,
924                         lastPositionY - 1 - zoom / 2, zoom + 2, zoom + 2);
925                 clip = clip.union(new Rectangle(oldX - 1 - zoom / 2,
926                         oldY - 1 - zoom / 2, zoom + 2, zoom + 2));
927                 repaint(clip);
928             }
929 
930             return locked;
931         }
932 
933         @Override
934         protected void paintComponent(Graphics g) {
935             int x = (getWidth() - size.width) / 2;
936             int y = helpPanel.getHeight() + (getHeight() - size.height) / 2;
937 
938             Graphics2D g2 = (Graphics2D) g.create();
939             g2.setColor(BACK_COLOR);
940             g2.fillRect(0, 0, getWidth(), getHeight());
941 
942             g2.translate(x, y);
943             g2.setPaint(texture);
944             g2.fillRect(0, 0, size.width, size.height);
945             g2.scale(zoom, zoom);
946             g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
947                     RenderingHints.VALUE_ANTIALIAS_ON);
948             g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
949                     RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR);
950             g2.drawImage(image, 0, 0, null);
951 
952             if (showPatches) {
953                 g2.setColor(PATCH_COLOR);
954                 for (Rectangle patch : patches) {
955                     g2.fillRect(patch.x, patch.y, patch.width, patch.height);
956                 }
957                 g2.setColor(PATCH_ONEWAY_COLOR);
958                 for (Rectangle patch : horizontalPatches) {
959                     g2.fillRect(patch.x, patch.y, patch.width, patch.height);
960                 }
961                 for (Rectangle patch : verticalPatches) {
962                     g2.fillRect(patch.x, patch.y, patch.width, patch.height);
963                 }
964             }
965 
966             if (corruptedPatches != null) {
967                 g2.setColor(CORRUPTED_COLOR);
968                 g2.setStroke(new BasicStroke(3.0f / zoom));
969                 for (Rectangle patch : corruptedPatches) {
970                     g2.draw(new RoundRectangle2D.Float(patch.x - 2.0f / zoom, patch.y - 2.0f / zoom,
971                             patch.width + 2.0f / zoom, patch.height + 2.0f / zoom,
972                             6.0f / zoom, 6.0f / zoom));
973                 }
974             }
975 
976             if (showLock && locked) {
977                 int width = image.getWidth();
978                 int height = image.getHeight();
979 
980                 g2.setColor(LOCK_COLOR);
981                 g2.fillRect(1, 1, width - 2, height - 2);
982 
983                 g2.setColor(STRIPES_COLOR);
984                 g2.translate(1, 1);
985                 paintStripes(g2, width - 2, height - 2);
986                 g2.translate(-1, -1);
987             }
988 
989             g2.dispose();
990 
991             if (showCursor) {
992                 Graphics cursor = g.create();
993                 cursor.setXORMode(Color.WHITE);
994                 cursor.setColor(Color.BLACK);
995                 cursor.drawRect(lastPositionX - zoom / 2, lastPositionY - zoom / 2, zoom, zoom);
996                 cursor.dispose();
997             }
998         }
999 
1000         private void paintStripes(Graphics2D g, int width, int height) {
1001             //draws pinstripes at the angle specified in this class
1002             //and at the given distance apart
1003             Shape oldClip = g.getClip();
1004             Area area = new Area(new Rectangle(0, 0, width, height));
1005             if(oldClip != null) {
1006                 area = new Area(oldClip);
1007             }
1008             area.intersect(new Area(new Rectangle(0,0,width,height)));
1009             g.setClip(area);
1010 
1011             g.setStroke(new BasicStroke(STRIPES_WIDTH));
1012 
1013             double hypLength = Math.sqrt((width * width) +
1014                     (height * height));
1015 
1016             double radians = Math.toRadians(STRIPES_ANGLE);
1017             g.rotate(radians);
1018 
1019             double spacing = STRIPES_SPACING;
1020             spacing += STRIPES_WIDTH;
1021             int numLines = (int)(hypLength / spacing);
1022 
1023             for (int i=0; i<numLines; i++) {
1024                 double x = i * spacing;
1025                 Line2D line = new Line2D.Double(x, -hypLength, x, hypLength);
1026                 g.draw(line);
1027             }
1028             g.setClip(oldClip);
1029         }
1030 
1031         @Override
1032         public Dimension getPreferredSize() {
1033             return size;
1034         }
1035 
1036         void setZoom(int value) {
1037             int width = image.getWidth();
1038             int height = image.getHeight();
1039 
1040             zoom = value;
1041             if (size.height == 0 || (getHeight() - size.height) == 0) {
1042                 size.setSize(width * zoom, height * zoom + helpPanel.getHeight());
1043             } else {
1044                 size.setSize(width * zoom, height * zoom);
1045             }
1046 
1047             if (!size.equals(getSize())) {
1048                 setSize(size);
1049                 ImageEditorPanel.this.validate();
1050                 repaint();
1051             }
1052         }
1053 
1054         void setPatchesVisible(boolean visible) {
1055             showPatches = visible;
1056             findPatches();
1057             repaint();
1058         }
1059 
1060         private void findPatches() {
1061             int width = image.getWidth();
1062             int height = image.getHeight();
1063 
1064             row = GraphicsUtilities.getPixels(image, 0, 0, width, 1, row);
1065             column = GraphicsUtilities.getPixels(image, 0, 0, 1, height, column);
1066 
1067             boolean[] result = new boolean[1];
1068             Pair<List<Pair<Integer>>> left = getPatches(column, result);
1069             verticalStartWithPatch = result[0];
1070 
1071             result = new boolean[1];
1072             Pair<List<Pair<Integer>>> top = getPatches(row, result);
1073             horizontalStartWithPatch = result[0];
1074 
1075             fixed = getRectangles(left.first, top.first);
1076             patches = getRectangles(left.second, top.second);
1077 
1078             if (fixed.size() > 0) {
1079                 horizontalPatches = getRectangles(left.first, top.second);
1080                 verticalPatches = getRectangles(left.second, top.first);
1081             } else {
1082                 if (top.first.size() > 0) {
1083                     horizontalPatches = new ArrayList<Rectangle>(0);
1084                     verticalPatches = getVerticalRectangles(top.first);
1085                 } else if (left.first.size() > 0) {
1086                     horizontalPatches = getHorizontalRectangles(left.first);
1087                     verticalPatches = new ArrayList<Rectangle>(0);
1088                 } else {
1089                     horizontalPatches = verticalPatches = new ArrayList<Rectangle>(0);
1090                 }
1091             }
1092 
1093             row = GraphicsUtilities.getPixels(image, 0, height - 1, width, 1, row);
1094             column = GraphicsUtilities.getPixels(image, width - 1, 0, 1, height, column);
1095 
1096             top = getPatches(row, result);
1097             horizontalPadding = getPadding(top.first);
1098 
1099             left = getPatches(column, result);
1100             verticalPadding = getPadding(left.first);
1101         }
1102 
getVerticalRectangles(List<Pair<Integer>> topPairs)1103         private List<Rectangle> getVerticalRectangles(List<Pair<Integer>> topPairs) {
1104             List<Rectangle> rectangles = new ArrayList<Rectangle>();
1105             for (Pair<Integer> top : topPairs) {
1106                 int x = top.first;
1107                 int width = top.second - top.first;
1108 
1109                 rectangles.add(new Rectangle(x, 1, width, image.getHeight() - 2));
1110             }
1111             return rectangles;
1112         }
1113 
getHorizontalRectangles(List<Pair<Integer>> leftPairs)1114         private List<Rectangle> getHorizontalRectangles(List<Pair<Integer>> leftPairs) {
1115             List<Rectangle> rectangles = new ArrayList<Rectangle>();
1116             for (Pair<Integer> left : leftPairs) {
1117                 int y = left.first;
1118                 int height = left.second - left.first;
1119 
1120                 rectangles.add(new Rectangle(1, y, image.getWidth() - 2, height));
1121             }
1122             return rectangles;
1123         }
1124 
getPadding(List<Pair<Integer>> pairs)1125         private Pair<Integer> getPadding(List<Pair<Integer>> pairs) {
1126             if (pairs.size() == 0) {
1127                 return new Pair<Integer>(0, 0);
1128             } else if (pairs.size() == 1) {
1129                 if (pairs.get(0).first == 1) {
1130                     return new Pair<Integer>(pairs.get(0).second - pairs.get(0).first, 0);
1131                 } else {
1132                     return new Pair<Integer>(0, pairs.get(0).second - pairs.get(0).first);
1133                 }
1134             } else {
1135                 int index = pairs.size() - 1;
1136                 return new Pair<Integer>(pairs.get(0).second - pairs.get(0).first,
1137                         pairs.get(index).second - pairs.get(index).first);
1138             }
1139         }
1140 
getRectangles(List<Pair<Integer>> leftPairs, List<Pair<Integer>> topPairs)1141         private List<Rectangle> getRectangles(List<Pair<Integer>> leftPairs,
1142                 List<Pair<Integer>> topPairs) {
1143             List<Rectangle> rectangles = new ArrayList<Rectangle>();
1144             for (Pair<Integer> left : leftPairs) {
1145                 int y = left.first;
1146                 int height = left.second - left.first;
1147                 for (Pair<Integer> top : topPairs) {
1148                     int x = top.first;
1149                     int width = top.second - top.first;
1150 
1151                     rectangles.add(new Rectangle(x, y, width, height));
1152                 }
1153             }
1154             return rectangles;
1155         }
1156 
getPatches(int[] pixels, boolean[] startWithPatch)1157         private Pair<List<Pair<Integer>>> getPatches(int[] pixels, boolean[] startWithPatch) {
1158             int lastIndex = 1;
1159             int lastPixel = pixels[1];
1160             boolean first = true;
1161 
1162             List<Pair<Integer>> fixed = new ArrayList<Pair<Integer>>();
1163             List<Pair<Integer>> patches = new ArrayList<Pair<Integer>>();
1164 
1165             for (int i = 1; i < pixels.length - 1; i++) {
1166                 int pixel = pixels[i];
1167                 if (pixel != lastPixel) {
1168                     if (lastPixel == BLACK_TICK) {
1169                         if (first) startWithPatch[0] = true;
1170                         patches.add(new Pair<Integer>(lastIndex, i));
1171                     } else {
1172                         fixed.add(new Pair<Integer>(lastIndex, i));
1173                     }
1174                     first = false;
1175 
1176                     lastIndex = i;
1177                     lastPixel = pixel;
1178                 }
1179             }
1180             if (lastPixel == BLACK_TICK) {
1181                 if (first) startWithPatch[0] = true;
1182                 patches.add(new Pair<Integer>(lastIndex, pixels.length - 1));
1183             } else {
1184                 fixed.add(new Pair<Integer>(lastIndex, pixels.length - 1));
1185             }
1186 
1187             if (patches.size() == 0) {
1188                 patches.add(new Pair<Integer>(1, pixels.length - 1));
1189                 startWithPatch[0] = true;
1190                 fixed.clear();
1191             }
1192 
1193             return new Pair<List<Pair<Integer>>>(fixed, patches);
1194         }
1195 
setLockVisible(boolean visible)1196         void setLockVisible(boolean visible) {
1197             showLock = visible;
1198             repaint();
1199         }
1200     }
1201 
1202     static class Pair<E> {
1203         E first;
1204         E second;
1205 
Pair(E first, E second)1206         Pair(E first, E second) {
1207             this.first = first;
1208             this.second = second;
1209         }
1210 
1211         @Override
toString()1212         public String toString() {
1213             return "Pair[" + first + ", " + second + "]";
1214         }
1215     }
1216 }
1217