mirror of
https://github.com/danbulant/introductionToProgramming
synced 2026-05-19 04:18:32 +00:00
269 lines
6.9 KiB
Java
269 lines
6.9 KiB
Java
package week14;
|
|
|
|
import javafx.application.Application;
|
|
import javafx.beans.property.SimpleStringProperty;
|
|
import javafx.beans.property.StringProperty;
|
|
import javafx.geometry.Pos;
|
|
import javafx.scene.Scene;
|
|
import javafx.scene.control.Button;
|
|
import javafx.scene.control.Label;
|
|
import javafx.scene.input.KeyCode;
|
|
import javafx.scene.layout.GridPane;
|
|
import javafx.scene.layout.HBox;
|
|
import javafx.scene.layout.Priority;
|
|
import javafx.scene.layout.VBox;
|
|
import javafx.stage.Stage;
|
|
|
|
/*
|
|
This could have been implemented with a possibly more readable state machine instead of this messy distributed state..
|
|
|
|
Value => result of operations, or the first number pressed in before selecting an operation
|
|
Buffer => currently typed number, right side of operations when activated
|
|
Number pressed => buffer is !empty / was interacted with
|
|
Buferred operation => operation selected but not acted on yet
|
|
Prev buffer => previously used buffer, in case the user wants to repeat operations (by pressing = multiple times)
|
|
Prev operation => last used operation
|
|
|
|
Display => text output (reactive string property)
|
|
*/
|
|
public class Calculator extends Application {
|
|
enum Operation {
|
|
ADD,
|
|
SUBSTRACT,
|
|
MULTIPLY,
|
|
DIVIDE;
|
|
|
|
public String toString() {
|
|
return switch(this) {
|
|
case ADD -> "+";
|
|
case SUBSTRACT -> "-";
|
|
case MULTIPLY -> "*";
|
|
case DIVIDE -> "/";
|
|
default -> "";
|
|
};
|
|
}
|
|
|
|
public static Operation fromString(String op) {
|
|
return switch(op) {
|
|
case "+" -> ADD;
|
|
case "-" -> SUBSTRACT;
|
|
case "*" -> MULTIPLY;
|
|
case "/" -> DIVIDE;
|
|
default -> null;
|
|
};
|
|
}
|
|
|
|
public double apply(double left, double right) {
|
|
return switch(this) {
|
|
case ADD -> left + right;
|
|
case SUBSTRACT -> left - right;
|
|
case MULTIPLY -> left * right;
|
|
case DIVIDE -> left / right;
|
|
};
|
|
}
|
|
}
|
|
|
|
double value = 0;
|
|
double buffer = 0;
|
|
boolean numberPressed = false;
|
|
|
|
Operation bufferredOperation = null;
|
|
|
|
double prevBuffer = 0;
|
|
Operation prevOperation = null;
|
|
|
|
StringProperty display = new SimpleStringProperty();
|
|
|
|
/**
|
|
* Event handler after a number was pressed
|
|
*/
|
|
void processNumberInput(int num) {
|
|
assert num >= 0 && num <= 9;
|
|
resetNumberIfNeeded();
|
|
numberPressed = true;
|
|
buffer *= 10;
|
|
buffer += num;
|
|
|
|
updateDisplay();
|
|
}
|
|
|
|
/**
|
|
* Event handler after backspace was pressed
|
|
*/
|
|
void backspace() {
|
|
resetNumberIfNeeded();
|
|
var num = buffer % 10;
|
|
buffer -= num;
|
|
buffer /= 10;
|
|
|
|
updateDisplay();
|
|
}
|
|
|
|
/**
|
|
* Applies operations, used as event handler after = / enter
|
|
*/
|
|
void processBuffered() {
|
|
if(numberPressed && bufferredOperation != null) {
|
|
value = bufferredOperation.apply(value, buffer);
|
|
prevOperation = bufferredOperation;
|
|
bufferredOperation = null;
|
|
resetBuffer();
|
|
} else if(prevOperation != null) {
|
|
value = prevOperation.apply(value, prevBuffer);
|
|
} else if(bufferredOperation != null) {
|
|
prevBuffer = value;
|
|
value = bufferredOperation.apply(value, value);
|
|
prevOperation = bufferredOperation;
|
|
bufferredOperation = null;
|
|
}
|
|
updateDisplay();
|
|
}
|
|
|
|
/**
|
|
* Selects an operation, computes previous operation if still buffered
|
|
*/
|
|
void processOperationInput(Operation op) {
|
|
assert op != null;
|
|
if(numberPressed && bufferredOperation != null) {
|
|
value = bufferredOperation.apply(value, buffer);
|
|
prevOperation = bufferredOperation;
|
|
bufferredOperation = null;
|
|
} else if(numberPressed) {
|
|
value = buffer;
|
|
}
|
|
bufferredOperation = op;
|
|
resetBuffer();
|
|
updateDisplay();
|
|
}
|
|
|
|
/**
|
|
* Resets all relevant state
|
|
*/
|
|
void reset() {
|
|
resetBuffer();
|
|
value = 0;
|
|
bufferredOperation = null;
|
|
prevBuffer = 0;
|
|
prevOperation = null;
|
|
updateDisplay();
|
|
}
|
|
|
|
/**
|
|
* Makes sure value is safe to interact with (enter numbers over)
|
|
* resets to 0 if NaN or infinite
|
|
*/
|
|
void resetNumberIfNeeded() {
|
|
if(!Double.isFinite(value)) value = 0;
|
|
}
|
|
|
|
void updateDisplay() {
|
|
if(numberPressed) display.set(Double.toString(buffer));
|
|
else if(Double.isNaN(value)) display.set("ERROR");
|
|
else display.set(Double.toString(value));
|
|
|
|
// System.out.format("value %s buffer %s numberPressed %s buferredOperation %s prevBuffer %s prevOperation %s\n", value, buffer, numberPressed, buferredOperation, prevBuffer, prevOperation);
|
|
}
|
|
|
|
void resetBuffer() {
|
|
prevBuffer = buffer;
|
|
buffer = 0;
|
|
numberPressed = false;
|
|
}
|
|
|
|
/**
|
|
* Builds the ui
|
|
*
|
|
* VALUE/BUFFER/ERROR
|
|
*
|
|
* 7 8 9 + ⌫
|
|
* 4 5 6 -
|
|
* 1 2 3 *
|
|
* 0 / =
|
|
*/
|
|
public void start(Stage primaryStage) {
|
|
updateDisplay();
|
|
var output = new Label();
|
|
output.setStyle("-fx-font-size: 30px; -fx-text-alignment: right;");
|
|
output.textProperty().bind(display);
|
|
|
|
var numberPanel = new GridPane();
|
|
|
|
for(var i = 0; i < 9; i++) {
|
|
var button = new Button(Integer.toString(i + 1));
|
|
var index = i;
|
|
button.setOnMouseClicked(e -> {
|
|
processNumberInput(index + 1);
|
|
});
|
|
numberPanel.add(button, i % 3, 2-(i / 3));
|
|
}
|
|
var zero = new Button("0");
|
|
zero.setOnMouseClicked(e -> {
|
|
processNumberInput(0);
|
|
});
|
|
numberPanel.add(zero, 1, 3);
|
|
|
|
var controlPanel = new GridPane();
|
|
|
|
var values = Operation.values();
|
|
for(var i = 0; i < values.length; i++) {
|
|
var op = values[i];
|
|
var button = new Button(op.toString());
|
|
button.setOnMouseClicked(e -> {
|
|
processOperationInput(op);
|
|
});
|
|
controlPanel.add(button, 0, i);
|
|
}
|
|
|
|
var backspace = new Button("⌫");
|
|
backspace.setOnMouseClicked(e -> backspace());
|
|
controlPanel.add(backspace, 1, 0);
|
|
var equals = new Button("=");
|
|
equals.setOnMouseClicked(e -> processBuffered());
|
|
controlPanel.add(equals, 1, 3);
|
|
|
|
var hbox = new HBox();
|
|
hbox.setAlignment(Pos.CENTER);
|
|
hbox.getChildren().add(numberPanel);
|
|
hbox.getChildren().add(controlPanel);
|
|
|
|
var vbox = new VBox();
|
|
vbox.setAlignment(Pos.TOP_CENTER);
|
|
vbox.getChildren().add(output);
|
|
vbox.getChildren().add(hbox);
|
|
VBox.setVgrow(hbox, Priority.SOMETIMES);
|
|
|
|
Scene scene = new Scene(vbox, 300, 250);
|
|
scene.setOnKeyPressed(ev -> {
|
|
final var ch = ev.getText();
|
|
try {
|
|
var num = Integer.parseInt(ch);
|
|
processNumberInput(num);
|
|
return;
|
|
} catch(NumberFormatException e) {}
|
|
var op = Operation.fromString(ch);
|
|
if(op != null) {
|
|
processOperationInput(op);
|
|
return;
|
|
}
|
|
if(ev.getCode() == KeyCode.BACK_SPACE) {
|
|
backspace();
|
|
return;
|
|
}
|
|
if(ev.getCode() == KeyCode.ESCAPE) {
|
|
reset();
|
|
return;
|
|
}
|
|
if(ev.getCode() == KeyCode.ENTER || ev.getCode() == KeyCode.EQUALS) {
|
|
processBuffered();
|
|
return;
|
|
}
|
|
});
|
|
primaryStage.setTitle("Calculator");
|
|
primaryStage.setScene(scene);
|
|
primaryStage.show();
|
|
}
|
|
|
|
public static void main(String[] args) {
|
|
launch(args);
|
|
}
|
|
}
|