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 @Override
public void stop() throws Exception { public void stop() throws Exception {
// Stop all threads and active connections before exiting
userList.destroy(); userList.destroy();
super.stop(); super.stop();
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -48,9 +48,11 @@ public class MainController implements Initializable {
private JFXSnackbar snackbar; private JFXSnackbar snackbar;
private UserList userList; private UserList userList;
private boolean historyLoaded; private boolean historyLoaded;
private boolean online;
public MainController() { public MainController() {
historyLoaded = false; historyLoaded = false;
online = false;
currentUser = CurrentUser.getInstance(); currentUser = CurrentUser.getInstance();
currentUser.addObserver(propertyChangeEvent -> { currentUser.addObserver(propertyChangeEvent -> {
if (propertyChangeEvent.getPropertyName().equals("state")) { 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) { private void onCurrentUserStateChange(CurrentUser.State newState) {
// Only do an action if the history is loaded // Only do an action if the history is loaded
if (historyLoaded) { 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() { public void onHistoryLoaded() {
historyLoaded = true; historyLoaded = true;
final CurrentUser.State userState = CurrentUser.getInstance().getState(); 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) { if (userState != CurrentUser.State.UNINITIALIZED) {
onCurrentUserStateChange(userState); 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) { private void openEditUsernameDialog(EditUsernameDialogController.Mode mode) {
final boolean initialMode = mode == EditUsernameDialogController.Mode.INITIAL; final boolean initialMode = mode == EditUsernameDialogController.Mode.INITIAL;
editUserDialogController.setOnCancelListener(() -> { editUserDialogController.setOnCancelListener(() -> {
@ -101,6 +128,12 @@ public class MainController implements Initializable {
Platform.runLater(() -> editUserDialogController.show(root, mode)); 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) { private void showSnackbarEvent(String text, SnackbarController.Mode mode) {
try { try {
final FXMLLoader loader = new FXMLLoader(getClass().getResource("dialogs/snackbar.fxml")); 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() { private void openAboutDialog() {
aboutDialogController.show(root); 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() { private void discoverActiveUsers() {
if (userList != null) { if (userList != null && online) {
userList.discoverActiveUsers((e) -> { userList.discoverActiveUsers((e) -> {
Log.e(this.getClass().getSimpleName(), "Error discovering users", e); Log.e(this.getClass().getSimpleName(), "Error discovering users", e);
CurrentUser.getInstance().setState(CurrentUser.State.INVALID); 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() { private void startListening() {
if (userList != null) { if (userList != null) {
userList.startDiscoveryListening(); userList.startDiscoveryListening();
@ -138,30 +182,46 @@ public class MainController implements Initializable {
} }
} }
private void stopListening() { /**
if (userList != null) { * Starts network related functions to allow for chat functionality.
userList.destroy(); */
}
}
private void startChat() { private void startChat() {
online = true;
listController.setRefreshButtonEnabled(true);
Log.v(this.getClass().getSimpleName(), "Chat started"); Log.v(this.getClass().getSimpleName(), "Chat started");
discoverActiveUsers(); discoverActiveUsers();
startListening(); startListening();
Platform.runLater(this::showChat); Platform.runLater(this::showChat);
} }
/**
* Stops any network related functions to disable chat functionality.
*/
private void endChat() { private void endChat() {
online = false;
listController.setRefreshButtonEnabled(false);
Log.v(this.getClass().getSimpleName(), "Chat ended"); 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() { private void showChat() {
Log.v(this.getClass().getSimpleName(), "Chat shown"); Log.v(this.getClass().getSimpleName(), "Chat shown");
loadingController.hide(); loadingController.hide();
mainContainer.setVisible(true); 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) { private void showLogin(boolean isError) {
Log.v(this.getClass().getSimpleName(), "Login shown"); Log.v(this.getClass().getSimpleName(), "Login shown");
mainContainer.setVisible(false); 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() { private void showError() {
Log.v(this.getClass().getSimpleName(), "Error shown"); Log.v(this.getClass().getSimpleName(), "Error shown");
mainContainer.setVisible(false); mainContainer.setVisible(false);
@ -193,12 +256,15 @@ public class MainController implements Initializable {
snackbar = new JFXSnackbar(root); snackbar = new JFXSnackbar(root);
listController.setUserSelectedListener((user) -> chatController.setRemoteUser(user)); listController.setUserSelectedListener((user) -> chatController.setRemoteUser(user));
chatController.addAttachmentListener(() -> System.out.println("attach event")); chatController.setAttachmentListener(() -> System.out.println("attach event"));
chatController.addSendErrorListener((e) -> showSnackbarEvent("Erreur: Message non envoyé", SnackbarController.Mode.ERROR)); chatController.setSendErrorListener((e) -> showSnackbarEvent("Erreur: Message non envoyé", SnackbarController.Mode.ERROR));
toolbarController.addEditListener(() -> openEditUsernameDialog(EditUsernameDialogController.Mode.EDIT)); toolbarController.setEditListener(() -> openEditUsernameDialog(EditUsernameDialogController.Mode.EDIT));
toolbarController.addAboutListener(this::openAboutDialog); toolbarController.setAboutListener(this::openAboutDialog);
} }
/**
* Creates database if needed, then init current user, and finally load user list
*/
private void initDb() { private void initDb() {
final DatabaseController db = new DatabaseController(); final DatabaseController db = new DatabaseController();
db.init(() -> { 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) { public void setUserList(UserList userList) {
this.userList = userList; this.userList = userList;
listController.setUserList(userList); listController.setUserList(userList);

View file

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

View file

@ -11,29 +11,40 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/**
* Controller for the main screen toolbar
*/
public class ToolbarController implements Initializable { public class ToolbarController implements Initializable {
@FXML @FXML
private Label currentUsernameLabel; private Label currentUsernameLabel;
private List<ButtonPressEvent> editListeners; private ButtonPressEvent editListeners;
private List<ButtonPressEvent> aboutListeners; private ButtonPressEvent aboutListeners;
public void addEditListener(ButtonPressEvent listener) { public void setEditListener(ButtonPressEvent listener) {
editListeners.add(listener); editListeners = listener;
} }
public void addAboutListener(ButtonPressEvent listener) { public void setAboutListener(ButtonPressEvent listener) {
aboutListeners.add(listener); aboutListeners = listener;
} }
public void onEditPress() { public void onEditPress() {
editListeners.forEach(ButtonPressEvent::onPress); if (editListeners != null) {
editListeners.onPress();
}
} }
public void onAboutPress() { 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) { private void onUsernameChange(PropertyChangeEvent propertyChangeEvent) {
if (propertyChangeEvent.getPropertyName().equals("username")) { if (propertyChangeEvent.getPropertyName().equals("username")) {
final String newUsername = (String) propertyChangeEvent.getNewValue(); final String newUsername = (String) propertyChangeEvent.getNewValue();
@ -44,7 +55,5 @@ public class ToolbarController implements Initializable {
@Override @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
CurrentUser.getInstance().addObserver(this::onUsernameChange); 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.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/**
* Controller for the chat window
*/
public class ChatController implements Initializable { public class ChatController implements Initializable {
@FXML @FXML
@ -32,40 +35,50 @@ public class ChatController implements Initializable {
@FXML @FXML
private VBox emptyContainer; private VBox emptyContainer;
private PeerUser remoteUser; 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)) { if (user.equals(remoteUser)) {
setState(State.DONE); 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) { public void setRemoteUser(PeerUser remoteUser) {
this.chatFooterController.setRemoteUser(remoteUser); this.chatFooterController.setRemoteUser(remoteUser);
this.chatHeaderController.setRemoteUser(remoteUser); this.chatHeaderController.setRemoteUser(remoteUser);
setState(State.LOADING); setState(State.LOADING);
if (this.remoteUser != null) {
this.remoteUser.getHistory().removeHistoryLoadedListener(onHistoryLoaded);
}
this.remoteUser = remoteUser; this.remoteUser = remoteUser;
final ChatHistory history = remoteUser.getHistory(); final ChatHistory history = remoteUser.getHistory();
history.addHistoryLoadedListener(onHistoryLoaded); history.setHistoryLoadedListener(this::onHistoryLoaded);
messageList.setItems(history.getHistory()); messageList.setItems(history.getHistory());
messageList.getItems().addListener((ListChangeListener<? super Message>) (c) -> { messageList.getItems().addListener((ListChangeListener<? super Message>) (c) -> {
c.next(); c.next();
// Make sure we always have the latest item on screen
scrollToEnd(); scrollToEnd();
}); });
history.load(); history.load();
} }
/**
* Scroll to the end of the message list
*/
private void scrollToEnd() { private void scrollToEnd() {
final int size = messageList.getItems().size(); final int size = messageList.getItems().size();
if (size > 0) { 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) { private void setState(State state) {
switch (state) { switch (state) {
case INITIAL: case INITIAL:
emptyContainer.setVisible(true); emptyContainer.setVisible(true);
chatFooterController.setEnabled(false);
chatContainer.setVisible(false); chatContainer.setVisible(false);
loadingController.hide(); loadingController.hide();
messageList.setItems(null); messageList.setItems(null);
@ -85,11 +113,9 @@ public class ChatController implements Initializable {
case LOADING: case LOADING:
chatContainer.setVisible(true); chatContainer.setVisible(true);
loadingController.show(); loadingController.show();
chatFooterController.setEnabled(false);
emptyContainer.setVisible(false); emptyContainer.setVisible(false);
break; break;
case DONE: case DONE:
chatFooterController.setEnabled(true);
chatContainer.setVisible(true); chatContainer.setVisible(true);
emptyContainer.setVisible(false); emptyContainer.setVisible(false);
loadingController.hide(); loadingController.hide();

View file

@ -6,18 +6,21 @@ import fr.insa.clavardator.ui.ButtonPressEvent;
import fr.insa.clavardator.users.PeerUser; import fr.insa.clavardator.users.PeerUser;
import fr.insa.clavardator.util.ErrorCallback; import fr.insa.clavardator.util.ErrorCallback;
import fr.insa.clavardator.util.Log; import fr.insa.clavardator.util.Log;
import javafx.application.Platform;
import javafx.beans.value.ObservableValue; import javafx.beans.value.ObservableValue;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.input.KeyCode; import javafx.scene.input.KeyCode;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import java.beans.PropertyChangeEvent;
import java.net.URL; import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/**
* Controller for the chat input field and associated buttons
*/
public class ChatFooterController implements Initializable { public class ChatFooterController implements Initializable {
@FXML @FXML
@ -27,25 +30,38 @@ public class ChatFooterController implements Initializable {
@FXML @FXML
private JFXButton sendButton; private JFXButton sendButton;
private List<ButtonPressEvent> attachmentListeners; private ButtonPressEvent attachmentListeners;
private List<ErrorCallback> sendErrorListeners; private ErrorCallback sendErrorListeners;
private PeerUser remoteUser; private PeerUser remoteUser;
private HashMap<PeerUser, String> savedText; private HashMap<PeerUser, String> savedText;
public void setAttachmentListener(ButtonPressEvent listener) {
public void addAttachmentListener(ButtonPressEvent listener) { attachmentListeners = listener;
attachmentListeners.add(listener);
} }
public void addSendErrorListener(ErrorCallback listener) {
sendErrorListeners.add(listener); public void setSendErrorListener(ErrorCallback listener) {
sendErrorListeners = listener;
} }
public void onAttachmentPress() { 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() { public void onSend() {
if(!textField.getText().isEmpty()) { if (!isTextFieldEmpty()) {
if (remoteUser != null) { if (remoteUser != null) {
remoteUser.sendTextMessage(textField.getText(), this::onSendError); remoteUser.sendTextMessage(textField.getText(), this::onSendError);
} else { } 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); * Enables or disables the chat controls
sendErrorListeners.forEach((l) -> l.onError(e)); *
} * @param enabled True to enable, false otherwise
*/
public void setEnabled(boolean enabled) { private void setEnabled(boolean enabled) {
container.setDisable(!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() { private String findSavedText() {
String text = null; String text = null;
if (remoteUser != null) { if (remoteUser != null) {
@ -72,22 +93,74 @@ public class ChatFooterController implements Initializable {
return text != null ? text : ""; 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) { private void saveText(String text) {
if (remoteUser != null) { if (remoteUser != null) {
savedText.put(remoteUser, text); 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) { public void onTextChange(ObservableValue<? extends String> observable, String oldText, String newText) {
saveText(newText); saveText(newText);
sendButton.setDisable(isTextFieldEmpty()); 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 @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
savedText = new HashMap<>(); savedText = new HashMap<>();
attachmentListeners = new ArrayList<>();
sendErrorListeners = new ArrayList<>();
textField.textProperty().addListener(this::onTextChange); textField.textProperty().addListener(this::onTextChange);
textField.setOnKeyPressed(event -> { textField.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) { 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.ArrayList;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/**
* Controller for the chat title
*/
public class ChatHeaderController implements Initializable { public class ChatHeaderController implements Initializable {
@FXML @FXML
@ -20,10 +23,11 @@ public class ChatHeaderController implements Initializable {
private UserActiveIndicatorController indicatorController; private UserActiveIndicatorController indicatorController;
private PeerUser user; 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) { private void onUsernameChange(PropertyChangeEvent propertyChangeEvent) {
if (propertyChangeEvent.getPropertyName().equals("username")) { if (propertyChangeEvent.getPropertyName().equals("username")) {
final String newUsername = (String) propertyChangeEvent.getNewValue(); 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) { public void setRemoteUser(PeerUser remoteUser) {
if (this.user != null) { if (this.user != null) {
// remove old observer before setting new user // remove old observer before setting new user
@ -42,4 +51,7 @@ public class ChatHeaderController implements Initializable {
indicatorController.setUser(remoteUser); indicatorController.setUser(remoteUser);
indicatorController.setSize(10.0); indicatorController.setSize(10.0);
} }
@Override
public void initialize(URL location, ResourceBundle resources) {}
} }

View file

@ -24,25 +24,35 @@ public class MessageListItemController implements Initializable {
@FXML @FXML
private Label timestamp; 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) { public void setMessage(Message message) {
if (!message.equals(currentMessage)) { if (!message.equals(currentMessage)) {
currentMessage = message; currentMessage = message;
button.setText(message.getText()); button.setText(message.getText());
timestamp.setText(DateFormat.getTimeInstance().format(message.getDate())); timestamp.setText(DateFormat.getTimeInstance().format(message.getDate()));
clearBackground();
if (CurrentUser.getInstance().isLocalId(message.getSender().id)) { if (CurrentUser.getInstance().isLocalId(message.getSender().id)) {
container.setAlignment(Pos.CENTER_RIGHT); container.setAlignment(Pos.CENTER_RIGHT);
button.getStyleClass().remove("message-other");
button.getStyleClass().add("message-self"); button.getStyleClass().add("message-self");
} else { } else {
container.setAlignment(Pos.CENTER_LEFT); container.setAlignment(Pos.CENTER_LEFT);
button.getStyleClass().remove("message-self");
button.getStyleClass().add("message-other"); 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; package fr.insa.clavardator.ui.dialogs;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXDialog; 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.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import java.net.URL; import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
import java.util.Timer;
import java.util.TimerTask;
/**
* Controller for the about dialog
*/
public class AboutDialogController implements Initializable { public class AboutDialogController implements Initializable {
@FXML @FXML

View file

@ -18,14 +18,11 @@ import javafx.scene.layout.StackPane;
import java.net.URL; import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/**
* Controller for the username edit dialog
*/
public class EditUsernameDialogController implements Initializable { public class EditUsernameDialogController implements Initializable {
enum State {
VALID,
INVALID,
NETWORK
}
@FXML @FXML
private Label titleLabel; private Label titleLabel;
@FXML @FXML
@ -45,8 +42,6 @@ public class EditUsernameDialogController implements Initializable {
private ButtonPressEvent cancelListener; private ButtonPressEvent cancelListener;
private UserList userList; private UserList userList;
private final int MAX_LENGTH = 16;
@FXML @FXML
private void onConfirm() { private void onConfirm() {
setLocked(true); setLocked(true);
@ -76,6 +71,16 @@ public class EditUsernameDialogController implements Initializable {
this.cancelListener = listener; 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) { public void show(StackPane root, Mode mode) {
setLocked(false); setLocked(false);
setFieldError(State.VALID); setFieldError(State.VALID);
@ -84,10 +89,22 @@ public class EditUsernameDialogController implements Initializable {
dialog.show(root); 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() { public void hide() {
dialog.close(); dialog.close();
} }
/**
* Locks the dialog on screen or frees it.
*
* @param state True to lock, false to unlock
*/
private void setLocked(boolean state) { private void setLocked(boolean state) {
confirmButton.setDisable(state || textField.getText().isEmpty()); confirmButton.setDisable(state || textField.getText().isEmpty());
cancelButton.setDisable(state); cancelButton.setDisable(state);
@ -95,20 +112,38 @@ public class EditUsernameDialogController implements Initializable {
textField.setDisable(state); textField.setDisable(state);
} }
/**
* Sets the input error
*
* @param state The input state to set
*/
private void setFieldError(State state) { private void setFieldError(State state) {
currentState = state; currentState = state;
textField.validate(); 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) { public void onUsernameChange(ObservableValue<? extends String> observable, String oldText, String newText) {
setFieldError(State.VALID); setFieldError(State.VALID);
confirmButton.setDisable(newText.isEmpty()); confirmButton.setDisable(newText.isEmpty());
final int MAX_LENGTH = 16;
if (textField.getText().length() > MAX_LENGTH) { if (textField.getText().length() > MAX_LENGTH) {
String s = textField.getText().substring(0, MAX_LENGTH); String s = textField.getText().substring(0, MAX_LENGTH);
textField.setText(s); textField.setText(s);
} }
} }
/**
* Sets the dialog display mode
*
* @param mode The mode to set
*/
public void setMode(Mode mode) { public void setMode(Mode mode) {
switch (mode) { switch (mode) {
case INITIAL: 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 @Override
public void initialize(URL location, ResourceBundle resources) { public void initialize(URL location, ResourceBundle resources) {
validator = new Validator(); 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 { public enum Mode {
INITIAL, INITIAL,
ERROR, ERROR,
EDIT EDIT
} }
enum State {
VALID,
INVALID
}
class Validator extends ValidatorBase { class Validator extends ValidatorBase {
@Override @Override
protected void eval() { protected void eval() {

View file

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

View file

@ -1,7 +1,6 @@
package fr.insa.clavardator.ui.users; package fr.insa.clavardator.ui.users;
import fr.insa.clavardator.users.PeerUser; import fr.insa.clavardator.users.PeerUser;
import fr.insa.clavardator.util.Log;
import javafx.application.Platform; import javafx.application.Platform;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.Initializable; import javafx.fxml.Initializable;
@ -11,17 +10,20 @@ import java.beans.PropertyChangeEvent;
import java.net.URL; import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
/**
* Controller for the user indicator, showing the connection status
*/
public class UserActiveIndicatorController implements Initializable { public class UserActiveIndicatorController implements Initializable {
@FXML @FXML
private Circle circle; private Circle circle;
private PeerUser user; 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) { private void updateState(boolean isActive) {
circle.getStyleClass().clear(); circle.getStyleClass().clear();
if (isActive) { 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) { private void onStateChange(PropertyChangeEvent propertyChangeEvent) {
if (propertyChangeEvent.getPropertyName().equals("state")) { if (propertyChangeEvent.getPropertyName().equals("state")) {
final PeerUser.State newState = (PeerUser.State) propertyChangeEvent.getNewValue(); 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) { public void setUser(PeerUser user) {
if (this.user != null) { if (this.user != null) {
// remove old observer before setting new user // remove old observer before setting new user
@ -48,8 +61,16 @@ public class UserActiveIndicatorController implements Initializable {
updateState(user.isActive()); updateState(user.isActive());
} }
/**
* Sets the indicator's radius
*
* @param value The radius in pixels
*/
public void setSize(double value) { public void setSize(double value) {
circle.setRadius(value); circle.setRadius(value);
} }
@Override
public void initialize(URL location, ResourceBundle resources) {}
} }

View file

@ -1,5 +1,6 @@
package fr.insa.clavardator.ui.users; package fr.insa.clavardator.ui.users;
import com.jfoenix.controls.JFXButton;
import fr.insa.clavardator.ui.ButtonPressEvent; import fr.insa.clavardator.ui.ButtonPressEvent;
import fr.insa.clavardator.ui.UserSelectedEvent; import fr.insa.clavardator.ui.UserSelectedEvent;
import fr.insa.clavardator.users.PeerUser; import fr.insa.clavardator.users.PeerUser;
@ -15,26 +16,34 @@ import java.net.URL;
import java.util.ResourceBundle; import java.util.ResourceBundle;
public class UserListController implements Initializable { public class UserListController implements Initializable {
@FXML
private ListView<PeerUser> peerUserListView;
@FXML
private JFXButton refreshButton;
private ButtonPressEvent refreshUserListener; private ButtonPressEvent refreshUserListener;
private UserSelectedEvent userSelectedListener; private UserSelectedEvent userSelectedListener;
@FXML
private ListView<PeerUser> peerUserListView;
private UserList userList;
public UserListController() { public UserListController() {}
}
public void setRefreshUserListener(ButtonPressEvent listener) { public void setRefreshUserListener(ButtonPressEvent listener) {
refreshUserListener = listener; refreshUserListener = listener;
} }
public void setUserSelectedListener(UserSelectedEvent listener) { public void setUserSelectedListener(UserSelectedEvent listener) {
userSelectedListener = listener; userSelectedListener = listener;
} }
public void onRefreshUserListPress() { 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) { 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) { private void onUserConnected(PeerUser user) {
Platform.runLater(() -> { Platform.runLater(() -> {
if (!peerUserListView.getItems().contains(user)) { 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) { public void setUserList(UserList userList) {
this.userList = userList; userList.setNewUserObserver(this::onUserConnected);
userList.addNewUserObserver(this::onUserConnected);
} }
} }

View file

@ -19,8 +19,8 @@ public class UserListItemController implements Initializable {
private UserActiveIndicatorController indicatorController; private UserActiveIndicatorController indicatorController;
private ButtonPressEvent listener; private ButtonPressEvent listener;
private PeerUser user; private PeerUser user;
private boolean selected;
public void setOnPressListener(ButtonPressEvent listener) { public void setOnPressListener(ButtonPressEvent listener) {
this.listener = 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) { public void setSelected(boolean selected) {
this.selected = selected;
if (selected) if (selected)
setBackgroundSelected(); setBackgroundSelected();
else else
resetBackground(); resetBackground(this.user.isActive());
} }
@Override /**
public void initialize(URL location, ResourceBundle resources) { * Updates shown username and item color when user changes
resetBackground(); *
} * @param propertyChangeEvent The change event
*/
private void onUsernameChange(PropertyChangeEvent propertyChangeEvent) { 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(); final String newUsername = (String) propertyChangeEvent.getNewValue();
Platform.runLater(() -> button.setText(newUsername)); 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) { public void setUser(PeerUser user) {
if (this.user != null) { if (this.user == null || !this.user.equals(user)) {
// remove old observer before setting new user if (this.user != null) {
this.user.removeObserver(this::onUsernameChange); // 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()) { * Sets the background color to active or inactive
button.getStyleClass().remove("inactive-user-item"); */
private void resetBackground(boolean isActive) {
clearBackground();
if (isActive) {
button.getStyleClass().add("active-user-item"); button.getStyleClass().add("active-user-item");
} else { } else {
button.getStyleClass().remove("active-user-item");
button.getStyleClass().add("inactive-user-item"); button.getStyleClass().add("inactive-user-item");
} }
} }
/**
* Sets the background color in selected mode
*/
private void setBackgroundSelected() { private void setBackgroundSelected() {
button.getStyleClass().remove("inactive-user-item"); clearBackground();
button.getStyleClass().remove("active-user-item");
button.getStyleClass().add("selected-user-item"); 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> inactiveUsers = new HashMap<>();
private final Map<Integer, PeerUser> activeUsers = 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 DatabaseController db = new DatabaseController();
private final NetDiscoverer netDiscoverer = new NetDiscoverer(); private final NetDiscoverer netDiscoverer = new NetDiscoverer();
@ -33,8 +33,8 @@ public class UserList {
* *
* @param connectionCallback The function to add as listener * @param connectionCallback The function to add as listener
*/ */
public void addNewUserObserver(UserConnectionCallback connectionCallback) { public void setNewUserObserver(UserConnectionCallback connectionCallback) {
newUsersObservers.add(connectionCallback); newUsersObservers = connectionCallback;
} }
/** /**
@ -43,7 +43,9 @@ public class UserList {
* @param user The newly connected user * @param user The newly connected user
*/ */
private void notifyNewUserObservers(PeerUser 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. * Observers are notified for each new successful connection.
* *
* @param errorCallback The function to call on error * @param errorCallback The function to call on error
* @see UserList#addNewUserObserver(UserConnectionCallback) * @see UserList#setNewUserObserver(UserConnectionCallback)
*/ */
public void discoverActiveUsers(ErrorCallback errorCallback) { public void discoverActiveUsers(ErrorCallback errorCallback) {
netDiscoverer.discoverActiveUsers("CLAVARDATOR_BROADCAST", (ipAddr, data) -> { netDiscoverer.discoverActiveUsers("CLAVARDATOR_BROADCAST", (ipAddr, data) -> {

View file

@ -10,7 +10,7 @@
<VBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" <VBox AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0"
AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
<HBox alignment="CENTER" prefHeight="64.0" spacing="10.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> <graphic>
<FontIcon iconLiteral="fas-sync-alt" iconSize="24"/> <FontIcon iconLiteral="fas-sync-alt" iconSize="24"/>
</graphic> </graphic>