package gui; // GUI imports import javax.swing.JPanel; import java.awt.GridLayout; // Event & action imports import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; // Processing & backend imports import gui.backend.Cell; import gui.backend.Settings; import gui.backend.SudokuChecker; /** * The Board class represents the Sudoku board in the GUI. * * It contains all other related GUI components, and is the main JPanel for * the board. Accessible information from the Board includes: * - the current state of the board, including filled values and notes; * * TODO: Future features: * - the current difficulty; * - the current time since the puzzle was started; * - the current hints available; and * - the current number of mistakes. */ public class Board extends JPanel { private Settings s; private Cell[][] grid; private Cell[][] solvedGrid; private CellGUI[][] gridGUI; private SudokuChecker sc; private CellGUI selected; /** * Create a new Board object. * * Board requires a Settings object for styling, sizing, and for different * toggleable features. * * A 2D Cell array is required to abstract the data away from the Board, * and the GUI in general. * * @param s * @param grid */ public Board(Settings s, Cell[][] grid) { super(new GridLayout(9, 9)); this.s = s; this.grid = grid; solvedGrid = new SudokuChecker(Cell.copyGrid(grid)).getSolution(); style(); createBoard(); } /** * Create a new Board object. * * Board requires a Settings object for styling, sizing, and for different * toggleable features. * * A 2D Cell array is required to abstract the data away from the Board, * and the GUI in general. * * A SudokuChecker is required if Settings.getAutoCheckValues() is true. * * @param s * @param grid * @param sc */ public Board(Settings s, Cell[][] grid, SudokuChecker sc) { super(new GridLayout(9, 9)); this.s = s; this.grid = grid; this.sc = sc; solvedGrid = new SudokuChecker(Cell.copyGrid(grid)).getSolution(); style(); createBoard(); } /** * Get the grid of the Board. * * @return */ public Cell[][] getGrid() { return grid; } /** * Set the grid of the Board to the given grid. * * Intended to be used when loading a new puzzle. * * @param grid */ public void setGrid(Cell[][] grid) { this.grid = grid; gridGUI = null; selected = null; removeAll(); createBoard(); repaint(); revalidate(); } /** * Set up the Board Panel with the appropriate styling. */ private void style() { setBackground(s.getTheme().getPrimaryBackground()); setForeground(s.getTheme().getPrimaryText()); } /** * Select the given cell and highlight all cells in the same row, column, * and box. * * Intended to be used in conjunction with a MouseListener. * * @param cell */ private void select(Cell cell) { // Select the CellGUI objects in the same row and column, but not in // the same box. for(int n = 0; n < 9; n++) { int row = cell.getRow(); int col = cell.getCol(); boolean colInSameBox = (gridGUI[n][col].cell.getBox() == cell.getBox()); boolean rowInSameBox = (gridGUI[row][n].cell.getBox() == cell.getBox()); // Handling edge cases where the column or row are in the box. if(colInSameBox && !rowInSameBox) { gridGUI[row][n].select(); continue; } else if(!colInSameBox && rowInSameBox) { gridGUI[n][col].select(); continue; // Skip the cell if it is in the same box. } else if(colInSameBox && rowInSameBox) continue; gridGUI[n][col].select(); gridGUI[row][n].select(); } // Select every cell in the box. int boxRow = cell.getRow() / 3; int boxCol = cell.getCol() / 3; for(int n = 0; n < 3; n++) { for(int m = 0; m < 3; m++) { int row = boxRow * 3 + n; int col = boxCol * 3 + m; // Skip the user-selected cell for clearer highlighting. if(row == cell.getRow() && col == cell.getCol()) continue; gridGUI[row][col].select(); } } } /** * Create the Board Panel with the appropriate cells. * * Every CellGUI object is created with the appropriate Cell object and * Settings object. The CellGUI objects are then added to the Board Panel. * * Each CellGUI object is also given a MouseListener to handle user input, * and a KeyListener to handle keyboard input if/when the cell is selected. */ private void createBoard() { if(s.getAutoFillNotes()) grid = sc.getPossibleValues(grid); gridGUI = new CellGUI[9][9]; for(int i = 0; i < 9; i++) { for(int j = 0; j < 9; j++) { // Create a new CellGUI object with the appropriate Cell object. CellGUI cell = new CellGUI(grid[i][j], s); // Add a MouseListener to the cell. cell.addMouseListener(createMouseListener(cell)); // Add the CellGUI object to the Board Panel. gridGUI[i][j] = cell; add(gridGUI[i][j]); } } } /** * Create a MouseListener for CellGUI objects. * * This MouseListener listens for mouse clicks, and selects the cell * if it is clicked. If the cell is already selected, it is deselected. * * Additionally, adds hover highlighting for the cell. * * mouseClicked() is implemented for cell selection functionality. * mouseEntered() and mouseExited() are implemented for hover functionality. * Other methods are not implemented. * * @param cell * @return MouseListener */ private MouseListener createMouseListener(CellGUI cell) { return new MouseListener() { @Override public void mouseClicked(MouseEvent e) { // Left click if(e.getButton() == 1) { // Deselect the already selected cell. if(selected != null) select(selected.cell); // Deselect the cell if it is already selected. if(selected == cell) { selected = null; return; } // Select the cell and add a KeyListener for input. select(cell.cell); selected = cell; selected.requestFocusInWindow(); selected.addKeyListener(createKeyListener()); // Right click } else if(e.getButton() == 3) { cell.setNoteMode(); return; } } @Override public void mousePressed(MouseEvent e) {} @Override public void mouseReleased(MouseEvent e) {} @Override public void mouseEntered(MouseEvent e) { if(selected == null) cell.select(); } @Override public void mouseExited(MouseEvent e) { if(selected == null) cell.select(); } }; } /** * Create a KeyListener for CellGUI objects. * * This KeyListener listens for key releases, and updates the * selected cell with additional possible values or a new value. * * Only keyReleased is implemented, as keyTyped does not allow for * certain features regarding key codes. * * @return KeyListener */ private KeyListener createKeyListener() { return new KeyListener() { @Override public void keyTyped(KeyEvent e) {} @Override public void keyPressed(KeyEvent e) {} @Override public void keyReleased(KeyEvent e) { // If a CellGUI is not selected, do nothing. if(selected == null) return; char key = e.getKeyChar(); Cell c = selected.cell; // If the Cell is an initial value read from the starting file // then skip it. if(c.isInitValue()) return; // Backspace support for removing values. if(e.getKeyCode() == KeyEvent.VK_BACK_SPACE) { selected.removeValue(); selected.repaint(); selected.revalidate(); return; } // Only allow digits to be entered. if(!Character.isDigit(key)) return; // Enter the value from the keyboard. if(!selected.isInNotesMode()) { // Set the value of the cell and check if it is correct. if(s.getAutoCheckValues()) { int row = c.getRow(); int col = c.getCol(); int expected = solvedGrid[row][col].getValue(); selected.setValue(Character.getNumericValue(key), expected); // Set the value of the cell. } else selected.setValue(Character.getNumericValue(key)); // Add the possible value to the cell. } else selected.addPossibleValue(Character.getNumericValue(key)); selected.repaint(); selected.revalidate(); } }; } }