diff --git a/build.gradle b/build.gradle index b3189f07fec..b7e1ff346d7 100644 --- a/build.gradle +++ b/build.gradle @@ -73,6 +73,12 @@ dependencies { implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'win' implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-media', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-media', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-media', version: javaFxVersion, classifier: 'linux' + implementation group: 'org.openjfx', name: 'javafx-web', version: javaFxVersion, classifier: 'win' + implementation group: 'org.openjfx', name: 'javafx-web', version: javaFxVersion, classifier: 'mac' + implementation group: 'org.openjfx', name: 'javafx-web', version: javaFxVersion, classifier: 'linux' implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' diff --git a/src/main/java/ay2021s1_cs2103_w16_3/finesse/ui/MainWindow.java b/src/main/java/ay2021s1_cs2103_w16_3/finesse/ui/MainWindow.java index a5c6a352c31..4f98f9c3511 100644 --- a/src/main/java/ay2021s1_cs2103_w16_3/finesse/ui/MainWindow.java +++ b/src/main/java/ay2021s1_cs2103_w16_3/finesse/ui/MainWindow.java @@ -15,11 +15,13 @@ import ay2021s1_cs2103_w16_3.finesse.ui.tabs.ExpenseTabPane; import ay2021s1_cs2103_w16_3.finesse.ui.tabs.IncomeTabPane; import ay2021s1_cs2103_w16_3.finesse.ui.tabs.OverviewTabPane; +import ay2021s1_cs2103_w16_3.finesse.ui.tabs.UserGuideTabPane; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.SelectionModel; import javafx.scene.control.Tab; import javafx.scene.control.TabPane; +import javafx.scene.control.ToggleButton; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.StackPane; import javafx.scene.text.Text; @@ -35,9 +37,6 @@ public class MainWindow extends UiPart { private static final String WELCOME_MESSAGE = "Welcome to Fine$$e - your personal finance tracker." + "\nPlease enter the command \"help\" to view the user guide on the various commands you can use."; - private static final String USERGUIDE_URL = "https://ay2021s1-cs2103t-w16-3.github.io/tp/UserGuide.html"; - private static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL + "." - + "\nPlease copy the url and paste it in your favourite browser to view all valid commands."; private final Logger logger = LogsCenter.getLogger(getClass()); @@ -53,8 +52,6 @@ public class MainWindow extends UiPart { @FXML private Button commandBoxButton; @FXML - private Button helpButton; - @FXML private StackPane resultDisplayPlaceholder; @FXML private StackPane statusbarPlaceholder; @@ -67,6 +64,10 @@ public class MainWindow extends UiPart { @FXML private Tab analyticsTab; @FXML + private Tab userGuideTab; + @FXML + private ToggleButton userGuideButton; + @FXML private TabPane tabPane; @FXML private Text expenseLimit; @@ -118,6 +119,7 @@ public void fillInnerParts() { * Initialize the contents of each tab. */ public void initializeTabs() { + // Set up tab contents. OverviewTabPane overviewTabPane = new OverviewTabPane(logic.getFilteredTransactionList(), logic.getMonthlyBudget()); overviewTab.setContent(overviewTabPane.getRoot()); @@ -133,15 +135,26 @@ public void initializeTabs() { AnalyticsTabPane analyticsTabPane = new AnalyticsTabPane(logic.getMonthlyBudget()); analyticsTab.setContent(analyticsTabPane.getRoot()); + UserGuideTabPane userGuideTabPane = new UserGuideTabPane(); + userGuideTab.setContent(userGuideTabPane.getRoot()); + + // Set default selection. SelectionModel selectionModel = tabPane.getSelectionModel(); selectionModel.select(overviewTab); // Set the bottom anchor after the tabs have been initialized. AnchorPane.setBottomAnchor(tabPane, 0.0); - // Update UI state on tab change. - tabPane.getSelectionModel().selectedIndexProperty().addListener((observable, oldTabIndex, newTabIndex) -> - uiState.setCurrentTab(UiState.Tab.values()[newTabIndex.intValue()])); + tabPane.getSelectionModel().selectedIndexProperty().addListener((observable, oldTabIndex, newTabIndex) -> { + // Update UI state on tab change. + uiState.setCurrentTab(UiState.Tab.values()[newTabIndex.intValue()]); + + // Disable the user guide tab when not selected to prevent the user from clicking the invisible tab. + userGuideTab.setDisable(newTabIndex.intValue() != UiState.Tab.USER_GUIDE.getTabIndex().getZeroBased()); + + // Set the user guide button to be unselected. + userGuideButton.setSelected(false); + }); } /** @@ -197,11 +210,12 @@ private void initializeWindowListeners() { } /** - * Displays the help message in the result display. + * Switches to the hidden user guide tab and sets the user guide button to be selected. */ @FXML - public void handleHelp() { - resultDisplay.setFeedbackToUser(HELP_MESSAGE); + public void switchToUserGuideTab() { + tabPane.getSelectionModel().select(userGuideTab); + userGuideButton.setSelected(true); } void show() { @@ -231,7 +245,7 @@ private CommandResult executeCommand(String commandText) throws CommandException resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); if (commandResult.isShowHelp()) { - handleHelp(); + switchToUserGuideTab(); } if (commandResult.isExit()) { diff --git a/src/main/java/ay2021s1_cs2103_w16_3/finesse/ui/UiState.java b/src/main/java/ay2021s1_cs2103_w16_3/finesse/ui/UiState.java index ffc8b71841b..55d11da1338 100644 --- a/src/main/java/ay2021s1_cs2103_w16_3/finesse/ui/UiState.java +++ b/src/main/java/ay2021s1_cs2103_w16_3/finesse/ui/UiState.java @@ -15,7 +15,8 @@ public enum Tab { OVERVIEW(1), INCOME(2), EXPENSES(3), - ANALYTICS(4); + ANALYTICS(4), + USER_GUIDE(5); /** The index of the tab in the {@code TabPane}. */ private final Index tabIndex; diff --git a/src/main/java/ay2021s1_cs2103_w16_3/finesse/ui/tabs/UserGuideTabPane.java b/src/main/java/ay2021s1_cs2103_w16_3/finesse/ui/tabs/UserGuideTabPane.java new file mode 100644 index 00000000000..35943cfa5b5 --- /dev/null +++ b/src/main/java/ay2021s1_cs2103_w16_3/finesse/ui/tabs/UserGuideTabPane.java @@ -0,0 +1,117 @@ +package ay2021s1_cs2103_w16_3.finesse.ui.tabs; + +import java.util.logging.Logger; + +import ay2021s1_cs2103_w16_3.finesse.commons.core.LogsCenter; +import ay2021s1_cs2103_w16_3.finesse.ui.UiPart; +import javafx.animation.KeyFrame; +import javafx.animation.Timeline; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.layout.StackPane; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebView; +import javafx.util.Duration; + +/** + * Tab pane that displays the user guide. + */ +public class UserGuideTabPane extends UiPart { + private static final String FXML = "UserGuideTabPane.fxml"; + + // Links + private static final String GITHUB_PAGES_DOMAIN = "https://ay2021s1-cs2103t-w16-3.github.io"; + private static final String USER_GUIDE_URL = "https://ay2021s1-cs2103t-w16-3.github.io/tp/UserGuide.html"; + private static final String NO_EXTERNAL_SITE_PAGE_URL = + "https://ay2021s1-cs2103t-w16-3.github.io/tp/NoExternalSite.html"; + + // Constants + private static final double REFRESH_TIMEOUT_DELAY = 10000.0; + + // Logging messages + private static final String WEB_ENGINE_EXTERNAL_SITE_REQUEST_BLOCKED = + "Request to load page on external site blocked"; + private static final String WEB_ENGINE_WORKER_STATE_CANCELLED = "Page load cancelled: "; + private static final String WEB_ENGINE_WORKER_STATE_FAILED = "Unable to load page (no internet connection): "; + private static final String WEB_ENGINE_WORKER_STATE_RUNNING = "Loading page: "; + private static final String WEB_ENGINE_WORKER_STATE_SCHEDULED = "Page load scheduled: "; + private static final String WEB_ENGINE_WORKER_STATE_SUCCEEDED = "Page successfully loaded: "; + + private final Logger logger = LogsCenter.getLogger(getClass()); + + @FXML + private WebView webView; + + /** + * Constructs a {@code UserGuideTabPane}. + */ + public UserGuideTabPane() { + super(FXML); + initializeUserGuide(); + } + + /** + * Initializes the {@code WebEngine} that is backing the {@code WebView}. + */ + private void initializeUserGuide() { + // Disable right click. + webView.setContextMenuEnabled(false); + + WebEngine webEngine = webView.getEngine(); + + webEngine.locationProperty().addListener((observableValue, oldUrl, newUrl) -> { + if (!newUrl.startsWith(GITHUB_PAGES_DOMAIN)) { + logger.info(WEB_ENGINE_EXTERNAL_SITE_REQUEST_BLOCKED); + + // Block requests to external sites. + Platform.runLater(() -> { + // Load the 'No External Site' page. + webEngine.load(NO_EXTERNAL_SITE_PAGE_URL); + }); + } + }); + + // Initialize refresh task. + Timeline refreshTask = new Timeline(new KeyFrame(Duration.millis(REFRESH_TIMEOUT_DELAY), + actionEvent -> refreshPage())); + + webEngine.getLoadWorker().stateProperty().addListener((observableValue, oldState, newState) -> { + String location = webEngine.getLocation(); + + // Stop the refresh task to prevent multiple tasks running. + refreshTask.stop(); + + switch (newState) { + case CANCELLED: + logger.info(WEB_ENGINE_WORKER_STATE_CANCELLED + location); + break; + case FAILED: + logger.warning(WEB_ENGINE_WORKER_STATE_FAILED + location); + refreshTask.playFromStart(); + break; + case RUNNING: + logger.info(WEB_ENGINE_WORKER_STATE_RUNNING + location); + break; + case SCHEDULED: + logger.info(WEB_ENGINE_WORKER_STATE_SCHEDULED + location); + break; + case SUCCEEDED: + logger.info(WEB_ENGINE_WORKER_STATE_SUCCEEDED + location); + break; + default: + } + }); + + webEngine.load(USER_GUIDE_URL); + } + + /** + * Refreshes the WebView. + */ + public void refreshPage() { + // Cannot simply reload the page because if the page has not loaded a single time, + // reloading does not work. + String currentPage = webView.getEngine().getLocation(); + webView.getEngine().load(currentPage); + } +} diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 12768048ac8..373116bfc2d 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -268,7 +268,7 @@ -fx-text-fill: white; } -#helpButton { +#userGuideButton { -fx-background-color: transparent; -fx-font-family: "Roboto Condensed"; -fx-font-size: 24px; @@ -276,8 +276,7 @@ -fx-padding: 20, 0, 0, 0; } -#helpButton:hover { - -fx-background-color: transparent; +#userGuideButton:hover, #userGuideButton:selected { -fx-text-fill: white; } diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 546c9748786..593ce59473c 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -3,9 +3,9 @@ - + @@ -31,8 +31,10 @@ + -