From 6d435c46ca19194b0929ce50da4475751eb0d56b Mon Sep 17 00:00:00 2001 From: ZZZank <47418975+ZZZank@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:11:37 +0800 Subject: [PATCH 1/2] line wrapping --- .../java/org/jackhuang/hmcl/ui/LogWindow.java | 45 ++++++++++++++++--- .../resources/assets/lang/I18N.properties | 1 + .../resources/assets/lang/I18N_zh.properties | 1 + .../assets/lang/I18N_zh_CN.properties | 1 + 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index 4a68ab04b35..d77955e6429 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -31,6 +31,8 @@ import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.layout.*; +import javafx.scene.text.Font; +import javafx.scene.text.Text; import javafx.stage.Stage; import org.jackhuang.hmcl.game.GameDumpGenerator; import org.jackhuang.hmcl.game.Log; @@ -164,6 +166,7 @@ private final class LogWindowImpl extends Control { private final ListView listView = new JFXListView<>(); private final BooleanProperty autoScroll = new SimpleBooleanProperty(); + private final BooleanProperty wrapText = new SimpleBooleanProperty(true); private final StringProperty[] buttonText = new StringProperty[LEVELS.length]; private final BooleanProperty[] showLevel = new BooleanProperty[LEVELS.length]; private final JFXButton btnAlwaysOnTop = FXUtils.newToggleButton4(SVG.KEEP, 20); @@ -171,6 +174,9 @@ private final class LogWindowImpl extends Control { private final JFXComboBox cboLines = new JFXComboBox<>(); private final StackPane stackPane = new StackPane(); + /// Bridge cell width and computed max width + private final DoubleProperty maxTextWidth = new SimpleDoubleProperty(0); + LogWindowImpl() { getStyleClass().add("log-window"); @@ -197,6 +203,30 @@ private final class LogWindowImpl extends Control { buttonText[i].bind(Bindings.concat(levelCountMap.get(LEVELS[i]), " " + LEVELS[i].name().toLowerCase(Locale.ROOT) + "s")); levelShownMap.get(LEVELS[i]).bind(showLevel[i]); } + + // for max width computation + Text textForMeasuring = new Text(); + String fontFamily = Lang.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT); + double fontSize = settings().logFontSizeProperty().get(); + textForMeasuring.setFont(Font.font(fontFamily, fontSize)); + + wrapText.addListener((obs, wrapTextOld, wrapTextNew) -> { + if (wrapTextNew) { + var clippedContainer = (Region) listView.lookup(".clipped-container"); + if (clippedContainer != null) { + maxTextWidth.bind(clippedContainer.widthProperty()); + } + } else { + maxTextWidth.unbind(); + + double max = 0; + for (Log log : LogWindow.this.logs) { + textForMeasuring.setText(log.getLog()); + max = Double.max(max, textForMeasuring.getLayoutBounds().getWidth() + 16); // 16: padding + } + maxTextWidth.set(max); + } + }); } private void onTerminateGame() { @@ -329,13 +359,10 @@ private static final class LogWindowSkin extends SkinBase { listView.setCellFactory(x -> new ListCell<>() { { getStyleClass().add("log-window-list-cell"); - Region clippedContainer = (Region) listView.lookup(".clipped-container"); - if (clippedContainer != null) { - maxWidthProperty().bind(clippedContainer.widthProperty()); - prefWidthProperty().bind(clippedContainer.widthProperty()); - } + maxWidthProperty().bind(control.maxTextWidth); + prefWidthProperty().bind(control.maxTextWidth); setPadding(new Insets(2)); - setWrapText(true); + wrapTextProperty().bind(control.wrapText); setGraphic(null); } @@ -395,6 +422,10 @@ protected void updateItem(Log item, boolean empty) { autoScrollCheckBox.setSelected(true); control.autoScroll.bind(autoScrollCheckBox.selectedProperty()); + JFXCheckBox wrapTextCheckBox = new JFXCheckBox(i18n("logwindow.wrap_text")); + wrapTextCheckBox.setSelected(true); + control.wrapText.bind(wrapTextCheckBox.selectedProperty()); + JFXButton exportLogsButton = new JFXButton(i18n("button.export")); exportLogsButton.setOnAction(e -> getSkinnable().onExportLogs()); @@ -414,7 +445,7 @@ protected void updateItem(Log item, boolean empty) { JFXButton clearButton = new JFXButton(i18n("button.clear")); clearButton.setOnAction(e -> getSkinnable().onClear()); - hBox.getChildren().setAll(autoScrollCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton); + hBox.getChildren().setAll(autoScrollCheckBox, wrapTextCheckBox, exportLogsButton, terminateButton, exportDumpPane, clearButton); control.getGameProcess().getProcess() .onExit() diff --git a/HMCL/src/main/resources/assets/lang/I18N.properties b/HMCL/src/main/resources/assets/lang/I18N.properties index 8f961f6a4b5..368276c4624 100644 --- a/HMCL/src/main/resources/assets/lang/I18N.properties +++ b/HMCL/src/main/resources/assets/lang/I18N.properties @@ -948,6 +948,7 @@ logwindow.terminate_game=Kill Game Process logwindow.title=Log logwindow.help=You can navigate to the HMCL community and find others for help. logwindow.autoscroll=Auto-scroll +logwindow.wrap_text=Wrap Text logwindow.export_game_crash_logs=Export Crash Logs logwindow.export_dump=Export Game Stack Dump logwindow.export_dump.no_dependency=Your Java does not contain the dependencies to create the stack dump. Please join our Discord or QQ group for help. diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh.properties b/HMCL/src/main/resources/assets/lang/I18N_zh.properties index 86955f95689..adc271801e3 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh.properties @@ -755,6 +755,7 @@ logwindow.terminate_game=結束遊戲處理程式 logwindow.title=日誌 logwindow.help=你可以前往 HMCL 社群,向他人尋求幫助 logwindow.autoscroll=自動滾動 +logwindow.wrap_text=自動換行 logwindow.export_game_crash_logs=匯出遊戲崩潰資訊 logwindow.export_dump=匯出遊戲執行堆疊 logwindow.export_dump.no_dependency=你的 Java 不包含用於建立遊戲執行堆疊的相依元件。請前往 Discord 或 HMCL QQ 群尋求幫助。 diff --git a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties index ad25c12b320..21745ab7744 100644 --- a/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties +++ b/HMCL/src/main/resources/assets/lang/I18N_zh_CN.properties @@ -760,6 +760,7 @@ logwindow.terminate_game=结束游戏进程 logwindow.title=日志 logwindow.help=你可以前往 HMCL 社区,寻找他人帮助 logwindow.autoscroll=自动滚动 +logwindow.wrap_text=自动换行 logwindow.export_game_crash_logs=导出游戏崩溃信息 logwindow.export_dump=导出游戏运行栈 logwindow.export_dump.no_dependency=你的 Java 不包含用于创建游戏运行栈的依赖。请前往 HMCL QQ 群或 Discord 寻求帮助。 From e67564123c2320aa0472634ceaad7468c52d3c02 Mon Sep 17 00:00:00 2001 From: ZZZank <47418975+ZZZank@users.noreply.github.com> Date: Sat, 18 Jul 2026 13:48:48 +0800 Subject: [PATCH 2/2] new idea from Glavo --- .../java/org/jackhuang/hmcl/ui/LogWindow.java | 46 ++++++------------- 1 file changed, 15 insertions(+), 31 deletions(-) diff --git a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java index d77955e6429..1a617abe43a 100644 --- a/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java +++ b/HMCL/src/main/java/org/jackhuang/hmcl/ui/LogWindow.java @@ -31,8 +31,6 @@ import javafx.scene.control.*; import javafx.scene.input.KeyCode; import javafx.scene.layout.*; -import javafx.scene.text.Font; -import javafx.scene.text.Text; import javafx.stage.Stage; import org.jackhuang.hmcl.game.GameDumpGenerator; import org.jackhuang.hmcl.game.Log; @@ -174,9 +172,6 @@ private final class LogWindowImpl extends Control { private final JFXComboBox cboLines = new JFXComboBox<>(); private final StackPane stackPane = new StackPane(); - /// Bridge cell width and computed max width - private final DoubleProperty maxTextWidth = new SimpleDoubleProperty(0); - LogWindowImpl() { getStyleClass().add("log-window"); @@ -203,30 +198,6 @@ private final class LogWindowImpl extends Control { buttonText[i].bind(Bindings.concat(levelCountMap.get(LEVELS[i]), " " + LEVELS[i].name().toLowerCase(Locale.ROOT) + "s")); levelShownMap.get(LEVELS[i]).bind(showLevel[i]); } - - // for max width computation - Text textForMeasuring = new Text(); - String fontFamily = Lang.requireNonNullElse(settings().logFontFamilyProperty().get(), FXUtils.DEFAULT_MONOSPACE_FONT); - double fontSize = settings().logFontSizeProperty().get(); - textForMeasuring.setFont(Font.font(fontFamily, fontSize)); - - wrapText.addListener((obs, wrapTextOld, wrapTextNew) -> { - if (wrapTextNew) { - var clippedContainer = (Region) listView.lookup(".clipped-container"); - if (clippedContainer != null) { - maxTextWidth.bind(clippedContainer.widthProperty()); - } - } else { - maxTextWidth.unbind(); - - double max = 0; - for (Log log : LogWindow.this.logs) { - textForMeasuring.setText(log.getLog()); - max = Double.max(max, textForMeasuring.getLayoutBounds().getWidth() + 16); // 16: padding - } - maxTextWidth.set(max); - } - }); } private void onTerminateGame() { @@ -359,8 +330,21 @@ private static final class LogWindowSkin extends SkinBase { listView.setCellFactory(x -> new ListCell<>() { { getStyleClass().add("log-window-list-cell"); - maxWidthProperty().bind(control.maxTextWidth); - prefWidthProperty().bind(control.maxTextWidth); + Region clippedContainer = (Region) listView.lookup(".clipped-container"); + if (clippedContainer != null) { + wrapTextProperty().addListener((obs, oldWrap, nowWrap) -> { + if (nowWrap) { + maxWidthProperty().bind(clippedContainer.widthProperty()); + prefWidthProperty().bind(clippedContainer.widthProperty()); + } else { + maxWidthProperty().unbind(); + prefWidthProperty().unbind(); + + setMaxWidth(Region.USE_PREF_SIZE); + setPrefWidth(Region.USE_COMPUTED_SIZE); + } + }); + } setPadding(new Insets(2)); wrapTextProperty().bind(control.wrapText); setGraphic(null);