-
Notifications
You must be signed in to change notification settings - Fork 0
Model #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Model #2
Changes from all commits
ba9d641
87e581f
906b4b2
404b1f1
16b1396
5de28bf
61b6033
ffa9303
05dae89
3270b81
1d1a4b2
e323a3c
c37ddfa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| target/ | ||
| /.idea/ | ||
| .env |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.example; | ||
|
|
||
| import javafx.application.Platform; | ||
|
|
||
| public class FxUtils { | ||
|
|
||
| /** | ||
| * Runs a task on the JavaFX thread. | ||
| * If already on the thread, runs immediately. | ||
| * If not, schedules it to run later. | ||
| * If JavaFX is not initialized, runs on the current thread. | ||
| */ | ||
|
|
||
| public static void runOnFx(Runnable task) { | ||
| try { | ||
| if (Platform.isFxApplicationThread()) { | ||
| task.run(); | ||
| } else { | ||
| Platform.runLater(task); | ||
| } | ||
| } catch (IllegalStateException notInitialized) { | ||
| //headless | ||
| task.run(); | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,156 @@ | ||
| package com.example; | ||
|
|
||
| import javafx.application.Platform; | ||
| import javafx.beans.binding.Bindings; | ||
| import javafx.event.ActionEvent; | ||
| import javafx.fxml.FXML; | ||
| import javafx.scene.control.Label; | ||
| import javafx.geometry.Insets; | ||
| import javafx.geometry.Pos; | ||
| import javafx.scene.control.*; | ||
| import javafx.scene.layout.HBox; | ||
|
|
||
| import java.time.ZoneId; | ||
| import java.time.format.DateTimeFormatter; | ||
|
|
||
| /** | ||
| * Controller layer: mediates between the view (FXML) and the model. | ||
| * Controller for the chat app. | ||
| * <p> | ||
| * Connects the FXML view to the HelloModel. | ||
| * Handles sending messages, changing topics, and updating the UI. | ||
| */ | ||
|
|
||
| public class HelloController { | ||
|
|
||
| private final HelloModel model = new HelloModel(); | ||
| private final HelloModel model = new HelloModel(new NtfyConnectionImpl()); | ||
|
|
||
| @FXML | ||
| private Button sendButton; | ||
|
|
||
| @FXML | ||
| private Label messageLabel; | ||
|
|
||
| @FXML | ||
| private Label topicLabel; | ||
|
|
||
| @FXML | ||
| private ListView<NtfyMessageDto> messageView; | ||
|
|
||
| @FXML | ||
| private TextArea messageInput; | ||
|
|
||
| @FXML | ||
| private TextField topicInput; | ||
|
|
||
| @FXML | ||
| private Button changeTopicButton; | ||
|
|
||
| private final DateTimeFormatter timeFormatter = | ||
| DateTimeFormatter.ofPattern("HH:mm:ss") | ||
| .withZone(ZoneId.systemDefault()); | ||
|
|
||
| @FXML | ||
| private void initialize() { | ||
| if (messageLabel != null) { | ||
| messageLabel.setText(model.getGreeting()); | ||
| messageLabel.setText(model.getGreeting()); | ||
|
|
||
| Platform.runLater(() -> messageInput.requestFocus()); | ||
|
|
||
| topicLabel.setText("/" + model.getCurrentTopic()); | ||
| model.currentTopicProperty().addListener((obs, oldVal, newVal) -> { | ||
| topicLabel.setText("/" + newVal); | ||
| }); | ||
|
|
||
| messageView.setItems(model.getMessages()); | ||
|
|
||
| messageInput.textProperty().bindBidirectional(model.messageToSendProperty()); | ||
|
|
||
| sendButton.disableProperty().bind(Bindings.createBooleanBinding( | ||
| () -> { | ||
| String text = messageInput.getText(); | ||
| return text == null || text.trim().isEmpty(); | ||
| }, | ||
| messageInput.textProperty() | ||
| )); | ||
|
|
||
| if (changeTopicButton != null) { | ||
| changeTopicButton.disableProperty().bind(Bindings.createBooleanBinding( | ||
| () -> { | ||
| String text = topicInput.getText(); | ||
| return text == null || text.trim().isEmpty(); | ||
| }, | ||
| topicInput.textProperty() | ||
| )); | ||
| } | ||
|
|
||
|
|
||
| messageView.setCellFactory(lv -> new ListCell<>() { | ||
| @Override | ||
| protected void updateItem(NtfyMessageDto msg, boolean empty) { | ||
| super.updateItem(msg, empty); | ||
|
|
||
| if (empty || msg == null || msg.message() == null || msg.message().isBlank()) { | ||
| setText(null); | ||
| setGraphic(null); | ||
| } else { | ||
| // Skapa bubble-label | ||
| Label bubble = new Label(msg.message()); | ||
| bubble.setWrapText(true); | ||
| bubble.setMaxWidth(250); | ||
| bubble.setPadding(new Insets(10)); | ||
| bubble.getStyleClass().add("chat-bubble"); // Basstyle | ||
|
|
||
| HBox container = new HBox(bubble); | ||
| container.setPadding(new Insets(5)); | ||
|
|
||
| // Använd CSS-klasser för skickat/mottaget | ||
| if (model.getUserId().equals(msg.id())) { | ||
| bubble.getStyleClass().add("chat-bubble-sent"); | ||
| container.setAlignment(Pos.CENTER_RIGHT); | ||
| } else { | ||
| bubble.getStyleClass().add("chat-bubble-received"); | ||
| container.setAlignment(Pos.CENTER_LEFT); | ||
| } | ||
|
|
||
| setText(null); | ||
| setGraphic(container); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
|
|
||
| // Scrolla ner till senaste meddelandet | ||
| model.getMessages().addListener((javafx.collections.ListChangeListener<NtfyMessageDto>) change -> { | ||
| Platform.runLater(() -> { | ||
| if (!messageView.getItems().isEmpty()) { | ||
| messageView.scrollTo(messageView.getItems().size() - 1); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| @FXML | ||
| private void sendMessage(ActionEvent actionEvent) { | ||
| model.sendMessageAsync(success -> { | ||
| if (success) { | ||
| Platform.runLater(() -> messageInput.clear()); | ||
| Platform.runLater(() -> messageInput.requestFocus()); | ||
| } else { | ||
| Platform.runLater(() -> { | ||
| Alert alert = new Alert(Alert.AlertType.ERROR); | ||
| alert.setTitle("Send Failed"); | ||
| alert.setHeaderText("Failed to send message"); | ||
| alert.setContentText("Could not send your message. Please try again."); | ||
| alert.showAndWait(); | ||
| }); | ||
| } | ||
| }); | ||
| } | ||
|
|
||
| @FXML | ||
| private void changeTopic(ActionEvent actionEvent) { | ||
| String newTopic = topicInput.getText(); | ||
| if (newTopic != null && !newTopic.isBlank()) { | ||
| model.setCurrentTopic(newTopic); | ||
| topicInput.clear(); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,109 @@ | ||
| package com.example; | ||
|
|
||
| import javafx.application.Platform; | ||
| import javafx.beans.property.SimpleStringProperty; | ||
| import javafx.beans.property.StringProperty; | ||
| import javafx.collections.FXCollections; | ||
| import javafx.collections.ObservableList; | ||
|
|
||
| import java.util.function.Consumer; | ||
|
|
||
| import static com.example.FxUtils.runOnFx; | ||
|
|
||
| /** | ||
| * Model layer: encapsulates application data and business logic. | ||
| * Model layer for the chatapp RuneChat. | ||
| * <p> | ||
| * Manages messages, the current topic, and sending/receiving messages via NtfyConnection. | ||
| */ | ||
|
|
||
| public class HelloModel { | ||
| /** | ||
| * Returns a greeting based on the current Java and JavaFX versions. | ||
| */ | ||
|
|
||
| private final NtfyConnection connection; | ||
| private final ObservableList<NtfyMessageDto> messages = FXCollections.observableArrayList(); | ||
| private final StringProperty messageToSend = new SimpleStringProperty(); | ||
| private final StringProperty currentTopic = new SimpleStringProperty(); | ||
|
|
||
| public HelloModel(NtfyConnection connection) { | ||
| this.connection = connection; | ||
| this.currentTopic.set(connection.getCurrentTopic()); | ||
| receiveMessage(); | ||
| } | ||
|
Comment on lines
+26
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changing topic appears to create multiple long‑lived receive streams
As a result, every topic change can leave the previous receive stream running indefinitely:
It would be safer to ensure only one active receive stream per model, e.g. by:
Also applies to: 50-56, 95-100 🤖 Prompt for AI Agents |
||
|
|
||
| public ObservableList<NtfyMessageDto> getMessages() { | ||
| return messages; | ||
| } | ||
|
|
||
| public String getMessageToSend() { | ||
| return messageToSend.get(); | ||
| } | ||
|
|
||
| public StringProperty messageToSendProperty() { | ||
| return messageToSend; | ||
| } | ||
|
|
||
| public void setMessageToSend(String message) { | ||
| messageToSend.set(message); | ||
| } | ||
|
|
||
| public String getCurrentTopic() { | ||
| return currentTopic.get(); | ||
| } | ||
|
|
||
| public StringProperty currentTopicProperty() { | ||
| return currentTopic; | ||
| } | ||
|
|
||
| public void setCurrentTopic(String topic) { | ||
| if (topic != null && !topic.isBlank()) { | ||
| connection.setCurrentTopic(topic); | ||
| this.currentTopic.set(topic); | ||
| messages.clear(); | ||
| receiveMessage(); | ||
| } | ||
| } | ||
|
|
||
| public String getUserId() { | ||
| return connection.getUserId(); | ||
| } | ||
|
|
||
| public String getGreeting() { | ||
| String javaVersion = System.getProperty("java.version"); | ||
| String javafxVersion = System.getProperty("javafx.version"); | ||
| return "Hello, JavaFX " + javafxVersion + ", running on Java " + javaVersion + "."; | ||
| return "RuneChat"; | ||
| } | ||
|
|
||
| public boolean canSendMessage() { | ||
| String msg = messageToSend.get(); | ||
| return msg != null && !msg.isBlank(); | ||
| } | ||
|
|
||
| public void sendMessageAsync(Consumer<Boolean> callback) { | ||
| String msg = messageToSend.get(); | ||
| if (msg == null || msg.isBlank()) { | ||
| System.out.println("Nothing to send!"); | ||
| callback.accept(false); | ||
| return; | ||
| } | ||
|
|
||
| connection.send(msg, success -> { | ||
| if (success) { | ||
| runOnFx(() -> { | ||
| if (msg.equals(messageToSend.get())) { | ||
| messageToSend.set(""); | ||
| } | ||
| }); | ||
| callback.accept(true); | ||
| } else { | ||
| System.out.println("Failed to send message!"); | ||
| callback.accept(false); | ||
| } | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| public void receiveMessage() { | ||
| connection.receive(m -> { | ||
| if (m == null || m.message() == null || m.message().isBlank()) return; | ||
| runOnFx(() -> messages.add(m)); | ||
| }); | ||
| } | ||
|
|
||
|
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace Swedish comments with English.
Code comments should be in English to ensure accessibility for international collaborators and maintainers.
Apply this diff:
Also applies to: 104-104, 120-120
🤖 Prompt for AI Agents