Converted the login dialog into a scene

This commit is contained in:
Kai S. K. Engelbart 2020-06-08 11:58:57 +02:00
parent 232439a564
commit c0d814ed38
5 changed files with 187 additions and 158 deletions

View File

@ -29,10 +29,45 @@ import envoy.event.EventBus;
*/
public final class SceneContext {
/**
* Contains information about different scenes and their FXML resource files.
*
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public static enum SceneInfo {
CHAT_SCENE("/fxml/ChatScene.fxml"), SETTINGS_SCENE("/fxml/SettingsScene.fxml"), CONTACT_SEARCH_SCENE("/fxml/ContactSearchScene.fxml");
/**
* The main scene in which chats are displayed.
*
* @since Envoy Client v0.1-beta
*/
CHAT_SCENE("/fxml/ChatScene.fxml"),
/**
* The scene in which settings are displayed.
*
* @since Envoy Client v0.1-beta
*/
SETTINGS_SCENE("/fxml/SettingsScene.fxml"),
/**
* The scene in which the contact search is displayed.
*
* @since Envoy Client v0.1-beta
*/
CONTACT_SEARCH_SCENE("/fxml/ContactSearchScene.fxml"),
/**
* The scene in which the login screen is displayed.
*
* @since Envoy Client v0.1-beta
*/
LOGIN_SCENE("/fxml/LoginScene.fxml");
/**
* The path to the FXML resource.
*/
public final String path;
SceneInfo(String path) { this.path = path; }
@ -59,20 +94,24 @@ public final class SceneContext {
* Loads a new scene specified by a scene info.
*
* @param sceneInfo specifies the scene to load
* @throws IOException if the loading process fails
* @throws RuntimeException if the loading process fails
* @since Envoy Client v0.1-beta
*/
public void load(SceneInfo sceneInfo) throws IOException {
public void load(SceneInfo sceneInfo) {
loader.setRoot(null);
loader.setController(null);
final var rootNode = (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
final var scene = new Scene(rootNode);
try {
final var rootNode = (Parent) loader.load(getClass().getResourceAsStream(sceneInfo.path));
final var scene = new Scene(rootNode);
sceneStack.push(scene);
stage.setScene(scene);
applyCSS();
stage.show();
sceneStack.push(scene);
stage.setScene(scene);
applyCSS();
stage.show();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
@ -104,4 +143,10 @@ public final class SceneContext {
* @since Envoy Client v0.1-beta
*/
public <T> T getController() { return loader.getController(); }
/**
* @return the stage in which the scenes are displayed
* @since Envoy Client v0.1-beta
*/
public Stage getStage() { return stage; }
}

View File

@ -1,7 +1,6 @@
package envoy.client.ui;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Properties;
import java.util.logging.Level;
@ -14,11 +13,9 @@ import javafx.stage.Stage;
import envoy.client.data.*;
import envoy.client.net.Client;
import envoy.client.net.WriteProxy;
import envoy.client.ui.controller.ChatScene;
import envoy.client.ui.SceneContext.SceneInfo;
import envoy.client.ui.controller.LoginScene;
import envoy.data.Message;
import envoy.data.User;
import envoy.data.User.UserStatus;
import envoy.exception.EnvoyException;
import envoy.util.EnvoyLog;
@ -34,7 +31,6 @@ public final class Startup extends Application {
private LocalDB localDB;
private Client client;
private WriteProxy writeProxy;
private Cache<Message> cache;
private static final ClientConfig config = ClientConfig.getInstance();
@ -86,53 +82,12 @@ public final class Startup extends Application {
client = new Client();
cache = new Cache<>();
// Try to connect to the server
new LoginDialog(client, localDB, cache).showAndWait();
// Set client user in local database
localDB.setUser(client.getSender());
// Initialize chats in local database
try {
localDB.initializeUserStorage();
localDB.loadUserData();
} catch (final FileNotFoundException e) {
// The local database file has not yet been created, probably first login
} catch (final Exception e) {
e.printStackTrace();
new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait();
}
// Initialize write proxy
writeProxy = client.createWriteProxy(localDB);
if (client.isOnline()) {
// Save all users to the local database and flush cache
localDB.setUsers(client.getUsers());
writeProxy.flushCache();
} else
// Set all contacts to offline mode
localDB.getUsers()
.values()
.stream()
.filter(User.class::isInstance)
.map(User.class::cast)
.forEach(u -> u.setStatus(UserStatus.OFFLINE));
// Prepare stage and load ChatScene
final var sceneContext = new SceneContext(stage);
sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE);
sceneContext.<ChatScene>getController().initializeData(sceneContext, localDB, client, writeProxy);
stage.setTitle("Envoy");
stage.setMinHeight(400);
stage.setMinWidth(350);
stage.getIcons().add(IconUtil.load("/icons/envoy_logo.png"));
stage.show();
// Relay unread messages from cache
if (cache != null && client.isOnline()) cache.relay();
final var sceneContext = new SceneContext(stage);
sceneContext.load(SceneInfo.LOGIN_SCENE);
sceneContext.<LoginScene>getController().initializeData(client, localDB, cache, sceneContext);
}
/**

View File

@ -119,6 +119,14 @@ public final class ChatScene {
});
}
/**
* @param sceneContext the scene context used to load other scenes
* @param localDB the local database form which chats and users are loaded
* @param client the client used to request ID generators
* @param writeProxy the write proxy used to send messages and other data to
* the server
* @since Envoy Client v0.1-beta
*/
public void initializeData(SceneContext sceneContext, LocalDB localDB, Client client, WriteProxy writeProxy) {
this.sceneContext = sceneContext;
this.localDB = localDB;
@ -158,22 +166,14 @@ public final class ChatScene {
@FXML
private void settingsButtonClicked() {
try {
sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE);
sceneContext.<SettingsScene>getController().initializeData(sceneContext);
} catch (final IOException e) {
e.printStackTrace();
}
sceneContext.load(SceneContext.SceneInfo.SETTINGS_SCENE);
sceneContext.<SettingsScene>getController().initializeData(sceneContext);
}
@FXML
private void addContactButtonClicked() {
try {
sceneContext.load(SceneContext.SceneInfo.CONTACT_SEARCH_SCENE);
sceneContext.<ContactSearchScene>getController().initializeData(sceneContext);
} catch (final IOException e) {
e.printStackTrace();
}
sceneContext.load(SceneContext.SceneInfo.CONTACT_SEARCH_SCENE);
sceneContext.<ContactSearchScene>getController().initializeData(sceneContext);
}
@FXML

View File

@ -1,24 +1,24 @@
package envoy.client.ui;
package envoy.client.ui.controller;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import java.util.logging.Logger;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
import javafx.scene.control.Alert.AlertType;
import javafx.stage.Stage;
import envoy.client.data.Cache;
import envoy.client.data.ClientConfig;
import envoy.client.data.LocalDB;
import envoy.client.net.Client;
import envoy.client.ui.SceneContext;
import envoy.data.LoginCredentials;
import envoy.data.Message;
import envoy.data.User;
import envoy.data.User.UserStatus;
import envoy.event.EventBus;
import envoy.event.HandshakeRejectionEvent;
import envoy.exception.EnvoyException;
@ -32,7 +32,7 @@ import envoy.util.EnvoyLog;
* @author Kai S. K. Engelbart
* @since Envoy Client v0.1-beta
*/
public final class LoginDialog extends Dialog<Void> {
public final class LoginScene {
@FXML
private TextField userTextField;
@ -49,77 +49,18 @@ public final class LoginDialog extends Dialog<Void> {
@FXML
private CheckBox registerCheckBox;
@FXML
private CheckBox offlineCheckBox;
@FXML
private Label connectionLabel;
private final Client client;
private final LocalDB localDB;
private final Cache<Message> receivedMessageCache;
private Client client;
private LocalDB localDB;
private Cache<Message> receivedMessageCache;
private SceneContext sceneContext;
private static final Logger logger = EnvoyLog.getLogger(LoginDialog.class);
private static final Logger logger = EnvoyLog.getLogger(LoginScene.class);
private static final EventBus eventBus = EventBus.getInstance();
private static final ClientConfig config = ClientConfig.getInstance();
/**
* Loads the login dialog using the FXML file {@code LoginDialog.fxml}.
*
* @param client the client used to perform the handshake
* @param localDB the local database used for offline login
* @param receivedMessageCache the cache storing messages received during
* the handshake
* @throws IOException if an exception occurs during loading
* @since Envoy Client v0.1-beta
*/
public LoginDialog(Client client, LocalDB localDB, Cache<Message> receivedMessageCache) throws IOException {
this.client = client;
this.localDB = localDB;
this.receivedMessageCache = receivedMessageCache;
// Prepare handshake
localDB.loadIDGenerator();
final var loader = new FXMLLoader(getClass().getResource("/fxml/LoginDialog.fxml"));
loader.setController(this);
final var dialogPane = loader.<DialogPane>load();
((Stage) getDialogPane().getScene().getWindow()).getIcons().add(IconUtil.load("/icons/envoy_logo.png"));
// Configure dialog buttons
dialogPane.getButtonTypes().addAll(ButtonType.CLOSE, ButtonType.OK);
// Close button
dialogPane.lookupButton(ButtonType.CLOSE).addEventHandler(ActionEvent.ACTION, e -> abortLogin());
// Login button
final var loginButton = (Button) dialogPane.lookupButton(ButtonType.OK);
loginButton.setText("Login");
loginButton.addEventFilter(ActionEvent.ACTION, e -> {
e.consume();
// Prevent registration with unequal passwords
if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) {
clearPasswordFields();
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
} else {
final var credentials = new LoginCredentials(userTextField.getText(), passwordField.getText().toCharArray(),
registerCheckBox.isSelected());
if (!offlineCheckBox.isSelected()) performHandshake(credentials);
else attemptOfflineMode(credentials);
}
});
// Perform automatic login if configured
setOnShown(e -> { if (config.hasLoginCredentials()) performHandshake(config.getLoginCredentials()); });
setDialogPane(dialogPane);
// Set initial cursor
Platform.runLater(userTextField::requestFocus);
}
@FXML
private void initialize() {
connectionLabel.setText("Server: " + config.getServer() + ":" + config.getPort());
@ -129,6 +70,50 @@ public final class LoginDialog extends Dialog<Void> {
e -> Platform.runLater(() -> { clearPasswordFields(); new Alert(AlertType.ERROR, e.get()).showAndWait(); }));
}
/**
* Loads the login dialog using the FXML file {@code LoginDialog.fxml}.
*
* @param client the client used to perform the handshake
* @param localDB the local database used for offline login
* @param receivedMessageCache the cache storing messages received during
* the handshake
* @param sceneContext the scene context used to initialize the chat
* scene
* @since Envoy Client v0.1-beta
*/
public void initializeData(Client client, LocalDB localDB, Cache<Message> receivedMessageCache, SceneContext sceneContext) {
this.client = client;
this.localDB = localDB;
this.receivedMessageCache = receivedMessageCache;
this.sceneContext = sceneContext;
// Prepare handshake
localDB.loadIDGenerator();
// Set initial cursor
userTextField.requestFocus();
// Perform automatic login if configured
if (config.hasLoginCredentials()) performHandshake(config.getLoginCredentials());
}
@FXML
private void loginButtonPressed() {
// Prevent registration with unequal passwords
if (registerCheckBox.isSelected() && !passwordField.getText().equals(repeatPasswordField.getText())) {
clearPasswordFields();
new Alert(AlertType.ERROR, "The entered password is unequal to the repeated one").showAndWait();
} else {
performHandshake(new LoginCredentials(userTextField.getText(), passwordField.getText().toCharArray(), registerCheckBox.isSelected()));
}
}
@FXML
private void offlineModeButtonPressed() {
attemptOfflineMode(new LoginCredentials(userTextField.getText(), passwordField.getText().toCharArray(), false));
}
@FXML
private void registerCheckboxChanged() {
@ -139,12 +124,18 @@ public final class LoginDialog extends Dialog<Void> {
clearPasswordFields();
}
@FXML
private void abortLogin() {
logger.info("The login process has been cancelled. Exiting...");
System.exit(0);
}
private void performHandshake(LoginCredentials credentials) {
try {
client.performHandshake(credentials, receivedMessageCache);
if (client.isOnline()) {
client.initReceiver(localDB, receivedMessageCache);
Platform.runLater(this::hide);
loadChatScene();
}
} catch (IOException | InterruptedException | TimeoutException e) {
logger.warning("Could not connect to server: " + e);
@ -161,20 +152,54 @@ public final class LoginDialog extends Dialog<Void> {
if (clientUser == null) throw new EnvoyException("Could not enter offline mode: user name unknown");
client.setSender(clientUser);
new Alert(AlertType.WARNING, "A connection to the server could not be established. Starting in offline mode.").showAndWait();
hide();
loadChatScene();
} catch (Exception e) {
new Alert(AlertType.ERROR, "Client error: " + e).showAndWait();
System.exit(1);
}
}
private void loadChatScene() {
// Set client user in local database
localDB.setUser(client.getSender());
// Initialize chats in local database
try {
localDB.initializeUserStorage();
localDB.loadUserData();
} catch (final FileNotFoundException e) {
// The local database file has not yet been created, probably first login
} catch (final Exception e) {
e.printStackTrace();
new Alert(AlertType.ERROR, "Error while loading local database: " + e + "\nChats will not be stored locally.").showAndWait();
}
// Initialize write proxy
final var writeProxy = client.createWriteProxy(localDB);
if (client.isOnline()) {
// Save all users to the local database and flush cache
localDB.setUsers(client.getUsers());
writeProxy.flushCache();
} else
// Set all contacts to offline mode
localDB.getUsers().values().stream().filter(User.class::isInstance).map(User.class::cast).forEach(u -> u.setStatus(UserStatus.OFFLINE));
// Load ChatScene
sceneContext.pop();
sceneContext.getStage().setMinHeight(400);
sceneContext.getStage().setMinWidth(350);
sceneContext.load(SceneContext.SceneInfo.CHAT_SCENE);
sceneContext.<ChatScene>getController().initializeData(sceneContext, localDB, client, writeProxy);
// Relay unread messages from cache
if (receivedMessageCache != null && client.isOnline()) receivedMessageCache.relay();
}
private void clearPasswordFields() {
passwordField.clear();
repeatPasswordField.clear();
}
private void abortLogin() {
logger.info("The login process has been cancelled. Exiting...");
System.exit(0);
}
}

View File

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.ButtonBar?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.DialogPane?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.PasswordField?>
<?import javafx.scene.control.TextField?>
@ -11,10 +12,9 @@
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Font?>
<DialogPane prefHeight="265.0" prefWidth="545.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1">
<content>
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="217.0" prefWidth="545.0">
<children>
<VBox prefHeight="206.0" prefWidth="440.0" xmlns="http://javafx.com/javafx/11.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="envoy.client.ui.controller.LoginScene">
<children>
<Label text="User Login">
<font>
<Font size="26.0" />
@ -40,9 +40,13 @@
</children>
</GridPane>
<CheckBox fx:id="registerCheckBox" mnemonicParsing="true" onAction="#registerCheckboxChanged" prefHeight="17.0" prefWidth="181.0" text="_Register" />
<Label fx:id="connectionLabel" />
<CheckBox fx:id="offlineCheckBox" layoutX="20.0" layoutY="144.0" mnemonicParsing="true" prefHeight="17.0" prefWidth="181.0" text="_Offline mode" />
</children>
</VBox>
</content>
</DialogPane>
<Label fx:id="connectionLabel" />
<ButtonBar prefHeight="40.0" prefWidth="200.0">
<buttons>
<Button mnemonicParsing="false" onAction="#abortLogin" text="Close" />
<Button mnemonicParsing="false" onAction="#offlineModeButtonPressed" text="Offline mode" />
<Button defaultButton="true" mnemonicParsing="false" onAction="#loginButtonPressed" text="Login" />
</buttons>
</ButtonBar>
</children>
</VBox>