diff --git a/src/main/java/app/MainController.java b/src/main/java/app/MainController.java index 0a078ee..f231514 100644 --- a/src/main/java/app/MainController.java +++ b/src/main/java/app/MainController.java @@ -13,7 +13,6 @@ import app.controllers.*; import app.events.ExitApplicationEvent; import app.events.LanguageChangedEvent; import app.events.OpenLinkInBrowserEvent; -import app.events.SaveFileEvent; import app.events.ThemeChangedEvent; import app.model.Model; import javafx.application.HostServices; @@ -21,6 +20,9 @@ import javafx.application.Platform; import javafx.fxml.FXML; import javafx.fxml.Initializable; +/** + * An FXML controller that controls the application and all subcontrollers + */ public class MainController implements Initializable { @FXML @@ -67,7 +69,9 @@ public class MainController implements Initializable { return hostServices; } - // TODO: Document + /** + * @return All subcontrollers of this controller + */ public List getInnerControllers() { return List.of(editorController, filetreeController, modelineController, menubarController); } @@ -95,13 +99,17 @@ public class MainController implements Initializable { Model.getScene().getStylesheets().set(position, nextStyleSheet); } + /* ------------------------------------------------------------------------ */ + /* EVENT BUS LISTENERS */ + /* ------------------------------------------------------------------------ */ + /** * Change the CSS according to which language is being used * * @param event */ @Subscribe - private void handle(LanguageChangedEvent event) { + public void handle(LanguageChangedEvent event) { this.setCSSAt(1, "/styling/languages/" + event.getLanguage().toLowerCase() + ".css"); } @@ -111,7 +119,7 @@ public class MainController implements Initializable { * @param event */ @Subscribe - private void handle(ThemeChangedEvent event) { + public void handle(ThemeChangedEvent event) { this.setCSSAt(0, "/styling/themes/" + event.getTheme().toLowerCase().replace(" ", "-") + ".css"); } @@ -121,7 +129,7 @@ public class MainController implements Initializable { * @param event */ @Subscribe - private void handle(OpenLinkInBrowserEvent event) { + public void handle(OpenLinkInBrowserEvent event) { this.getHostServices().showDocument(event.getLink()); } @@ -133,18 +141,14 @@ public class MainController implements Initializable { * @param event */ @Subscribe - private void handle(ExitApplicationEvent event) { + public void handle(ExitApplicationEvent event) { if (!Model.getFileIsSaved()) { int g = JOptionPane.showConfirmDialog(null, "Your files are not saved.\nSave before exit?", "Exit", JOptionPane.YES_NO_OPTION); - if (g == JOptionPane.YES_OPTION) { - this.eventBus.post(new SaveFileEvent()); - Platform.exit(); - } - } else { - Platform.exit(); + if (g == JOptionPane.YES_OPTION) + this.editorController.saveCodeArea(Model.getActiveFilePath().isEmpty()); } - + Platform.exit(); } } diff --git a/src/main/java/app/MainLauncher.java b/src/main/java/app/MainLauncher.java index 14ef3ce..882f3c8 100644 --- a/src/main/java/app/MainLauncher.java +++ b/src/main/java/app/MainLauncher.java @@ -1,6 +1,14 @@ package app; +/** + * A launcher class to point towards as the start point for a packaged JAR + */ public class MainLauncher { + /** + * The root function of the call stack + * + * @param args Commandline arguments + */ public static void main(String[] args) { Main.main(args); } diff --git a/src/main/java/app/controllers/Controller.java b/src/main/java/app/controllers/Controller.java index 499b3af..65007c6 100644 --- a/src/main/java/app/controllers/Controller.java +++ b/src/main/java/app/controllers/Controller.java @@ -3,12 +3,12 @@ package app.controllers; import com.google.common.eventbus.EventBus; /** - * Interface describing a controller that contains an EventBus + * Interface describing a JavaFX controller that contains an EventBus */ public interface Controller { /** * Registers the main EventBus into the controller. - * @param eventBus The EventBus + * @param eventBus */ public void setEventBus(EventBus eventBus); } diff --git a/src/main/java/app/controllers/EditorController.java b/src/main/java/app/controllers/EditorController.java index 02d9af0..e225116 100644 --- a/src/main/java/app/controllers/EditorController.java +++ b/src/main/java/app/controllers/EditorController.java @@ -34,7 +34,7 @@ import javafx.fxml.Initializable; import javafx.stage.Stage; /** - * A FXML controller that controls the editor component of the UI + * An FXML controller that controls the CodeArea */ public class EditorController implements Initializable, Controller { @@ -60,15 +60,10 @@ public class EditorController implements Initializable, Controller { this.eventBus.register(this); } - // TODO: document - public CodeArea getEditor() { - return editor; - } - /** * Applies highlighting to the editor. * - * @param highlighting highlighting data + * @param highlighting Syntax highlighting data */ private void setHighlighting(StyleSpans> highlighting) { this.editor.setStyleSpans(0, highlighting); @@ -90,7 +85,6 @@ public class EditorController implements Initializable, Controller { * ProgrammingLanguage.commentLine(line) */ private void toggleComment() { - // TODO: This logic might need to be moved to LanguageOperations if (editor.getSelectedText().equals("")) { String currentLine = editor.getText(editor.getCurrentParagraph()); @@ -118,7 +112,7 @@ public class EditorController implements Initializable, Controller { /** * Updates the wraptext setting of the code area * - * @param isWrapText The new value for the setting + * @param isWrapText The updated setting value */ private void setWrapText(boolean isWrapText) { this.editor.setWrapText(isWrapText); @@ -143,20 +137,20 @@ public class EditorController implements Initializable, Controller { * * @param newContent The String to be inserted into the editor */ - public void setEditorContent(String newContent) { + private void setEditorContent(String newContent) { editor.clear(); editor.appendText(newContent); } /** - * Saving/Writing to the file based on the spesific filepath. Otherwise it will - * open an error dialog to give the user feedback about what has happened. + * Saving/Writing to the file based on the active filepath in {@link app.model.Model Model} + * if it is a new File. Otherwise it will open a dialog to ask the user where to save the file. + * + * @param isNewFile Whether or not the file already has a path */ public void saveCodeArea(boolean isNewFile) { Stage stage = (Stage) editor.getScene().getWindow(); - isNewFile = Model.getActiveFilePath().isEmpty(); - if (isNewFile && FileOperations.saveFileWithDialog(stage, editor.getText())) { this.eventBus.post(new OpenFileEvent(Model.getActiveFilePath())); this.eventBus.post(new FileSaveStateChangedEvent(true)); @@ -166,17 +160,14 @@ public class EditorController implements Initializable, Controller { } } - /** - * Checking if all is saved before closing the app. The user can either choose - * to exit or go back to the application and save. - */ - /* ------------------------------------------------------------------------ */ - /* SUBSCRIPTIONS */ + /* EVENT BUS LISTENERS */ /* ------------------------------------------------------------------------ */ /** - * Updates Code Area (read from file) whenever the FileSelected is changed + * Updates the CodeArea whenever a new file is opened. + * + * @param event */ @Subscribe public void handle(OpenFileEvent event) { @@ -189,54 +180,96 @@ public class EditorController implements Initializable, Controller { } /** - * Save file (write to file) whenever the save in the menubare is selected + * Saves the editor content to a file + * + * @param event */ @Subscribe - private void handle(SaveFileEvent event) { + public void handle(SaveFileEvent event) { this.saveCodeArea(event.getIsNewFile()); } + /** + * Refreshes the syntax highlighting when the Programming language is changed + * + * @param event + */ @Subscribe - private void handle(LanguageChangedEvent event) { + public void handle(LanguageChangedEvent event) { this.refreshHighlighting(); } + /** + * Toggles a comment based on the editor state + * + * @param event + */ @Subscribe public void handle(ToggleCommentEvent event) { this.toggleComment(); } + /** + * Toggles the WrapText setting + * + * @param event + */ @Subscribe - private void handle(ToggleWrapTextEvent event) { + public void handle(ToggleWrapTextEvent event) { this.setWrapText(event.getIsWrapped()); } + /** + * Undo if focused + * + * @param event + */ @Subscribe - private void handle(UndoEvent event) { + public void handle(UndoEvent event) { if (this.editor.isFocused()) this.editor.undo(); } + /** + * Redo if focused + * + * @param event + */ @Subscribe - private void handle(RedoEvent event) { + public void handle(RedoEvent event) { if (this.editor.isFocused()) this.editor.redo(); } + /** + * Copy selected content if focused + * + * @param event + */ @Subscribe - private void handle(CopyEvent event) { + public void handle(CopyEvent event) { if (this.editor.isFocused()) this.editor.copy(); } + /** + * Cut selected content if focused + * + * @param event + */ @Subscribe - private void handle(CutEvent event) { + public void handle(CutEvent event) { if (this.editor.isFocused()) this.editor.cut(); } + /** + * Paste from clipboard if focused + * + * @param event + */ @Subscribe - private void handle(PasteEvent event) { + public void handle(PasteEvent event) { if (this.editor.isFocused()) this.editor.paste(); } diff --git a/src/main/java/app/controllers/FiletreeController.java b/src/main/java/app/controllers/FiletreeController.java index 5146680..475663c 100644 --- a/src/main/java/app/controllers/FiletreeController.java +++ b/src/main/java/app/controllers/FiletreeController.java @@ -26,7 +26,7 @@ import app.service.FiletreeOperations; import javafx.fxml.Initializable; /** - * A FXML controller that controls the filetree component of the UI + * An FXML controller that controls the Filetree */ public class FiletreeController implements Initializable, Controller { @@ -44,19 +44,16 @@ public class FiletreeController implements Initializable, Controller { this.eventBus.register(this); } - /* ------------------------------------------------------------------------ */ - /* FILETREE */ - /* ------------------------------------------------------------------------ */ - /** - * The displaying of the fileTree. The inputChosen(the path) is aquired from the - * eventBus (OpeFileProjectEvent). The root is created as a CheckBoxItems and - * sends it to generateTree, and after that setting it to the root. + * Generate a tree structure of a directory, and set the filetree to + * show the new tree + * + * @param rootDir Path to the directory to be the root of the tree */ - private void showTree(String inputChosen) { - CheckBoxTreeItem root = new CheckBoxTreeItem<>(inputChosen); + private void showTree(Path rootDir) { + CheckBoxTreeItem root = new CheckBoxTreeItem<>(rootDir.getFileName().toString()); filetree.setShowRoot(false); - File fileInputChosen = new File(inputChosen); + File fileInputChosen = rootDir.toFile(); try { FiletreeOperations.generateTree(fileInputChosen, root); @@ -67,18 +64,13 @@ public class FiletreeController implements Initializable, Controller { DialogBoxes.showErrorMessage( "Could not open folder.\n\n" + "Do you have the right permissions for this folder?\n" - + "Or does the folder contain any shortcut to somewhere within itself?"); + + "Or does the folder contain any shortcut to somewhere within itself?" + ); } } - /* ------------------------------------------------------------------------ */ - /* MouseClick */ - /* ------------------------------------------------------------------------ */ - /** - * Handles whenever a filetree item is clicked twice. A while loop to create the - * correct filepath. - */ + * Handles opening a file whenever a filetree item is clicked twice. */ @FXML private void handleMouseClick(MouseEvent event) { if (event.getClickCount() == 2) { @@ -98,27 +90,33 @@ public class FiletreeController implements Initializable, Controller { } /* ------------------------------------------------------------------------ */ - /* SUBSCRIPTIONS */ + /* EVENT BUS LISTENERS */ /* ------------------------------------------------------------------------ */ /** - * Updates the filetree whenever a new ProjectPath is selected. + * Updates the filetree whenever a new project is opened + * + * @param event */ - @Subscribe private void handle(OpenProjectEvent event) { event.getPath().ifPresentOrElse( - path -> this.showTree(path.toString()), + path -> this.showTree(path), () -> System.err.println("[ERROR] OpenProjectEvent was empty") ); } + /** + * Updates the filetree whenever a new file gets saved + * + * @param event + */ @Subscribe private void handle(SaveFileEvent event) { if (event.getIsNewFile()) Model .getProjectPath() - .ifPresent(path -> this.showTree(path.toString())); + .ifPresent(path -> this.showTree(path)); } } diff --git a/src/main/java/app/controllers/MenubarController.java b/src/main/java/app/controllers/MenubarController.java index 26ff94b..34bafd9 100644 --- a/src/main/java/app/controllers/MenubarController.java +++ b/src/main/java/app/controllers/MenubarController.java @@ -60,28 +60,25 @@ public class MenubarController implements Initializable, Controller { this.eventBus.register(this); } - /* ------------------------------------------------------------------------ */ - /* CREATE FILE/DIRECTORY */ - /* ------------------------------------------------------------------------ */ + /* ---------------------------------- File ---------------------------------- */ + + /** + * Handles whenever the New File button is pressed in the menubar + * + * @param event + */ @FXML private void handleNewFile() { this.eventBus.post(new OpenFileEvent(Optional.empty())); } - @FXML - private void handleNewFolder() { - - } - - /* ------------------------------------------------------------------------ */ - /* OPEN FILE/PROJECT */ - /* ------------------------------------------------------------------------ */ - /** * Handles whenever the Open File button is pressed in the menubar + * + * @param event */ @FXML - public void handleOpenFile() { + private void handleOpenFile() { Stage stage = (Stage) menubar.getScene().getWindow(); try { @@ -95,6 +92,8 @@ public class MenubarController implements Initializable, Controller { /** * Handles whenever the Open Project button is pressed in the menubar + * + * @param event */ @FXML private void handleOpenProject() { @@ -107,12 +106,11 @@ public class MenubarController implements Initializable, Controller { } catch (FileNotFoundException e) {} } - /* ------------------------------------------------------------------------ */ - /* SAVE FILE */ - /* ------------------------------------------------------------------------ */ /** * Handles whenever the Save button is pressed in the menubar + * + * @param event */ @FXML private void handleSaveFile() { @@ -121,14 +119,18 @@ public class MenubarController implements Initializable, Controller { /** * Handles whenever the Save as button is pressed in the menubar + * + * @param event */ @FXML private void handleSaveAsFile() { - this.eventBus.post(new SaveFileEvent(false)); + this.eventBus.post(new SaveFileEvent(true)); } /** * Handles whenever the programming language is changed from the menubar. + * + * @param event */ @FXML private void handleLanguageChange(ActionEvent event) { @@ -137,6 +139,8 @@ public class MenubarController implements Initializable, Controller { /** * Handles whenever the wraptext togglebutton is pressed in the menubar + * + * @param event */ @FXML private void handleToggleWraptext(ActionEvent event) { @@ -146,6 +150,8 @@ public class MenubarController implements Initializable, Controller { /** * Handles whenever the theme is changed from the menubar + * + * @param event */ @FXML private void handleThemeChange(ActionEvent event) { @@ -154,18 +160,20 @@ public class MenubarController implements Initializable, Controller { /** * Handles whenever the exit button is pressed in the menubar + * + * @param event */ @FXML private void handleExitApplication(ActionEvent event) { this.eventBus.post(new ExitApplicationEvent()); } - /* ------------------------------------------------------------------------ */ - /* EDIT */ - /* ------------------------------------------------------------------------ */ + /* ---------------------------------- Edit ---------------------------------- */ /** * Handles whenever the undo button is pressed in the menubar + * + * @param event */ @FXML private void handleUndo(ActionEvent event) { @@ -174,6 +182,8 @@ public class MenubarController implements Initializable, Controller { /** * Handles whenever the redo button is pressed in the menubar + * + * @param event */ @FXML private void handleRedo(ActionEvent event) { @@ -182,6 +192,8 @@ public class MenubarController implements Initializable, Controller { /** * Handles whenever the copy button is pressed in the menubar + * + * @param event */ @FXML private void handleCopy(ActionEvent event) { @@ -190,6 +202,8 @@ public class MenubarController implements Initializable, Controller { /** * Handles whenever the cut button is pressed in the menubar + * + * @param event */ @FXML private void handleCut(ActionEvent event) { @@ -198,6 +212,8 @@ public class MenubarController implements Initializable, Controller { /** * Handles whenever the paste button is pressed in the menubar + * + * @param event */ @FXML private void handlePaste(ActionEvent event) { @@ -206,18 +222,20 @@ public class MenubarController implements Initializable, Controller { /** * Handles whenever the Toggle Comment button is pressed in the menubar + * + * @param event */ @FXML private void handleToggleComment(ActionEvent event) { this.eventBus.post(new ToggleCommentEvent()); } - /* ------------------------------------------------------------------------ */ - /* ABOUT */ - /* ------------------------------------------------------------------------ */ + /* ---------------------------------- About --------------------------------- */ /** * Handles whenever the About button is pressed in the menubar + * + * @param event */ @FXML private void handleAbout(ActionEvent event) { @@ -231,22 +249,37 @@ public class MenubarController implements Initializable, Controller { /** * Updates menubuttons whenever the language is changed + * + * @param event */ @Subscribe - private void handle(LanguageChangedEvent event) { - this.languageToggleGroup.getToggles().stream().map(RadioMenuItem.class::cast) - .filter(t -> t.getId().equals("toggle" + event.getLanguage())).findFirst().orElseThrow().setSelected(true); + public void handle(LanguageChangedEvent event) { + this.languageToggleGroup + .getToggles() + .stream() + .map(RadioMenuItem.class::cast) + .filter(t -> t.getId().equals("toggle" + event.getLanguage())) + .findFirst() + // This should never happen! + .orElseThrow(() -> new IllegalStateException("Language button missing: " + event.getLanguage())) + .setSelected(true); } /** * Updates menubuttons whenever the theme is changed + * + * @param event */ - @Subscribe - private void handle(ThemeChangedEvent event) { - this.themeToggleGroup.getToggles().stream().map(RadioMenuItem.class::cast) - .filter(t -> t.getId().equals("toggle" + event.getTheme().replace(" ", "_"))).findFirst().orElseThrow().setSelected(true); - + public void handle(ThemeChangedEvent event) { + this.themeToggleGroup + .getToggles() + .stream() + .map(RadioMenuItem.class::cast) + .filter(t -> t.getId().equals("toggle" + event.getTheme().replace(" ", "_"))) + .findFirst() + // This should never happen! + .orElseThrow(() -> new IllegalStateException("Theme button missing: " + event.getTheme())) + .setSelected(true); } - } diff --git a/src/main/java/app/controllers/ModelineController.java b/src/main/java/app/controllers/ModelineController.java index f4c6e4d..1432791 100644 --- a/src/main/java/app/controllers/ModelineController.java +++ b/src/main/java/app/controllers/ModelineController.java @@ -53,34 +53,48 @@ public class ModelineController implements Initializable, Controller { this.columnrow.setText(String.format("[%d:%d]", row, column)); } + /* ------------------------------------------------------------------------ */ + /* SUBSCRIPTIONS */ + /* ------------------------------------------------------------------------ */ + /** * Updates the column-row number display whenever the editor cursor * changes position. + * + * @param event */ @Subscribe - private void handle(EditorChangedEvent event) { + public void handle(EditorChangedEvent event) { this.setColumnRow(event.getColumn(), event.getLine()); } /** * Updates the saveState label whenever the file either is saved or modified + * + * @param event */ @Subscribe - private void handle(FileSaveStateChangedEvent event) { + public void handle(FileSaveStateChangedEvent event) { // TODO: Add CSS styleclass for coloring the saveState label // whenever it changes this.saveState.setText(event.getIsSaved() ? "Saved!" : "Modified"); } /** - * Updates the modeline to display a new language - * whenever it is changed. + * Updates the modeline to display a new language when changed. + * + * @param event */ @Subscribe private void handle(LanguageChangedEvent event) { this.language.setText(event.getLanguage()); } + /** + * Updates the modeline to display the name of the current file when changed + * + * @param event + */ @Subscribe private void handle(OpenFileEvent event) { this.filename.setText( diff --git a/src/main/java/app/model/ProgrammingLanguage.java b/src/main/java/app/model/ProgrammingLanguage.java index 8c4c553..65bbd44 100644 --- a/src/main/java/app/model/ProgrammingLanguage.java +++ b/src/main/java/app/model/ProgrammingLanguage.java @@ -5,22 +5,21 @@ import java.util.regex.Pattern; /** * An interface describing functions required for a class to - * provide language specific details and functionality to the - * editor + * provide language specific details and functionality. */ public interface ProgrammingLanguage { /** - * The name of the programming language + * @return The name of the programming language */ public String getName(); /** - * The map containing the regex and corresponding style-classes to be used for syntax highlighting + * @return The map containing the regexes and corresponding style-classes to be used for syntax highlighting */ public Map getPatternMap(); /** - * The pattern containing all regexes for syntax highlighting + * @return A combined regex for syntax highlighting */ public Pattern getPattern(); @@ -39,8 +38,8 @@ public interface ProgrammingLanguage { public String unCommentLine(String line); /** - * Whether or not a line is commented * @param line The text of the line + * @return Whether or not a line is commented */ public boolean isCommentedLine(String line); @@ -52,15 +51,15 @@ public interface ProgrammingLanguage { public String commentSelection(String selection); /** - * Uncomment a line - * @param selection The text of the line to uncomment + * Uncomment an area of text + * @param selection The text of the area to uncomment * @return The uncommented area */ public String unCommentSelection(String selection); /** - * Whether or not an area of text is commented * @param selection The content of the area + * @return Whether or not an area of text is commented */ public boolean isCommentedSelection(String selection); diff --git a/src/main/java/app/model/languages/Java.java b/src/main/java/app/model/languages/Java.java index f531914..9dd2d2b 100644 --- a/src/main/java/app/model/languages/Java.java +++ b/src/main/java/app/model/languages/Java.java @@ -15,6 +15,7 @@ import app.model.ProgrammingLanguage; public class Java implements ProgrammingLanguage { private String name = "Java"; + private static Map pattern; private static final String[] keywords = new String[] { "abstract", "assert", "boolean", "break", "byte", @@ -52,9 +53,11 @@ public class Java implements ProgrammingLanguage { e("(?://.*)|/\\*(?:\\n|.)*?\\*/", "comment") ); - private static Map pattern; - public Java() { + this.initializePatternMap(); + } + + private void initializePatternMap() { pattern = new LinkedHashMap<>(); patternList .forEach(e -> pattern.put(e.getKey(), e.getValue())); diff --git a/src/main/java/app/model/languages/Markdown.java b/src/main/java/app/model/languages/Markdown.java index 4acc047..0aff825 100644 --- a/src/main/java/app/model/languages/Markdown.java +++ b/src/main/java/app/model/languages/Markdown.java @@ -14,6 +14,7 @@ import app.model.ProgrammingLanguage; public class Markdown implements ProgrammingLanguage { private String name = "Markdown"; + private static Map pattern; private static Entry e(String k, String v) { return new AbstractMap.SimpleEntry<>(Pattern.compile(k), v); @@ -38,9 +39,12 @@ public class Markdown implements ProgrammingLanguage { e("\\[\\d+\\]: .*", "source") ); - private static Map pattern; public Markdown() { + this.initializePatternMap(); + } + + private void initializePatternMap() { pattern = new LinkedHashMap<>(); patternList .forEach(e -> pattern.put(e.getKey(), e.getValue())); diff --git a/src/main/java/app/service/DialogBoxes.java b/src/main/java/app/service/DialogBoxes.java index 4c59f7b..f438db4 100644 --- a/src/main/java/app/service/DialogBoxes.java +++ b/src/main/java/app/service/DialogBoxes.java @@ -1,16 +1,51 @@ package app.service; +import java.io.File; + +import app.model.Model; import javafx.scene.control.Alert; import javafx.scene.control.Alert.AlertType; +import javafx.stage.DirectoryChooser; +import javafx.stage.FileChooser; +import javafx.stage.Stage; public class DialogBoxes { private DialogBoxes() {} + private static FileChooser fc = new FileChooser(); + private static DirectoryChooser dc = new DirectoryChooser(); + private static Alert error = new Alert(AlertType.ERROR); public static void showErrorMessage(String errorMessage) { - Alert error = new Alert(AlertType.ERROR); error.setContentText(errorMessage); error.showAndWait(); } + + public static File showopenFileWithDialog(Stage stage) { + fc.setTitle("Open File"); + File chosenFile = fc.showOpenDialog(stage); + + return chosenFile; + } + + public static File showOpenFolderWithDialog(Stage stage) { + dc.setTitle("Open Project"); + File dir = dc.showDialog(stage); + + return dir; + } + + public static File showSaveFileWithDialog(Stage stage) { + FileChooser fc = new FileChooser(); + fc.setTitle("Save as"); + + Model + .getProjectPath() + .ifPresent(path -> fc.setInitialDirectory(path.toFile())); + + File chosenLocation = fc.showSaveDialog(stage); + + return chosenLocation; + } } diff --git a/src/main/java/app/service/FileOperations.java b/src/main/java/app/service/FileOperations.java index 1820a21..6215aca 100644 --- a/src/main/java/app/service/FileOperations.java +++ b/src/main/java/app/service/FileOperations.java @@ -4,12 +4,11 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.PrintWriter; import java.nio.file.Path; +import java.util.NoSuchElementException; import java.util.Optional; import java.util.Scanner; import app.model.Model; -import javafx.stage.DirectoryChooser; -import javafx.stage.FileChooser; import javafx.stage.Stage; public class FileOperations { @@ -20,11 +19,7 @@ public class FileOperations { // TODO: This class needs to be extensively error checked public static File openFileWithDialog(Stage stage) throws FileNotFoundException { - - FileChooser fc = new FileChooser(); - fc.setTitle("Open File"); - - File chosenFile = fc.showOpenDialog(stage); + File chosenFile = DialogBoxes.showopenFileWithDialog(stage); if (chosenFile == null) throw new FileNotFoundException(); @@ -34,11 +29,7 @@ public class FileOperations { } public static File openFolderWithDialog(Stage stage) throws FileNotFoundException { - - DirectoryChooser dc = new DirectoryChooser(); - dc.setTitle("Open Project"); - - File dir = dc.showDialog(stage); + File dir = DialogBoxes.showOpenFolderWithDialog(stage); if (dir == null) throw new FileNotFoundException(); @@ -58,16 +49,15 @@ public class FileOperations { } public static boolean saveFileWithDialog(Stage stage, String content) { - FileChooser fc = new FileChooser(); - fc.setTitle("Save as"); + File chosenLocation; - Model - .getProjectPath() - .ifPresent(path -> fc.setInitialDirectory(path.toFile())); - - File chosenLocation = fc.showSaveDialog(stage); - if (chosenLocation == null) + try { + chosenLocation = DialogBoxes.showSaveFileWithDialog(stage); + } catch (NoSuchElementException e) { return false; + } + + if (chosenLocation == null) return false; if (saveFile(chosenLocation.toPath(), content)) { Model.setActiveFilePath(Optional.of(chosenLocation.toPath())); diff --git a/src/main/java/app/settings/SettingsProvider.java b/src/main/java/app/settings/SettingsProvider.java index a71d255..6ed5a87 100644 --- a/src/main/java/app/settings/SettingsProvider.java +++ b/src/main/java/app/settings/SettingsProvider.java @@ -17,15 +17,21 @@ import app.model.Model; public class SettingsProvider implements SettingsProviderI { - private static EventBus eventBus; + private EventBus eventBus; - private static final String SETTINGS_PATH = + private String settingsPath = (System.getProperty("os.name").startsWith("Windows")) ? System.getProperty("user.home") + "\\AppData\\Roaming\\/BNNsettings.dat" : System.getProperty("user.home") + System.getProperty("file.separator") + ".BNNsettings.dat"; - private static List legalSettings = + private List legalSettings = Arrays.asList("Java", "Markdown", "Monokai", "Solarized Light"); + + + // Only for testing purposes + protected void setSettingsPath(String settingsPath) { + this.settingsPath = settingsPath; + } public SettingsProvider(EventBus eB) { @@ -35,13 +41,13 @@ public class SettingsProvider implements SettingsProviderI { public void setEventBus(EventBus eB) { eventBus = eB; - SettingsProvider.eventBus.register(this); + eventBus.register(this); } @Override public void loadSettings() { List settings = new ArrayList<>(); - try (Scanner sc = new Scanner(new File(SETTINGS_PATH))) { + try (Scanner sc = new Scanner(new File(settingsPath))) { while (sc.hasNextLine()) { var nextLine = sc.nextLine().trim(); @@ -69,7 +75,7 @@ public class SettingsProvider implements SettingsProviderI { @Override public void saveSettings() { - try (PrintWriter writer = new PrintWriter(new File(SETTINGS_PATH))) { + try (PrintWriter writer = new PrintWriter(new File(settingsPath))) { writer.println("- Settings:"); writer.println("Programming Language = " + Model.getLanguage().getName()); writer.println("Theme = " + Model.getTheme()); diff --git a/src/main/java/app/settings/SettingsProviderI.java b/src/main/java/app/settings/SettingsProviderI.java index 8bed651..e8e3ba6 100644 --- a/src/main/java/app/settings/SettingsProviderI.java +++ b/src/main/java/app/settings/SettingsProviderI.java @@ -2,8 +2,14 @@ package app.settings; public interface SettingsProviderI { - void loadSettings(); + /** + * Load settings from disk, and fire events to update the program state + */ + void loadSettings(); - void saveSettings(); + /** + * Save the state from {@link app.model.Model Model} to disk. + */ + void saveSettings(); } diff --git a/src/main/resources/fxml/components/Menubar.fxml b/src/main/resources/fxml/components/Menubar.fxml index 0131d6b..ba0144a 100644 --- a/src/main/resources/fxml/components/Menubar.fxml +++ b/src/main/resources/fxml/components/Menubar.fxml @@ -19,7 +19,6 @@ - diff --git a/src/test/java/app/FxTestTemplate.java b/src/test/java/app/FxTestTemplate.java deleted file mode 100644 index 13886ee..0000000 --- a/src/test/java/app/FxTestTemplate.java +++ /dev/null @@ -1,44 +0,0 @@ -package app; - -import javafx.scene.Node; -import javafx.stage.Stage; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.testfx.api.FxToolkit; -import org.testfx.framework.junit5.ApplicationTest; -import org.testfx.util.WaitForAsyncUtils; - -import java.util.concurrent.TimeoutException; - -public class FxTestTemplate extends ApplicationTest { - - private Stage stage; - - @BeforeEach - public void runAppToTests() throws Exception { - FxToolkit.registerPrimaryStage(); - FxToolkit.setupApplication(Main::new); - FxToolkit.showStage(); - WaitForAsyncUtils.waitForFxEvents(100); - } - - @AfterEach - public void stopApp() throws TimeoutException { - FxToolkit.cleanupStages(); - } - - @Override - public void start(Stage primaryStage){ - this.stage = primaryStage; - primaryStage.toFront(); - } - - public Stage getStage() { - return stage; - } - - public T find(final String query) { - /** TestFX provides many operations to retrieve elements from the loaded GUI. */ - return lookup(query).query(); - } -} \ No newline at end of file diff --git a/src/test/java/app/controllers/EditorControllerTest.java b/src/test/java/app/controllers/EditorControllerTest.java index a8d14d1..500538f 100644 --- a/src/test/java/app/controllers/EditorControllerTest.java +++ b/src/test/java/app/controllers/EditorControllerTest.java @@ -2,6 +2,7 @@ package app.controllers; import java.io.File; import java.io.IOException; +import java.nio.file.Paths; import java.util.Optional; import org.fxmisc.richtext.CodeArea; @@ -19,7 +20,6 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -63,7 +63,7 @@ public class EditorControllerTest extends FxTestTemplate { private String mockContent = """ class HelloWorld { - private String message = "Hello world"; + private String message = \"Hello world\"; public String getMessage() { return message; @@ -100,11 +100,15 @@ public class EditorControllerTest extends FxTestTemplate { @Test @DisplayName("Test handling of OpenFileEvent with a file that doesn't exist") public void testOpenFileEventWithUnrealFile() throws IOException { + try (MockedStatic mocked = mockStatic(FileOperations.class)) { + mocked.when(() -> FileOperations.readFile(any())) + .thenReturn(null); - String brokenFilePath = "/doesNotExist.txt"; - eventBus.post(new OpenFileEvent(Optional.ofNullable(new File(brokenFilePath).toPath()))); + String brokenFilePath = "/doesNotExist.txt"; + eventBus.post(new OpenFileEvent(Optional.ofNullable(Paths.get(brokenFilePath)))); - verify(editor, never()).clear(); + verify(editor).appendText(""); + } } @Test diff --git a/src/test/java/app/service/DialogBoxesTest.java b/src/test/java/app/service/DialogBoxesTest.java new file mode 100644 index 0000000..532f3b9 --- /dev/null +++ b/src/test/java/app/service/DialogBoxesTest.java @@ -0,0 +1,8 @@ +package app.service; + +public class DialogBoxesTest { + + // THIS CLASS COULD NOT BE UNITTESTED BECAUSE OF LACKING SUPPORT FOR MOCKING + // STATIC OBJECTS WITH MOCKITO AND JUNI5 + +} diff --git a/src/test/java/app/service/FileOperationsTest.java b/src/test/java/app/service/FileOperationsTest.java new file mode 100644 index 0000000..2ccedaa --- /dev/null +++ b/src/test/java/app/service/FileOperationsTest.java @@ -0,0 +1,190 @@ +package app.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import com.google.common.io.Files; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; + +import app.model.Model; +import javafx.scene.control.Alert; +import javafx.stage.DirectoryChooser; +import javafx.stage.FileChooser; +import javafx.stage.Stage; + +@ExtendWith(MockitoExtension.class) +public class FileOperationsTest { + + // THIS CLASS COULD NOT BE UNITTESTED BECAUSE OF LACKING SUPPORT FOR MOCKING + // STATIC OBJECTS WITH MOCKITO AND JUNI5 + + // @TempDir + // File tmp; + + // @Mock + // FileChooser fc = mock(FileChooser.class); + + // @Mock + // DirectoryChooser dc = mock(DirectoryChooser.class); + + // @Mock + // Alert error = mock(Alert.class); + + // @InjectMocks + // MockedStatic db = mockStatic(DialogBoxes.class); + + // @Test + // @DisplayName("Test openFileWithDialog") + // public void testOpenFileWithDialog() { + // // try (MockedStatic mocked = mockStatic(DialogBoxes.class)) { + // Stage stage = mock(Stage.class); + + // db.when(() -> DialogBoxes.showopenFileWithDialog(any())) + // .thenReturn(null); + // assertThrows(FileNotFoundException.class, () -> FileOperations.openFileWithDialog(stage)); + + // File file = mock(File.class); + // db.when(() -> DialogBoxes.showopenFileWithDialog(any())) + // .thenReturn(file); + // try { + // assertEquals(file, FileOperations.openFileWithDialog(stage)); + // } catch (FileNotFoundException e) { + // fail("Chosen file was null when it was expected to be mock file"); + // } + // // } + // } + + + // @Test + // @DisplayName("Test openFolderWithDialog") + // public void testOpenFolderWithDialog() { + // try (MockedStatic mocked = mockStatic(DialogBoxes.class)) { + // Stage stage = mock(Stage.class); + + // mocked.when(() -> DialogBoxes.showOpenFolderWithDialog(any())) + // .thenReturn(null); + // assertThrows(FileNotFoundException.class, () -> FileOperations.openFolderWithDialog(stage)); + + // File file = mock(File.class); + // mocked.when(() -> DialogBoxes.showOpenFolderWithDialog(any())) + // .thenReturn(file); + // try { + // assertEquals(file, FileOperations.openFolderWithDialog(stage)); + // } catch (FileNotFoundException e) { + // fail("Chosen file was null when it was expected to be mock file"); + // } + + // } + // } + + // private File createTemporaryFile() throws IOException { + // File f = new File(tmp, "test.txt"); + // f.createNewFile(); + // return f; + // } + + // @Test + // @DisplayName("Test saveFile") + // public void testSaveFile() { + // String content = "test\ncontent\nfor\nyou"; + // File f; + + // try (MockedStatic mocked = mockStatic(DialogBoxes.class)) { + // // mocked.when(() -> DialogBoxes.showErrorMessage(anyString())); + + // f = createTemporaryFile(); + // assertTrue(FileOperations.saveFile(f.toPath(), content)); + + // List read = Files.readLines(f, StandardCharsets.UTF_8); + // String value = String.join("\n", read); + // assertEquals(content, value); + + // Path wrongPath = Paths.get("wrongPath.txt"); + // assertFalse(FileOperations.saveFile(wrongPath, content)); + + // } catch (IOException e) { + // fail("Unexpected temporary file failure"); + // } + // } + + // @Test + // @DisplayName("Test saveFileWithDialog") + // public void testSaveFileWithDialog() { + // String content = "test\ncontent\nfor\nyou"; + // File f; + + // try (MockedStatic mocked = mockStatic(DialogBoxes.class)) { + // Stage stage = mock(Stage.class); + + // mocked.when(() -> DialogBoxes.showSaveFileWithDialog(any())) + // .thenReturn(false); + // assertFalse(FileOperations.saveFileWithDialog(stage, content)); + + // mocked.when(() -> DialogBoxes.showSaveFileWithDialog(any())) + // .thenReturn(null); + // assertFalse(FileOperations.saveFileWithDialog(stage, content)); + + // f = createTemporaryFile(); + // mocked.when(() -> DialogBoxes.showSaveFileWithDialog(any())) + // .thenReturn(f); + // assertTrue(FileOperations.saveFileWithDialog(stage, content)); + // assertEquals(Model.getActiveFilePath(), f.toPath()); + + // File wrongFile = new File("Does not exist"); + // mocked.when(() -> DialogBoxes.showSaveFileWithDialog(any())) + // .thenReturn(wrongFile); + // assertFalse(FileOperations.saveFileWithDialog(stage, content)); + // } catch (IOException e) { + // fail("Unexpected IOexception when creating temporary file"); + // } + // } + + // @Test + // @DisplayName("Test readFile") + // public void testReadFile() { + // File f; + + // try (MockedStatic mocked = mockStatic(DialogBoxes.class)) { + // // mocked.when(() -> DialogBoxes.showErrorMessage(anyString())); + + // assertEquals("", FileOperations.readFile(null)); + + // String content = "test\ncontent\nfor\nyou"; + // f = createTemporaryFile(); + + // Files.write(content.getBytes(), f); + + // assertEquals(content, FileOperations.readFile(f.toPath())); + + // Path wrongPath = Paths.get("wrongPath.txt"); + // assertThrows(FileNotFoundException.class, () -> FileOperations.readFile(wrongPath)); + + + // } catch (IOException e) { + // fail("Unexpected temporary file failure"); + // } + // } + +} diff --git a/src/test/java/app/settings/SettingsProviderTest.java b/src/test/java/app/settings/SettingsProviderTest.java new file mode 100644 index 0000000..4346020 --- /dev/null +++ b/src/test/java/app/settings/SettingsProviderTest.java @@ -0,0 +1,108 @@ +package app.settings; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; + +import com.google.common.eventbus.EventBus; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.junit.jupiter.MockitoExtension; + +import app.model.Model; +import app.model.languages.Java; +import app.model.languages.Markdown; + +@ExtendWith(MockitoExtension.class) +public class SettingsProviderTest { + + @TempDir + File tmp; + + private EventBus eventBus = new EventBus(); + + private SettingsProvider sp = new SettingsProvider(eventBus); + + @BeforeEach + private void initializeSettingsPath() { + sp.setSettingsPath(Paths.get(tmp.toPath().toString(), "BNNsettings.dat").toString()); + } + + @Test + @DisplayName("Test loadSettings with pre-existing settings file") + public void testLoadSettings() throws IOException { + File f = new File(tmp, "BNNsettings.dat"); + f.createNewFile(); + + Files.writeString( + f.toPath(), + "- Settings:\n" + + "Programming Language = Markdown\n" + + "Theme = Solarized Light", + StandardOpenOption.WRITE + ); + + sp.loadSettings(); + assertTrue(Model.getLanguage() instanceof Markdown); + assertEquals("Solarized Light", Model.getTheme()); + } + + @Test + @DisplayName("Test loadSettings without pre-existing settings file") + public void testLoadSettingsWithoutFile() throws IOException { + + sp.loadSettings(); + assertTrue(Model.getLanguage() instanceof Java); + assertEquals("Monokai", Model.getTheme()); + } + + @Test + @DisplayName("Test loadSettings with broken settings file") + public void testLoadSettingsWithErrorFile() throws IOException { + File f = new File(tmp, "BNNsettings.dat"); + f.createNewFile(); + + Files.writeString( + f.toPath(), + "- Settings:\n" + + "Programming Language = Nonexisting Language\n" + + "Theme = Solarized Light", + StandardOpenOption.WRITE + ); + + sp.loadSettings(); + assertTrue(Model.getLanguage() instanceof Java); + assertEquals("Monokai", Model.getTheme()); + } + + @Test + @DisplayName("Test save settings") + public void testSaveSettings() { + Model.setLanguage(new Markdown()); + Model.setTheme("Solarized Light"); + + sp.saveSettings(); + + try { + assertEquals( + "- Settings:\n" + + "Programming Language = Markdown\n" + + "Theme = Solarized Light\n", + Files.readString(Paths.get(tmp.toString(), "BNNsettings.dat")) + ); + } catch (IOException e) { + fail("Couldn't read settings file"); + } + } + +}