diff --git a/src/no/ntnu/idata2001/contacts/controllers/MainController.java b/src/no/ntnu/idata2001/contacts/controllers/MainController.java index 96d5a118c1673ca087b484c172eb055d246834af..1713ec8e1039ffc5e021776bfc0e1c70713897ab 100644 --- a/src/no/ntnu/idata2001/contacts/controllers/MainController.java +++ b/src/no/ntnu/idata2001/contacts/controllers/MainController.java @@ -1,7 +1,24 @@ package no.ntnu.idata2001.contacts.controllers; +import java.util.Optional; +import java.util.logging.Level; import java.util.logging.Logger; +import javafx.application.Platform; +import javafx.geometry.Insets; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.ButtonBar; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import javafx.util.Pair; +import no.ntnu.idata2001.contacts.model.AddressBook; import no.ntnu.idata2001.contacts.model.ContactDetails; +import no.ntnu.idata2001.contacts.views.ContactDetailsDialog; +import no.ntnu.idata2001.contacts.views.ContactsApp; /** * The main controller for the application. Handles all actions related to the @@ -13,34 +30,254 @@ import no.ntnu.idata2001.contacts.model.ContactDetails; public class MainController { private final Logger logger; + /** + * Creates an instance of the MainController class, initialising + * the logger. + */ public MainController() { this.logger = Logger.getLogger(getClass().toString()); } - public void addContact() { + + /** + * Display the input dialog to get input to create a new Contact. + * If the user confirms creating a new contact, a new instance + * of ContactDetails is created and added to the AddressBook provided. + * + * @param addressBook the address book to add the new contact to. + * @param parent the parent calling this method. Use this parameter to access public methods + * in the parent, like updateObservableList(). + */ + public void addContact(AddressBook addressBook, ContactsApp parent) { + + ContactDetailsDialog contactsDialog = new ContactDetailsDialog(); + + Optional<ContactDetails> result = contactsDialog.showAndWait(); + + if (result.isPresent()) { + ContactDetails newContactDetails = result.get(); + addressBook.addContact(newContactDetails); + parent.updateObservableList(); + } } + /** + * Edit the selected item. + * + * @param selectedContact the contact to edit. Changes made by the user are updated on the + * selectedContact object provided. + * @param parent the parent view making the call + */ + public void editContact(ContactDetails selectedContact, ContactsApp parent) { + if (selectedContact == null) { + showPleaseSelectItemDialog(); + } else { + ContactDetailsDialog contactDialog = new ContactDetailsDialog(selectedContact, true); + contactDialog.showAndWait(); + + parent.updateObservableList(); + } + } + + /** + * Deletes the Contact selected in the table. If no Contact is + * selected, nothing is deleted, and the user is informed that he/she must + * select which Contact to delete. + * + * @param selectedContact the Contact to delete. If no Contact has been selected, + * this parameter will be <code>null</code> + * @param addressBook the contact register to delete the selectedContact from + * @param parent the parent view making the call. + */ + public void deleteContact(ContactDetails selectedContact, + AddressBook addressBook, + ContactsApp parent) { + if (selectedContact == null) { + showPleaseSelectItemDialog(); + } else { + if (showDeleteConfirmationDialog()) { + addressBook.removeContact(selectedContact.getPhone()); + parent.updateObservableList(); + } + } + } + + public void importFromCsv() { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle("Information"); + alert.setHeaderText("Functionality not yet implemented."); + alert.setContentText("Will be released in v0.2"); + + alert.showAndWait(); } public void exportToCsv() { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle("Information"); + alert.setHeaderText("Functionality not yet implemented."); + alert.setContentText("Will be released in v0.2"); + + alert.showAndWait(); } + /** + * Exit the application. Displays a confirmation dialog. + */ public void exitApplication() { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Confirmation Dialog"); + alert.setHeaderText("Exit Application ?"); + alert.setContentText("Are you sure you want to exit this application?"); + + Optional<ButtonType> result = alert.showAndWait(); + + if (result.isPresent()) { + if (result.get() == ButtonType.OK) { + // ... user choose OK + Platform.exit(); + } else { + // ... user chose CANCEL or closed the dialog + // then do nothing. + } + } } - public void showAboutDialog() { + /** + * Displays an example of an alert (info) dialog. In this case an "about" + * type of dialog. + * + * @param version the version of the application, to be displayed in the dialog. + */ + public void showAboutDialog(String version) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Information Dialog - About"); + alert.setHeaderText("Contacts Register\nv" + version); + alert.setContentText("A brilliant application created by\n" + + "(C)Arne Styve\n" + + "2020-03-16"); + + alert.showAndWait(); } + /** + * Displays a login dialog using a custom dialog. + * Just to demonstrate the {@link javafx.scene.control.PasswordField}-control. + */ public void showLogInDialog() { + // Create the custom dialog. + Dialog<Pair<String, String>> dialog = new Dialog<>(); + dialog.setTitle("Login Dialog"); + dialog.setHeaderText("Look, a Custom Login Dialog"); + + // Set the button types. + ButtonType loginButtonType = new ButtonType("Login", ButtonBar.ButtonData.OK_DONE); + dialog.getDialogPane().getButtonTypes().addAll(loginButtonType, ButtonType.CANCEL); + + // Create the username and password labels and fields. + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + grid.setPadding(new Insets(20, 150, 10, 10)); + + TextField username = new TextField(); + username.setPromptText("Username"); + + PasswordField password = new PasswordField(); + password.setPromptText("Password"); + + grid.add(new Label("Username:"), 0, 0); + grid.add(username, 1, 0); + grid.add(new Label("Password:"), 0, 1); + grid.add(password, 1, 1); + + // Enable/Disable login button depending on whether a username was entered. + Node loginButton = dialog.getDialogPane().lookupButton(loginButtonType); + loginButton.setDisable(true); + + // Do some validation . + username.textProperty().addListener((observable, oldValue, newValue) -> + loginButton.setDisable(newValue.trim().isEmpty())); + + dialog.getDialogPane().setContent(grid); + + // Request focus on the username field by default. + Platform.runLater(username::requestFocus); + + // Convert the result to a username-password-pair when the login button is clicked. + dialog.setResultConverter( + dialogButton -> { + if (dialogButton == loginButtonType) { + return new Pair<>(username.getText(), password.getText()); + } + return null; + }); + + Optional<Pair<String, String>> result = dialog.showAndWait(); + + result.ifPresent( + usernamePassword -> logger.log(Level.INFO, "Username=" + usernamePassword.getKey() + + ", Password=" + usernamePassword.getValue())); } - public void editContact() { + + + /** + * Show details of the selected contact item. + * + * @param selectedContact the contact object to display the details of + */ + public void showDetails(ContactDetails selectedContact) { + if (selectedContact == null) { + showPleaseSelectItemDialog(); + } else { + + ContactDetailsDialog detailsDialog = new ContactDetailsDialog(selectedContact, false); + + detailsDialog.showAndWait(); + } } - public void deleteContact() { + + // ----------------------------------------------------------- + // DIALOGS - minor dialogs lig confirm deletion, enter password etc. + // ----------------------------------------------------------- + + + /** + * Displays a warning informing the user that an item must be selected from + * the table. + */ + public void showPleaseSelectItemDialog() { + Alert alert = new Alert(Alert.AlertType.WARNING); + alert.setTitle("Information"); + alert.setHeaderText("No items selected"); + alert.setContentText("No item is selected from the table.\n" + + "Please select an item from the table."); + + alert.showAndWait(); } - public void showDetails(ContactDetails selectedContact) { + /** + * Displays a delete confirmation dialog. If the user confirms the delete, + * <code>true</code> is returned. + * + * @return <code>true</code> if the user confirms the delete + */ + public boolean showDeleteConfirmationDialog() { + boolean deleteConfirmed = false; + + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Delete confirmation"); + alert.setHeaderText("Delete confirmation"); + alert.setContentText("Are you sure you want to delete this item?"); + + Optional<ButtonType> result = alert.showAndWait(); + + if (result.isPresent()) { + deleteConfirmed = (result.get() == ButtonType.OK); + } + return deleteConfirmed; } + } diff --git a/src/no/ntnu/idata2001/contacts/model/ContactDetails.java b/src/no/ntnu/idata2001/contacts/model/ContactDetails.java index 0c6c4042192aeba48272c31b7c3c28450e76744c..c9f81ea14e0e9131e23e2d0ec6d38934867e26d1 100644 --- a/src/no/ntnu/idata2001/contacts/model/ContactDetails.java +++ b/src/no/ntnu/idata2001/contacts/model/ContactDetails.java @@ -48,6 +48,25 @@ public class ContactDetails implements Comparable<ContactDetails>, Serializable return name; } + /** + * Sets the name of the contact. + * If the name is <code>null</code>, or is empty (length is 0), a + * {@link java.lang.IllegalArgumentException} will be thrown. + * + * @param name the new name of the contact + * @throws IllegalArgumentException if the name is <code>null</code> or empty. + */ + public void setName(String name) { + if (name == null) { + throw new IllegalArgumentException("Name should not be null!!"); + } + + if (name.trim().length() == 0) { + throw new IllegalArgumentException("Name cannot be empty."); + } + this.name = name; + } + /** * Returns the phone number. * @@ -57,6 +76,25 @@ public class ContactDetails implements Comparable<ContactDetails>, Serializable return phone; } + /** + * Sets the phone number of the contact. + * If the phone number is <code>null</code>, or is empty (length is 0), a + * {@link java.lang.IllegalArgumentException} will be thrown. + * + * @param phoneNumber the new name of the contact + * @throws IllegalArgumentException if the name is <code>null</code> or empty. + */ + public void setPhone(String phoneNumber) { + if (phoneNumber == null) { + throw new IllegalArgumentException("Phone number should not be null!!"); + } + + if (phoneNumber.trim().length() == 0) { + throw new IllegalArgumentException("Phone number cannot be empty."); + } + this.phone = phoneNumber; + } + /** * Returns the address. * @@ -66,6 +104,25 @@ public class ContactDetails implements Comparable<ContactDetails>, Serializable return address; } + /** + * Sets the address of the contact. + * If the address is <code>null</code>, or is empty (length is 0), a + * {@link java.lang.IllegalArgumentException} will be thrown. + * + * @param address the new name of the contact + * @throws IllegalArgumentException if the name is <code>null</code> or empty. + */ + public void setAddress(String address) { + if (address == null) { + throw new IllegalArgumentException("Address should not be null!!"); + } + + if (address.trim().length() == 0) { + throw new IllegalArgumentException("Address cannot be empty."); + } + this.address = address; + } + /** * Test for content equality between two objects. * diff --git a/src/no/ntnu/idata2001/contacts/views/ContactDetailsDialog.java b/src/no/ntnu/idata2001/contacts/views/ContactDetailsDialog.java new file mode 100644 index 0000000000000000000000000000000000000000..6352bb5ac32a445e70bb4584c626933a37562eb5 --- /dev/null +++ b/src/no/ntnu/idata2001/contacts/views/ContactDetailsDialog.java @@ -0,0 +1,165 @@ +package no.ntnu.idata2001.contacts.views; + +import javafx.geometry.Insets; +import javafx.scene.control.ButtonType; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; +import no.ntnu.idata2001.contacts.model.ContactDetails; + + +/** + * A dialog used to get the necessary information about a contact from the + * user, in order to be able to create a ContactDetails instance to be added to the + * register. The dialog can be opened in 3 different modes: + * <ul> + * <li> EDIT - Used for editing an existing contact.</li> + * <li> NEW - Used for entering information to create a new contact.</li> + * <li> INFO - Used to show non-editable info of a contact</li> + * </ul> + * + * @author Arne Styve + * @version 2020-03-16 + */ +public class ContactDetailsDialog extends Dialog<ContactDetails> { + + /** + * The mode of the dialog. If the dialog is opened to edit an existing + * Contact, the mode is set to <code>Mode.EDIT</code>. If the dialog is + * opened to create a new contact, the <code>Mode.NEW</code> is used. + */ + public enum Mode { + NEW, EDIT, INFO + } + + /** + * The mode of the dialog. NEW if new contact, EDIT if edit existing + * contact. + */ + private final Mode mode; + + /** + * Holds the ContactDetails instance to edit, if any. + */ + private ContactDetails existingContact = null; + + /** + * Creates an instance of the ContactDetailsDialog to get information to + * create a new instance of ContactDetails. + */ + public ContactDetailsDialog() { + super(); + this.mode = Mode.NEW; + // Create the content of the dialog + createContent(); + + } + + /** + * Creates an instance of the ContactDetailsDialog dialog. + * + * @param contact the contact instance to edit + * @param editable if set to <code>true</code>, the dialog will enable + * editing of the fields in the dialog. if <code>false</code> the + * information will be displayed in non-editable fields. + */ + public ContactDetailsDialog(ContactDetails contact, boolean editable) { + super(); + if (editable) { + this.mode = Mode.EDIT; + } else { + this.mode = Mode.INFO; + } + this.existingContact = contact; + // Create the content of the dialog + createContent(); + } + + /** + * Creates the content of the dialog. + */ + private void createContent() { + // Set title depending upon mode... + switch (this.mode) { + case EDIT: + setTitle("Contact Details - Edit"); + break; + + case NEW: + setTitle("Contact Details - Add"); + break; + + case INFO: + setTitle("Contact Details"); + break; + + default: + setTitle("Contact Details - UNKNOWN MODE..."); + break; + + } + + // Set the button types. + getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + GridPane grid = new GridPane(); + grid.setHgap(10); + grid.setVgap(10); + grid.setPadding(new Insets(20, 150, 10, 10)); + + TextField name = new TextField(); + name.setPromptText("Name"); + + TextField address = new TextField(); + address.setPromptText("Address"); + + TextField phoneNumber = new TextField(); + phoneNumber.setPromptText("Phone number"); + + // Fill inn data from the provided Newspaper, if not null. + if ((mode == Mode.EDIT) || (mode == Mode.INFO)) { + name.setText(existingContact.getName()); + address.setText(existingContact.getAddress()); + phoneNumber.setText(existingContact.getPhone()); + // Set to non-editable if Mode.INFO + if (mode == Mode.INFO) { + name.setEditable(false); + address.setEditable(false); + phoneNumber.setEditable(false); + } + } + + grid.add(new Label("Name:"), 0, 0); + grid.add(name, 1, 0); + grid.add(new Label("Address:"), 0, 1); + grid.add(address, 1, 1); + grid.add(new Label("Phone number:"), 0, 2); + grid.add(phoneNumber, 1, 2); + + getDialogPane().setContent(grid); + + // Convert the result to ContactDetails-instance when the OK button is clicked. + // Check out: https://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/Dialog.html#setResultConverter-javafx.util.Callback- + // and: https://docs.oracle.com/javase/8/javafx/api/javafx/util/Callback.html + setResultConverter( + (ButtonType button) -> { + ContactDetails result = null; + if (button == ButtonType.OK) { + + if (mode == Mode.NEW) { + result = new ContactDetails(name.getText(), address.getText(), phoneNumber.getText()); + } else if (mode == Mode.EDIT) { + existingContact.setName(name.getText()); + existingContact.setAddress(address.getText()); + existingContact.setPhone(phoneNumber.getText()); + + result = existingContact; + } + } + return result; + } + ); + } +} + diff --git a/src/no/ntnu/idata2001/contacts/views/ContactsApp.java b/src/no/ntnu/idata2001/contacts/views/ContactsApp.java index 570b7f9bbdfb897f1d641eb6e2dc86d9d2126371..685b35f34a6496fe3fc8c72b90dbf9e238293210 100644 --- a/src/no/ntnu/idata2001/contacts/views/ContactsApp.java +++ b/src/no/ntnu/idata2001/contacts/views/ContactsApp.java @@ -32,7 +32,7 @@ import no.ntnu.idata2001.contacts.model.ContactDetails; */ public class ContactsApp extends Application { - private static final String version = "0.1"; + private static final String VERSION = "0.2"; private final MainController mainController; private final AddressBook addressBook; @@ -40,6 +40,10 @@ public class ContactsApp extends Application { // The JavaFX ObservableListWrapper used to connect tot he underlying AddressBook private ObservableList<ContactDetails> addressBookListWrapper; + // Need to keep track of the TableView-instance since we need to access it + // from different places in our GUI (menu, doubleclicking, toolbar etc.) + private TableView<ContactDetails> contactDetailsTableView; + /** * Creates an instance of the ContactsApp, initialising the * main controller and the address book. @@ -81,14 +85,15 @@ public class ContactsApp extends Application { root.setTop(topContainer); // Place the StatusBar at the bottom root.setBottom(createStatusBar()); - // Place the centre content - root.setCenter(createCentreContent()); + // Place the table view in the centre + this.contactDetailsTableView = this.createCentreContent(); + root.setCenter(this.contactDetailsTableView); // Create the scene, adding the rootNode and setting the default size - Scene scene = new Scene(root, 400, 500); + Scene scene = new Scene(root, 600, 500); // Set title of the stage (window) and add the scene - primaryStage.setTitle("Contacts v" + version); + primaryStage.setTitle("Contacts v" + VERSION); primaryStage.setScene(scene); // Finally, make the stage (window) visible @@ -112,7 +117,7 @@ public class ContactsApp extends Application { * * @return The node to be placed in the centre of the main window. */ - private Node createCentreContent() { + private TableView<ContactDetails> createCentreContent() { // Create the Table to display all the literature in @@ -203,21 +208,26 @@ public class ContactsApp extends Application { new ImageView( new Image(getClass().getResource("./icons/add_contact@2x.png").toExternalForm()))); - addContactBtn.setOnAction(actionEvent -> mainController.addContact()); + addContactBtn.setOnAction(actionEvent -> mainController.addContact(this.addressBook, this)); // Add the edit contact-button in the toolbar Button editContactBtn = new Button(); editContactBtn.setGraphic(new ImageView( new Image(getClass().getResource("./icons/edit_contact@2x.png").toExternalForm()))); - editContactBtn.setOnAction(event -> mainController.editContact()); + editContactBtn.setOnAction(event -> + mainController.editContact( + this.contactDetailsTableView.getSelectionModel().getSelectedItem(), this)); // Add the delete contact-button in the tool bar Button deleteContactBtn = new Button(); deleteContactBtn.setGraphic(new ImageView( new Image(getClass().getResource("./icons/remove_contact@2x.png").toExternalForm()))); - deleteContactBtn.setOnAction(event -> mainController.deleteContact()); + deleteContactBtn.setOnAction(event -> + mainController.deleteContact( + this.contactDetailsTableView.getSelectionModel().getSelectedItem(), + this.addressBook, this)); //Add the Buttons to the ToolBar. @@ -254,7 +264,7 @@ public class ContactsApp extends Application { // ----- The Help-menu ------ Menu menuHelp = new Menu("Help"); MenuItem about = new MenuItem("About"); - about.setOnAction(event -> mainController.showAboutDialog()); + about.setOnAction(event -> mainController.showAboutDialog(VERSION)); menuHelp.getItems().add(about); // ----- The LogIn-menu ------ @@ -273,9 +283,9 @@ public class ContactsApp extends Application { addContactView.setFitHeight(15); MenuItem addContactMenu = new MenuItem("Add new Contact ..."); addContactMenu.setGraphic(addContactView); - addContactMenu.setAccelerator(new KeyCodeCombination(KeyCode.ADD)); + addContactMenu.setAccelerator(new KeyCodeCombination(KeyCode.A)); - addContactMenu.setOnAction(event -> mainController.addContact()); + addContactMenu.setOnAction(event -> mainController.addContact(this.addressBook, this)); Menu menuEdit = new Menu("Edit"); menuEdit.getItems().add(addContactMenu); @@ -290,7 +300,9 @@ public class ContactsApp extends Application { editContactMenu.setGraphic(editContactView); editContactMenu.setAccelerator(new KeyCodeCombination(KeyCode.E)); - editContactMenu.setOnAction(event -> mainController.editContact()); + editContactMenu.setOnAction(event -> + mainController.editContact( + this.contactDetailsTableView.getSelectionModel().getSelectedItem(), this)); menuEdit.getItems().add(editContactMenu); @@ -304,7 +316,9 @@ public class ContactsApp extends Application { removeContactMenu.setGraphic(removeContactView); removeContactMenu.setAccelerator(new KeyCodeCombination(KeyCode.DELETE)); - removeContactMenu.setOnAction(event -> mainController.deleteContact()); + removeContactMenu.setOnAction(event -> mainController.deleteContact( + this.contactDetailsTableView.getSelectionModel() + .getSelectedItem(), this.addressBook, this)); menuEdit.getItems().add(removeContactMenu);