diff --git a/src/App.java b/src/App.java index 9ef280a..594cbba 100644 --- a/src/App.java +++ b/src/App.java @@ -3,13 +3,15 @@ import javax.swing.BoxLayout; import javax.swing.JFrame; import javax.swing.UIManager; import java.awt.BorderLayout; -import java.awt.event.KeyEvent; -import java.awt.event.KeyListener; // Project imports import gui.*; import gui.backend.*; +// Unused imports +//import java.awt.event.KeyEvent; +//import java.awt.event.KeyListener; + /** * Runner class for the Sudoku App, managing the GUI and coordinating * internal logic. diff --git a/src/gui/Board.java b/src/gui/Board.java index bd2522f..6376136 100644 --- a/src/gui/Board.java +++ b/src/gui/Board.java @@ -1,17 +1,16 @@ package gui; -import java.awt.Dimension; +// GUI imports +import javax.swing.JPanel; import java.awt.GridLayout; -import java.awt.LayoutManager; -import java.awt.event.KeyAdapter; + +// Event & action imports import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.MouseEvent; import java.awt.event.MouseListener; -import javax.swing.JPanel; -import javax.swing.Box; - +// Processing & backend imports import gui.backend.Cell; import gui.backend.Settings; import gui.backend.SudokuChecker; @@ -22,6 +21,8 @@ import gui.backend.SudokuChecker; * 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 @@ -36,9 +37,41 @@ public class Board extends JPanel { private CellGUI selected; /** - * Create a new Board object with the default styling from settings. + * 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)); @@ -51,48 +84,6 @@ public class Board extends JPanel { createBoard(); } - private KeyListener createKeyListener() { - return new KeyListener() { - @Override - public void keyTyped(KeyEvent e) {} - - @Override - public void keyPressed(KeyEvent e) {} - - @Override - public void keyReleased(KeyEvent e) { - char key = e.getKeyChar(); - if(selected.cell.isInitValue()) return; - - // Backspace key code is 0 - if(e.getKeyCode() == e.VK_BACK_SPACE) { - selected.removeValue(); - selected.repaint(); - selected.revalidate(); - return; - } - - if(!Character.isDigit(key)) return; - - if(!selected.isInNotesMode()) { - if(s.getAutoCheckValues()) { - int row = selected.cell.getRow(); - int col = selected.cell.getCol(); - int expected = solvedGrid[row][col].getValue(); - selected.setValue(Character.getNumericValue(key), expected); - } else { - selected.setValue(Character.getNumericValue(key)); - } - } else { - selected.addPossibleValue(Character.getNumericValue(key)); - } - - selected.repaint(); - selected.revalidate(); - } - }; - } - /** * Get the grid of the Board. * @@ -137,6 +128,8 @@ public class Board extends JPanel { * @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(); @@ -148,18 +141,22 @@ public class Board extends JPanel { (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++) { @@ -177,6 +174,12 @@ public class Board extends JPanel { /** * 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); @@ -184,50 +187,137 @@ public class Board extends JPanel { for(int i = 0; i < 9; i++) { for(int j = 0; j < 9; j++) { - CellGUI cell = new CellGUI( - grid[i][j], - s - ); - cell.addMouseListener(new MouseListener() { - @Override - public void mouseClicked(MouseEvent e) { - // Left click - if(e.getButton() == 1) { - if(selected != null) select(selected.cell); - - // Deselect the cell if it is already selected. - if(selected == cell) { - selected = null; - return; - } + // Create a new CellGUI object with the appropriate Cell object. + CellGUI cell = new CellGUI(grid[i][j], s); - select(cell.cell); - selected = cell; - selected.requestFocusInWindow(); - selected.addKeyListener(createKeyListener()); - } 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) { - cell.select(); - } - @Override - public void mouseExited(MouseEvent e) { - cell.select(); - } - - }); + // 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(); + } + }; + } } \ No newline at end of file diff --git a/src/gui/CellGUI.java b/src/gui/CellGUI.java index 7f7e39e..62c765d 100644 --- a/src/gui/CellGUI.java +++ b/src/gui/CellGUI.java @@ -1,14 +1,14 @@ package gui; -import java.awt.BorderLayout; +// GUI imports +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.border.LineBorder; import java.awt.Dimension; import java.awt.Font; import java.awt.GridLayout; -import javax.swing.JLabel; -import javax.swing.JPanel; -import javax.swing.border.LineBorder; - +// Processing & backend imports import gui.backend.Cell; import gui.backend.Settings; import gui.backend.Theme; @@ -18,20 +18,22 @@ import gui.backend.Theme; */ class CellGUI extends JPanel { // GUI fields - private JPanel internalPanel = new JPanel(new BorderLayout()); - private GridLayout noteLayout = new GridLayout(3, 3); - private GridLayout valueLayout = new GridLayout(0, 1); - private Dimension size; - private JLabel valueLabel; - private Note[] notesLabels = new Note[9]; + private GridLayout noteLayout = new GridLayout(3, 3); + private GridLayout valueLayout = new GridLayout(0, 1); + private JPanel internalPanel = new JPanel(valueLayout); + private JLabel valueLabel; + private Label[] notesLabels = new Label[9]; + + // GUI data fields read from a Settings object. + private Dimension size; private Theme theme; private Font font; - private boolean incorrect; // Data fields Cell cell; private boolean noteMode; // true to display notes, or display value private boolean selected = false; // true if the cell is selected + private boolean incorrect = false; // true if the cell has an invalid value /** * Create a new CellGUI object with the given single cell. @@ -52,9 +54,10 @@ class CellGUI extends JPanel { selected = false; size = s.getCellDimensions(); + // Set the layout and size of the cell. setFocusable(true); setLayout(valueLayout); - valueLabel = new Note("", theme, font); + valueLabel = new Label("", theme, font); setSize(size); internalPanel.setSize(size); defaultStyle(); @@ -71,25 +74,6 @@ class CellGUI extends JPanel { add(internalPanel); } - /** - * Get the size of the cell. - */ - public Dimension getSize() { - return size; - } - - /** - * Set the size of the cell. - */ - public void setSize(Dimension size) { - this.size = size; - super.setSize(size); - internalPanel.setSize(size); - valueLabel.setSize(size); - for(int i = 0; i < 9; i++) - if(notesLabels[i] != null) notesLabels[i].setSize(size); - } - /** * Get the value of the underlying Cell object. * @@ -105,11 +89,12 @@ class CellGUI extends JPanel { * @param value */ public void setValue(int value) { + if(!selected) return; + cell.setValue(value); // Update the GUI with the actual Cell value (unchanged if invalid) valueLabel.setText(Integer.toString(cell.getValue())); - internalPanel.repaint(); - internalPanel.revalidate(); + refresh(); } /** @@ -128,10 +113,7 @@ class CellGUI extends JPanel { if(value != expected) { incorrect = true; errorStyle(); - } - - internalPanel.repaint(); - internalPanel.revalidate(); + } else refresh(); } /** @@ -146,9 +128,6 @@ class CellGUI extends JPanel { incorrect = false; highlightedStyle(); - - internalPanel.repaint(); - internalPanel.revalidate(); } /** @@ -204,10 +183,7 @@ class CellGUI extends JPanel { generateNotes(true); } - internalPanel.repaint(); - internalPanel.revalidate(); - repaint(); - revalidate(); + refresh(); noteMode = !noteMode; } @@ -219,8 +195,6 @@ class CellGUI extends JPanel { selected = !selected; if(incorrect) { errorStyle(); - repaint(); - revalidate(); return; } @@ -228,13 +202,12 @@ class CellGUI extends JPanel { defaultStyle(); else highlightedStyle(); - - repaint(); - revalidate(); } /** - * Style the cell with the appropriate colors from the theme. + * Style the CellGUI with the primary colors from the Theme. + * + * @see Theme.java */ private void defaultStyle() { setBackground(theme.getPrimaryBackground()); @@ -250,10 +223,14 @@ class CellGUI extends JPanel { notesLabels[i].setForeground(theme.getPrimaryText()); } } + + refresh(); } /** - * Style the cell with the appropriate colors from the theme. + * Style the CellGUI with the secondary colors from the Theme. + * + * @see Theme.java */ private void highlightedStyle() { setBackground(theme.getSecondaryBackground()); @@ -269,10 +246,14 @@ class CellGUI extends JPanel { notesLabels[i].setForeground(theme.getSecondaryText()); } } + + refresh(); } /** - * Style the cell with the appropriate colors from the theme. + * Style the CellGUI with the error colors from the Theme. + * + * @see Theme.java */ private void errorStyle() { setBackground(theme.getErrorBackground()); @@ -288,18 +269,42 @@ class CellGUI extends JPanel { notesLabels[i].setForeground(theme.getErrorText()); } } + + refresh(); + } + + /** + * Helper method to repaint and revalidate the internal panel and CellGUI. + */ + private void refresh() { + internalPanel.repaint(); + internalPanel.revalidate(); + repaint(); + revalidate(); } /** * Create the GUI components for the cell's notes. * + * If autoFill is true, then the CellGUI will automatically switch to + * noteMode. + * * @param autoFill */ private void generateNotes(boolean autoFill) { int[] possibleValues = cell.getPossibleValues(); + + // Prepare the internalPanel for noteMode. + if(autoFill) { + internalPanel.removeAll(); + noteMode = true; + } + + // Handle cases where there are no possible values stored. if(possibleValues.length == 0) { for(int i = 0; i < 9; i++) { - notesLabels[i] = new Note("", theme, font); + notesLabels[i] = new Label("", theme, font); + if(autoFill) internalPanel.add(notesLabels[i]); } return; @@ -308,11 +313,15 @@ class CellGUI extends JPanel { int index = 0; // Add the noted possible values to the cell. for(int i = 1; i <= 9 && index < possibleValues.length; i++) { + // Add the possible value if it exists. if(possibleValues[index] == i) { - notesLabels[i - 1] = new Note(Integer.toString(i), theme, font); + notesLabels[i - 1] = new Label(Integer.toString(i), theme, font); index++; + + // Otherwise, add a blank value. } else - notesLabels[i - 1] = new Note("", theme, font); + notesLabels[i - 1] = new Label("", theme, font); + if(autoFill) internalPanel.add(notesLabels[i - 1]); } } diff --git a/src/gui/ComboBox.java b/src/gui/ComboBox.java index fec2702..75d4d9d 100644 --- a/src/gui/ComboBox.java +++ b/src/gui/ComboBox.java @@ -1,14 +1,18 @@ package gui; +import java.awt.Font; + import javax.swing.JComboBox; import gui.backend.Settings; +import gui.backend.Theme; /** * A custom JComboBox with the appropriate styling for the Sudoku app. */ public class ComboBox extends JComboBox { - private Settings s; + private Theme theme; + private Font font; /** * Create a new ComboBox with the default styling and without initial items. @@ -19,7 +23,8 @@ public class ComboBox extends JComboBox { public ComboBox(Settings s) { super(); - this.s = s; + theme = s.getTheme(); + font = s.getFont(); style(); } @@ -27,7 +32,8 @@ public class ComboBox extends JComboBox { * Set up the ComboBox with the appropriate styling. */ private void style() { - // TODO: Style the ComboBox. - setFont(s.getFont()); + setFont(font); + setBackground(theme.getPrimaryBackground()); + setForeground(theme.getPrimaryText()); } } diff --git a/src/gui/FileChooser.java b/src/gui/FileChooser.java index 184cf81..d14cb62 100644 --- a/src/gui/FileChooser.java +++ b/src/gui/FileChooser.java @@ -1,19 +1,17 @@ package gui; -import java.io.File; +// GUI imports +import javax.swing.JFileChooser; import javax.swing.filechooser.FileFilter; -import gui.backend.Settings; - -import javax.swing.JFileChooser; +// Processing & backend imports +import java.io.File; /** * A custom FileChooser with the appropriate styling for the Sudoku app. * Additionally, this FileChooser only allows .sdku files to be selected. */ public class FileChooser extends JFileChooser { - private Settings s; - /** * Create a new FileChooser with the default directory set to the user's * Sudoku directory. @@ -21,9 +19,8 @@ public class FileChooser extends JFileChooser { * @param defaultDirectory * @param s */ - public FileChooser(File defaultDirectory, Settings s) { + public FileChooser(File defaultDirectory) { super(defaultDirectory); - this.s = s; for (File f: defaultDirectory.listFiles()) { ensureFileIsVisible(f); @@ -38,7 +35,6 @@ public class FileChooser extends JFileChooser { */ private void style() { // TODO: Style the FileChooser. - setFont(s.getFont()); } /** diff --git a/src/gui/Note.java b/src/gui/Label.java similarity index 85% rename from src/gui/Note.java rename to src/gui/Label.java index 947bc55..bf1a587 100644 --- a/src/gui/Note.java +++ b/src/gui/Label.java @@ -11,9 +11,9 @@ import gui.backend.Theme; * The Note class represents a single note of a cell in the Sudoku board. * * This is the GUI abstraction for the underlying Cell object, and is used to - * display and manage the note for a cell. + * display and manage the note or value for a cell. */ -public class Note extends JLabel { +public class Label extends JLabel { private Theme t; private Font f; @@ -23,7 +23,7 @@ public class Note extends JLabel { * @param text * @param t */ - public Note(String text, Theme t, Font f) { + public Label(String text, Theme t, Font f) { super(text, SwingConstants.CENTER); this.t = t; this.f = f; diff --git a/src/gui/Nav.java b/src/gui/Nav.java index a363e12..364017b 100644 --- a/src/gui/Nav.java +++ b/src/gui/Nav.java @@ -1,36 +1,53 @@ package gui; + +// File IO imports import java.util.Scanner; -import java.awt.event.MouseEvent; -import java.awt.event.MouseListener; import java.io.File; import java.io.FileNotFoundException; +import java.io.IOException; +// GUI imports import javax.swing.JPanel; +// Processing & backend imports import gui.backend.Cell; import gui.backend.Settings; import gui.backend.SudokuChecker; -import javax.swing.JLabel; -import javax.swing.JButton; +// Unused imports +//import javax.swing.JLabel; +//import javax.swing.JButton; + /** * The Nav class represents the Navigation bar at the top of the JFrame window. * * The Nav class is responsible for basic File IO operations. The most basic * functionality is the following: - * - creating a new, completely empty .sdku file for a user to create - * their own puzzle with; * - open an existing .sdku file in the user's file system (a standard * or default location is TBD); - * - save the currently open .sdku file with any changes the user has made. * - * In the future, the Nav class would ideally incorporate additional elements, - * such as the difficulty of the current problem, providing hints, displaying - * the length of time since a puzzle was started, etc. + * NOTE: Needed features + * - default open state from Settings + * - creating a new, completely empty .sdku file for a user to create + * their own puzzle with; + * - save the currently open .sdku file with any changes the user has made; + * + * NOTE: Future features + * - auto-save + * - display elapsed time; + * - open a settings GUI interface; + * - display the number of mistakes made; + * - display how many hints have been used; + * - button to globally enable noteMode in CellGUI objects contained in + * the Board; and + * - toggleable number pad instead of using keyboard inputs. */ public class Nav extends JPanel { - private Settings s; + @SuppressWarnings("unused") private SudokuChecker sc; + + private Settings s; + private File f; private Board b; private Cell[][] grid; @@ -41,6 +58,8 @@ public class Nav extends JPanel { * * Additionally, even for debugging, this should not be used since default * file I/O behavior is dictated by the values in the Settings object. + * + * @Deprecated */ public Nav() { super(); @@ -120,13 +139,19 @@ public class Nav extends JPanel { * Set the SudokuChecker object for the Nav bar. * * This is necessary for the Nav bar to be able to interact with the - * SudokuChecker object for the solve button. This shouldn't be necessary - * to change once the program is running. + * SudokuChecker object for the solve button. + * + * In the future, the SudokuChecker object is also used for hints. + * + * This shouldn't be necessary to change once the program is running. */ public void setChecker(SudokuChecker sc) { this.sc = sc; } + /** + * Style the Nav with the primary colors from Theme. + */ private void style() { setBackground(s.getTheme().getPrimaryBackground()); setForeground(s.getTheme().getPrimaryText()); @@ -136,21 +161,27 @@ public class Nav extends JPanel { * Create the GUI for the Nav bar. * * This method is called when the Nav object is created in the App - * class if the program is not being run in the command-line for + * class and if the program is not being run in the command-line for * debugging purposes. */ private void createGUI() { style(); + + /* + // TODO: Make custom Label class. JLabel elapsedTime = new JLabel("Elapsed Time: 00:00:00"); - elapsedTime.setFont(s.getFont()); // TODO: Make custom Label class. + elapsedTime.setFont(s.getFont()); + + // TODO: Make custom Button class. JButton solve = new JButton("Solve"); - solve.setFont(s.getFont()); // TODO: Make custom Button class. + solve.setFont(s.getFont()); + */ // Add the ComboBox for the file options, the elapsed time label, and // the solve button to the Nav Panel. add(createFileOptions()); - add(elapsedTime); - add(solve); + //add(elapsedTime); + //add(solve); } /** @@ -164,6 +195,7 @@ public class Nav extends JPanel { fileOptions.addItem("New"); fileOptions.addItem("Open"); fileOptions.addItem("Save"); + fileOptions.addItem("Save As"); fileOptions.addItem("Exit"); fileOptions.addActionListener(e -> { @@ -176,39 +208,67 @@ public class Nav extends JPanel { openFile(); break; case 2: - saveFile(); + saveFile(false); break; case 3: + saveFile(true); + break; + case 4: System.exit(0); break; default: System.out.println("Invalid selection."); } }); - return fileOptions; } /** - * Given the user's selection from the JComboBox, perform the appropriate + * Given the user's selection from the ComboBox, perform the appropriate * action. + * + * See Settings.getNewFileProperties() for more information on the expected + * actions. + * + * @see Settings.java */ private void newFile() { int defaultOption = s.getNewFileProperties(); - - // See Settings.getNewFileProperties for possible values and - // the expected behavior of each value. + switch(defaultOption) { case 0: + f = new File("new.sdku"); + try { + f.createNewFile(); + + // Create a new, blank .sdku file. + for(int i = 0; i < 9; i++) + for(int j = 0; j < 9; j++) + grid[i][j] = new Cell(i, j, 0); + + // Update the Board with the new, blank .sdku puzzle. + b.setGrid(grid); + + // Save the file if auto-save is on. + if(s.getAutoSave()) saveFile(true); + } catch (IOException e1) { + System.err.println("Unable to create new file " + f.getName()); + // TODO: Add a dialog box GUI to notify. + + // Open the default, pre-downloaded default.sdku file. + openFile(s.getDefaultDirectory() + "default"); + } case 1: case 2: default: } - // TODO: Create a new .sdku file in the default directory. } /** - * Open an existing .sdku file in the user's file system, using a GUI. + * Open an existing .sdku file in the user's file system, selected using + * a GUI. + * + * Create the Grid and update the Board with the loaded Grid. */ private void openFile() { // No error handling is needed here, since the Settings class ensures @@ -217,7 +277,7 @@ public class Nav extends JPanel { // Custom FileChooser class for style and to ensure the user can only // select .sdku files. - FileChooser fc = new FileChooser(defaultDirectory, s); + FileChooser fc = new FileChooser(defaultDirectory); int result = fc.showOpenDialog(this); // If the user selects a file, open it. @@ -263,8 +323,22 @@ public class Nav extends JPanel { /** * Save the currently open .sdku file with any changes the user has made. * This needs to pull the current state of the grid from the Board JPanel. + * + * If called from 'Save' option, then saveAs should be false. + * If called from 'Save As' option, then saveAs should be true. + * + * 'Save' will simply overwrite the existing file that was opened, + * while 'Save As' while prompt the user to select or create a file. + * + * 'Save As' functionality is the default behavior for new .sdku files + * created. Therefore, if the filename is 'new.sdku', then saveAs will + * be made true. + * + * @param boolean */ - private void saveFile() { + private void saveFile(boolean saveAs) { + if(f.getName().equals("new.sdku")) + saveAs = true; // TODO: Get the current Cell[][] from the Board JPanel // TODO: Write to a file. } @@ -282,9 +356,8 @@ public class Nav extends JPanel { * Update the loaded grid with the current version with any changes the * user has made. * - * For future auto-save functionality, this could simply be called after - * each change the user makes on the Sudoku board. - * + * If the user has enabled auto-save in Settings, then save the file. + * * In the future, an optimization could be made where a different data * structure is used to pass only the changes the player has made within * the last x minutes, for eg. {row, col, value, noteValues[]}. Given @@ -295,6 +368,7 @@ public class Nav extends JPanel { */ public void updateLoadedGrid(Cell[][] grid) { this.grid = grid; + if(s.getAutoSave()) saveFile(true); } /** @@ -331,13 +405,7 @@ public class Nav extends JPanel { // If the file is not found, print an error message and return. } catch (FileNotFoundException e) { - String filename = f.getName(); - System.err.println("An error occurred while reading the file " + - filename + ".sdku." - ); - - e.printStackTrace(); - grid = new Cell[9][9]; + newFile(); return; } } diff --git a/src/gui/Root.java b/src/gui/Root.java index c24104d..2f80f41 100644 --- a/src/gui/Root.java +++ b/src/gui/Root.java @@ -1,9 +1,9 @@ package gui; -import java.awt.LayoutManager; import javax.swing.JPanel; import gui.backend.Settings; +import gui.backend.Theme; /** * The Root class represents the root JPanel for the Sudoku app. @@ -13,17 +13,16 @@ import gui.backend.Settings; * styling purposes. */ public class Root extends JPanel { - private Settings s; + private Theme theme; /** * Create a new Root object with the default styling from settings. * * @param s */ - public Root(Settings s) { + public Root(Settings settings) { super(); - - this.s = s; + this.theme = settings.getTheme(); style(); } @@ -31,6 +30,7 @@ public class Root extends JPanel { * Set up the Root Panel with the appropriate styling. */ private void style() { - // TODO: Style the Root Panel. + setBackground(theme.getPrimaryBackground()); + setForeground(theme.getPrimaryText()); } } diff --git a/src/gui/backend/Cell.java b/src/gui/backend/Cell.java index c77cabc..8f2f9a2 100644 --- a/src/gui/backend/Cell.java +++ b/src/gui/backend/Cell.java @@ -230,20 +230,6 @@ public class Cell { max = 0; min = 0; } - - /** - * Create a new List with the given initial value. - * - * By default, the head and tail are the same, the size is 1, and the max and min are the initial value. - * @param initValue - */ - public List(int initValue) { - head = new Value(initValue); - tail = head; - size = 1; - max = initValue; - min = initValue; - } /** * Create a new List with the given initial values. diff --git a/src/gui/backend/Settings.java b/src/gui/backend/Settings.java index 545aeac..a48b5a1 100644 --- a/src/gui/backend/Settings.java +++ b/src/gui/backend/Settings.java @@ -18,10 +18,18 @@ import java.awt.FontFormatException; * system. * * (Currently) Configurable settings include: - * - Default Directory - * - New File Properties - * - Font - * + * - Default puzzles directory for .sdku files; + * - New file Properties; + * - Font; + * - Window size; + * - Window resizing (experimental); + * - Cell start mode, meaning if a cell is blank and selected, input + * is either going to the notes or entering a value; + * - Application default open behavior; + * - Theme, specifically by providing a default.theme file formatted for JSON; + * - Auto-fill notes with possible values; + * - Auto-check user-entered values with the correct values; + * - Auto-save and auto-save frequency; */ public class Settings { private final String os; @@ -62,6 +70,12 @@ public class Settings { // Auto check values private boolean autoCheckValues; + // Auto-save whenever a new note or value is changed. + private boolean autoSave; + + // Auto-save frequency + private int autoSaveFrequency; + /** * Create a new Settings object, initializing the default settings * or reading in the settings from settings file if it exists. @@ -70,6 +84,8 @@ public class Settings { * the settings file is located. The settings file is always located in * either "~/.config/sudoku/" or "%APPDATA%/Local/Sudoku/", depending * on the user's operating system. + * + * The settings file is settings.json. */ public Settings() { os = System.getProperty("os.name").toLowerCase(); @@ -81,12 +97,114 @@ public class Settings { "/.config/sudoku/"; File dir = new File(appDirectory); + + // If the app directory does not exist, create it and populate a + // settings.json file with the default settings. if (!dir.exists() || !dir.isDirectory()) { dir.mkdirs(); - defaultSettings(); + updateSettingsFile(); + + } else readSettings(); + } + + /** + * Set the default settings for the application and write them to the + * settings file. + */ + private void defaultSettings() { + // Setup the default directory for .sdku puzzle files. + defaultDirectory = appDirectory + "puzzles"; + File dir = new File(defaultDirectory); + + // Create the puzzles directory. + if(!dir.exists() || !dir.isDirectory()) + dir.mkdirs(); + + // Load the font from the system directory. + if(loadFont("HackNerdFont-Regular") == 0) + font = new Font("Hack Nerd Font", Font.PLAIN, 16); + + // If the font does not exist, use Arial as the default font. + else + font = new Font("Arial", Font.PLAIN, 16); + + // Set other default settings. + newFileProperties = 0; + dimension = new Dimension(600, 800); + resizable = false; + cellGUIStartMode = false; + defaultOpenState = 0; + theme = new Theme(new File(appDirectory + "default.theme")); + autoFillNotes = false; + autoCheckValues = true; + autoSave = false; + autoSaveFrequency = 0; + setCellDimensions(); + + // Write the default settings to the settings.json file. + updateSettingsFile(); + } + + /** + * Open and write the settings to the settings file. + * + * This method is called whenever a setting is updated. + */ + private void updateSettingsFile() { + // TODO: Write the settings to the settings file. + } + + /** + * Read the settings from the settings file. + * + * If the settings file does not exist, the default settings will be used + * to populate the Settings file. + */ + private void readSettings() { + File settingsFile = new File(appDirectory + "/settings.json"); + if (!settingsFile.exists() && !settingsFile.isDirectory()) { + defaultSettings(); + return; + } + + // TODO: Read the settings from the settings file. + } + + /** + * Load the font from the resources folder in the user's system directory. + * + * @return int 0 if successful, 1 if the file does not exist. + */ + private int loadFont(String fontName) { + // Load the font from the system directory. + String fontFilepath; + if(os.startsWith("windows")) + fontFilepath = System.getProperty("user.home") + + "\\AppData\\Local\\Microsoft\\Windows\\Fonts\\" + + fontName + ".ttf"; + else if(os.startsWith("mac")) + fontFilepath = "/Library/Fonts/" + fontName + ".ttf"; + else + fontFilepath = "/usr/share/fonts/" + fontName + ".ttf"; + + // Register the font with the GraphicsEnvironment. + try { + GraphicsEnvironment ge = GraphicsEnvironment. + getLocalGraphicsEnvironment(); + + ge.registerFont(Font.createFont( + Font.TRUETYPE_FONT, + new File(fontFilepath) + )); + + return 0; + + } catch(IOException | FontFormatException e) { + System.out.println("Filepath of font not found: " + + fontFilepath + " does not exist."); + return 1; } - readSettings(); } /** @@ -117,9 +235,11 @@ public class Settings { /** * New File Properties is a configurable setting, where the following * values are allowable: - * 0 - Load the last opened .sdku file, default behavior - * 1 - Create a new, blank .sdku file - * 2 - Create a new, random .sdku file + * 0 - Create a new, blank .sdku file + * 1 - Create a new, random .sdku file + * + * Integer is being used instead of boolean since additional properties + * may be added in the future. * * @return int */ @@ -364,97 +484,58 @@ public class Settings { } /** - * Open and write the settings to the settings file. + * Auto-save is a user-configurable setting to automatically save the + * puzzle anytime they make a change. Additionally, auto-save will + * result in the file being automatically saved every 60 seconds, to + * preserve the elapsed time tracking. * - * This method is called whenever a setting is updated. + * @return boolean */ - private void updateSettingsFile() { - // TODO: Write the settings to the settings file. + public boolean getAutoSave() { + return autoSave; } /** - * Set the default settings for the application and write them to the - * settings file. + * Set the auto-save feature to a specified state. + * + * @param autoSave */ - private void defaultSettings() { - // Setup the default directory for .sdku puzzle files. - defaultDirectory = appDirectory + "puzzles"; - File dir = new File(defaultDirectory); - - if(!dir.exists() || !dir.isDirectory()) - dir.mkdirs(); - - newFileProperties = 0; - - // Load the font from the system directory. - if(loadFont("HackNerdFont-Regular") == 0) - font = new Font("Hack Nerd Font", Font.PLAIN, 16); - - // If the font does not exist, use Arial as the default font. - else - font = new Font("Arial", Font.PLAIN, 16); - - dimension = new Dimension(600, 800); - resizable = false; - cellGUIStartMode = false; - defaultOpenState = 0; - theme = new Theme(new File(appDirectory + "default.theme")); - autoFillNotes = false; - autoCheckValues = true; - setCellDimensions(); - + public void setAutoSave(boolean autoSave) { + this.autoSave = autoSave; updateSettingsFile(); } /** - * Read the settings from the settings file. + * The auto save frequnecy, in seconds. * - * If the settings file does not exist, the default settings will be used - * to populate the Settings file. + * By default, auto-save will save whenever the user makes changes on the + * puzzle. However, since elapsed time and other future features are not + * user-controlled events, the frequency is used to specify in a separate + * thread how often the .sdku file should be saved. + * + * For performance and realistic necessity for preserving this kind of + * information, the frequency should (not required) be following: + * - 0: off; + * - 5: 5 second intervals; + * - 30: 30 second intervals; + * - 60: 60 second intervals. + * + * If auto-save is off, then this value is 0 automatically. When auto-save + * is turned on, by default the value is 30. + * + * @return auto-save frequency */ - private void readSettings() { - File settingsFile = new File(appDirectory + "/settings.json"); - if (!settingsFile.exists() && !settingsFile.isDirectory()) { - defaultSettings(); - return; - } - - // TODO: Read the settings from the settings file. - } + public int getAutoSaveFrequency() { + return autoSaveFrequency; + } /** - * Load the font from the resources folder in the user's system directory. + * Set the auto-save frequency, in seconds. * - * @return int 0 if successful, 1 if the file does not exist. + * @param autoSaveFrequency */ - private int loadFont(String fontName) { - // Load the font from the system directory. - String fontFilepath; - if(os.startsWith("windows")) - fontFilepath = System.getProperty("user.home") + - "\\AppData\\Local\\Microsoft\\Windows\\Fonts\\" + - fontName + ".ttf"; - else if(os.startsWith("mac")) - fontFilepath = "/Library/Fonts/" + fontName + ".ttf"; - else - fontFilepath = "/usr/share/fonts/" + fontName + ".ttf"; - - // Register the font with the GraphicsEnvironment. - try { - GraphicsEnvironment ge = GraphicsEnvironment. - getLocalGraphicsEnvironment(); - - ge.registerFont(Font.createFont( - Font.TRUETYPE_FONT, - new File(fontFilepath) - )); - - return 0; - - } catch(IOException | FontFormatException e) { - System.out.println("Filepath of font not found: " + - fontFilepath + " does not exist."); - return 1; - } + public void setAutoSaveFrequency(int autoSaveFrequency) { + this.autoSaveFrequency = autoSaveFrequency; + updateSettingsFile(); } } \ No newline at end of file diff --git a/src/gui/backend/SudokuChecker.java b/src/gui/backend/SudokuChecker.java index e2641ae..89c8ff5 100644 --- a/src/gui/backend/SudokuChecker.java +++ b/src/gui/backend/SudokuChecker.java @@ -12,6 +12,8 @@ import java.util.ArrayList; */ public class SudokuChecker { private Cell[][] grid; + private Cell[][] origGrid; + /** * Create a new SudokuChecker object, initializing the grid to the given @@ -19,13 +21,60 @@ public class SudokuChecker { * inputs a value, or be used to check the validity of a puzzle when the * user requests it. * - * TODO: Currently, this class is not used in the GUI. It is only used in - * the command-line interface. - * * @param grid */ public SudokuChecker(Cell[][] grid) { this.grid = grid; + + // Create a copy of the original grid to be used for resetting the + // grid to its original state. + origGrid = new Cell[9][9]; + for(int i = 0; i < 9; i++) { + for(int j = 0; j < 9; j++) { + origGrid[i][j] = new Cell(i, j, grid[i][j].getValue()); + } + } + } + + /** + * Check if the given value can be placed in the given cell of the grid. + * + * False means that the value is incorrect, and true means that the value + * is correct. + * + * @param row + * @param col + * @param value + * @return boolean + */ + public boolean checkValue(int row, int col, int value) { + // Check the row + for(int i = 0; i < 9; i++) { + if(grid[row][i].getValue() == value && i != col) + return false; + } + + // Check the column + for(int i = 0; i < 9; i++) { + if(grid[i][col].getValue() == value && i != row) + return false; + } + + // Check the box + int boxRow = row / 3; + int boxCol = col / 3; + for(int i = 0; i < 3; i++) { + for(int j = 0; j < 3; j++) { + if( + grid[boxRow * 3 + i][boxCol * 3 + j].getValue() == value && + (boxRow * 3 + i != row || boxCol * 3 + j != col) + ) { + return false; + } + } + } + + return true; } /** @@ -91,39 +140,13 @@ public class SudokuChecker { ) { ArrayList intersection = new ArrayList(); for(int i = 0; i < a.size(); i++) { - if(b.contains(a.get(i))) { + if(b.contains(a.get(i))) intersection.add(a.get(i)); - } } return intersection; } - /** - * Determine what numbers are available to be placed in the given cell of - * the grid. This is effectively an union of the numbers available in the - * row, column, and box of the cell. - * - * @param row - * @param col - */ - private ArrayList union( - ArrayList a, - ArrayList b - ) { - ArrayList union = new ArrayList(); - for(int i = 0; i < a.size(); i++) { - union.add(a.get(i)); - } - for(int i = 0; i < b.size(); i++) { - if(!union.contains(b.get(i))) { - union.add(b.get(i)); - } - } - - return union; - } - /** * Determine what numbers are available to be placed in the given cell of * the grid. This is effectively an intersection of the numbers available @@ -145,9 +168,9 @@ public class SudokuChecker { getBoxRemainingNumbers(row, col) ); - if(intersection.size() == 0) { + if(intersection.size() == 0) return; - } else if(intersection.size() == 1) { + else if(intersection.size() == 1) { int value = intersection.get(0); grid[row][col].setValue(value); @@ -156,9 +179,8 @@ public class SudokuChecker { } else { // Join the arrays and find the intersection of the three arrays. ArrayList availableNumbers = new ArrayList(); - for(int i = 0; i < intersection.size(); i++) { + for(int i = 0; i < intersection.size(); i++) availableNumbers.add(intersection.get(i)); - } grid[row][col].setPossibleValues( arrayListToArray(availableNumbers) @@ -213,9 +235,8 @@ public class SudokuChecker { while(!isValidSolution()) { for(int i = 0; i < 9; i++) { for(int j = 0; j < 9; j++) { - if(grid[i][j].getValue() == 0) { + if(grid[i][j].getValue() == 0) getAvailableNumbers(i, j); - } } } } @@ -239,9 +260,7 @@ public class SudokuChecker { break; } } - if(!found) { - remainingNumbers.add(i); - } + if(!found) remainingNumbers.add(i); } return remainingNumbers; @@ -263,9 +282,8 @@ public class SudokuChecker { break; } } - if(!found) { + if(!found) remainingNumbers.add(i); - } } return remainingNumbers; @@ -293,9 +311,8 @@ public class SudokuChecker { } } } - if(!found) { + if(!found) remainingNumbers.add(i); - } } return remainingNumbers; @@ -312,9 +329,9 @@ public class SudokuChecker { */ private int[] arrayListToArray(ArrayList list) { int[] array = new int[list.size()]; - for(int i = 0; i < list.size(); i++) { + for(int i = 0; i < list.size(); i++) array[i] = list.get(i); - } + return array; } @@ -331,17 +348,19 @@ public class SudokuChecker { // Check that every cell has a value between 1 and 9. for(int i = 0; i < 9; i++) { for(int j = 0; j < 9; j++) { - if(grid[i][j].getValue() < 1 || grid[i][j].getValue() > 9) - return false; + if( + grid[i][j].getValue() < 1 || + grid[i][j].getValue() > 9 + ) return false; } } // Check that every row contains the numbers 1-9 exactly once. for(int i = 0; i < 9; i++) { int[] row = new int[9]; - for(int j = 0; j < 9; j++) { + for(int j = 0; j < 9; j++) row[j] = grid[i][j].getValue(); - } + if(!isValidSet(row)) { System.out.println("Row " + i + " is invalid."); return false; @@ -351,9 +370,9 @@ public class SudokuChecker { // Check that every column contains the numbers 1-9 exactly once. for(int i = 0; i < 9; i++) { int[] col = new int[9]; - for(int j = 0; j < 9; j++) { + for(int j = 0; j < 9; j++) col[j] = grid[j][i].getValue(); - } + if(!isValidSet(col)) { System.out.println("Column " + i + " is invalid."); return false; @@ -365,9 +384,8 @@ public class SudokuChecker { for(int j = 0; j < 3; j++) { int[] box = new int[9]; for(int k = 0; k < 3; k++) { - for(int l = 0; l < 3; l++) { + for(int l = 0; l < 3; l++) box[k * 3 + l] = grid[i * 3 + k][j * 3 + l].getValue(); - } } if(!isValidSet(box)) { System.out.println( @@ -393,13 +411,15 @@ public class SudokuChecker { private boolean isValidSet(int[] set) { boolean[] found = new boolean[9]; for(int i = 0; i < 9; i++) { - if(set[i] < 1 || set[i] > 9) { + if(set[i] < 1 || set[i] > 9) return false; - } else if(found[set[i] - 1]) { + + else if(found[set[i] - 1]) return false; - } else { + + else found[set[i] - 1] = true; - } + } return true; } diff --git a/src/gui/backend/Theme.java b/src/gui/backend/Theme.java index 4ea4e29..eae087f 100644 --- a/src/gui/backend/Theme.java +++ b/src/gui/backend/Theme.java @@ -6,7 +6,8 @@ import java.io.File; /** * The Theme class represents the color scheme for the GUI components. * - * The Theme class reads a theme file and sets the colors for the GUI components. + * The Theme class reads a .theme file and sets the colors for the GUI components. + * .theme files are .json files for standardized data processing. * * The Theme class provides the following color customizations: * - primaryBackground @@ -24,16 +25,7 @@ import java.io.File; * - primaryButtonBorder * - secondaryButtonBorder */ -import java.awt.Color; -import java.io.File; - -/** - * The Theme class represents a theme for the GUI components. - * It provides colors for various GUI elements. - */ public class Theme { - private File theme; - private Color primaryBackground; private Color secondaryBackground; private Color primaryText; @@ -58,16 +50,15 @@ public class Theme { * * @param theme the theme file */ - public Theme(File theme) { - this.theme = theme; - readTheme(); + public Theme(File file) { + readTheme(file); } /** * Reads the .theme file and sets the colors for the GUI components. * Once complete, sets the theme for the GUI components. */ - private void readTheme() { + private void readTheme(File file) { // TODO: Read the .theme file setTheme(); }