Merge branch 'subtree'
[fanfix.git] / src / jexer / TImage.java
index 1a9aa9e62e6d5ec2b9ce4c52de3461a39c297c25..b7bfbd00a2ff165becc701575023b42e50b6deed 100644 (file)
@@ -30,23 +30,60 @@ package jexer;
 
 import java.awt.image.BufferedImage;
 
-import jexer.backend.ECMA48Terminal;
-import jexer.backend.MultiScreen;
-import jexer.backend.SwingTerminal;
 import jexer.bits.Cell;
+import jexer.event.TCommandEvent;
 import jexer.event.TKeypressEvent;
 import jexer.event.TMouseEvent;
+import jexer.event.TResizeEvent;
+import static jexer.TCommand.*;
 import static jexer.TKeypress.*;
 
 /**
  * TImage renders a piece of a bitmap image on screen.
  */
-public class TImage extends TWidget {
+public class TImage extends TWidget implements EditMenuUser {
+
+    // ------------------------------------------------------------------------
+    // Constants --------------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Selections for fitting the image to the text cells.
+     */
+    public enum Scale {
+        /**
+         * No scaling.
+         */
+        NONE,
+
+        /**
+         * Stretch/shrink the image in both directions to fully fill the text
+         * area width/height.
+         */
+        STRETCH,
+
+        /**
+         * Scale the image, preserving aspect ratio, to fill the text area
+         * width/height (like letterbox).  The background color for the
+         * letterboxed area is specified in scaleBackColor.
+         */
+        SCALE,
+    }
 
     // ------------------------------------------------------------------------
     // Variables --------------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Scaling strategy to use.
+     */
+    private Scale scale = Scale.NONE;
+
+    /**
+     * Scaling strategy to use.
+     */
+    private java.awt.Color scaleBackColor = java.awt.Color.BLACK;
+
     /**
      * The action to perform when the user clicks on the image.
      */
@@ -72,6 +109,12 @@ public class TImage extends TWidget {
      */
     private int clockwise = 0;
 
+    /**
+     * If true, this widget was resized and a new scaled image must be
+     * produced.
+     */
+    private boolean resized = false;
+
     /**
      * Left column of the image.  0 is the left-most column.
      */
@@ -111,6 +154,25 @@ public class TImage extends TWidget {
     // Constructors -----------------------------------------------------------
     // ------------------------------------------------------------------------
 
+    /**
+     * Public constructor.
+     *
+     * @param parent parent widget
+     * @param x column relative to parent
+     * @param y row relative to parent
+     * @param width number of text cells for width of the image
+     * @param height number of text cells for height of the image
+     * @param image the image to display
+     * @param left left column of the image.  0 is the left-most column.
+     * @param top top row of the image.  0 is the top-most row.
+     */
+    public TImage(final TWidget parent, final int x, final int y,
+        final int width, final int height,
+        final BufferedImage image, final int left, final int top) {
+
+        this(parent, x, y, width, height, image, left, top, null);
+    }
+
     /**
      * Public constructor.
      *
@@ -139,24 +201,12 @@ public class TImage extends TWidget {
         this.clickAction = clickAction;
 
         sizeToImage(true);
-
-        getApplication().addImage(this);
     }
 
     // ------------------------------------------------------------------------
     // Event handlers ---------------------------------------------------------
     // ------------------------------------------------------------------------
 
-    /**
-     * Subclasses should override this method to cleanup resources.  This is
-     * called by TWindow.onClose().
-     */
-    @Override
-    protected void close() {
-        getApplication().removeImage(this);
-        super.close();
-    }
-
     /**
      * Handle mouse press events.
      *
@@ -165,7 +215,7 @@ public class TImage extends TWidget {
     @Override
     public void onMouseDown(final TMouseEvent mouse) {
         if (clickAction != null) {
-            clickAction.DO();
+            clickAction.DO(this);
             return;
         }
     }
@@ -226,10 +276,68 @@ public class TImage extends TWidget {
             return;
         }
 
+        if (keypress.equals(kbShiftLeft)) {
+            switch (scale) {
+            case NONE:
+                setScaleType(Scale.SCALE);
+                return;
+            case STRETCH:
+                setScaleType(Scale.NONE);
+                return;
+            case SCALE:
+                setScaleType(Scale.STRETCH);
+                return;
+            }
+        }
+        if (keypress.equals(kbShiftRight)) {
+            switch (scale) {
+            case NONE:
+                setScaleType(Scale.STRETCH);
+                return;
+            case STRETCH:
+                setScaleType(Scale.SCALE);
+                return;
+            case SCALE:
+                setScaleType(Scale.NONE);
+                return;
+            }
+        }
+
         // Pass to parent for the things we don't care about.
         super.onKeypress(keypress);
     }
 
+    /**
+     * Handle resize events.
+     *
+     * @param event resize event
+     */
+    @Override
+    public void onResize(final TResizeEvent event) {
+        // Get my width/height set correctly.
+        super.onResize(event);
+
+        if (scale == Scale.NONE) {
+            return;
+        }
+        image = null;
+        resized = true;
+    }
+
+    /**
+     * Handle posted command events.
+     *
+     * @param command command event
+     */
+    @Override
+    public void onCommand(final TCommandEvent command) {
+        if (command.equals(cmCopy)) {
+            // Copy image to clipboard.
+            getClipboard().copyImage(image);
+            return;
+        }
+    }
+
     // ------------------------------------------------------------------------
     // TWidget ----------------------------------------------------------------
     // ------------------------------------------------------------------------
@@ -271,37 +379,24 @@ public class TImage extends TWidget {
      * @param always if true, always resize the cells
      */
     private void sizeToImage(final boolean always) {
-        int textWidth = 16;
-        int textHeight = 20;
-
-        if (getScreen() instanceof SwingTerminal) {
-            SwingTerminal terminal = (SwingTerminal) getScreen();
-
-            textWidth = terminal.getTextWidth();
-            textHeight = terminal.getTextHeight();
-        } if (getScreen() instanceof MultiScreen) {
-            MultiScreen terminal = (MultiScreen) getScreen();
-
-            textWidth = terminal.getTextWidth();
-            textHeight = terminal.getTextHeight();
-        } else if (getScreen() instanceof ECMA48Terminal) {
-            ECMA48Terminal terminal = (ECMA48Terminal) getScreen();
-
-            textWidth = terminal.getTextWidth();
-            textHeight = terminal.getTextHeight();
-        }
+        int textWidth = getScreen().getTextWidth();
+        int textHeight = getScreen().getTextHeight();
 
         if (image == null) {
-            image = scaleImage(originalImage, scaleFactor);
-            image = rotateImage(image, clockwise);
+            image = rotateImage(originalImage, clockwise);
+            image = scaleImage(image, scaleFactor, getWidth(), getHeight(),
+                textWidth, textHeight);
         }
 
         if ((always == true) ||
+            (resized == true) ||
             ((textWidth > 0)
                 && (textWidth != lastTextWidth)
                 && (textHeight > 0)
                 && (textHeight != lastTextHeight))
         ) {
+            resized = false;
+
             cellColumns = image.getWidth() / textWidth;
             if (cellColumns * textWidth < image.getWidth()) {
                 cellColumns++;
@@ -327,8 +422,21 @@ public class TImage extends TWidget {
                     }
 
                     Cell cell = new Cell();
-                    cell.setImage(image.getSubimage(x * textWidth,
-                            y * textHeight, width, height));
+                    if ((width != textWidth) || (height != textHeight)) {
+                        BufferedImage newImage;
+                        newImage = new BufferedImage(textWidth, textHeight,
+                            BufferedImage.TYPE_INT_ARGB);
+
+                        java.awt.Graphics gr = newImage.getGraphics();
+                        gr.drawImage(image.getSubimage(x * textWidth,
+                                y * textHeight, width, height),
+                            0, 0, null, null);
+                        gr.dispose();
+                        cell.setImage(newImage);
+                    } else {
+                        cell.setImage(image.getSubimage(x * textWidth,
+                                y * textHeight, width, height));
+                    }
 
                     cells[x][y] = cell;
                 }
@@ -418,30 +526,214 @@ public class TImage extends TWidget {
         return cellColumns;
     }
 
+    /**
+     * Get the raw (unprocessed) image.
+     *
+     * @return the image
+     */
+    public BufferedImage getImage() {
+        return originalImage;
+    }
+
+    /**
+     * Set the raw image, and reprocess to make the visible image.
+     *
+     * @param image the new image
+     */
+    public void setImage(final BufferedImage image) {
+        this.originalImage = image;
+        this.image = null;
+        sizeToImage(true);
+    }
+
+    /**
+     * Get the visible (processed) image.
+     *
+     * @return the image that is currently on screen
+     */
+    public BufferedImage getVisibleImage() {
+        return image;
+    }
+
+    /**
+     * Get the scaling strategy.
+     *
+     * @return Scale.NONE, Scale.STRETCH, etc.
+     */
+    public Scale getScaleType() {
+        return scale;
+    }
+
+    /**
+     * Set the scaling strategy.
+     *
+     * @param scale Scale.NONE, Scale.STRETCH, etc.
+     */
+    public void setScaleType(final Scale scale) {
+        this.scale = scale;
+        this.image = null;
+        sizeToImage(true);
+    }
+
+    /**
+     * Get the scale factor.
+     *
+     * @return the scale factor
+     */
+    public double getScaleFactor() {
+        return scaleFactor;
+    }
+
+    /**
+     * Set the scale factor.  1.0 means no scaling.
+     *
+     * @param scaleFactor the new scale factor
+     */
+    public void setScaleFactor(final double scaleFactor) {
+        this.scaleFactor = scaleFactor;
+        image = null;
+        sizeToImage(true);
+    }
+
+    /**
+     * Get the rotation, as degrees.
+     *
+     * @return the rotation in degrees
+     */
+    public int getRotation() {
+        switch (clockwise) {
+        case 0:
+            return 0;
+        case 1:
+            return 90;
+        case 2:
+            return 180;
+        case 3:
+            return 270;
+        default:
+            // Don't know how this happened, but fix it.
+            clockwise = 0;
+            image = null;
+            sizeToImage(true);
+            return 0;
+        }
+    }
+
+    /**
+     * Set the rotation, as degrees clockwise.
+     *
+     * @param rotation 0, 90, 180, or 270
+     */
+    public void setRotation(final int rotation) {
+        switch (rotation) {
+        case 0:
+            clockwise = 0;
+            break;
+        case 90:
+            clockwise = 1;
+            break;
+        case 180:
+            clockwise = 2;
+            break;
+        case 270:
+            clockwise = 3;
+            break;
+        default:
+            // Don't know how this happened, but fix it.
+            clockwise = 0;
+            break;
+        }
+
+        image = null;
+        sizeToImage(true);
+    }
+
     /**
      * Scale an image by to be scaleFactor size.
      *
      * @param image the image to scale
      * @param factor the scale to make the new image
+     * @param width the number of text cell columns for the destination image
+     * @param height the number of text cell rows for the destination image
+     * @param textWidth the width in pixels for one text cell
+     * @param textHeight the height in pixels for one text cell
      */
     private BufferedImage scaleImage(final BufferedImage image,
-        final double factor) {
+        final double factor, final int width, final int height,
+        final int textWidth, final int textHeight) {
 
-        if (Math.abs(factor - 1.0) < 0.03) {
+        if ((scale == Scale.NONE) && (Math.abs(factor - 1.0) < 0.03)) {
             // If we are within 3% of 1.0, just return the original image.
             return image;
         }
 
-        int width = (int) (image.getWidth() * factor);
-        int height = (int) (image.getHeight() * factor);
+        int destWidth = 0;
+        int destHeight = 0;
+        int x = 0;
+        int y = 0;
+
+        BufferedImage newImage = null;
 
-        BufferedImage newImage = new BufferedImage(width, height,
-            BufferedImage.TYPE_INT_ARGB);
+        switch (scale) {
+        case NONE:
+            destWidth = (int) (image.getWidth() * factor);
+            destHeight = (int) (image.getHeight() * factor);
+            newImage = new BufferedImage(destWidth, destHeight,
+                BufferedImage.TYPE_INT_ARGB);
+            break;
+        case STRETCH:
+            destWidth = width * textWidth;
+            destHeight = height * textHeight;
+            newImage = new BufferedImage(destWidth, destHeight,
+                BufferedImage.TYPE_INT_ARGB);
+            break;
+        case SCALE:
+            double a = (double) image.getWidth() / image.getHeight();
+            double b = (double) (width * textWidth) / (height * textHeight);
+            assert (a > 0);
+            assert (b > 0);
+
+            /*
+            System.err.println("Scale: original " + image.getWidth() +
+                "x" + image.getHeight());
+            System.err.println("         screen " + (width * textWidth) +
+                "x" + (height * textHeight));
+            System.err.println("A " + a + " B " + b);
+             */
+
+            if (a > b) {
+                // Horizontal letterbox
+                destWidth = width * textWidth;
+                destHeight = (int) (destWidth / a);
+                y = ((height * textHeight) - destHeight) / 2;
+                assert (y >= 0);
+                /*
+                System.err.println("Horizontal letterbox: " + destWidth +
+                    "x" + destHeight + ", Y offset " + y);
+                 */
+            } else {
+                // Vertical letterbox
+                destHeight = height * textHeight;
+                destWidth = (int) (destHeight * a);
+                x = ((width * textWidth) - destWidth) / 2;
+                assert (x >= 0);
+                /*
+                System.err.println("Vertical letterbox: " + destWidth +
+                    "x" + destHeight + ", X offset " + x);
+                 */
+            }
+            newImage = new BufferedImage(width * textWidth, height * textHeight,
+                BufferedImage.TYPE_INT_ARGB);
+            break;
+        }
 
         java.awt.Graphics gr = newImage.createGraphics();
-        gr.drawImage(image, 0, 0, width, height, null);
+        if (scale == Scale.SCALE) {
+            gr.setColor(scaleBackColor);
+            gr.fillRect(0, 0, width * textWidth, height * textHeight);
+        }
+        gr.drawImage(image, x, y, destWidth, destHeight, null);
         gr.dispose();
-
         return newImage;
     }
 
@@ -496,4 +788,44 @@ public class TImage extends TWidget {
         return newImage;
     }
 
+    // ------------------------------------------------------------------------
+    // EditMenuUser -----------------------------------------------------------
+    // ------------------------------------------------------------------------
+
+    /**
+     * Check if the cut menu item should be enabled.
+     *
+     * @return true if the cut menu item should be enabled
+     */
+    public boolean isEditMenuCut() {
+        return false;
+    }
+
+    /**
+     * Check if the copy menu item should be enabled.
+     *
+     * @return true if the copy menu item should be enabled
+     */
+    public boolean isEditMenuCopy() {
+        return true;
+    }
+
+    /**
+     * Check if the paste menu item should be enabled.
+     *
+     * @return true if the paste menu item should be enabled
+     */
+    public boolean isEditMenuPaste() {
+        return false;
+    }
+
+    /**
+     * Check if the clear menu item should be enabled.
+     *
+     * @return true if the clear menu item should be enabled
+     */
+    public boolean isEditMenuClear() {
+        return false;
+    }
+
 }