Improve UI doc and fix minor UI bugs

This commit is contained in:
Arnaud Vergnet 2021-01-05 10:37:41 +01:00
parent a3873394be
commit 6d3971be40
20 changed files with 541 additions and 185 deletions

View file

@ -43,6 +43,7 @@ public class MainApp extends Application {
@Override
public void stop() throws Exception {
// Stop all threads and active connections before exiting
userList.destroy();
super.stop();
}

View file

@ -15,13 +15,14 @@ public class ChatHistory {
private final DatabaseController db;
private final PeerUser user;
private final ObservableList<Message> history;
private final ArrayList<HistoryLoadedCallback> historyListener;
private HistoryLoadedCallback historyListener;
private boolean historyLoaded;
public ChatHistory(PeerUser user) {
this.user = user;
db = new DatabaseController();
history = FXCollections.observableArrayList();
historyListener = new ArrayList<>();
historyLoaded = false;
}
/**
@ -29,34 +30,29 @@ public class ChatHistory {
*
* @param listener The listener to add
*/
public void addHistoryLoadedListener(HistoryLoadedCallback listener) {
historyListener.add(listener);
}
/**
* Removes a listener for history loaded event
*
* @param listener The listener to remove
*/
public void removeHistoryLoadedListener(HistoryLoadedCallback listener) {
historyListener.remove(listener);
public void setHistoryLoadedListener(HistoryLoadedCallback listener) {
historyListener = listener;
}
/**
* Notifies all listeners of a history loaded event
*/
private void notifyHistoryLoaded() {
historyListener.forEach(l -> l.onHistoryLoaded(user));
historyListener.onHistoryLoaded(user);
}
/**
* Loads history from database
* Loads history from database only if it has not previously been loaded
*/
public void load() {
final Date from = new Date();
// Load whole history
from.setTime(0);
db.getChatHistory(new UserInformation(user), from, new Date(), this::onLoaded);
if (!historyLoaded) {
final Date from = new Date();
// Load whole history
from.setTime(0);
db.getChatHistory(new UserInformation(user), from, new Date(), this::onLoaded);
} else {
notifyHistoryLoaded();
}
}
/**
@ -65,6 +61,7 @@ public class ChatHistory {
* @param newHistory The fetched history
*/
private void onLoaded(ArrayList<Message> newHistory) {
historyLoaded = true;
history.addAll(newHistory);
history.sort((message, t1) -> (int) (message.getDate().getTime() - t1.getDate().getTime()));
Log.v(getClass().getSimpleName(), "Message history loaded");

View file

@ -1,5 +1,8 @@
package fr.insa.clavardator.ui;
/**
* Interface used to create callbacks for button press events
*/
public interface ButtonPressEvent {
public void onPress();
}

View file

@ -7,18 +7,31 @@ import javafx.scene.layout.VBox;
import java.net.URL;
import java.util.ResourceBundle;
/**
* Controller for the error screen
*/
public class ErrorScreenController implements Initializable {
@FXML
private VBox container;
/**
* Instantly shows the error screen
*/
public void show() {
container.setVisible(true);
}
/**
* Instantly hides the error screen
*/
public void hide() {
container.setVisible(false);
}
/**
* Exits the app on button press
*/
@FXML
private void onPress() {
System.exit(0);

View file

@ -6,13 +6,16 @@ import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jetbrains.annotations.Nullable;
import java.net.URL;
import java.util.ResourceBundle;
/**
* Controller for the loading screen
*/
public class LoadingScreenController implements Initializable {
@FXML
public VBox indicatorOnlyContainer;
@FXML
@ -22,27 +25,45 @@ public class LoadingScreenController implements Initializable {
@FXML
public StackPane container;
/**
* Instantly shows the loading screen
*/
public void show() {
container.setVisible(true);
setLabel(null);
}
/**
* Instantly shows the loading screen
* @param label The text to display bellow the loading indicator
*/
public void show(String label) {
container.setVisible(true);
setLabel(label);
}
/**
* Instantly hides the loading screen
*/
public void hide() {
container.setVisible(false);
}
public void setLabel(String label) {
/**
* Sets the text to display bellow the loading indicator
* @param label The text to use
*/
public void setLabel(@Nullable String label) {
loadingLabel.setText(label);
final boolean showLabel = label != null && !label.isEmpty();
labeledContainer.setVisible(showLabel);
indicatorOnlyContainer.setVisible(!showLabel);
}
/**
* Gets the main container styles to apply custom styling
* @return The style list
*/
public ObservableList<String> getRootStyle() {
return container.getStyleClass();
}

View file

@ -48,9 +48,11 @@ public class MainController implements Initializable {
private JFXSnackbar snackbar;
private UserList userList;
private boolean historyLoaded;
private boolean online;
public MainController() {
historyLoaded = false;
online = false;
currentUser = CurrentUser.getInstance();
currentUser.addObserver(propertyChangeEvent -> {
if (propertyChangeEvent.getPropertyName().equals("state")) {
@ -60,6 +62,14 @@ public class MainController implements Initializable {
});
}
/**
* If the current user becomes valid, start the chat.
* If it is invalid or not set, show the login screen.
* <p>
* If the history is not yet loaded or the user is not initialized, do nothing.
*
* @param newState The new user state
*/
private void onCurrentUserStateChange(CurrentUser.State newState) {
// Only do an action if the history is loaded
if (historyLoaded) {
@ -75,15 +85,32 @@ public class MainController implements Initializable {
}
}
/**
* If the history loaded after the current user, fire the state change event again
*/
public void onHistoryLoaded() {
historyLoaded = true;
final CurrentUser.State userState = CurrentUser.getInstance().getState();
// If the history loaded after the current user, fire the state change event again
if (userState != CurrentUser.State.UNINITIALIZED) {
onCurrentUserStateChange(userState);
}
}
/**
* Opens a dialog allowing the user to set its username.
* Depending on the mode, the behavior is different.
* <ul>
* <li>
* On initial mode, canceling the dialog exists the app.
* Any other mode shows the chat.
* </li>
* <li>
* On edit mode, confirming the dialog shows a snackbar.
* </li>
* </ul>
*
* @param mode
*/
private void openEditUsernameDialog(EditUsernameDialogController.Mode mode) {
final boolean initialMode = mode == EditUsernameDialogController.Mode.INITIAL;
editUserDialogController.setOnCancelListener(() -> {
@ -101,6 +128,12 @@ public class MainController implements Initializable {
Platform.runLater(() -> editUserDialogController.show(root, mode));
}
/**
* Shows a snackbar
*
* @param text The text to show
* @param mode The mode to use
*/
private void showSnackbarEvent(String text, SnackbarController.Mode mode) {
try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource("dialogs/snackbar.fxml"));
@ -115,12 +148,19 @@ public class MainController implements Initializable {
}
}
/**
* Opens the about dialog
*/
private void openAboutDialog() {
aboutDialogController.show(root);
}
/**
* Sends a broadcast over the network to discover active users.
* If any error happens, disconnect the user from chat and ask for reconnection.
*/
private void discoverActiveUsers() {
if (userList != null) {
if (userList != null && online) {
userList.discoverActiveUsers((e) -> {
Log.e(this.getClass().getSimpleName(), "Error discovering users", e);
CurrentUser.getInstance().setState(CurrentUser.State.INVALID);
@ -128,6 +168,10 @@ public class MainController implements Initializable {
}
}
/**
* Start listening to broadcast and chat requests.
* If any error happens, disconnect the user from chat and ask for reconnection.
*/
private void startListening() {
if (userList != null) {
userList.startDiscoveryListening();
@ -138,30 +182,46 @@ public class MainController implements Initializable {
}
}
private void stopListening() {
if (userList != null) {
userList.destroy();
}
}
/**
* Starts network related functions to allow for chat functionality.
*/
private void startChat() {
online = true;
listController.setRefreshButtonEnabled(true);
Log.v(this.getClass().getSimpleName(), "Chat started");
discoverActiveUsers();
startListening();
Platform.runLater(this::showChat);
}
/**
* Stops any network related functions to disable chat functionality.
*/
private void endChat() {
online = false;
listController.setRefreshButtonEnabled(false);
Log.v(this.getClass().getSimpleName(), "Chat ended");
stopListening();
if (userList != null) {
userList.destroy();
}
}
/**
* Simply shows the chat.
* This does not start any network related functions.
* Use this for offline browsing.
*/
private void showChat() {
Log.v(this.getClass().getSimpleName(), "Chat shown");
loadingController.hide();
mainContainer.setVisible(true);
}
/**
* Shows a screen allowing the user to set its username.
*
* @param isError True is the screen is shown because of an error, false otherwise
*/
private void showLogin(boolean isError) {
Log.v(this.getClass().getSimpleName(), "Login shown");
mainContainer.setVisible(false);
@ -180,6 +240,9 @@ public class MainController implements Initializable {
}
}
/**
* Shows an error screen telling the user the app failed to start
*/
private void showError() {
Log.v(this.getClass().getSimpleName(), "Error shown");
mainContainer.setVisible(false);
@ -193,12 +256,15 @@ public class MainController implements Initializable {
snackbar = new JFXSnackbar(root);
listController.setUserSelectedListener((user) -> chatController.setRemoteUser(user));
chatController.addAttachmentListener(() -> System.out.println("attach event"));
chatController.addSendErrorListener((e) -> showSnackbarEvent("Erreur: Message non envoyé", SnackbarController.Mode.ERROR));
toolbarController.addEditListener(() -> openEditUsernameDialog(EditUsernameDialogController.Mode.EDIT));
toolbarController.addAboutListener(this::openAboutDialog);
chatController.setAttachmentListener(() -> System.out.println("attach event"));
chatController.setSendErrorListener((e) -> showSnackbarEvent("Erreur: Message non envoyé", SnackbarController.Mode.ERROR));
toolbarController.setEditListener(() -> openEditUsernameDialog(EditUsernameDialogController.Mode.EDIT));
toolbarController.setAboutListener(this::openAboutDialog);
}
/**
* Creates database if needed, then init current user, and finally load user list
*/
private void initDb() {
final DatabaseController db = new DatabaseController();
db.init(() -> {
@ -212,6 +278,12 @@ public class MainController implements Initializable {
});
}
/**
* Sets the user list to use.
* We must set it from the MainApp to allow destroying it on exit.
*
* @param userList The user list to use
*/
public void setUserList(UserList userList) {
this.userList = userList;
listController.setUserList(userList);

View file

@ -4,6 +4,11 @@ import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.MultipleSelectionModel;
/**
* Model used to disable list selection
*
* @param <T>
*/
public class NoSelectionModel<T> extends MultipleSelectionModel<T> {
@Override
public ObservableList<Integer> getSelectedIndices() {

View file

@ -11,29 +11,40 @@ import java.util.ArrayList;
import java.util.List;
import java.util.ResourceBundle;
/**
* Controller for the main screen toolbar
*/
public class ToolbarController implements Initializable {
@FXML
private Label currentUsernameLabel;
private List<ButtonPressEvent> editListeners;
private List<ButtonPressEvent> aboutListeners;
private ButtonPressEvent editListeners;
private ButtonPressEvent aboutListeners;
public void addEditListener(ButtonPressEvent listener) {
editListeners.add(listener);
public void setEditListener(ButtonPressEvent listener) {
editListeners = listener;
}
public void addAboutListener(ButtonPressEvent listener) {
aboutListeners.add(listener);
public void setAboutListener(ButtonPressEvent listener) {
aboutListeners = listener;
}
public void onEditPress() {
editListeners.forEach(ButtonPressEvent::onPress);
if (editListeners != null) {
editListeners.onPress();
}
}
public void onAboutPress() {
aboutListeners.forEach(ButtonPressEvent::onPress);
if (aboutListeners != null) {
aboutListeners.onPress();
}
}
/**
* Update the text on current username change
*
* @param propertyChangeEvent The change event
*/
private void onUsernameChange(PropertyChangeEvent propertyChangeEvent) {
if (propertyChangeEvent.getPropertyName().equals("username")) {
final String newUsername = (String) propertyChangeEvent.getNewValue();
@ -44,7 +55,5 @@ public class ToolbarController implements Initializable {
@Override
public void initialize(URL location, ResourceBundle resources) {
CurrentUser.getInstance().addObserver(this::onUsernameChange);
editListeners = new ArrayList<>();
aboutListeners = new ArrayList<>();
}
}

View file

@ -17,6 +17,9 @@ import javafx.scene.layout.VBox;
import java.net.URL;
import java.util.ResourceBundle;
/**
* Controller for the chat window
*/
public class ChatController implements Initializable {
@FXML
@ -32,40 +35,50 @@ public class ChatController implements Initializable {
@FXML
private VBox emptyContainer;
private PeerUser remoteUser;
private final ChatHistory.HistoryLoadedCallback onHistoryLoaded = (PeerUser user) -> {
public void setAttachmentListener(ButtonPressEvent listener) {
chatFooterController.setAttachmentListener(listener);
}
public void setSendErrorListener(ErrorCallback listener) {
chatFooterController.setSendErrorListener(listener);
}
/**
* Check the user that finished loading is the right one then set the chat state to done
* @param user The user that finished loading
*/
private void onHistoryLoaded (PeerUser user) {
if (user.equals(remoteUser)) {
setState(State.DONE);
scrollToEnd();
}
};
public void addAttachmentListener(ButtonPressEvent listener) {
chatFooterController.addAttachmentListener(listener);
}
public void addSendErrorListener(ErrorCallback listener) {
chatFooterController.addSendErrorListener(listener);
}
/**
* Sets the remote user to chat with and subscribe to its messages
*
* @param remoteUser The user to chat with
*/
public void setRemoteUser(PeerUser remoteUser) {
this.chatFooterController.setRemoteUser(remoteUser);
this.chatHeaderController.setRemoteUser(remoteUser);
setState(State.LOADING);
if (this.remoteUser != null) {
this.remoteUser.getHistory().removeHistoryLoadedListener(onHistoryLoaded);
}
this.remoteUser = remoteUser;
final ChatHistory history = remoteUser.getHistory();
history.addHistoryLoadedListener(onHistoryLoaded);
history.setHistoryLoadedListener(this::onHistoryLoaded);
messageList.setItems(history.getHistory());
messageList.getItems().addListener((ListChangeListener<? super Message>) (c) -> {
c.next();
// Make sure we always have the latest item on screen
scrollToEnd();
});
history.load();
}
/**
* Scroll to the end of the message list
*/
private void scrollToEnd() {
final int size = messageList.getItems().size();
if (size > 0) {
@ -73,11 +86,26 @@ public class ChatController implements Initializable {
}
}
/**
* Sets the chat state.
* <ul>
* <li>
* On Initial state, show an empty screen
* </li>
* <li>
* on Loading state, show the loading screen
* </li>
* <li>
* on Done state, show the chat
* </li>
* </ul>
*
* @param state The new state to set
*/
private void setState(State state) {
switch (state) {
case INITIAL:
emptyContainer.setVisible(true);
chatFooterController.setEnabled(false);
chatContainer.setVisible(false);
loadingController.hide();
messageList.setItems(null);
@ -85,11 +113,9 @@ public class ChatController implements Initializable {
case LOADING:
chatContainer.setVisible(true);
loadingController.show();
chatFooterController.setEnabled(false);
emptyContainer.setVisible(false);
break;
case DONE:
chatFooterController.setEnabled(true);
chatContainer.setVisible(true);
emptyContainer.setVisible(false);
loadingController.hide();

View file

@ -6,18 +6,21 @@ import fr.insa.clavardator.ui.ButtonPressEvent;
import fr.insa.clavardator.users.PeerUser;
import fr.insa.clavardator.util.ErrorCallback;
import fr.insa.clavardator.util.Log;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox;
import java.beans.PropertyChangeEvent;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.ResourceBundle;
/**
* Controller for the chat input field and associated buttons
*/
public class ChatFooterController implements Initializable {
@FXML
@ -27,25 +30,38 @@ public class ChatFooterController implements Initializable {
@FXML
private JFXButton sendButton;
private List<ButtonPressEvent> attachmentListeners;
private List<ErrorCallback> sendErrorListeners;
private ButtonPressEvent attachmentListeners;
private ErrorCallback sendErrorListeners;
private PeerUser remoteUser;
private HashMap<PeerUser, String> savedText;
public void addAttachmentListener(ButtonPressEvent listener) {
attachmentListeners.add(listener);
public void setAttachmentListener(ButtonPressEvent listener) {
attachmentListeners = listener;
}
public void addSendErrorListener(ErrorCallback listener) {
sendErrorListeners.add(listener);
public void setSendErrorListener(ErrorCallback listener) {
sendErrorListeners = listener;
}
public void onAttachmentPress() {
attachmentListeners.forEach(ButtonPressEvent::onPress);
if (attachmentListeners != null) {
attachmentListeners.onPress();
}
}
public void onSendError(Exception e) {
Log.e(this.getClass().getSimpleName(), "Error: Could not send message", e);
if (sendErrorListeners != null) {
sendErrorListeners.onError(e);
}
}
/**
* If the input text is not empty and the remote user set, send the message
*/
public void onSend() {
if(!textField.getText().isEmpty()) {
if (!isTextFieldEmpty()) {
if (remoteUser != null) {
remoteUser.sendTextMessage(textField.getText(), this::onSendError);
} else {
@ -55,15 +71,20 @@ public class ChatFooterController implements Initializable {
}
}
public void onSendError(Exception e) {
Log.e(this.getClass().getSimpleName(), "Error: Could not send message", e);
sendErrorListeners.forEach((l) -> l.onError(e));
}
public void setEnabled(boolean enabled) {
/**
* Enables or disables the chat controls
*
* @param enabled True to enable, false otherwise
*/
private void setEnabled(boolean enabled) {
container.setDisable(!enabled);
}
/**
* Find saved input for the current user
*
* @return The saved text, or an empty string if none found
*/
private String findSavedText() {
String text = null;
if (remoteUser != null) {
@ -72,22 +93,74 @@ public class ChatFooterController implements Initializable {
return text != null ? text : "";
}
/**
* Save the give text for the current user.
* This is used to save input when jumping between users
*
* @param text The text to save
*/
private void saveText(String text) {
if (remoteUser != null) {
savedText.put(remoteUser, text);
}
}
/**
* Saves text and checks if we should disable the send button on input change.
*
* @param observable The observed string
* @param oldText The previous text
* @param newText The new text
*/
public void onTextChange(ObservableValue<? extends String> observable, String oldText, String newText) {
saveText(newText);
sendButton.setDisable(isTextFieldEmpty());
}
/**
* Check if the text field is empty
*
* @return True if empty, false otherwise
*/
private boolean isTextFieldEmpty() {
return textField.getText().isEmpty();
}
/**
* Updates shown username and item color when user changes
*
* @param propertyChangeEvent The change event
*/
private void onUserStateChange(PropertyChangeEvent propertyChangeEvent) {
final String propName = propertyChangeEvent.getPropertyName();
if (propName.equals("state")) {
final PeerUser.State newState = (PeerUser.State) propertyChangeEvent.getNewValue();
Platform.runLater(() -> setEnabled(newState == PeerUser.State.CONNECTED));
}
}
/**
* Sets the remote user to send messages to, and tries to find saved input.
*
* @param user The remote user to set
*/
public void setRemoteUser(PeerUser user) {
if (this.remoteUser == null || !this.remoteUser.equals(user)) {
if (this.remoteUser != null) {
// remove old observer before setting new user
this.remoteUser.removeObserver(this::onUserStateChange);
}
this.remoteUser = user;
user.addObserver(this::onUserStateChange);
textField.setText(findSavedText());
sendButton.setDisable(isTextFieldEmpty());
setEnabled(user.isActive());
}
}
@Override
public void initialize(URL location, ResourceBundle resources) {
savedText = new HashMap<>();
attachmentListeners = new ArrayList<>();
sendErrorListeners = new ArrayList<>();
textField.textProperty().addListener(this::onTextChange);
textField.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) {
@ -96,14 +169,4 @@ public class ChatFooterController implements Initializable {
}
});
}
private boolean isTextFieldEmpty() {
return textField.getText().equals("");
}
public void setRemoteUser(PeerUser remoteUser) {
this.remoteUser = remoteUser;
textField.setText(findSavedText());
sendButton.setDisable(isTextFieldEmpty());
}
}

View file

@ -12,6 +12,9 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.ResourceBundle;
/**
* Controller for the chat title
*/
public class ChatHeaderController implements Initializable {
@FXML
@ -20,10 +23,11 @@ public class ChatHeaderController implements Initializable {
private UserActiveIndicatorController indicatorController;
private PeerUser user;
@Override
public void initialize(URL location, ResourceBundle resources) {
}
/**
* Updates username on remote user change
*
* @param propertyChangeEvent THe change event
*/
private void onUsernameChange(PropertyChangeEvent propertyChangeEvent) {
if (propertyChangeEvent.getPropertyName().equals("username")) {
final String newUsername = (String) propertyChangeEvent.getNewValue();
@ -31,6 +35,11 @@ public class ChatHeaderController implements Initializable {
}
}
/**
* Sets the remote user we are currently chatting to
*
* @param remoteUser The remote user to set
*/
public void setRemoteUser(PeerUser remoteUser) {
if (this.user != null) {
// remove old observer before setting new user
@ -42,4 +51,7 @@ public class ChatHeaderController implements Initializable {
indicatorController.setUser(remoteUser);
indicatorController.setSize(10.0);
}
@Override
public void initialize(URL location, ResourceBundle resources) {}
}

View file

@ -24,25 +24,35 @@ public class MessageListItemController implements Initializable {
@FXML
private Label timestamp;
@Override
public void initialize(URL url, ResourceBundle rb) {
}
/**
* Sets the message to display
*
* @param message The message to display
*/
public void setMessage(Message message) {
if (!message.equals(currentMessage)) {
currentMessage = message;
button.setText(message.getText());
timestamp.setText(DateFormat.getTimeInstance().format(message.getDate()));
clearBackground();
if (CurrentUser.getInstance().isLocalId(message.getSender().id)) {
container.setAlignment(Pos.CENTER_RIGHT);
button.getStyleClass().remove("message-other");
button.getStyleClass().add("message-self");
} else {
container.setAlignment(Pos.CENTER_LEFT);
button.getStyleClass().remove("message-self");
button.getStyleClass().add("message-other");
}
}
}
/**
* Removes any style applied to the background
*/
private void clearBackground() {
button.getStyleClass().remove("message-self");
button.getStyleClass().remove("message-other");
}
@Override
public void initialize(URL url, ResourceBundle rb) {}
}

View file

@ -1,22 +1,16 @@
package fr.insa.clavardator.ui.dialogs;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog;
import com.jfoenix.controls.JFXTextField;
import com.jfoenix.validation.base.ValidatorBase;
import fr.insa.clavardator.ui.ButtonPressEvent;
import javafx.beans.property.ReadOnlyBooleanWrapper;
import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import java.net.URL;
import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
/**
* Controller for the about dialog
*/
public class AboutDialogController implements Initializable {
@FXML

View file

@ -18,14 +18,11 @@ import javafx.scene.layout.StackPane;
import java.net.URL;
import java.util.ResourceBundle;
/**
* Controller for the username edit dialog
*/
public class EditUsernameDialogController implements Initializable {
enum State {
VALID,
INVALID,
NETWORK
}
@FXML
private Label titleLabel;
@FXML
@ -45,8 +42,6 @@ public class EditUsernameDialogController implements Initializable {
private ButtonPressEvent cancelListener;
private UserList userList;
private final int MAX_LENGTH = 16;
@FXML
private void onConfirm() {
setLocked(true);
@ -76,6 +71,16 @@ public class EditUsernameDialogController implements Initializable {
this.cancelListener = listener;
}
/**
* Plays the dialog show animation
*
* @implNote WARNING: Do not try to open the dialog instantly after closing it.
* Due to JFoenix animations, the dialog will not reopen.
* If you must, wait at least 500ms before reopening.
*
* @param root The dialog's root component
* @param mode The dialog display mode
*/
public void show(StackPane root, Mode mode) {
setLocked(false);
setFieldError(State.VALID);
@ -84,10 +89,22 @@ public class EditUsernameDialogController implements Initializable {
dialog.show(root);
}
/**
* Hides the dialog.
*
* @implNote WARNING: Do not try to open the dialog instantly after closing it.
* Due to JFoenix animations, the dialog will not reopen.
* If you must, wait at least 500ms before reopening.
*/
public void hide() {
dialog.close();
}
/**
* Locks the dialog on screen or frees it.
*
* @param state True to lock, false to unlock
*/
private void setLocked(boolean state) {
confirmButton.setDisable(state || textField.getText().isEmpty());
cancelButton.setDisable(state);
@ -95,20 +112,38 @@ public class EditUsernameDialogController implements Initializable {
textField.setDisable(state);
}
/**
* Sets the input error
*
* @param state The input state to set
*/
private void setFieldError(State state) {
currentState = state;
textField.validate();
}
/**
* Enables save button if username is not empty, and stop input at max length.
*
* @param observable The observed string
* @param oldText The previous text
* @param newText The new text
*/
public void onUsernameChange(ObservableValue<? extends String> observable, String oldText, String newText) {
setFieldError(State.VALID);
confirmButton.setDisable(newText.isEmpty());
final int MAX_LENGTH = 16;
if (textField.getText().length() > MAX_LENGTH) {
String s = textField.getText().substring(0, MAX_LENGTH);
textField.setText(s);
}
}
/**
* Sets the dialog display mode
*
* @param mode The mode to set
*/
public void setMode(Mode mode) {
switch (mode) {
case INITIAL:
@ -132,6 +167,32 @@ public class EditUsernameDialogController implements Initializable {
}
}
/**
* Propagates the username change to active users and unlocks the dialog.
*/
private void onSuccess() {
setFieldError(State.VALID);
setLocked(false);
hide();
final String newName = textField.getText();
CurrentUser.getInstance().setUsername(newName);
if (successListener != null) {
successListener.onPress();
}
if (userList != null) {
userList.propagateUsernameChange();
}
}
/**
* Sets the field error and unlocks the dialog
*/
private void onError() {
validator.setMessage("Nom d'utilisateur déjà utilisé");
setFieldError(State.INVALID);
setLocked(false);
}
@Override
public void initialize(URL location, ResourceBundle resources) {
validator = new Validator();
@ -149,33 +210,17 @@ public class EditUsernameDialogController implements Initializable {
});
}
private void onSuccess() {
setFieldError(State.VALID);
setLocked(false);
hide();
final String newName = textField.getText();
CurrentUser.getInstance().setUsername(newName);
if (successListener != null) {
successListener.onPress();
}
if (userList != null) {
userList.propagateUsernameChange();
}
}
private void onError() {
validator.setMessage("Nom d'utilisateur invalide");
setFieldError(State.INVALID);
setLocked(false);
}
public enum Mode {
INITIAL,
ERROR,
EDIT
}
enum State {
VALID,
INVALID
}
class Validator extends ValidatorBase {
@Override
protected void eval() {

View file

@ -12,6 +12,9 @@ import java.net.URL;
import java.util.Arrays;
import java.util.ResourceBundle;
/**
* Controller for the notification snackbar
*/
public class SnackbarController implements Initializable {
@FXML
@ -21,10 +24,20 @@ public class SnackbarController implements Initializable {
@FXML
public HBox container;
/**
* Sets the snackbar text
*
* @param text The text to display
*/
public void setText(String text) {
label.setText(text);
}
/**
* Sets the snackbar display mode
*
* @param mode The mode to set
*/
public void setMode(Mode mode) {
ObservableList<String> styleClasses = container.getStyleClass();
styleClasses.clear();
@ -52,6 +65,7 @@ public class SnackbarController implements Initializable {
@Override
public void initialize(URL location, ResourceBundle resources) {
setMode(Mode.INFO);
// Set a small shadow bellow the snackbar
JFXDepthManager.setDepth(container, 1);
}

View file

@ -1,7 +1,6 @@
package fr.insa.clavardator.ui.users;
import fr.insa.clavardator.users.PeerUser;
import fr.insa.clavardator.util.Log;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
@ -11,17 +10,20 @@ import java.beans.PropertyChangeEvent;
import java.net.URL;
import java.util.ResourceBundle;
/**
* Controller for the user indicator, showing the connection status
*/
public class UserActiveIndicatorController implements Initializable {
@FXML
private Circle circle;
private PeerUser user;
@Override
public void initialize(URL location, ResourceBundle resources) {
}
/**
* Sets the new circle color
*
* @param isActive True for active color, false for inactive
*/
private void updateState(boolean isActive) {
circle.getStyleClass().clear();
if (isActive) {
@ -31,6 +33,12 @@ public class UserActiveIndicatorController implements Initializable {
}
}
/**
* Update the indicator color on user state change.
* The indicator becomes active only if the user state goes to connected.
*
* @param propertyChangeEvent The change event
*/
private void onStateChange(PropertyChangeEvent propertyChangeEvent) {
if (propertyChangeEvent.getPropertyName().equals("state")) {
final PeerUser.State newState = (PeerUser.State) propertyChangeEvent.getNewValue();
@ -38,6 +46,11 @@ public class UserActiveIndicatorController implements Initializable {
}
}
/**
* Sets the user associated to this indicator
*
* @param user The user to set
*/
public void setUser(PeerUser user) {
if (this.user != null) {
// remove old observer before setting new user
@ -48,8 +61,16 @@ public class UserActiveIndicatorController implements Initializable {
updateState(user.isActive());
}
/**
* Sets the indicator's radius
*
* @param value The radius in pixels
*/
public void setSize(double value) {
circle.setRadius(value);
}
@Override
public void initialize(URL location, ResourceBundle resources) {}
}

View file

@ -1,5 +1,6 @@
package fr.insa.clavardator.ui.users;
import com.jfoenix.controls.JFXButton;
import fr.insa.clavardator.ui.ButtonPressEvent;
import fr.insa.clavardator.ui.UserSelectedEvent;
import fr.insa.clavardator.users.PeerUser;
@ -15,26 +16,34 @@ import java.net.URL;
import java.util.ResourceBundle;
public class UserListController implements Initializable {
@FXML
private ListView<PeerUser> peerUserListView;
@FXML
private JFXButton refreshButton;
private ButtonPressEvent refreshUserListener;
private UserSelectedEvent userSelectedListener;
@FXML
private ListView<PeerUser> peerUserListView;
private UserList userList;
public UserListController() {
}
public UserListController() {}
public void setRefreshUserListener(ButtonPressEvent listener) {
refreshUserListener = listener;
}
public void setUserSelectedListener(UserSelectedEvent listener) {
userSelectedListener = listener;
}
public void onRefreshUserListPress() {
refreshUserListener.onPress();
if (refreshUserListener != null) {
refreshUserListener.onPress();
}
}
/**
* Enables or disables the refresh button
* @param enabled True to enable, false otherwise
*/
public void setRefreshButtonEnabled(boolean enabled) {
refreshButton.setDisable(!enabled);
}
private void onUserSelected(@org.jetbrains.annotations.NotNull PeerUser user) {
@ -54,6 +63,10 @@ public class UserListController implements Initializable {
});
}
/**
* Add user to UI if not already present
* @param user The new user to display
*/
private void onUserConnected(PeerUser user) {
Platform.runLater(() -> {
if (!peerUserListView.getItems().contains(user)) {
@ -65,8 +78,11 @@ public class UserListController implements Initializable {
});
}
/**
* Sets the user list to subscribe to
* @param userList The user list to use
*/
public void setUserList(UserList userList) {
this.userList = userList;
userList.addNewUserObserver(this::onUserConnected);
userList.setNewUserObserver(this::onUserConnected);
}
}

View file

@ -19,8 +19,8 @@ public class UserListItemController implements Initializable {
private UserActiveIndicatorController indicatorController;
private ButtonPressEvent listener;
private PeerUser user;
private boolean selected;
public void setOnPressListener(ButtonPressEvent listener) {
this.listener = listener;
@ -32,51 +32,83 @@ public class UserListItemController implements Initializable {
}
}
/**
* Sets the current item selection state
*
* @param selected True to select the item, false otherwise
*/
public void setSelected(boolean selected) {
this.selected = selected;
if (selected)
setBackgroundSelected();
else
resetBackground();
resetBackground(this.user.isActive());
}
@Override
public void initialize(URL location, ResourceBundle resources) {
resetBackground();
}
/**
* Updates shown username and item color when user changes
*
* @param propertyChangeEvent The change event
*/
private void onUsernameChange(PropertyChangeEvent propertyChangeEvent) {
if (propertyChangeEvent.getPropertyName().equals("username")) {
final String propName = propertyChangeEvent.getPropertyName();
if (propName.equals("username")) {
final String newUsername = (String) propertyChangeEvent.getNewValue();
Platform.runLater(() -> button.setText(newUsername));
} else if (propName.equals("state") && !selected) {
final PeerUser.State newState = (PeerUser.State) propertyChangeEvent.getNewValue();
Platform.runLater(() -> resetBackground(newState == PeerUser.State.CONNECTED));
}
}
/**
* Sets the user to subscribe to for this item.
*
* @param user The user to subscribe to
*/
public void setUser(PeerUser user) {
if (this.user != null) {
// remove old observer before setting new user
this.user.removeObserver(this::onUsernameChange);
if (this.user == null || !this.user.equals(user)) {
if (this.user != null) {
// remove old observer before setting new user
this.user.removeObserver(this::onUsernameChange);
}
this.user = user;
user.addObserver(this::onUsernameChange);
indicatorController.setUser(user);
button.setText(user.getUsername());
setSelected(false);
}
this.user = user;
user.addObserver(this::onUsernameChange);
indicatorController.setUser(user);
button.setText(user.getUsername());
resetBackground();
}
private void resetBackground() {
if (user != null && user.isActive()) {
button.getStyleClass().remove("inactive-user-item");
/**
* Sets the background color to active or inactive
*/
private void resetBackground(boolean isActive) {
clearBackground();
if (isActive) {
button.getStyleClass().add("active-user-item");
} else {
button.getStyleClass().remove("active-user-item");
button.getStyleClass().add("inactive-user-item");
}
}
/**
* Sets the background color in selected mode
*/
private void setBackgroundSelected() {
button.getStyleClass().remove("inactive-user-item");
button.getStyleClass().remove("active-user-item");
clearBackground();
button.getStyleClass().add("selected-user-item");
}
/**
* Clears the background of any color class
*/
private void clearBackground() {
button.getStyleClass().remove("inactive-user-item");
button.getStyleClass().remove("active-user-item");
button.getStyleClass().remove("selected-user-item");
}
@Override
public void initialize(URL location, ResourceBundle resources) {}
}

View file

@ -19,7 +19,7 @@ public class UserList {
private final Map<Integer, PeerUser> inactiveUsers = new HashMap<>();
private final Map<Integer, PeerUser> activeUsers = new HashMap<>();
private final ArrayList<UserConnectionCallback> newUsersObservers = new ArrayList<>();
private UserConnectionCallback newUsersObservers = null;
private final DatabaseController db = new DatabaseController();
private final NetDiscoverer netDiscoverer = new NetDiscoverer();
@ -33,8 +33,8 @@ public class UserList {
*
* @param connectionCallback The function to add as listener
*/
public void addNewUserObserver(UserConnectionCallback connectionCallback) {
newUsersObservers.add(connectionCallback);
public void setNewUserObserver(UserConnectionCallback connectionCallback) {
newUsersObservers = connectionCallback;
}
/**
@ -43,7 +43,9 @@ public class UserList {
* @param user The newly connected user
*/
private void notifyNewUserObservers(PeerUser user) {
newUsersObservers.forEach(callback -> callback.onUserConnected(user));
if (newUsersObservers != null) {
newUsersObservers.onUserConnected(user);
}
}
/**
@ -52,7 +54,7 @@ public class UserList {
* Observers are notified for each new successful connection.
*
* @param errorCallback The function to call on error
* @see UserList#addNewUserObserver(UserConnectionCallback)
* @see UserList#setNewUserObserver(UserConnectionCallback)
*/
public void discoverActiveUsers(ErrorCallback errorCallback) {
netDiscoverer.discoverActiveUsers("CLAVARDATOR_BROADCAST", (ipAddr, data) -> {

View file

@ -10,7 +10,7 @@
<VBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<HBox alignment="CENTER" prefHeight="64.0" spacing="10.0">
<JFXButton mnemonicParsing="false" text="Utilisateurs" onMouseClicked="#onRefreshUserListPress">
<JFXButton mnemonicParsing="false" text="Utilisateurs" onMouseClicked="#onRefreshUserListPress" fx:id="refreshButton">
<graphic>
<FontIcon iconLiteral="fas-sync-alt" iconSize="24"/>
</graphic>