/** * Copyright (c) 2012-2013 Nokia Corporation. All rights reserved. * Nokia and Nokia Connecting People are registered trademarks of Nokia Corporation. * Oracle and Java are trademarks or registered trademarks of Oracle and/or its * affiliates. Other product and company names mentioned herein may be trademarks * or trade names of their respective owners. * See LICENSE.TXT for license information. */ package com.nokia.example.sudokumaster; import java.util.Timer; import java.util.TimerTask; import javax.microedition.lcdui.Command; import javax.microedition.lcdui.CommandListener; import javax.microedition.lcdui.Displayable; import javax.microedition.lcdui.Font; import javax.microedition.lcdui.Graphics; import javax.microedition.lcdui.Image; import javax.microedition.lcdui.game.GameCanvas; import javax.microedition.rms.RecordStore; import javax.microedition.rms.RecordStoreException; /** * Holds a reference to the Layout to draw all the views. */ public class SudokuCanvas extends GameCanvas implements CommandListener { private static final int LEFT_SOFTKEY = -6; private static final int RIGHT_SOFTKEY = -7; private Main main; private Timer timer; private Layout layout; private ImageView title; private Button exit; private Button back; private Button options; private ImageView backX; private SudokuView sudoku; private StatusView empty; private StatusView moves; private StatusView elapsed; private NumberSelector numberSelector; private VictoryDialog victoryDialog; private OptionsDialog optionsDialog; private Command backCommand; public SudokuCanvas(final Main main) { super(false); setFullScreenMode(true); this.main = main; backCommand = new Command("Back", Command.BACK, 0); addCommand(backCommand); setCommandListener(this); } /** * @see javax.microedition.lcdui.CommandListener#commandAction( * javax.microedition.lcdui.Command, javax.microedition.lcdui.Displayable) */ public void commandAction(Command c, Displayable d) { if (c == backCommand) { if (numberSelector.isVisible() || optionsDialog.isVisible() || victoryDialog.isVisible()) { back(); } else { main.close(); } } } /** * Called when canvas is shown. * @see javax.microedition.lcdui.Canvas#showNotify() */ protected void showNotify() { if (sudoku == null) { generateLayout(); } loadGameState(); updateEmptyAndMoves(); startTimer(); } /** * Called when canvas is hidden. * @see javax.microedition.lcdui.Canvas#hideNotify() */ protected void hideNotify() { stopTimer(); if (!main.isClosed()) { saveGameState(); } } /** * Called when the drawable area of the Canvas has been changed. * @see javax.microedition.lcdui.Canvas#sizeChanged(int, int) * @param w the new width in pixels of the drawable area of the Canvas * @param h the new height in pixels of the drawable area of the Canvas */ protected void sizeChanged(int w, int h) { if (timer != null) { stopTimer(); updateLayout(w, h); startTimer(); } } private void startTimer() { final Graphics g = getGraphics(); if (isSmallScreen(getWidth(), getHeight())) { g.setFont(Font.getFont(Font.FACE_SYSTEM, Font.STYLE_PLAIN, Font.SIZE_SMALL)); } timer = new Timer(); timer.schedule(new TimerTask() { public void run() { render(g); } }, 0, 20); timer.scheduleAtFixedRate(new TimerTask() { public void run() { updateElapsed(); } }, 0, 1000); } private void stopTimer() { timer.cancel(); timer = null; } /** * Saves the current state of the game using RecordStore. * @see javax.microedition.rms.RecordStore */ public void saveGameState() { if (sudoku == null) { return; } try { RecordStore gameState = RecordStore.openRecordStore("GameState", true); if (gameState.getNumRecords() == 0) { gameState.addRecord(null, 0, 0); } byte[] data = sudoku.getState(); gameState.setRecord(1, data, 0, data.length); } catch (RecordStoreException e) { // Empty implementation } } /** * Load the saved state of the game using RecordStore. * @see javax.microedition.rms.RecordStore */ public void loadGameState() { if (sudoku == null) { return; } try { RecordStore gameState = RecordStore.openRecordStore("GameState", true); if (gameState.getNumRecords() == 0) { sudoku.newGame(SudokuGenerator.newPuzzle()); } else { sudoku.setState(gameState.getRecord(1)); if (sudoku.isComplete()) { showVictoryDialog(); } } } catch (RecordStoreException e) { // Empty implementation } } private void render(Graphics g) { if (layout.needsRendering()) { layout.render(g); flushGraphics(); } layout.update(); } private void updateElapsed() { long s = sudoku.getElapsedSeconds(); long m = s / 60; s = s % 60; StringBuffer text = new StringBuffer(5); if (m < 10) { text.append('0'); } text.append(m); text.append(':'); if (s < 10) { text.append('0'); } text.append(s); elapsed.setText(text.toString()); } private void updateEmptyAndMoves() { empty.setText(String.valueOf(sudoku.getEmpty())); moves.setText(String.valueOf(sudoku.getMoves())); } /** * Handle pointer press by redirecting. * @see javax.microedition.lcdui.Canvas#pointerPressed(int, int) * @param x coordinate of press * @param y coordinate of press */ protected void pointerPressed(int x, int y) { handlePointerEvent(View.POINTER_PRESSED, x, y); } /** * Handle pointer drag by redirecting. * @see javax.microedition.lcdui.Canvas#pointerDragged(int, int) * @param x coordinate of press * @param y coordinate of press */ protected void pointerDragged(int x, int y) { handlePointerEvent(View.POINTER_DRAGGED, x, y); } /** * Handle pointer release by redirecting. * @see javax.microedition.lcdui.Canvas#pointerRelease(int, int) * @param x coordinate of press * @param y coordinate of press */ protected void pointerReleased(int x, int y) { handlePointerEvent(View.POINTER_RELEASED, x, y); } /** * Each view handles the pointer event the way it suits them. * @param type pointer press, drag or release * @param x coordinate of event * @param y coordinate of event */ private void handlePointerEvent(int type, int x, int y) { options.handlePointerEvent(type, x, y); if (exit != null && back != null) { exit.handlePointerEvent(type, x, y); back.handlePointerEvent(type, x, y); } if (optionsDialog.isVisible()) { if (backX == null || !backX.handlePointerEvent(type, x, y)) { optionsDialog.handlePointerEvent(type, x, y); } } else if (victoryDialog.isVisible()) { return; } else if (numberSelector.isVisible()) { numberSelector.handlePointerEvent(type, x, y); } else { sudoku.handlePointerEvent(type, x, y); } } /** * Redirect key press event. * @see javax.microedition.lcdui.Canvas#keyPressed(int) * @param i key code */ protected void keyPressed(int i) { handleKeyEvent(View.KEY_PRESSED, i); } /** * Redirect key repeated event. * @see javax.microedition.lcdui.Canvas#keyRepeated(int) * @param i key code */ protected void keyRepeated(int i) { handleKeyEvent(View.KEY_REPEAT, i); } /** * Redirect key released event. * @see javax.microedition.lcdui.Canvas#keyReleased(int) * @param i key code */ protected void keyReleased(int i) { handleKeyEvent(View.KEY_RELEASED, i); } /** * Each view handles the key event the way it suits them. * @param type key press, release or repeated * @param i key code */ private void handleKeyEvent(int type, int i) { if (i == LEFT_SOFTKEY) { options.keyEvent(type); } else if (i == RIGHT_SOFTKEY) { if (exit.isVisible()) { exit.keyEvent(type); } else if (back != null && back.isVisible()) { back.keyEvent(type); } } else if (optionsDialog.isVisible()) { if (getNumber(i) < 0) { optionsDialog.keyEvent(type, getViewKey(i)); } } else if (victoryDialog.isVisible()) { return; } else { int n = getNumber(i); if (n >= 0) { if (type == View.KEY_PRESSED) { sudoku.setNumber(n); } } else if (numberSelector.isVisible()) { numberSelector.keyEvent(type, getViewKey(i)); } else { sudoku.keyEvent(type, getViewKey(i)); } } } public int getNumber(int keyCode) { switch (keyCode) { case KEY_NUM0: return 0; case KEY_NUM1: return 1; case KEY_NUM2: return 2; case KEY_NUM3: return 3; case KEY_NUM4: return 4; case KEY_NUM5: return 5; case KEY_NUM6: return 6; case KEY_NUM7: return 7; case KEY_NUM8: return 8; case KEY_NUM9: return 9; default: return -1; } } public int getViewKey(int keyCode) { switch (getGameAction(keyCode)) { case UP: return View.KEY_UP; case DOWN: return View.KEY_DOWN; case LEFT: return View.KEY_LEFT; case RIGHT: return View.KEY_RIGHT; case FIRE: return View.KEY_SELECT; default: return View.KEY_UNKNOWN; } } private boolean isPortrait(int w, int h) { return w < h; } private boolean isSmallScreen(int w, int h) { final int min = Math.min(w, h); final int max = Math.max(w, h); if (min < 240 || max < 320) { return true; } return false; } private void generateLayout() { final int w = getWidth(); final int h = getHeight(); layout = new Layout(w, h); generateTitle(w, h); generateButtons(); generateBoard(w, h); generateStatusViews(w, h); generateNumberSelector(w, h); generateVictoryDialog(w, h); generateOptionsDialog(w, h); updateLayout(w, h); } private synchronized void updateLayout(int w, int h) { updateButtons(w, h); updateBoard(w, h); updateTitle(w, h); updateStatusViews(w, h); updateNumberSelector(w, h); updateVictoryDialog(w, h); updateOptionsDialog(w, h); layout.setSize(w, h); layout.invalidate(); } private void generateTitle(int w, int h) { title = new ImageView(ImageLoader.loadImage("title.png", isSmallScreen(w, h))); layout.addView(title); } private void updateTitle(int w, int h) { title.setLeft(0); title.setTop(0); title.setWidth(w); title.setHeight(sudoku.top); title.invalidate(); } private int getMinTitleHeight(int w, int h) { return h / 10; } /** * Generates the menu, exit and back buttons. Exit and back buttons are not * created if the phone has a physical back key. */ private void generateButtons() { final Button.Listener buttonListener = new Button.Listener() { public void onClick(Button b) { if (exit != null && b == exit) { main.close(); } else if (back != null && b == back) { back(); } else if (b == options) { showOptionsDialog(); } } }; if (!main.hasOneKeyBack()) { // Exit and back buttons are not needed if the phone has a // physical back button exit = new Button("Exit", buttonListener); layout.addView(exit); back = new Button("Back", buttonListener); back.setVisible(false); layout.addView(back); } options = new Button("Menu", buttonListener); layout.addView(options); } private void back() { if (optionsDialog.isVisible()) { hideOptionsDialog(); } else { hideNumberSelector(); } } private int getButtonsHeight(int w, int h) { return isPortrait(w, h) ? h / 13 : h / 10; } /** * Updates the button sizes and positions based on the given dimensions. * @param w The width of the content area. * @param h The height of the content area. */ private void updateButtons(int w, int h) { final int width = isPortrait(w, h) ? w / 3 : w / 4; final int height = getButtonsHeight(w, h); if (exit != null) { exit.setSize(width, height); exit.setRight(w); exit.setBottom(h); exit.invalidate(); } if (back != null) { back.setSize(width, height); back.setRight(w); back.setBottom(h); back.invalidate(); } options.setSize(width, height); options.setLeft(0); options.setBottom(h); options.invalidate(); } private int getStatusItemHeight(int w, int h) { return h / 10 - 4; } private void generateBoard(int w, int h) { sudoku = new SudokuView(new SudokuView.Listener() { public void onCellSelected() { showNumberSelector(); } public void onSetNumber() { if (numberSelector.isVisible()) { hideNumberSelector(); } updateEmptyAndMoves(); if (sudoku.isComplete()) { showVictoryDialog(); } } }); layout.addView(sudoku); } private void updateBoard(int w, int h) { final int horizontalPadding = 4; Image image = ImageLoader.loadImage("board_tiles.png", isSmallScreen(w, h)); final int maxBoardWidth = isPortrait(w, h) ? w : w * 3 / 4 - horizontalPadding; final int maxBoardHeight = h - getMinTitleHeight(w, h) - getButtonsHeight( w, h) - (isPortrait(w, h) ? getStatusItemHeight(w, h) : 0); final int maxBoardSize = Math.min(maxBoardWidth, maxBoardHeight); int boardSize = image.getHeight() * 9 + 1; if (boardSize > maxBoardSize || boardSize < maxBoardSize) { int newHeight = image.getHeight() * maxBoardSize / boardSize; if (newHeight % 2 == 0) { newHeight -= 1; } if (newHeight != image.getHeight()) { int newWidth = image.getWidth() / image.getHeight() * newHeight; image = ImageLoader.scaleImage(image, newWidth, newHeight); boardSize = image.getHeight() * 9 + 1; } } sudoku.setLeft( isPortrait(w, h) ? (w - boardSize) / 2 : (w - boardSize * 4 / 3) / 2); sudoku.setTop(getMinTitleHeight(w, h) + (maxBoardHeight - boardSize) / 2); sudoku.setBoardSize(boardSize); sudoku.setBoardImage(image); } private void generateStatusViews(int w, int h) { final boolean smallScreen = isSmallScreen(w, h); empty = new StatusView(ImageLoader.loadImage("empty.png", smallScreen)); moves = new StatusView(ImageLoader.loadImage("moves.png", smallScreen)); elapsed = new StatusView(ImageLoader.loadImage("time.png", smallScreen)); layout.addView(empty); layout.addView(moves); layout.addView(elapsed); } private void updateStatusViews(int w, int h) { int width = sudoku.width / 3; int height = getStatusItemHeight(w, h); empty.setSize(width, height); moves.setSize(width, height); elapsed.setSize(width, height); if (isPortrait(w, h)) { empty.setLeft(sudoku.left); moves.setLeft(empty.right); elapsed.setLeft(moves.right); empty.setTop(sudoku.bottom); moves.setTop(sudoku.bottom); elapsed.setTop(sudoku.bottom); } else { int left = sudoku.right + 2; int top = sudoku.top + sudoku.height / 2 - height * 3 / 2; empty.setLeft(left); moves.setLeft(left); elapsed.setLeft(left); empty.setTop(top); moves.setTop(empty.bottom); elapsed.setTop(moves.bottom); } empty.invalidate(); moves.invalidate(); elapsed.invalidate(); } /** * Generate the number selector view, where user can choose the number to be * inserted on the board. * @param w The width of the screen * @param h The height of the screen */ private void generateNumberSelector(int w, int h) { final Image nButtonImage = ImageLoader.loadImage("dial.png", isSmallScreen(w, h)); final Image cButtonImage = ImageLoader.loadImage("dial_c.png", isSmallScreen(w, h)); final int selectorWidth = nButtonImage.getWidth() / 2 * 3; final int selectorHeight = nButtonImage.getHeight() * 3 + cButtonImage.getHeight(); numberSelector = new NumberSelector(selectorWidth, selectorHeight, nButtonImage, cButtonImage, new NumberSelector.Listener() { public void numberSelected(int n) { sudoku.setNumber(n); } }); numberSelector.setVisible(false); layout.addView(numberSelector); } private void updateNumberSelector(int w, int h) { numberSelector.setLeft((w - numberSelector.width) / 2); numberSelector.setTop( sudoku.top + (sudoku.height - numberSelector.height) / 2); numberSelector.invalidate(); } private void generateVictoryDialog(int w, int h) { victoryDialog = new VictoryDialog( ImageLoader.loadImage("victory.png", isSmallScreen(w, h))); victoryDialog.setVisible(false); layout.addView(victoryDialog); } private void updateVictoryDialog(int w, int h) { victoryDialog.setLeft((w - victoryDialog.width) / 2); victoryDialog.setTop( sudoku.top + (sudoku.height - victoryDialog.height) / 2); victoryDialog.invalidate(); } /** * Generate the options dialog. * @param w width of the screen * @param h height of the screen */ private void generateOptionsDialog(int w, int h) { optionsDialog = new OptionsDialog( ImageLoader.loadImage("panel.png", isSmallScreen(w, h)), new OptionsDialog.Listener() { public void onItemClick(int itemIndex) { hideOptionsDialog(); switch (itemIndex) { case 0: sudoku.restart(); hideVictoryDialog(); updateEmptyAndMoves(); updateElapsed(); break; case 1: sudoku.newGame(SudokuGenerator.newPuzzle()); hideVictoryDialog(); updateEmptyAndMoves(); updateElapsed(); break; case 2: main.close(); break; default: break; } } }); optionsDialog.setVisible(false); layout.addView(optionsDialog); if (!main.hasOneKeyBack()) { backX = new ImageView( ImageLoader.loadImage("close.png", isSmallScreen(w, h)), new ImageView.Listener() { public void onClick() { back(); } }); backX.setVisible(false); layout.addView(backX); } } private void updateOptionsDialog(int w, int h) { optionsDialog.setLeft((w - optionsDialog.width) / 2); optionsDialog.setTop( sudoku.top + (sudoku.height - optionsDialog.height) / 2); optionsDialog.invalidate(); if (backX != null) { backX.setLeft(optionsDialog.right - backX.width / 2); backX.setTop(optionsDialog.top - backX.height / 2); backX.invalidate(); } } private void refreshBackButton() { if (exit == null || back == null) { return; } if (numberSelector.isVisible() || optionsDialog.isVisible()) { exit.setVisible(false); back.setVisible(true); } else { back.setVisible(false); exit.setVisible(true); } } private void showNumberSelector() { numberSelector.setVisible(true); if (!hasPointerEvents()) { numberSelector.setKeyPressed(5); } refreshBackButton(); } private void hideNumberSelector() { numberSelector.setVisible(false); refreshBackButton(); } private void showVictoryDialog() { if (numberSelector.isVisible()) { hideNumberSelector(); } victoryDialog.setVisible(true); victoryDialog.moves = sudoku.getMoves(); victoryDialog.seconds = sudoku.getElapsedSeconds(); } private void hideVictoryDialog() { victoryDialog.setVisible(false); } private void showOptionsDialog() { if (numberSelector.isVisible()) { hideNumberSelector(); } optionsDialog.setVisible(true); if (!hasPointerEvents()) { optionsDialog.highlightItem(0); } else if (backX != null) { backX.setVisible(true); } refreshBackButton(); } private void hideOptionsDialog() { optionsDialog.setVisible(false); if (backX != null) { backX.setVisible(false); } refreshBackButton(); } }