From d919af7458176b755118f6f3d44f658d0034da32 Mon Sep 17 00:00:00 2001 From: Argent77 <4519923+Argent77@users.noreply.github.com> Date: Mon, 23 Oct 2023 18:58:00 +0200 Subject: [PATCH] Add option: Use separate folder for .debug files --- .../io/infinitytools/wit/Configuration.java | 9 ++ .../io/infinitytools/wit/gui/MainWindow.java | 152 +++++++++++++++++- .../wit/gui/MainWindowController.java | 4 + .../io/infinitytools/wit/utils/Utils.java | 51 ++++++ .../io/infinitytools/wit/gui/main.fxml | 25 +++ .../io/infinitytools/wit/gui/mainStyles.css | 4 + src/main/resources/l10n/wit.properties | 2 + src/main/resources/l10n/wit_de.properties | 2 + src/main/resources/l10n/wit_fr.properties | 3 + src/main/resources/l10n/wit_pt_BR.properties | 3 + 10 files changed, 254 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/infinitytools/wit/Configuration.java b/src/main/java/io/infinitytools/wit/Configuration.java index bf58ef0..804417d 100644 --- a/src/main/java/io/infinitytools/wit/Configuration.java +++ b/src/main/java/io/infinitytools/wit/Configuration.java @@ -119,6 +119,15 @@ public enum Key { * Specifies the directory to the last used mod's tp2 file. */ LAST_MOD_PATH("Last Mod Path", String.class, null), + /** + * Specifies whether the mod installation log should be saved in a separate folder. + */ + DEBUG_FOLDER_ENABLED("Enable Debug Folder", Boolean.class, false), + /** + * Specifies the folder name where mod installation logs should be saved. + * Folder name is relative to the game directory. + */ + DEBUG_FOLDER_NAME("Debug Folder Name", String.class, "debug"), /** * Specifies the language in which all text of the application should be displayed. * System language is used if this option is not defined. diff --git a/src/main/java/io/infinitytools/wit/gui/MainWindow.java b/src/main/java/io/infinitytools/wit/gui/MainWindow.java index f6adc03..58ad369 100644 --- a/src/main/java/io/infinitytools/wit/gui/MainWindow.java +++ b/src/main/java/io/infinitytools/wit/gui/MainWindow.java @@ -652,6 +652,40 @@ private void setTrayIconFeedbackEnabled(boolean newValue) { } } + /** + * Returns whether debug files from guided mod installations should be stored in a separate folder. + */ + public boolean isDebugFolderEnabled() { + return getController().debugFolderCheckBox.isSelected(); + } + + /** + * Defines whether debug files from guided mod installations should be stored in a separate folder. + */ + public void setDebugFolderEnabled(boolean newValue) { + getController().debugFolderCheckBox.setSelected(newValue); + } + + /** + * Returns the folder name for debug files from guided mod installations. + */ + public String getDebugFolderName() { + return getController().debugFolderTextField.getText(); + } + + /** + * Specifies the folder name for debug files from guided mod installations. + * + * @param folderName Folder name, relative to the game directory. + */ + public void setDebugFolderName(String folderName) { + String name = Utils.getValidatedFolderPath(folderName, true); + getController().debugFolderTextField.setText(name); + if (name.isEmpty()) { + getController().debugFolderCheckBox.setSelected(false); + } + } + /** * Returns whether the Details window is visible. */ @@ -828,6 +862,69 @@ private void onDetailsButtonSelected(boolean newValue) { } } + /** + * Ensures that the debug folder input field does not contain invalid characters. + */ + private void onDebugFolderTextChanged() { + final String text = getController().debugFolderTextField.getText(); + if (text.isEmpty()) { + return; + } + + int caret = getController().debugFolderTextField.getCaretPosition(); + StringBuilder sb = new StringBuilder(text); + + final String forbiddenChars = "<>:|?* \"\t\n\r\f"; + int i = 0; + while (i < sb.length()) { + final char ch = sb.charAt(i); + if (forbiddenChars.indexOf(ch) >= 0) { + sb.delete(i, i + 1); + if (i < caret) { + caret--; + } + } else { + // Unix uses forward slashes for path separators + if (!SystemInfo.IS_WINDOWS && ch == '\\') { + sb.replace(i, i+1, "/"); + } + i++; + } + } + caret = Math.min(caret, sb.length()); + + getController().debugFolderTextField.positionCaret(caret); + getController().debugFolderTextField.setText(sb.toString()); + + // updating info message + updateDebugFolderMessage(); + } + + /** + * Called whenever the selected state of the "Debug Folder" option is changed. + */ + private void onDebugFolderSelectionChanged() { + final boolean selected = getController().debugFolderCheckBox.isSelected(); + getController().debugFolderTextField.setDisable(!selected); + + // updating info message + updateDebugFolderMessage(); + } + + /** + * Shows or hides an info text associated with the "Debug Folder" option depending on the current option state. + */ + private void updateDebugFolderMessage() { + final boolean defEnabled = Configuration.getInstance().getOption(Configuration.Key.DEBUG_FOLDER_ENABLED); + final boolean curEnabled = getController().debugFolderCheckBox.isSelected(); + final String defText = Configuration.getInstance().getOption(Configuration.Key.DEBUG_FOLDER_NAME); + final String curText = getController().debugFolderTextField.getText(); + final boolean msgEnabled = defEnabled != curEnabled || !defText.contentEquals(curText); + + getController().debugFolderMessageLabel.setVisible(msgEnabled); + getController().debugFolderMessageLabel.setManaged(msgEnabled); + } + /** * Called by process event handler when a process starts or terminates. */ @@ -1316,6 +1413,12 @@ private void setupUI(Scene scene) { getController().bufferSizeSlider.valueProperty().addListener((ob, ov, nv) -> setOutputBufferSizeLabel(nv.doubleValue())); getController().outputFontSizeValueFactory.valueProperty().addListener((ob, ov, nv) -> setOutputAreaFontSize(nv)); + getController().debugFolderCheckBox.selectedProperty().addListener((ob, ov, nv) -> onDebugFolderSelectionChanged()); + getController().debugFolderTextField.textProperty().addListener((ob, ov, nv) -> onDebugFolderTextChanged()); + getController().debugFolderTextField.focusedProperty().addListener((ob, ov, nv) -> + getController().debugFolderTextField + .setText(Utils.getValidatedFolderPath(getController().debugFolderTextField.getText(), true))); + getController().showLogCheckItem.setOnAction(event -> { final boolean selected = getController().showLogCheckItem.isSelected(); try { @@ -1393,6 +1496,10 @@ private void setupUI(Scene scene) { setTrayIconFeedbackEnabled(Configuration.getInstance().getOption(Configuration.Key.TRAY_ICON_FEEDBACK)); setOutputBufferSize(Configuration.getInstance().getOption(Configuration.Key.BUFFER_LIMIT)); + setDebugFolderEnabled(Configuration.getInstance().getOption(Configuration.Key.DEBUG_FOLDER_ENABLED)); + setDebugFolderName(Configuration.getInstance().getOption(Configuration.Key.DEBUG_FOLDER_NAME)); + onDebugFolderSelectionChanged(); + applyOutputFontSize(Configuration.getInstance().getOption(Configuration.Key.OUTPUT_FONT_SIZE, getController().outputArea.getFont().getSize())); @@ -1765,6 +1872,8 @@ private void updateConfiguration() { cfg.setOption(Configuration.Key.TRAY_ICON_FEEDBACK, isTrayIconFeedbackEnabled()); cfg.setOption(Configuration.Key.QUIT_ON_ENTER, isAutoQuitEnabled()); cfg.setOption(Configuration.Key.VISUALIZE_RESULT, isVisualizeResultsEnabled()); + cfg.setOption(Configuration.Key.DEBUG_FOLDER_ENABLED, isDebugFolderEnabled()); + cfg.setOption(Configuration.Key.DEBUG_FOLDER_NAME, getDebugFolderName()); cfg.setOption(Configuration.Key.BUFFER_LIMIT, getOutputBufferSize()); cfg.setOption(Configuration.Key.OUTPUT_FONT_SIZE, getOutputAreaFontSize()); if (modInfo != null) { @@ -2121,7 +2230,7 @@ private String[] getWeiduCommand(String gameLang, Path tp2File) { final List command = new ArrayList<>(Configuration.getInstance().getWeiduArgs()); if (!command.contains("--log")) { - final Path logFile = getModInfo().getLogFile(); + final Path logFile = getEffectiveLogFilePath(true); command.add(0, logFile.toString()); command.add(0, "--log"); } @@ -2148,6 +2257,47 @@ private String[] getWeiduCommand(String gameLang, Path tp2File) { return command.toArray(new String[0]); } + /** + * Returns the effective path of the .debug file created by the mod installation process, which also considers + * the option to redirect debug files to custom folders. + * + * @param autoCreateFolders Specify {@code true} to ensure that the folder for the log file exists. + * @return Effective log file path. + */ + public Path getEffectiveLogFilePath(boolean autoCreateFolders) { + Path logFile = getModInfo().getLogFile(); + + // relocate log path if needed + if (isDebugFolderEnabled()) { + try { + final Path subPath = Path.of(getDebugFolderName()); + if (subPath.getNameCount() > 0) { + Path path; + if (logFile.getParent() == null) { + path = subPath; + } else { + path = logFile.getParent().resolve(subPath); + } + path = path.resolve(logFile.getFileName()); + logFile = path; + } + } catch (Exception ignored) { + } + } + + if (autoCreateFolders && logFile.getParent() != null && !Files.isDirectory(logFile.getParent())) { + try { + Files.createDirectories(logFile.getParent()); + } catch (IOException e) { + // falling back to original path on error + Logger.warn(e, "Could not create folder for debug file"); + logFile = getModInfo().getLogFile(); + } + } + + return logFile; + } + /** * Handles files dropped by a Drag&Drop operation or similar action. * diff --git a/src/main/java/io/infinitytools/wit/gui/MainWindowController.java b/src/main/java/io/infinitytools/wit/gui/MainWindowController.java index 1545ecc..29e8471 100644 --- a/src/main/java/io/infinitytools/wit/gui/MainWindowController.java +++ b/src/main/java/io/infinitytools/wit/gui/MainWindowController.java @@ -68,6 +68,10 @@ public class MainWindowController implements Initializable { public CheckMenuItem trayIconFeedbackCheckItem; public CheckMenuItem showLogCheckItem; public CustomMenuItem bufferSizeMenuItem; + public CustomMenuItem debugFolderMenuItem; + public CheckBox debugFolderCheckBox; + public TextField debugFolderTextField; + public Label debugFolderMessageLabel; public CustomMenuItem outputFontSizeMenuItem; public Spinner outputFontSizeSpinner; public SpinnerValueFactory.DoubleSpinnerValueFactory outputFontSizeValueFactory; diff --git a/src/main/java/io/infinitytools/wit/utils/Utils.java b/src/main/java/io/infinitytools/wit/utils/Utils.java index 4231fc4..b776d18 100644 --- a/src/main/java/io/infinitytools/wit/utils/Utils.java +++ b/src/main/java/io/infinitytools/wit/utils/Utils.java @@ -27,6 +27,7 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; +import java.nio.file.InvalidPathException; import java.nio.file.Path; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; @@ -294,6 +295,56 @@ public static String normalizePath(String path) { return retVal; } + /** + * Ensures that the specified folder name conforms to the file name specification. + * + * @param pathName Folder path as string. + * @param relative Indicates whether only relative folder paths are allowed. + * @return A valid path name conforming to the specified parameters. + */ + public static String getValidatedFolderPath(String pathName, boolean relative) { + if (pathName == null) { + pathName = ""; + } + + try { + Path path = Path.of(pathName); + if (relative && path.getNameCount() > 0 && path.isAbsolute()) { + path = path.subpath(1, path.getNameCount()); + } + + // removing or fixing invalid path names + List pathNames = new ArrayList<>(); + for (Path value : path) { + String item = value.toString().strip(); + + boolean check = !item.isEmpty() && !item.equals("..") && !item.equals("."); + if (check) { + while (item.endsWith(".")) { + item = item.substring(0, item.length() - 1); + } + } + + if (!item.isEmpty() && !item.equals("..") && !item.equals(".")) { + pathNames.add(item); + } + } + + // assembling final path + if (pathNames.isEmpty()) { + pathName = ""; + } else { + final String firstName = pathNames.removeFirst(); + pathName = Path.of(firstName, pathNames.toArray(new String[0])).toString(); + } + } catch (InvalidPathException e) { + Logger.debug("Invalid folder path: {}", pathName); + pathName = ""; + } + + return pathName; + } + /** * Attempts to resolve the specified path to an existing file or directory by looking at the parent path at each * unsuccessful pass. diff --git a/src/main/resources/io/infinitytools/wit/gui/main.fxml b/src/main/resources/io/infinitytools/wit/gui/main.fxml index b11874f..115cf0e 100644 --- a/src/main/resources/io/infinitytools/wit/gui/main.fxml +++ b/src/main/resources/io/infinitytools/wit/gui/main.fxml @@ -145,6 +145,31 @@ + + + + + + + + + + + + + + +