From fc80e98eb4dcff584d31eca32b973aa05acb09f1 Mon Sep 17 00:00:00 2001 From: Daniel Bulant Date: Wed, 3 Dec 2025 14:20:35 +0100 Subject: [PATCH] week14 --- main.typ | 2 +- shell.nix | 6 +- week14/Calculator.java | 269 +++++++++++++++++++++++++++++++++++++++++ week14/common | 1 + week14/doc.typ | 27 +++++ 5 files changed, 300 insertions(+), 5 deletions(-) create mode 100644 week14/Calculator.java create mode 120000 week14/common create mode 100644 week14/doc.typ diff --git a/main.typ b/main.typ index b22c62c..5c53001 100644 --- a/main.typ +++ b/main.typ @@ -15,7 +15,7 @@ Collection of solutions to programming exercises as part of Introduction to Prog ) #{ - let count = 13; + let count = 14; for week in range(1, count + 1).filter(it => it != 8) { let a = "./week" + str(week) + "/doc.typ" include a diff --git a/shell.nix b/shell.nix index a425110..f79eb02 100644 --- a/shell.nix +++ b/shell.nix @@ -3,14 +3,12 @@ }: pkgs.mkShell rec { buildInputs = with pkgs; [ - jdk + (jdk.override { enableJavaFX = true; }) typst tinymist nushell ]; - nativeBuildInputs = with pkgs; [ - jre - ]; + nativeBuildInputs = with pkgs; []; TYPST_FEATURES = "html"; } diff --git a/week14/Calculator.java b/week14/Calculator.java new file mode 100644 index 0000000..7148d58 --- /dev/null +++ b/week14/Calculator.java @@ -0,0 +1,269 @@ +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); + } +} diff --git a/week14/common b/week14/common new file mode 120000 index 0000000..60d3b0a --- /dev/null +++ b/week14/common @@ -0,0 +1 @@ +../common \ No newline at end of file diff --git a/week14/doc.typ b/week14/doc.typ new file mode 100644 index 0000000..56383ab --- /dev/null +++ b/week14/doc.typ @@ -0,0 +1,27 @@ +#import "./common/common.typ" : * + +#show: template + += Week 14 + +Write a graphical user interface for a simple calculator application. + +Here are some steps to get started: + +- Add a label to show the currently entered number. +- Use horizontal and vertical boxes, or a grid, to construct a layout of buttons numbered from 0 to 9. +- Add buttons for addition, subtraction, multiplication, and division. + +The calculator should work as follows: You press “5”, it shows up in the display, then you press “+”, and then you press “7” and it becomes “12”. + +Hint: Experiment with the calculator on your system to discover how it works. + +Here are some optional extensions to consider: + +- Style the label to make the result bigger. +- Style the digit buttons so they are bigger. +- Add an event handler such that user can press the keys 0 to 9 on the keyboard with the same effect as using the buttons. +- Add a try-catch block to detect division by zero and show an appropriate alert message. +- Add an event handler such that the escape key resets the calculator. + +#embedClass(name: "Calculator") \ No newline at end of file