Improved ScreenshotAppState

I made an improved version of the ScreenshotAppState. Instead of the app name and a shot index being used to name the files, the app name and the current time is used. Also, when two images have the same name, a suffix similar to what windows does is added to the file.

When trying to take screenshots in the game I am currently making, it would always annoy me when screenshots would overwrite each other, so I decided to improve it so that doesn’t happen. Tell me what you think!

package org.mseal.jme3.states;

import com.jme3.app.Application;
import com.jme3.app.state.AbstractAppState;
import com.jme3.app.state.AppStateManager;
import com.jme3.input.InputManager;
import com.jme3.input.KeyInput;
import com.jme3.input.controls.ActionListener;
import com.jme3.input.controls.KeyTrigger;
import com.jme3.post.SceneProcessor;
import com.jme3.renderer.Camera;
import com.jme3.renderer.RenderManager;
import com.jme3.renderer.Renderer;
import com.jme3.renderer.ViewPort;
import com.jme3.renderer.queue.RenderQueue;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeSystem;
import com.jme3.texture.FrameBuffer;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.lwjgl.BufferUtils;

/**
 *
 * @author Matthew Seal
 */
public class ScreenshotAppState extends AbstractAppState implements SceneProcessor {

    private File dir;
    private boolean capture = false;
    private String appName;
    private String imgFormat = "png";
    //render
    private Renderer renderer;
    private RenderManager rm;
    private int width, height;
    private ByteBuffer outBuf;

    /**
     * Set the current path for saving screenshots
     *
     * @param dir the file directory
     */
    public void setPath(File dir) {
        this.dir = dir;
    }

    public File getPath() {
        return dir;
    }

    /**
     * Set the current image format and extension, without the period
     *
     * @param format a string
     */
    public void setImageFormat(String format) {
        imgFormat = format;
    }

    public String getImageFormat() {
        return imgFormat;
    }

    /**
     * Set the current app name used when naming saved images
     *
     * @param name the name to use
     */
    public void setAppName(String name) {
        appName = name;
    }

    public String getAppName() {
        return appName;
    }
    private final ActionListener listener = new ActionListener() {
        public void onAction(String name, boolean isPressed, float tpf) {
            if (isPressed) {
                takeScreenshot();
            }
        }
    };

    @Override
    public void initialize(AppStateManager stateManager, Application app) {
        super.initialize(stateManager, app);
        if (appName == null) {
            AppSettings settings = app.getContext().getSettings();
            appName = settings.getTitle();
        }
        InputManager inputManager = app.getInputManager();
        inputManager.addMapping("takescreenshot", new KeyTrigger(KeyInput.KEY_SYSRQ));
        inputManager.addListener(listener, "takescreenshot");
        List<ViewPort> vps = app.getRenderManager().getPostViews();
        ViewPort last = vps.get(vps.size() - 1);
        last.addProcessor(this);
    }

    @Override
    public void update(float tpf) {
    }

    @Override
    public void cleanup() {
        super.cleanup();
    }

    public void takeScreenshot() {
        capture = true;
    }

    public void initialize(RenderManager rm, ViewPort vp) {
        this.rm = rm;
        this.renderer = rm.getRenderer();
        Camera cam = vp.getCamera();
        reshape(vp, cam.getWidth(), cam.getHeight());
    }

    @Override
    public boolean isInitialized() {
        return super.isInitialized() && renderer != null;
    }

    public void reshape(ViewPort vp, int w, int h) {
        outBuf = BufferUtils.createByteBuffer(w * h * 4);
        width = w;
        height = h;
    }

    public void preFrame(float tpf) {
    }

    public void postQueue(RenderQueue rq) {
    }

    public void postFrame(FrameBuffer out) {
        if (capture) {
            capture = false;

            Camera cam = rm.getCurrentCamera();
            int viewX = (int) (cam.getViewPortLeft() * cam.getWidth());
            int viewY = (int) (cam.getViewPortBottom() * cam.getHeight());
            int viewW = (int) (cam.getViewPortRight() - cam.getViewPortLeft())
                    * cam.getWidth();
            int viewH = (int) (cam.getViewPortTop() - cam.getViewPortBottom())
                    * cam.getHeight();

            renderer.setViewPort(0, 0, width, height);
            renderer.readFrameBuffer(out, outBuf);
            renderer.setViewPort(viewX, viewY, viewW, viewH);

            File file;
            String time = getTime();
            if (dir == null) {
                file = new File(appName + "-" + time + "." + imgFormat);
            } else {
                file = new File(dir + File.separator + appName
                        + "-" + time + "." + imgFormat);
            }
            if (file.exists()) {
                file = nextFile(file);
            }
            file.getParentFile().mkdirs();
            try {
                OutputStream outStream = new FileOutputStream(file);
                JmeSystem.writeImageFile(outStream, imgFormat, outBuf, width, height);
                outStream.close();
            } catch (IOException ex) {
                Logger.getLogger(ScreenshotAppState.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }

    File nextFile(File f) {
        File file = f;
        int i = 1;
        while (file.exists()) {
            file = new File(f.getParent() + File.separator
                    + filename(f) + " (" + i++ + ")." + imgFormat);
        }
        return file;
    }

    String filename(File f) {
        String name = f.getName();
        int index = name.lastIndexOf(".");
        if (index != -1) {
            return name.substring(0, index);
        } else {
            return name;
        }
    }

    String getTime() {
        Date date = new Date();
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss");
        return dateFormat.format(date);
    }
}

In trunk, (don’t know about 3.0) the screen shot app state already has this constructor:
ScreenshotAppState(String filePath, long shotIndex)

…if you pass System.currentTimeMillis() as the second parameter then you won’t overwrite screen shots. It has the added benefit that a particular sessions screen shots are all nicely grouped together.

In 3.0 that constructor doesn’t exist so I didn’t notice that

Well, that’s the other thing… if you are going to submit patches then you should probably do it against the latest code.

Ok, I will remember that for next time. Thanks