introductionToProgramming/week14/Calculator.java
Daniel Bulant fc80e98eb4 week14
2025-12-03 14:20:35 +01:00

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);
}
}