wrote thorough documentation and made minor fixes for highlighting/selection

This commit is contained in:
Josh Ashton
2024-03-25 13:10:31 -06:00
parent 0409a8f41d
commit 600c215b7d
12 changed files with 625 additions and 376 deletions
+4 -2
View File
@@ -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.
+178 -88
View File
@@ -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();
}
};
}
}
+65 -56
View File
@@ -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]);
}
}
+10 -4
View File
@@ -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<E> extends JComboBox<E> {
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<E> extends JComboBox<E> {
public ComboBox(Settings s) {
super();
this.s = s;
theme = s.getTheme();
font = s.getFont();
style();
}
@@ -27,7 +32,8 @@ public class ComboBox<E> extends JComboBox<E> {
* 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());
}
}
+5 -9
View File
@@ -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());
}
/**
+3 -3
View File
@@ -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;
+106 -38
View File
@@ -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;
}
}
+6 -6
View File
@@ -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());
}
}
-14
View File
@@ -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.
+166 -85
View File
@@ -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();
}
}
+77 -57
View File
@@ -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<Integer> intersection = new ArrayList<Integer>();
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<Integer> union(
ArrayList<Integer> a,
ArrayList<Integer> b
) {
ArrayList<Integer> union = new ArrayList<Integer>();
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<Integer> availableNumbers = new ArrayList<Integer>();
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<Integer> 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;
}
+5 -14
View File
@@ -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();
}