diff --git a/CHANGELOG.md b/CHANGELOG.md
index 41a596a3abb..904df94ba45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ Note that this project **does not** adhere to [Semantic Versioning](https://semv
### Added
+- We added a "Directory structure" RadioButton that automatically creates a group with the same name as an imported local directory. [#10930](https://github.com/JabRef/jabref/issues/10930)
- We converted the "Custom API key" list to a table to be more accessible. [#10926](https://github.com/JabRef/jabref/issues/10926)
- We added a "refresh" button for the LaTeX citations tab in the entry editor. [#10584](https://github.com/JabRef/jabref/issues/10584)
- We added the possibility to show the BibTeX source in the [web search](https://docs.jabref.org/collect/import-using-online-bibliographic-database) import screen. [#560](https://github.com/koppor/jabref/issues/560)
diff --git a/src/main/java/org/jabref/gui/groups/GroupDialog.fxml b/src/main/java/org/jabref/gui/groups/GroupDialog.fxml
index 02b19876250..7a443c5a162 100644
--- a/src/main/java/org/jabref/gui/groups/GroupDialog.fxml
+++ b/src/main/java/org/jabref/gui/groups/GroupDialog.fxml
@@ -99,6 +99,12 @@
+
+
+
+
+
@@ -175,6 +181,13 @@
+
+
+
+
+
+
+
diff --git a/src/main/java/org/jabref/gui/groups/GroupDialogView.java b/src/main/java/org/jabref/gui/groups/GroupDialogView.java
index 9edf95944e4..f901033edb7 100644
--- a/src/main/java/org/jabref/gui/groups/GroupDialogView.java
+++ b/src/main/java/org/jabref/gui/groups/GroupDialogView.java
@@ -82,6 +82,7 @@ public class GroupDialogView extends BaseDialog {
@FXML private RadioButton searchRadioButton;
@FXML private RadioButton autoRadioButton;
@FXML private RadioButton texRadioButton;
+ @FXML private RadioButton dirRadioButton;
// Option Groups
@FXML private TextField keywordGroupSearchTerm;
@@ -101,6 +102,7 @@ public class GroupDialogView extends BaseDialog {
@FXML private TextField autoGroupPersonsField;
@FXML private TextField texGroupFilePath;
+ @FXML private TextField dirGroupFilePath;
private final EnumMap hierarchyText = new EnumMap<>(GroupHierarchyType.class);
private final EnumMap hierarchyToolTip = new EnumMap<>(GroupHierarchyType.class);
@@ -201,6 +203,7 @@ public void initialize() {
searchRadioButton.selectedProperty().bindBidirectional(viewModel.typeSearchProperty());
autoRadioButton.selectedProperty().bindBidirectional(viewModel.typeAutoProperty());
texRadioButton.selectedProperty().bindBidirectional(viewModel.typeTexProperty());
+ dirRadioButton.selectedProperty().bindBidirectional(viewModel.typeDirProperty());
keywordGroupSearchTerm.textProperty().bindBidirectional(viewModel.keywordGroupSearchTermProperty());
keywordGroupSearchField.textProperty().bindBidirectional(viewModel.keywordGroupSearchFieldProperty());
@@ -237,6 +240,7 @@ public void initialize() {
autoGroupPersonsField.textProperty().bindBidirectional(viewModel.autoGroupPersonsFieldProperty());
texGroupFilePath.textProperty().bindBidirectional(viewModel.texGroupFilePathProperty());
+ dirGroupFilePath.textProperty().bindBidirectional(viewModel.dirGroupFilePathProperty());
validationVisualizer.setDecoration(new IconValidationDecorator());
Platform.runLater(() -> {
@@ -249,6 +253,7 @@ public void initialize() {
validationVisualizer.initVisualization(viewModel.keywordSearchTermEmptyValidationStatus(), keywordGroupSearchTerm);
validationVisualizer.initVisualization(viewModel.keywordFieldEmptyValidationStatus(), keywordGroupSearchField);
validationVisualizer.initVisualization(viewModel.texGroupFilePathValidatonStatus(), texGroupFilePath);
+ validationVisualizer.initVisualization(viewModel.dirGroupFilePathValidatonStatus(), dirGroupFilePath);
nameField.requestFocus();
});
@@ -277,6 +282,11 @@ private void texGroupBrowse() {
viewModel.texGroupBrowse();
}
+ @FXML
+ private void dirGroupBrowse() {
+ viewModel.dirGroupBrowse();
+ }
+
@FXML
private void openIconPicker() {
ObservableList ikonList = FXCollections.observableArrayList();
diff --git a/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java b/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java
index d6610388d98..e52df834697 100644
--- a/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java
+++ b/src/main/java/org/jabref/gui/groups/GroupDialogViewModel.java
@@ -25,6 +25,7 @@
import org.jabref.gui.DialogService;
import org.jabref.gui.icon.IconTheme;
+import org.jabref.gui.util.DirectoryDialogConfiguration;
import org.jabref.gui.util.FileDialogConfiguration;
import org.jabref.logic.auxparser.DefaultAuxParser;
import org.jabref.logic.groups.DefaultGroupsFactory;
@@ -76,6 +77,7 @@ public class GroupDialogViewModel {
private final BooleanProperty typeSearchProperty = new SimpleBooleanProperty();
private final BooleanProperty typeAutoProperty = new SimpleBooleanProperty();
private final BooleanProperty typeTexProperty = new SimpleBooleanProperty();
+ private final BooleanProperty typeDirProperty = new SimpleBooleanProperty();
// Option Groups
private final StringProperty keywordGroupSearchTermProperty = new SimpleStringProperty("");
@@ -94,6 +96,7 @@ public class GroupDialogViewModel {
private final StringProperty autoGroupPersonsFieldProperty = new SimpleStringProperty("");
private final StringProperty texGroupFilePathProperty = new SimpleStringProperty("");
+ private final StringProperty dirGroupFilePathProperty = new SimpleStringProperty("");
private Validator nameValidator;
private Validator nameContainsDelimiterValidator;
@@ -104,6 +107,7 @@ public class GroupDialogViewModel {
private Validator searchRegexValidator;
private Validator searchSearchTermEmptyValidator;
private Validator texGroupFilePathValidator;
+ private Validator dirGroupFilePathValidator;
private CompositeValidator validator;
private final DialogService dialogService;
@@ -252,6 +256,21 @@ private void setupValidation() {
},
ValidationMessage.error(Localization.lang("Please provide a valid aux file.")));
+ dirGroupFilePathValidator = new FunctionBasedValidator<>(
+ dirGroupFilePathProperty,
+ input -> {
+ if (StringUtil.isBlank(input)) {
+ return false;
+ } else {
+ Path inputPath = getAbsoluteTexGroupPath(input);
+ if (!inputPath.isAbsolute() || !Files.isDirectory(inputPath)) {
+ return false;
+ }
+ return Files.isDirectory(inputPath);
+ }
+ },
+ ValidationMessage.error(Localization.lang("Please provide a valid directory")));
+
typeSearchProperty.addListener((obs, _oldValue, isSelected) -> {
if (isSelected) {
validator.addValidators(searchRegexValidator, searchSearchTermEmptyValidator);
@@ -276,6 +295,14 @@ private void setupValidation() {
}
});
+ typeDirProperty.addListener((obs, oldValue, isSelected) -> {
+ if (isSelected) {
+ validator.addValidators(dirGroupFilePathValidator);
+ } else {
+ validator.removeValidators(dirGroupFilePathValidator);
+ }
+ });
+
validator.addValidators(nameValidator,
nameContainsDelimiterValidator,
sameNameValidator);
@@ -371,6 +398,14 @@ public AbstractGroup resultConverter(ButtonType button) {
new DefaultAuxParser(new BibDatabase()),
fileUpdateMonitor,
currentDatabase.getMetaData());
+ } else if (typeDirProperty.getValue()) {
+ resultingGroup = TexGroup.create(
+ groupName,
+ groupHierarchySelectedProperty.getValue(),
+ Path.of(dirGroupFilePathProperty.getValue().trim()),
+ new DefaultAuxParser(new BibDatabase()),
+ fileUpdateMonitor,
+ currentDatabase.getMetaData());
}
if (resultingGroup != null) {
@@ -491,6 +526,20 @@ public void texGroupBrowse() {
));
}
+ public void dirGroupBrowse() {
+ DirectoryDialogConfiguration directoryDialogConfiguration = new DirectoryDialogConfiguration.Builder()
+ .withInitialDirectory(currentDatabase.getMetaData()
+ .getLatexFileDirectory(preferencesService.getFilePreferences().getUserAndHost())
+ .orElse(FileUtil.getInitialDirectory(currentDatabase, preferencesService.getFilePreferences().getWorkingDirectory()))).build();
+ dialogService.showDirectorySelectionDialog(directoryDialogConfiguration)
+ .ifPresent(file -> {
+ nameProperty.setValue(file.getFileName().toString());
+ dirGroupFilePathProperty.setValue(
+ FileUtil.relativize(file.toAbsolutePath(), getFileDirectoriesAsPaths()).toString()
+ );
+ });
+ }
+
private List getFileDirectoriesAsPaths() {
List fileDirs = new ArrayList<>();
MetaData metaData = currentDatabase.getMetaData();
@@ -539,6 +588,10 @@ public ValidationStatus texGroupFilePathValidatonStatus() {
return texGroupFilePathValidator.getValidationStatus();
}
+ public ValidationStatus dirGroupFilePathValidatonStatus() {
+ return dirGroupFilePathValidator.getValidationStatus();
+ }
+
public StringProperty nameProperty() {
return nameProperty;
}
@@ -587,6 +640,10 @@ public BooleanProperty typeTexProperty() {
return typeTexProperty;
}
+ public BooleanProperty typeDirProperty() {
+ return typeDirProperty;
+ }
+
public StringProperty keywordGroupSearchTermProperty() {
return keywordGroupSearchTermProperty;
}
@@ -638,4 +695,8 @@ public StringProperty autoGroupPersonsFieldProperty() {
public StringProperty texGroupFilePathProperty() {
return texGroupFilePathProperty;
}
+
+ public StringProperty dirGroupFilePathProperty() {
+ return dirGroupFilePathProperty;
+ }
}
diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties
index 26eb731b57d..0787a4ff2e7 100644
--- a/src/main/resources/l10n/JabRef_en.properties
+++ b/src/main/resources/l10n/JabRef_en.properties
@@ -2109,6 +2109,9 @@ Collect\ by=Collect by
Explicit\ selection=Explicit selection
Specified\ keywords=Specified keywords
Cited\ entries=Cited entries
+Directory\ structure=Directory structure
+Directory\ tree=Directory tree
+Please\ provide\ a\ valid\ directory=Please provide a valid directory
Please\ provide\ a\ valid\ aux\ file.=Please provide a valid aux file.
Keyword\ delimiter=Keyword delimiter
Hierarchical\ keyword\ delimiter=Hierarchical keyword delimiter
diff --git a/src/test/java/org/jabref/gui/groups/GroupDialogViewModelTest.java b/src/test/java/org/jabref/gui/groups/GroupDialogViewModelTest.java
index 1c47527d0f8..25e662be01c 100644
--- a/src/test/java/org/jabref/gui/groups/GroupDialogViewModelTest.java
+++ b/src/test/java/org/jabref/gui/groups/GroupDialogViewModelTest.java
@@ -84,6 +84,47 @@ void validateExistingRelativePath() throws Exception {
assertTrue(viewModel.texGroupFilePathValidatonStatus().isValid());
}
+ @Test
+ void validateExistingDirectoryAbsolutePath() throws Exception {
+ var directory = temporaryFolder.resolve("directory").toAbsolutePath();
+
+ Files.createDirectory(directory);
+ when(metaData.getLatexFileDirectory(any(String.class))).thenReturn(Optional.of(temporaryFolder));
+
+ viewModel.dirGroupFilePathProperty().setValue(directory.toString());
+ assertTrue(viewModel.dirGroupFilePathValidatonStatus().isValid());
+ }
+
+ @Test
+ void validateNonExistingDirectoryAbsolutePath() {
+ var notDirectory = temporaryFolder.resolve("notdirectory").toAbsolutePath();
+ viewModel.dirGroupFilePathProperty().setValue(notDirectory.toString());
+ assertFalse(viewModel.dirGroupFilePathValidatonStatus().isValid());
+ }
+
+ @Test
+ void validateNonExistingDirectoryAsFileAbsolutePath() throws Exception {
+ var file = temporaryFolder.resolve("file").toAbsolutePath(); // File without .extension
+
+ Files.createFile(file);
+ when(metaData.getLatexFileDirectory(any(String.class))).thenReturn(Optional.of(temporaryFolder));
+
+ viewModel.dirGroupFilePathProperty().setValue(file.toString());
+ assertFalse(viewModel.dirGroupFilePathValidatonStatus().isValid());
+ }
+
+ @Test
+ void validateExistingDirectoryRelativePath() throws Exception {
+ var directory = Path.of("directory");
+
+ // The file needs to exist
+ Files.createDirectory(temporaryFolder.resolve(directory));
+ when(metaData.getLatexFileDirectory(any(String.class))).thenReturn(Optional.of(temporaryFolder));
+
+ viewModel.dirGroupFilePathProperty().setValue(directory.toString());
+ assertTrue(viewModel.dirGroupFilePathValidatonStatus().isValid());
+ }
+
@Test
void hierarchicalContextFromGroup() throws Exception {
GroupHierarchyType groupHierarchyType = GroupHierarchyType.INCLUDING;