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