[
Home |
Documentation |
Gallery |
Download |
Demo |
Resources
]
[
Index |
Tutorial |
Sheets |
Pack |
JB |
Hello |
Paint |
MicroIO
]
This page describes a real, complex module; it covers: procedural abstraction, advanced interaction with JBit, advanced handling of JB files, simple pattern matching and state-based user interface design. It assumes you have read the Hello document; the assumptions stated in that document (in particular about the reader being an experienced Java programmer) are still relevant.
This is a work in progress; the code is complete, but the commentary is not available yet.
From the user's point of view, Paint is very limited (e.g. only palette-based images of depth 1 are supported and very limited navigation and editing functionality is provided); this should leave you plenty of room to play with the code. Please understand that, at least in the short term, I will NOT include contributed versions of Paint in the standard JBit distribution; doing so would discourage tinkering by others (the whole point of me writing Paint in the first place). You are of course free to redistribute your own version of Paint; if you do so, please use a different name. The code of Paint is in the public domain.
The first screenshot shows what happens when you activate Paint without a program loaded or if the program has no data pages; an error is shown and the user can only deactivate the module. The second screenshot shows what happens when you activate Paint with the "maze" program loaded; the program is scanned for plausible images that are then listed. The third screenshot shows what happens when you activate Paint with the "bgcol2" program loaded; no images are found in the program. The fourth screenshot shows what happens when the command New is selected; the location (page/offset) and the size (width/height) of the image are asked, the data section of the program is changed accordingly and the image is added to the list of plausible images; if the target area is not filled by 0s the image is not created and an error is shown. The fifth screenshot shows what happens when you select a plausible image; a grid is shown and you can edit the image using the direction keys to move the cursor and the fire key to invert the status of the current pixel.
To get an idea of how Paint is used, you can download Paint.class and pack it into the standard JBit distribution. The module is compatible with both MIDP1 and MIDP2 phones (it works with JBit-20070908 too), but on some of the oldest phones you might exceed the heap available. The problem is not the Paint module itself. The fact is, while some optimization (e.g. in the CPU module) should produce some space saving in the future, the amount of code of the standard JBit is already near the limit. On a very old phone you might need to remove some modules (e.g. Demo, Monitor or Store) if you want to use Paint. As an example, on the Nokia 3410 emulator I removed the Demo module and the sample programs to be able to test Paint.
Start from the JBit source distribution (configured as Standard) and edit build.xml, adding the following module:
<attribute name="JBit-Module-7" value="Paint" unless="jbitrt"/>
Create the file Paint.java in the src directory:
import java.util.Vector;
import javax.microedition.lcdui.*;
public final class Paint extends Canvas implements Module, CommandListener {
}
We will now complete the Paint class.
Copy the adapter/metadata code from the module Hello and change the metadata LABEL to "Paint".
private Object getMetadata(int metadataId) {
switch (metadataId) {
case METADATA_ID_LABEL:
return "Paint";
case METADATA_ID_TYPE:
return new Integer(TYPE_TOP_LEVEL);
case METADATA_ID_SERVICES:
return new String[] {};
default:
return null;
}
}
private Display getDisplay() {
return (Display)jbit.opO(JBitSvc.OP_GET_DISPLAY, 0, null);
}
private void back() {
jbit.opI(JBitSvc.OP_REPLACE_WITH_SERVICE, 0, null);
}
private Module jbit;
public Object opO(int opId, int iArg, Object oArg) {
switch (opId) {
case OP_GET_METADATA:
return getMetadata(iArg);
case OP_GET_DISPLAYABLE:
return getDisplayable();
}
return null;
}
public int opI(int opId, int iArg, Object oArg) {
switch (opId) {
case OP_INIT:
jbit = (Module)oArg;
return init();
case OP_ACTIVATE:
return activate();
case OP_DEACTIVATE:
return deactivate();
}
return -1;
}
public short get(int address) {
return -1;
}
public short put(int address, short value) {
return -1;
}
private byte[][] programFile;
private void attachProgram() {
programFile = (byte[][])jbit.opO(JBitSvc.OP_POOL_GET, 0,
JBitSvc.POOL_ID_PROGRAM_FILE);
}
private void detachProgram() {
programFile = null;
}
private void markProgramAsModified() {
jbit.opI(JBitSvc.OP_POOL_PUT, 0, new Object[] {
JBitSvc.POOL_ID_PROGRAM_MODIFIED, new Boolean(true)
});
}
private boolean programAlreadyMarkedAsModified;
private void attachData() {
attachProgram();
programAlreadyMarkedAsModified = false;
}
private void detachData() {
detachProgram();
}
private void dataHasBeenSaved() {
programAlreadyMarkedAsModified = false;
}
private String addressToTag(int address) {
int page = 3 + (programFile[0][JBFile.OFFSET_CODEPAGES] & 0xFF);
page += address >> 8;
int offset = address & 0xFF;
return page + ":" + offset;
}
private int makeAddress(int page, int offset) {
page -= 3 + (programFile[0][JBFile.OFFSET_CODEPAGES] & 0xFF);
int address = (page << 8) + offset;
if (address < 0 || address > getDataSize())
return -1;
return address;
}
private int getDataSize() {
if (programFile == null)
return 0;
return (programFile[0][JBFile.OFFSET_DATAPAGES] & 0xFF) << 8;
}
private int getFirstDataPage() {
return 3 + (programFile[0][JBFile.OFFSET_CODEPAGES] & 0xFF);
}
private int getLastDataPage() {
return getFirstDataPage() - 1
+ (programFile[0][JBFile.OFFSET_DATAPAGES] & 0xFF);
}
private byte[] getPage(int address, boolean create) {
int programPage = 1 + (address >> 8) +
(programFile[0][JBFile.OFFSET_CODEPAGES] & 0xFF);
byte[] page = programFile[programPage];
if (page == null && create)
page = programFile[programPage] = new byte[256];
return page;
}
private void putData8(int address, int value) {
if (!programAlreadyMarkedAsModified) {
markProgramAsModified();
programAlreadyMarkedAsModified = true;
}
getPage(address, true)[address & 0xFF] = (byte)value;
}
private int getData8(int address) {
byte[] page = getPage(address, false);
if (page == null)
return 0;
return page[address & 0xFF] & 0xFF;
}
private void putData16(int address, int value) {
putData8(address, value & 0xFF);
putData8(address + 1, value >> 8);
}
private int getData16(int address) {
return getData8(address) | (getData8(address + 1) << 8);
}
private static final int WHITE = 0xFFFFFF;
private static final int BLACK = 0x000000;
private static final int RED = 0xC00000;
private static final int MAX_WIDTH = 1024;
private static final int MAX_HEIGHT = 1024;
private static final int PALETTE_SIZE = 3;
private Vector images;
private int lastImage;
private int rowLength;
private int getImageLength(int width, int height) {
return IO.PNG_HEADER_SIZE + PALETTE_SIZE
+ ((width + 7) >> 3) * height;
}
private void scanDataForImages() {
images = new Vector();
lastImage = 0;
int n = getDataSize();
for (int image = 2; image < n - IO.PNG_HEADER_SIZE; image++) {
if (getData8(image) != IO.REQ_IPNGGEN)
continue;
int length = getData16(image - 2);
if (image + length > n)
continue;
int width = getImageWidth(image);
if (width == 0 || width > MAX_WIDTH)
continue;
int height = getImageHeight(image);
if (height == 0 || height > MAX_HEIGHT)
continue;
if (length != getImageLength(width, height))
continue;
images.addElement(new Integer(image));
}
}
private void releaseImages() {
images = null;
}
private int getNumberOfImages() {
return images == null ? 0 : images.size();
}
private int getImageByIndex(int index) {
return ((Integer)images.elementAt(index)).intValue();
}
private int getImageWidth(int image) {
return getData16(image + IO.PNG_HEADER_WIDTH_OFFSET);
}
private int getImageHeight(int image) {
return getData16(image + IO.PNG_HEADER_HEIGHT_OFFSET);
}
private String getImageTag(int image, boolean compact) {
String tag = addressToTag(image - 2);
if (!compact)
tag += " (" + getImageWidth(image)
+ "x" + getImageHeight(image) + ")";
return tag;
}
private void computeRowLength(int image) {
lastImage = image;
rowLength = ((getImageWidth(image) + 7) >> 3);
}
private void putPixel(int image, int x, int y, int color) {
if (lastImage != image)
computeRowLength(image);
int address = image
+ IO.PNG_HEADER_SIZE + PALETTE_SIZE
+ y * rowLength + (x >> 3);
int value = getData8(address);
int mask = 0x80 >> (x & 0x7);
if (color == BLACK)
putData8(address, value | mask);
else
putData8(address, value & (0xFF ^ mask));
}
private int getPixel(int image, int x, int y) {
if (lastImage != image)
computeRowLength(image);
int address = image
+ IO.PNG_HEADER_SIZE + PALETTE_SIZE
+ y * rowLength + (x >> 3);
int value = getData8(address);
int mask = 0x80 >> (x & 0x7);
return (value & mask) != 0 ? BLACK : WHITE;
}
private void makeImage(int page, int offset, int width, int height)
throws Exception {
int address = makeAddress(page, offset);
if (address < 0)
throw new Exception("Starting address is out of range");
int length = getImageLength(width, height);
if (address + length + 2 > getDataSize())
throw new Exception("Image is too big to fit in data");
for (int i = 0; i < length + 2; i++)
if (getData8(address + i) != 0)
throw new Exception("Target area is not empty");
putData16(address, length);
int image = address + 2;
putData8(image, IO.REQ_IPNGGEN);
putData8(image + IO.PNG_HEADER_IMAGEID_OFFSET, 1);
putData16(image + IO.PNG_HEADER_WIDTH_OFFSET, width);
putData16(image + IO.PNG_HEADER_HEIGHT_OFFSET, height);
putData8(image + IO.PNG_HEADER_DEPTH_OFFSET, 1);
putData8(image + IO.PNG_HEADER_COLOR_OFFSET,
IO.VAL_IPNGGEN_CT_INDEXED_COLOR);
putData8(image + IO.PNG_HEADER_FLAGS_OFFSET,
IO.VAL_IPNGGEN_FLAGS_PALREF);
address = image + IO.PNG_HEADER_SIZE;
putData8(address, 1);
putData8(address + 1, IO.COLOR_WHITE);
putData8(address + 2, IO.COLOR_BLACK);
}
private Command backCmd = new Command("Back", Command.BACK, 1);
private Command newCmd = new Command("New", Command.SCREEN, 1);
private Command saveCmd = new Command("Save", Command.SCREEN, 2);
private Command saveAsCmd = new Command("SaveAs", Command.SCREEN, 3);
private Command aboutCmd = new Command("About", Command.SCREEN, 4);
private static final int INACTIVE = 0;
private static final int LIST = 1;
private static final int NEW = 2;
private static final int EDIT = 3;
private static final int ERROR = 4;
private int status;
private Form errorForm;
private List imageList;
private Form newForm;
private Displayable getDisplayable() {
switch (status) {
case ERROR:
return errorForm;
case LIST:
return imageList;
case NEW:
return newForm;
}
return this;
}
private int init() {
if (findSaveSvc() != null) {
addCommand(saveCmd);
addCommand(saveAsCmd);
}
addCommand(aboutCmd);
addCommand(backCmd);
setCommandListener(this);
status = INACTIVE;
return 0;
}
private int activate() {
attachData();
if (getDataSize() == 0)
enterErrorStatus("No data available!");
else
enterListStatus();
return 0;
}
private int deactivate() {
detachData();
releaseImages();
errorForm = null;
imageList = null;
releaseNewForm();
status = INACTIVE;
return 0;
}
public void commandAction(Command c, Displayable d) {
if (d != null && d == newForm) {
handleNewCommands(c);
} else if (c == backCmd) {
if (status == EDIT) {
enterListStatus();
getDisplay().setCurrent(imageList);
} else {
back();
}
} else if (c == newCmd) {
enterNewStatus();
getDisplay().setCurrent(newForm);
} else if (c == saveCmd) {
save(false);
} else if (c == saveAsCmd) {
save(true);
} else if (c == aboutCmd) {
Alert alert = new Alert("Paint 1.0");
alert.setString("Written by Emanuele Fornara");
alert.setTimeout(Alert.FOREVER);
getDisplay().setCurrent(alert);
} else if (c == List.SELECT_COMMAND) {
if (getNumberOfImages() != 0) {
enterEditStatus(getImageByIndex(imageList.getSelectedIndex()));
getDisplay().setCurrent(this);
repaint();
}
} else if (c == saveOkCmd) {
dataHasBeenSaved();
}
}
private void enterErrorStatus(String msg) {
errorForm = new Form("Error");
errorForm.append(msg);
errorForm.addCommand(backCmd);
errorForm.addCommand(aboutCmd);
errorForm.setCommandListener(this);
status = ERROR;
}
private void enterListStatus() {
scanDataForImages();
imageList = new List("Images", List.IMPLICIT);
int n = getNumberOfImages();
if (n == 0) {
imageList.append("- none -", null);
} else {
for (int i = 0; i < n; i++)
imageList.append(getImageTag(getImageByIndex(i), false), null);
}
imageList.addCommand(backCmd);
imageList.addCommand(newCmd);
imageList.addCommand(aboutCmd);
imageList.setCommandListener(this);
status = LIST;
}
private Command okCmd = new Command("OK", Command.OK, 1);
private Command cancelCmd = new Command("Cancel", Command.CANCEL, 1);
private TextField pageItem;
private TextField offsetItem;
private TextField widthItem;
private TextField heightItem;
private void enterNewStatus() {
newForm = new Form("New");
pageItem = new TextField("Page", "", 3, TextField.NUMERIC);
newForm.append(pageItem);
offsetItem = new TextField("Offset", "", 3, TextField.NUMERIC);
newForm.append(offsetItem);
widthItem = new TextField("Width", "", 4, TextField.NUMERIC);
newForm.append(widthItem);
heightItem = new TextField("Height", "", 4, TextField.NUMERIC);
newForm.append(heightItem);
newForm.addCommand(okCmd);
newForm.addCommand(cancelCmd);
newForm.setCommandListener(this);
status = NEW;
}
private void releaseNewForm() {
newForm = null;
pageItem = null;
offsetItem = null;
widthItem = null;
heightItem = null;
}
private void showError(String msg) {
Alert alert = new Alert("New", msg + "!", null, AlertType.ERROR);
alert.setTimeout(Alert.FOREVER);
getDisplay().setCurrent(alert, newForm);
}
private void handleNewCommands(Command c) {
if (c == okCmd) {
try {
int page, offset, width, height;
try {
page = Integer.parseInt(pageItem.getString());
if (page < getFirstDataPage() || page > getLastDataPage())
throw new Throwable();
} catch (Throwable e) {
throw new Exception("Invalid Page [" + getFirstDataPage()
+ "," + getLastDataPage() + "]");
}
try {
offset = Integer.parseInt(offsetItem.getString());
if (offset < 0 || offset > 255)
throw new Throwable();
} catch (Throwable e) {
throw new Exception("Invalid Offset [0,255]");
}
try {
width = Integer.parseInt(widthItem.getString());
if (width <= 0 || width > MAX_WIDTH)
throw new Throwable();
} catch (Throwable e) {
throw new Exception("Invalid Width [1," + MAX_WIDTH + "]");
}
try {
height = Integer.parseInt(heightItem.getString());
if (height <= 0 || height > MAX_HEIGHT)
throw new Throwable();
} catch (Throwable e) {
throw new Exception("Invalid Height [1," + MAX_HEIGHT + "]");
}
makeImage(page, offset, width, height);
} catch (Exception e) {
showError(e.getMessage());
return;
}
}
enterListStatus();
getDisplay().setCurrent(imageList);
}
private Command saveOkCmd = new Command("", Command.SCREEN, 0);
private Command saveFailedCmd = new Command("", Command.SCREEN, 0);
private Module findSaveSvc() {
return (Module)jbit.opO(JBitSvc.OP_FIND_SERVICE, 0, SaveSvc.TAG);
}
private void save(boolean saveAs) {
findSaveSvc().opI(SaveSvc.OP_SAVE, 0, new Object[] {
new Boolean(saveAs),
this,
this,
saveOkCmd,
saveFailedCmd
});
}
private static final int STATUSBAR_SPACE = 10;
private static final int PIXEL_OFFSET = 6;
private static final int PIXEL_SIZE = 5;
private static final int PIXEL_CENTER = 2;
private int currentImage;
private int viewOX;
private int viewOY;
private int viewWidth;
private int viewHeight;
private int cursorX;
private int cursorY;
private boolean compactTag;
private void enterEditStatus(int image) {
currentImage = image;
String s =
getImageTag(image, false) +
getImageWidth(image) +
"," +
getImageHeight(image);
int w = Font.getDefaultFont().stringWidth(s);
int h = Font.getDefaultFont().getHeight();
viewOX = 0;
viewOY = 0;
compactTag = getWidth() - w < STATUSBAR_SPACE;
viewWidth = (getWidth() - 1) / PIXEL_OFFSET;
viewHeight = (getHeight() - h - 1) / PIXEL_OFFSET;
cursorX = 0;
cursorY = 0;
status = EDIT;
}
private void updateView() {
if (cursorX < viewOX)
viewOX = cursorX;
else if (cursorX >= viewOX + viewWidth)
viewOX = cursorX - viewWidth + 1;
if (cursorY < viewOY)
viewOY = cursorY;
else if (cursorY >= viewOY + viewHeight)
viewOY = cursorY - viewHeight + 1;
}
protected void keyPressed(int keyCode) {
if (status != EDIT)
return;
switch (getGameAction(keyCode)) {
case UP:
if (cursorY > 0)
cursorY--;
break;
case DOWN:
if (cursorY < getImageHeight(currentImage) - 1)
cursorY++;
break;
case LEFT:
if (cursorX > 0)
cursorX--;
break;
case RIGHT:
if (cursorX < getImageWidth(currentImage) - 1)
cursorX++;
break;
case FIRE:
putPixel(currentImage, cursorX, cursorY,
getPixel(currentImage, cursorX, cursorY) == BLACK ?
WHITE : BLACK);
break;
default:
return;
}
updateView();
repaint();
}
protected void paint(Graphics g) {
g.setColor(WHITE);
g.fillRect(0, 0, getWidth(), getHeight());
if (status != EDIT)
return;
g.setColor(BLACK);
int width = getImageWidth(currentImage);
int height = getImageHeight(currentImage);
int oy = Font.getDefaultFont().getHeight() + 1;
if (g.getClipY() < oy) {
g.drawString(getImageTag(currentImage, compactTag), 0, 0,
Graphics.TOP | Graphics.LEFT);
g.drawString(cursorX + "," + cursorY, getWidth(), 0,
Graphics.TOP | Graphics.RIGHT);
}
for (int y = viewOY; y < height && (y - viewOY) < viewHeight; y++, oy += PIXEL_OFFSET) {
if (oy + PIXEL_OFFSET < g.getClipY())
continue;
if (oy > g.getClipY() + g.getClipHeight())
continue;
int ox = 1;
for (int x = viewOX; x < width && (x - viewOX) < viewWidth; x++, ox += PIXEL_OFFSET) {
if (ox + PIXEL_OFFSET < g.getClipX())
continue;
if (ox > g.getClipX() + g.getClipWidth())
continue;
if (getPixel(currentImage, x, y) == BLACK)
g.fillRect(ox, oy, PIXEL_SIZE, PIXEL_SIZE);
else
g.drawLine(ox + PIXEL_CENTER, oy + PIXEL_CENTER,
ox + PIXEL_CENTER, oy + PIXEL_CENTER);
if (x == cursorX && y == cursorY) {
boolean isColor = getDisplay().isColor();
if (isColor)
g.setColor(RED);
else
g.setStrokeStyle(Graphics.DOTTED);
g.drawRect(ox - 1, oy - 1, PIXEL_OFFSET, PIXEL_OFFSET);
if (isColor)
g.setColor(BLACK);
else
g.setStrokeStyle(Graphics.SOLID);
}
}
}
}