diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/seedu/address/Main.java index 052a5068631..6aa3add163f 100644 --- a/src/main/java/seedu/address/Main.java +++ b/src/main/java/seedu/address/Main.java @@ -20,6 +20,6 @@ */ public class Main { public static void main(String[] args) { - Application.launch(MainApp.class, args); + Application.launch(NewMainApp.class, args); } } diff --git a/src/main/java/seedu/address/NewMainApp.java b/src/main/java/seedu/address/NewMainApp.java new file mode 100644 index 00000000000..d78900504ab --- /dev/null +++ b/src/main/java/seedu/address/NewMainApp.java @@ -0,0 +1,183 @@ +package seedu.address; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import javafx.application.Application; +import javafx.stage.Stage; +import seedu.address.commons.core.Config; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.Version; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.util.ConfigUtil; +import seedu.address.commons.util.StringUtil; +import seedu.address.logic.Logic; +import seedu.address.logic.LogicManager; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ModelManager; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.UserPrefs; +import seedu.address.model.util.SampleDataUtil; +import seedu.address.storage.AddressBookStorage; +import seedu.address.storage.JsonAddressBookStorage; +import seedu.address.storage.JsonUserPrefsStorage; +import seedu.address.storage.Storage; +import seedu.address.storage.StorageManager; +import seedu.address.storage.UserPrefsStorage; +import seedu.address.ui.NewUiManager; +import seedu.address.ui.Ui; + +/** + * Runs the application. + */ +public class NewMainApp extends Application { + + public static final Version VERSION = new Version(0, 6, 0, true); + + private static final Logger logger = LogsCenter.getLogger(MainApp.class); + + protected Ui ui; + protected Logic logic; + protected Storage storage; + protected Model model; + protected Config config; + + @Override + public void init() throws Exception { + logger.info("=============================[ Initializing AddressBook ]==========================="); + super.init(); + + AppParameters appParameters = AppParameters.parse(getParameters()); + config = initConfig(appParameters.getConfigPath()); + + UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); + UserPrefs userPrefs = initPrefs(userPrefsStorage); + AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath()); + storage = new StorageManager(addressBookStorage, userPrefsStorage); + + initLogging(config); + + model = initModelManager(storage, userPrefs); + + logic = new LogicManager(model, storage); + + ui = new NewUiManager(logic); + } + + /** + * Returns a {@code ModelManager} with the data from {@code storage}'s address book and {@code userPrefs}.
+ * The data from the sample address book will be used instead if {@code storage}'s address book is not found, + * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. + */ + private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { + Optional addressBookOptional; + ReadOnlyAddressBook initialData; + try { + addressBookOptional = storage.readAddressBook(); + if (!addressBookOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a sample AddressBook"); + } + initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + } catch (DataConversionException e) { + logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); + initialData = new AddressBook(); + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); + initialData = new AddressBook(); + } + + return new ModelManager(initialData, userPrefs); + } + + private void initLogging(Config config) { + LogsCenter.init(config); + } + + /** + * Returns a {@code Config} using the file at {@code configFilePath}.
+ * The default file path {@code Config#DEFAULT_CONFIG_FILE} will be used instead + * if {@code configFilePath} is null. + */ + protected Config initConfig(Path configFilePath) { + Config initializedConfig; + Path configFilePathUsed; + + configFilePathUsed = Config.DEFAULT_CONFIG_FILE; + + if (configFilePath != null) { + logger.info("Custom Config file specified " + configFilePath); + configFilePathUsed = configFilePath; + } + + logger.info("Using config file : " + configFilePathUsed); + + try { + Optional configOptional = ConfigUtil.readConfig(configFilePathUsed); + initializedConfig = configOptional.orElse(new Config()); + } catch (DataConversionException e) { + logger.warning("Config file at " + configFilePathUsed + " is not in the correct format. " + + "Using default config properties"); + initializedConfig = new Config(); + } + + //Update config file in case it was missing to begin with or there are new/unused fields + try { + ConfigUtil.saveConfig(initializedConfig, configFilePathUsed); + } catch (IOException e) { + logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + } + return initializedConfig; + } + + /** + * Returns a {@code UserPrefs} using the file at {@code storage}'s user prefs file path, + * or a new {@code UserPrefs} with default configuration if errors occur when + * reading from the file. + */ + protected UserPrefs initPrefs(UserPrefsStorage storage) { + Path prefsFilePath = storage.getUserPrefsFilePath(); + logger.info("Using prefs file : " + prefsFilePath); + + UserPrefs initializedPrefs; + try { + Optional prefsOptional = storage.readUserPrefs(); + initializedPrefs = prefsOptional.orElse(new UserPrefs()); + } catch (DataConversionException e) { + logger.warning("UserPrefs file at " + prefsFilePath + " is not in the correct format. " + + "Using default user prefs"); + initializedPrefs = new UserPrefs(); + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); + initializedPrefs = new UserPrefs(); + } + + //Update prefs file in case it was missing to begin with or there are new/unused fields + try { + storage.saveUserPrefs(initializedPrefs); + } catch (IOException e) { + logger.warning("Failed to save config file : " + StringUtil.getDetails(e)); + } + + return initializedPrefs; + } + + @Override + public void start(Stage primaryStage) { + logger.info("Starting AddressBook " + MainApp.VERSION); + ui.start(primaryStage); + } + + @Override + public void stop() { + logger.info("============================ [ Stopping Address Book ] ============================="); + try { + storage.saveUserPrefs(model.getUserPrefs()); + } catch (IOException e) { + logger.severe("Failed to save preferences " + StringUtil.getDetails(e)); + } + } +} diff --git a/src/main/java/seedu/address/ui/FooterBar.java b/src/main/java/seedu/address/ui/FooterBar.java new file mode 100644 index 00000000000..6f90e3c7f0d --- /dev/null +++ b/src/main/java/seedu/address/ui/FooterBar.java @@ -0,0 +1,24 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; + +/** + * A ui for the footer bar that is displayed at the footer of the application. + */ +public class FooterBar extends UiPart { + + private static final String FXML = "FooterBar.fxml"; + + @FXML + private Label versionNumber; + + /** + * Creates a {@code StatusBarFooter} with the given {@code Path}. + */ + public FooterBar(String versionNum) { + super(FXML); + versionNumber.setText(versionNum); + } +} diff --git a/src/main/java/seedu/address/ui/LastInputDisplay.java b/src/main/java/seedu/address/ui/LastInputDisplay.java new file mode 100644 index 00000000000..2aa861c5b52 --- /dev/null +++ b/src/main/java/seedu/address/ui/LastInputDisplay.java @@ -0,0 +1,28 @@ +package seedu.address.ui; + +import static java.util.Objects.requireNonNull; + +import javafx.fxml.FXML; +import javafx.scene.control.TextArea; +import javafx.scene.layout.Region; + +/** + * A ui for the status bar that is displayed at the header of the application. + */ +public class LastInputDisplay extends UiPart { + + private static final String FXML = "ResultDisplay.fxml"; + + @FXML + private TextArea resultDisplay; + + public LastInputDisplay() { + super(FXML); + } + + public void setLastInput(String feedbackToUser) { + requireNonNull(feedbackToUser); + resultDisplay.setText(feedbackToUser); + } + +} diff --git a/src/main/java/seedu/address/ui/NewCommandBox.java b/src/main/java/seedu/address/ui/NewCommandBox.java new file mode 100644 index 00000000000..81625786d84 --- /dev/null +++ b/src/main/java/seedu/address/ui/NewCommandBox.java @@ -0,0 +1,81 @@ +package seedu.address.ui; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.TextField; +import javafx.scene.layout.Region; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * The UI component that is responsible for receiving user command inputs. + */ +public class NewCommandBox extends UiPart { + + public static final String ERROR_STYLE_CLASS = "error"; + private static final String FXML = "CommandBox.fxml"; + + private final CommandExecutor commandExecutor; + + @FXML + private TextField commandTextField; + + /** + * Creates a {@code CommandBox} with the given {@code CommandExecutor}. + */ + public NewCommandBox(CommandExecutor commandExecutor) { + super(FXML); + this.commandExecutor = commandExecutor; + // calls #setStyleToDefault() whenever there is a change to the text of the command box. + commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); + } + + /** + * Handles the Enter button pressed event. + */ + @FXML + private void handleCommandEntered() { + try { + commandExecutor.execute(commandTextField.getText()); + } catch (CommandException | ParseException e) { + // setStyleToIndicateCommandFailure(); + } + + commandTextField.setText(""); + } + + /** + * Sets the command box style to use the default style. + */ + private void setStyleToDefault() { + commandTextField.getStyleClass().remove(ERROR_STYLE_CLASS); + } + + /** + * Sets the command box style to indicate a failed command. + */ + private void setStyleToIndicateCommandFailure() { + ObservableList styleClass = commandTextField.getStyleClass(); + + if (styleClass.contains(ERROR_STYLE_CLASS)) { + return; + } + + styleClass.add(ERROR_STYLE_CLASS); + } + + /** + * Represents a function that can execute commands. + */ + @FunctionalInterface + public interface CommandExecutor { + /** + * Executes the command and returns the result. + * + * @see seedu.address.logic.Logic#execute(String) + */ + CommandResult execute(String commandText) throws CommandException, ParseException; + } + +} diff --git a/src/main/java/seedu/address/ui/NewMainWindow.java b/src/main/java/seedu/address/ui/NewMainWindow.java new file mode 100644 index 00000000000..a70b476ed65 --- /dev/null +++ b/src/main/java/seedu/address/ui/NewMainWindow.java @@ -0,0 +1,194 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.MenuItem; +import javafx.scene.control.TextInputControl; +import javafx.scene.input.KeyCombination; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import seedu.address.commons.core.GuiSettings; +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.Logic; +import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * The Main Window. Provides the basic application layout containing + * a menu bar and space where other JavaFX elements can be placed. + */ +public class NewMainWindow extends UiPart { + + private static final String FXML = "NewMainWindow.fxml"; + + private final Logger logger = LogsCenter.getLogger(getClass()); + + private Stage primaryStage; + private ResultDisplay resultDisplay; + private LastInputDisplay lastInputDisplay; + private Logic logic; + + // Independent Ui parts residing in this Ui container + + @FXML + private StackPane resultDisplayPlaceHolder; + + @FXML + private StackPane tagListPlaceholder; + + @FXML + private StackPane lastInputPlaceHolder; + + @FXML + private StackPane commandBoxPlaceHolder; + + @FXML + private StackPane footerbarPlaceHolder; + + @FXML + private MenuItem helpMenuItem; + + /** + * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. + */ + public NewMainWindow(Stage primaryStage, Logic logic) { + super(FXML, primaryStage); + + // Set dependencies + this.primaryStage = primaryStage; + this.logic = logic; + + // Configure the UI + setWindowDefaultSize(logic.getGuiSettings()); + + // setAccelerators(); + } + + public Stage getPrimaryStage() { + return primaryStage; + } + + private void setAccelerators() { + // setAccelerator(helpMenuItem, KeyCombination.valueOf("F1")); + } + + /** + * Sets the accelerator of a MenuItem. + * @param keyCombination the KeyCombination value of the accelerator + */ + private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { + menuItem.setAccelerator(keyCombination); + + /* + * TODO: the code below can be removed once the bug reported here + * https://bugs.openjdk.java.net/browse/JDK-8131666 + * is fixed in later version of SDK. + * + * According to the bug report, TextInputControl (TextField, TextArea) will + * consume function-key events. Because CommandBox contains a TextField, and + * ResultDisplay contains a TextArea, thus some accelerators (e.g F1) will + * not work when the focus is in them because the key event is consumed by + * the TextInputControl(s). + * + * For now, we add following event filter to capture such key events and open + * help window purposely so to support accelerators even when focus is + * in CommandBox or ResultDisplay. + */ + getRoot().addEventFilter(KeyEvent.KEY_PRESSED, event -> { + if (event.getTarget() instanceof TextInputControl && keyCombination.match(event)) { + menuItem.getOnAction().handle(new ActionEvent()); + event.consume(); + } + }); + } + + /** + * Fills up all the placeholders of this window. + */ + void fillInnerParts() { + + // result display + resultDisplay = new ResultDisplay(); + resultDisplayPlaceHolder.getChildren().add(resultDisplay.getRoot()); + + // last input display + lastInputDisplay = new LastInputDisplay(); + lastInputPlaceHolder.getChildren().add(lastInputDisplay.getRoot()); + + // command box + NewCommandBox commandBox = new NewCommandBox(this::executeCommand); + commandBoxPlaceHolder.getChildren().add(commandBox.getRoot()); + + // footer bar + FooterBar footerBar = new FooterBar("1.2"); + footerbarPlaceHolder.getChildren().add(footerBar.getRoot()); + } + + /** + * Sets the default size based on {@code guiSettings}. + */ + private void setWindowDefaultSize(GuiSettings guiSettings) { + primaryStage.setHeight(guiSettings.getWindowHeight()); + primaryStage.setWidth(guiSettings.getWindowWidth()); + if (guiSettings.getWindowCoordinates() != null) { + primaryStage.setX(guiSettings.getWindowCoordinates().getX()); + primaryStage.setY(guiSettings.getWindowCoordinates().getY()); + } + } + + /** + * Opens the help window or focuses on it if it's already opened. + */ + @FXML + public void handleHelp() { + + } + + void show() { + primaryStage.show(); + } + + /** + * Closes the application. + */ + @FXML + private void handleExit() { + GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), + (int) primaryStage.getX(), (int) primaryStage.getY()); + logic.setGuiSettings(guiSettings); + primaryStage.hide(); + } + + /** + * Executes the command and returns the result. + * + * @see seedu.address.logic.Logic#execute(String) + */ + private CommandResult executeCommand(String commandText) throws CommandException, ParseException { + try { + CommandResult commandResult = logic.execute(commandText); + logger.info("Result: " + commandResult.getFeedbackToUser()); + lastInputDisplay.setLastInput(commandText); + resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + + if (commandResult.isShowHelp()) { + handleHelp(); + } + + if (commandResult.isExit()) { + handleExit(); + } + + return commandResult; + } catch (CommandException | ParseException e) { + logger.info("Invalid command: " + commandText); + lastInputDisplay.setLastInput(commandText); + resultDisplay.setFeedbackToUser(e.getMessage()); + throw e; + } + } +} diff --git a/src/main/java/seedu/address/ui/NewUiManager.java b/src/main/java/seedu/address/ui/NewUiManager.java new file mode 100644 index 00000000000..5e27ac8c123 --- /dev/null +++ b/src/main/java/seedu/address/ui/NewUiManager.java @@ -0,0 +1,89 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.application.Platform; +import javafx.scene.control.Alert; +import javafx.scene.control.Alert.AlertType; +import javafx.scene.image.Image; +import javafx.stage.Stage; +import seedu.address.MainApp; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.StringUtil; +import seedu.address.logic.Logic; + +/** + * The manager of the UI component. + */ +public class NewUiManager implements Ui { + + public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; + + private static final Logger logger = LogsCenter.getLogger(UiManager.class); + private static final String ICON_APPLICATION = "/images/address_book_32.png"; + + private Logic logic; + private NewMainWindow mainWindow; + + /** + * Creates a {@code UiManager} with the given {@code Logic}. + */ + public NewUiManager(Logic logic) { + super(); + this.logic = logic; + } + + @Override + public void start(Stage primaryStage) { + logger.info("Starting UI..."); + + //Set the application icon. + primaryStage.getIcons().add(getImage(ICON_APPLICATION)); + + try { + mainWindow = new NewMainWindow(primaryStage, logic); + mainWindow.show(); //This should be called before creating other UI parts + mainWindow.fillInnerParts(); + + } catch (Throwable e) { + logger.severe(StringUtil.getDetails(e)); + showFatalErrorDialogAndShutdown("Fatal error during initializing", e); + } + } + + private Image getImage(String imagePath) { + return new Image(MainApp.class.getResourceAsStream(imagePath)); + } + + void showAlertDialogAndWait(Alert.AlertType type, String title, String headerText, String contentText) { + showAlertDialogAndWait(mainWindow.getPrimaryStage(), type, title, headerText, contentText); + } + + /** + * Shows an alert dialog on {@code owner} with the given parameters. + * This method only returns after the user has closed the alert dialog. + */ + private static void showAlertDialogAndWait(Stage owner, AlertType type, String title, String headerText, + String contentText) { + final Alert alert = new Alert(type); + alert.getDialogPane().getStylesheets().add("view/DarkTheme.css"); + alert.initOwner(owner); + alert.setTitle(title); + alert.setHeaderText(headerText); + alert.setContentText(contentText); + alert.getDialogPane().setId(ALERT_DIALOG_PANE_FIELD_ID); + alert.showAndWait(); + } + + /** + * Shows an error alert dialog with {@code title} and error message, {@code e}, + * and exits the application after the user has closed the alert dialog. + */ + private void showFatalErrorDialogAndShutdown(String title, Throwable e) { + logger.severe(title + " " + e.getMessage() + StringUtil.getDetails(e)); + showAlertDialogAndWait(Alert.AlertType.ERROR, title, e.getMessage(), e.toString()); + Platform.exit(); + System.exit(1); + } + +} diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 09f6d6fe9e4..fa312ff9f6a 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -2,8 +2,11 @@ + - - + + + + + - diff --git a/src/main/resources/view/FooterBar.fxml b/src/main/resources/view/FooterBar.fxml new file mode 100644 index 00000000000..7f420ecf82b --- /dev/null +++ b/src/main/resources/view/FooterBar.fxml @@ -0,0 +1,21 @@ + + + + + + + + + + + + diff --git a/src/main/resources/view/NewMainWindow.fxml b/src/main/resources/view/NewMainWindow.fxml new file mode 100644 index 00000000000..82162cdbe8e --- /dev/null +++ b/src/main/resources/view/NewMainWindow.fxml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 58d5ad3dc56..a5cf7ad5c63 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -2,8 +2,11 @@ + - -