feat: 文件选择器支持选择文件和文件夹 (#5806)

Co-authored-by: Glavo <[email protected]>
Co-authored-by: 3gf8jv4dv <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-04-27 21:25:50 +08:00
1 parent d6648a840e
commit 4e6e832e9c
10 files changed
+62 -30

No files matched your search

@@ -62,6 +62,7 @@ public enum SVG {
EXTENSION_FILL("M8.8 21H5q-.825 0-1.4125-.5875T3 19V15.2q1.2 0 2.1-.7625T6 12.5q0-1.175-.9-1.9375T3 9.8V6q0-.825.5875-1.4125T5 4H9q0-1.05.725-1.775T11.5 1.5q1.05 0 1.775.725T14 4h4q.825 0 1.4125.5875T20 6v4q1.05 0 1.775.725T22.5 12.5q0 1.05-.725 1.775T20 15v4q0 .825-.5875 1.4125T18 21H14.2q0-1.25-.7875-2.125T11.5 18q-1.125 0-1.9125.875T8.8 21Z"),
FEEDBACK("M12 15Q12.425 15 12.7125 14.7125T13 14Q13 13.575 12.7125 13.2875T12 13Q11.575 13 11.2875 13.2875T11 14Q11 14.425 11.2875 14.7125T12 15ZM11 11H13V5H11V11ZM2 22V4Q2 3.175 2.5875 2.5875T4 2H20Q20.825 2 21.4125 2.5875T22 4V16Q22 16.825 21.4125 17.4125T20 18H6L2 22ZM5.15 16H20V4H4V17.125L5.15 16ZM4 16V4 16Z"),
FEEDBACK_FILL("M2 22V4q0-.825.5875-1.4125T4 2H20q.825 0 1.4125.5875T22 4V16q0 .825-.5875 1.4125T20 18H6L2 22Zm10-7q.425 0 .7125-.2875T13 14t-.2875-.7125T12 13t-.7125.2875T11 14t.2875.7125T12 15Zm-1-4h2V5H11v6Z"),
FILE_OPEN("M6 22q-0.825 0-1.4125-0.5875T4 20v-16q0-0.825 0.5875-1.4125T6 2h8l6 6v6h-2v-5H13v-5H6v16h9v2H6Zm15.95 0.375L19 19.425v2.225h-2v-5.65h5.65v2h-2.25l2.95 2.95-1.4 1.425Zm-15.95-2.375v-16 16Z"),
FOLDER("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8V18Q22 18.825 21.4125 19.4125T20 20H4ZM4 18H20V8H11.175L9.175 6H4V18ZM4 18V6 18Z"),
FOLDER_COPY("M3 21Q2.175 21 1.5875 20.4125T1 19V6H3V19H20V21H3ZM7 17Q6.175 17 5.5875 16.4125T5 15V4Q5 3.175 5.5875 2.5875T7 2H12L14 4H21Q21.825 4 22.4125 4.5875T23 6V15Q23 15.825 22.4125 16.4125T21 17H7ZM7 15H21V6H13.175L11.175 4H7V15ZM7 15V4 15Z"),
FOLDER_OPEN("M4 20Q3.175 20 2.5875 19.4125T2 18V6Q2 5.175 2.5875 4.5875T4 4H10L12 6H20Q20.825 6 21.4125 6.5875T22 8H11.175L9.175 6H4V18L6.4 10H23.5L20.925 18.575Q20.725 19.225 20.1875 19.6125T19 20H4ZM6.1 18H19L20.8 12H7.9L6.1 18ZM6.1 18 7.9 12 6.1 18ZM4 8V6 8Z"),
@@ -18,6 +18,7 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXPopup;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
@@ -30,6 +31,7 @@ import javafx.stage.FileChooser;
import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.io.FileUtils;
import java.nio.file.Path;
@@ -38,10 +40,18 @@ import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
public class FileSelector extends HBox {
private final StringProperty value = new SimpleStringProperty();
private String chooserTitle = i18n("selector.choose_file");
private boolean directory = false;
private String chooserTitle = "";
private SelectionMode selectionMode = SelectionMode.FILE;
private final ObservableList<FileChooser.ExtensionFilter> extensionFilters = FXCollections.observableArrayList();
JFXButton selectButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN, 15);
public enum SelectionMode {
FILE,
DIRECTORY,
FILE_OR_DIRECTORY
}
public String getValue() {
return value.get();
}
@@ -63,12 +73,12 @@ public class FileSelector extends HBox {
return this;
}
public boolean isDirectory() {
return directory;
public SelectionMode getSelectionMode() {
return selectionMode;
}
public FileSelector setDirectory(boolean directory) {
this.directory = directory;
public FileSelector setSelectionMode(SelectionMode selectionMode) {
this.selectionMode = selectionMode;
return this;
}
@@ -80,26 +90,20 @@ public class FileSelector extends HBox {
JFXTextField customField = new JFXTextField();
FXUtils.bindString(customField, valueProperty());
JFXButton selectButton = FXUtils.newToggleButton4(SVG.FOLDER_OPEN, 15);
selectButton.setOnAction(e -> {
if (directory) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(chooserTitle);
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
if (dir != null) {
String path = FileUtils.getAbsolutePath(dir);
customField.setText(path);
value.setValue(path);
}
} else {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(chooserTitle);
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (file != null) {
String path = FileUtils.getAbsolutePath(file);
customField.setText(path);
value.setValue(path);
switch (selectionMode) {
case FILE -> openFileChooser(customField);
case DIRECTORY -> openDirectoryChooser(customField);
case FILE_OR_DIRECTORY -> {
PopupMenu selectPopupMenu = new PopupMenu();
JFXPopup selectModePopup = new JFXPopup(selectPopupMenu);
selectPopupMenu.getContent().addAll(
new IconedMenuItem(SVG.FILE_OPEN, i18n("selector.choose_file"), () -> openFileChooser(customField), selectModePopup),
new IconedMenuItem(SVG.FOLDER_OPEN, i18n("selector.choose_directory"), () -> openDirectoryChooser(customField), selectModePopup)
);
selectModePopup.show(selectButton, JFXPopup.PopupVPosition.TOP, JFXPopup.PopupHPosition.RIGHT, -selectButton.getWidth(), 0);
}
}
});
@@ -108,4 +112,27 @@ public class FileSelector extends HBox {
setSpacing(3);
getChildren().addAll(customField, selectButton);
}
private void openFileChooser(JFXTextField customField) {
FileChooser chooser = new FileChooser();
chooser.getExtensionFilters().addAll(getExtensionFilters());
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_file") : chooserTitle);
Path file = FileUtils.toPath(chooser.showOpenDialog(Controllers.getStage()));
if (file != null) {
String path = FileUtils.getAbsolutePath(file);
customField.setText(path);
value.setValue(path);
}
}
private void openDirectoryChooser(JFXTextField customField) {
DirectoryChooser chooser = new DirectoryChooser();
chooser.setTitle(StringUtils.isBlank(chooserTitle) ? i18n("selector.choose_directory") : chooserTitle);
Path dir = FileUtils.toPath(chooser.showDialog(Controllers.getStage()));
if (dir != null) {
String path = FileUtils.getAbsolutePath(dir);
customField.setText(path);
value.setValue(path);
}
}
}
@@ -267,8 +267,8 @@ public final class MultiFileItem<T> extends VBox {
selector.setValue(value);
}
public FileOption<T> setDirectory(boolean directory) {
selector.setDirectory(directory);
public FileOption<T> setSelectionMode(FileSelector.SelectionMode selectionMode) {
selector.setSelectionMode(selectionMode);
return this;
}
@@ -135,6 +135,7 @@ public class PersonalizationPage extends StackPane {
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumBackgroundImage.CUSTOM)
.setChooserTitle(i18n("launcher.background.choose"))
.addExtensionFilter(FXUtils.getImageExtensionFilter())
.setSelectionMode(FileSelector.SelectionMode.FILE_OR_DIRECTORY)
.bindBidirectional(config().backgroundImageProperty()),
new MultiFileItem.StringOption<>(i18n("launcher.background.network"), EnumBackgroundImage.NETWORK)
.setValidators(new URLValidator(true))
@@ -210,7 +210,7 @@ public final class SettingsPage extends ScrollPane {
new MultiFileItem.Option<>(i18n("launcher.cache_directory.default"), EnumCommonDirectory.DEFAULT),
new MultiFileItem.FileOption<>(i18n("settings.custom"), EnumCommonDirectory.CUSTOM)
.setChooserTitle(i18n("launcher.cache_directory.choose"))
.setDirectory(true)
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY)
.bindBidirectional(config().commonDirectoryProperty())
));
fileCommonLocation.selectedDataProperty().bindBidirectional(config().commonDirTypeProperty());
@@ -183,7 +183,7 @@ public final class AdvancedVersionSettingPage extends StackPane implements Decor
nativesDirSublist.setHasSubtitle(true);
nativesDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.advanced.natives_directory.custom"), NativesDirectoryType.CUSTOM)
.setChooserTitle(i18n("settings.advanced.natives_directory.choose"))
.setDirectory(true);
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY);
nativesDirItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("settings.advanced.natives_directory.default"), NativesDirectoryType.VERSION_FOLDER),
nativesDirCustomOption
@@ -239,7 +239,7 @@ public final class VersionSettingsPage extends StackPane implements DecoratorPag
gameDirItem.disableProperty().bind(modpack);
gameDirCustomOption = new MultiFileItem.FileOption<>(i18n("settings.custom"), GameDirectoryType.CUSTOM)
.setChooserTitle(i18n("settings.game.working_directory.choose"))
.setDirectory(true);
.setSelectionMode(FileSelector.SelectionMode.DIRECTORY);
gameDirItem.loadChildren(Arrays.asList(
new MultiFileItem.Option<>(i18n("settings.advanced.game_dir.default"), GameDirectoryType.ROOT_FOLDER),
@@ -1307,6 +1307,7 @@ search.page_n=%1$d / %2$s
selector.choose=Choose
selector.choose_file=Choose file
selector.choose_directory=Choose directory
selector.custom=Custom
settings=Settings
@@ -1099,6 +1099,7 @@ search.page_n=%d / %s
selector.choose=選取
selector.choose_file=選取檔案
selector.choose_directory=選取目錄
selector.custom=自訂
settings=設定
@@ -1104,6 +1104,7 @@ search.page_n=%d / %s
selector.choose=选择
selector.choose_file=选择文件
selector.choose_directory=选择文件夹
selector.custom=自定义
settings=设置