diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ac0763c54e..33c53b44973 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ to the page field for cases where the page numbers are missing. [#7019](https:// - We added a new fetcher to enable users to search jstor.org [#6627](https://github.com/JabRef/jabref/issues/6627) - We added an error message in the New Entry dialog that is shown in case the fetcher did not find anything . [#7000](https://github.com/JabRef/jabref/issues/7000) - We added a new formatter to output shorthand month format. [#6579](https://github.com/JabRef/jabref/issues/6579) +- We reintroduced emacs/bash-like keybindings. [#6017](https://github.com/JabRef/jabref/issues/6017) ### Changed diff --git a/src/main/java/org/jabref/gui/JabRefGUI.java b/src/main/java/org/jabref/gui/JabRefGUI.java index f9c9d646282..a4dc5d1b284 100644 --- a/src/main/java/org/jabref/gui/JabRefGUI.java +++ b/src/main/java/org/jabref/gui/JabRefGUI.java @@ -8,6 +8,7 @@ import javafx.application.Platform; import javafx.scene.Scene; +import javafx.scene.input.KeyEvent; import javafx.stage.Screen; import javafx.stage.Stage; @@ -16,6 +17,7 @@ import org.jabref.gui.icon.IconTheme; import org.jabref.gui.importer.ParserResultWarningDialog; import org.jabref.gui.importer.actions.OpenDatabaseAction; +import org.jabref.gui.keyboard.TextInputKeyBindings; import org.jabref.gui.shared.SharedDatabaseUIManager; import org.jabref.logic.autosaveandbackup.BackupManager; import org.jabref.logic.importer.OpenDatabase; @@ -86,6 +88,10 @@ private void openWindow(Stage mainStage) { Scene scene = new Scene(root, 800, 800); Globals.prefs.getTheme().installCss(scene); + + // Handle TextEditor key bindings + scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> TextInputKeyBindings.call(scene, event)); + mainStage.setTitle(JabRefFrame.FRAME_TITLE); mainStage.getIcons().addAll(IconTheme.getLogoSetFX()); mainStage.setScene(scene); @@ -219,14 +225,13 @@ private void saveWindowState(Stage mainStage) { */ private void debugLogWindowState(Stage mainStage) { if (LOGGER.isDebugEnabled()) { - StringBuilder debugLogString = new StringBuilder(); - debugLogString.append("SCREEN DATA:"); - debugLogString.append("mainStage.WINDOW_MAXIMISED: ").append(mainStage.isMaximized()).append("\n"); - debugLogString.append("mainStage.POS_X: ").append(mainStage.getX()).append("\n"); - debugLogString.append("mainStage.POS_Y: ").append(mainStage.getY()).append("\n"); - debugLogString.append("mainStage.SIZE_X: ").append(mainStage.getWidth()).append("\n"); - debugLogString.append("mainStages.SIZE_Y: ").append(mainStage.getHeight()).append("\n"); - LOGGER.debug(debugLogString.toString()); + String debugLogString = "SCREEN DATA:" + + "mainStage.WINDOW_MAXIMISED: " + mainStage.isMaximized() + "\n" + + "mainStage.POS_X: " + mainStage.getX() + "\n" + + "mainStage.POS_Y: " + mainStage.getY() + "\n" + + "mainStage.SIZE_X: " + mainStage.getWidth() + "\n" + + "mainStages.SIZE_Y: " + mainStage.getHeight() + "\n"; + LOGGER.debug(debugLogString); } } diff --git a/src/main/java/org/jabref/gui/entryeditor/SourceTab.java b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java index 66c32a2ad25..9161758a58d 100644 --- a/src/main/java/org/jabref/gui/entryeditor/SourceTab.java +++ b/src/main/java/org/jabref/gui/entryeditor/SourceTab.java @@ -18,6 +18,7 @@ import javafx.scene.control.ContextMenu; import javafx.scene.control.Tooltip; import javafx.scene.input.InputMethodRequests; +import javafx.scene.input.KeyEvent; import org.jabref.gui.DialogService; import org.jabref.gui.Globals; @@ -26,6 +27,7 @@ import org.jabref.gui.actions.SimpleCommand; import org.jabref.gui.actions.StandardActions; import org.jabref.gui.icon.IconTheme; +import org.jabref.gui.keyboard.CodeAreaKeyBindings; import org.jabref.gui.keyboard.KeyBindingRepository; import org.jabref.gui.undo.CountingUndoManager; import org.jabref.gui.undo.NamedCompound; @@ -85,18 +87,10 @@ public EditAction(StandardActions command) { @Override public void execute() { switch (command) { - case COPY: - codeArea.copy(); - break; - case CUT: - codeArea.cut(); - break; - case PASTE: - codeArea.paste(); - break; - case SELECT_ALL: - codeArea.selectAll(); - break; + case COPY -> codeArea.copy(); + case CUT -> codeArea.cut(); + case PASTE -> codeArea.paste(); + case SELECT_ALL -> codeArea.selectAll(); } codeArea.requestFocus(); } @@ -178,6 +172,7 @@ private void setupSourceEditor() { } }); codeArea.setId("bibtexSourceCodeArea"); + codeArea.addEventFilter(KeyEvent.KEY_PRESSED, event -> CodeAreaKeyBindings.call(codeArea, event)); ActionFactory factory = new ActionFactory(keyBindingRepository); ContextMenu contextMenu = new ContextMenu(); diff --git a/src/main/java/org/jabref/gui/keyboard/CodeAreaKeyBindings.java b/src/main/java/org/jabref/gui/keyboard/CodeAreaKeyBindings.java new file mode 100644 index 00000000000..d055eaed84c --- /dev/null +++ b/src/main/java/org/jabref/gui/keyboard/CodeAreaKeyBindings.java @@ -0,0 +1,113 @@ +package org.jabref.gui.keyboard; + +import javafx.scene.input.KeyEvent; + +import org.jabref.gui.Globals; +import org.jabref.logic.util.strings.StringManipulator; +import org.jabref.model.util.ResultingStringState; + +import org.fxmisc.richtext.CodeArea; +import org.fxmisc.richtext.NavigationActions; + +public class CodeAreaKeyBindings { + + public static void call(CodeArea codeArea, KeyEvent event) { + KeyBindingRepository keyBindingRepository = Globals.getKeyPrefs(); + keyBindingRepository.mapToKeyBinding(event).ifPresent(binding -> { + switch (binding) { + case EDITOR_DELETE -> { + codeArea.deleteNextChar(); + event.consume(); + } + case EDITOR_BACKWARD -> { + codeArea.previousChar(NavigationActions.SelectionPolicy.CLEAR); + event.consume(); + } + case EDITOR_FORWARD -> { + codeArea.nextChar(NavigationActions.SelectionPolicy.CLEAR); + event.consume(); + } + case EDITOR_WORD_BACKWARD -> { + codeArea.wordBreaksBackwards(2, NavigationActions.SelectionPolicy.CLEAR); + event.consume(); + } + case EDITOR_WORD_FORWARD -> { + codeArea.wordBreaksForwards(2, NavigationActions.SelectionPolicy.CLEAR); + event.consume(); + } + case EDITOR_BEGINNING_DOC -> { + codeArea.start(NavigationActions.SelectionPolicy.CLEAR); + event.consume(); + } + case EDITOR_UP -> { + codeArea.paragraphStart(NavigationActions.SelectionPolicy.CLEAR); + event.consume(); + } + case EDITOR_BEGINNING -> { + codeArea.lineStart(NavigationActions.SelectionPolicy.CLEAR); + event.consume(); + } + case EDITOR_END_DOC -> { + codeArea.end(NavigationActions.SelectionPolicy.CLEAR); + event.consume(); + } + case EDITOR_DOWN -> { + codeArea.paragraphEnd(NavigationActions.SelectionPolicy.CLEAR); + event.consume(); + } + case EDITOR_END -> { + codeArea.lineEnd(NavigationActions.SelectionPolicy.CLEAR); + event.consume(); + } + case EDITOR_CAPITALIZE -> { + int pos = codeArea.getCaretPosition(); + String text = codeArea.getText(0, codeArea.getText().length()); + ResultingStringState res = StringManipulator.capitalize(pos, text); + codeArea.replaceText(res.text); + codeArea.displaceCaret(res.caretPosition); + event.consume(); + } + case EDITOR_LOWERCASE -> { + int pos = codeArea.getCaretPosition(); + String text = codeArea.getText(0, codeArea.getText().length()); + ResultingStringState res = StringManipulator.lowercase(pos, text); + codeArea.replaceText(res.text); + codeArea.displaceCaret(res.caretPosition); + event.consume(); + } + case EDITOR_UPPERCASE -> { + int pos = codeArea.getCaretPosition(); + String text = codeArea.getText(0, codeArea.getText().length()); + ResultingStringState res = StringManipulator.uppercase(pos, text); + codeArea.clear(); + codeArea.replaceText(res.text); + codeArea.displaceCaret(res.caretPosition); + event.consume(); + } + case EDITOR_KILL_LINE -> { + int pos = codeArea.getCaretPosition(); + codeArea.replaceText(codeArea.getText(0, pos)); + codeArea.displaceCaret(pos); + event.consume(); + } + case EDITOR_KILL_WORD -> { + int pos = codeArea.getCaretPosition(); + String text = codeArea.getText(0, codeArea.getText().length()); + ResultingStringState res = StringManipulator.killWord(pos, text); + codeArea.replaceText(res.text); + codeArea.displaceCaret(res.caretPosition); + event.consume(); + } + case EDITOR_KILL_WORD_BACKWARD -> { + int pos = codeArea.getCaretPosition(); + String text = codeArea.getText(0, codeArea.getText().length()); + ResultingStringState res = StringManipulator.backwardKillWord(pos, text); + codeArea.replaceText(res.text); + codeArea.displaceCaret(res.caretPosition); + event.consume(); + } + } + }); + } +} + diff --git a/src/main/java/org/jabref/gui/keyboard/KeyBinding.java b/src/main/java/org/jabref/gui/keyboard/KeyBinding.java index 06bac3e5c89..4c3363c9c6e 100644 --- a/src/main/java/org/jabref/gui/keyboard/KeyBinding.java +++ b/src/main/java/org/jabref/gui/keyboard/KeyBinding.java @@ -3,6 +3,24 @@ import org.jabref.logic.l10n.Localization; public enum KeyBinding { + EDITOR_DELETE("Delete", Localization.lang("Delete text"), "", KeyBindingCategory.EDITOR), + // DELETE BACKWARDS = Rubout + EDITOR_BACKWARD("Move caret left", Localization.lang("Move caret left"), "", KeyBindingCategory.EDITOR), + EDITOR_FORWARD("Move caret right", Localization.lang("Move caret right"), "", KeyBindingCategory.EDITOR), + EDITOR_WORD_BACKWARD("Move caret to previous word", Localization.lang("Move caret to previous word"), "", KeyBindingCategory.EDITOR), + EDITOR_WORD_FORWARD("Move caret to next word", Localization.lang("Move caret to next word"), "", KeyBindingCategory.EDITOR), + EDITOR_BEGINNING("Move caret to beginning of line", Localization.lang("Move caret to beginning of line"), "", KeyBindingCategory.EDITOR), + EDITOR_END("Move caret to of line", Localization.lang("Move caret to end of line"), "", KeyBindingCategory.EDITOR), + EDITOR_BEGINNING_DOC("Move caret to beginning of text", Localization.lang("Move the caret to the beginning of text"), "", KeyBindingCategory.EDITOR), + EDITOR_END_DOC("Move caret to end of text", Localization.lang("Move the caret to the end of text"), "", KeyBindingCategory.EDITOR), + EDITOR_UP("Move caret up", Localization.lang("Move the caret up"), "", KeyBindingCategory.EDITOR), + EDITOR_DOWN("Move caret down", Localization.lang("Move the caret down"), "", KeyBindingCategory.EDITOR), + EDITOR_CAPITALIZE("Capitalize word", Localization.lang("Capitalize current word"), "", KeyBindingCategory.EDITOR), + EDITOR_LOWERCASE("Lowercase word", Localization.lang("Make current word lowercase"), "", KeyBindingCategory.EDITOR), + EDITOR_UPPERCASE("Uppercase word", Localization.lang("Make current word uppercase"), "", KeyBindingCategory.EDITOR), + EDITOR_KILL_LINE("Remove all characters caret to end of line", Localization.lang("Remove line after caret"), "", KeyBindingCategory.EDITOR), + EDITOR_KILL_WORD("Remove characters until next word", Localization.lang("Remove characters until next word"), "", KeyBindingCategory.EDITOR), + EDITOR_KILL_WORD_BACKWARD("Characters until previous word", Localization.lang("Remove the current word backwards"), "", KeyBindingCategory.EDITOR), ABBREVIATE("Abbreviate", Localization.lang("Abbreviate journal names"), "ctrl+alt+A", KeyBindingCategory.TOOLS), AUTOGENERATE_CITATION_KEYS("Autogenerate citation keys", Localization.lang("Autogenerate citation keys"), "ctrl+G", KeyBindingCategory.QUALITY), diff --git a/src/main/java/org/jabref/gui/keyboard/KeyBindingCategory.java b/src/main/java/org/jabref/gui/keyboard/KeyBindingCategory.java index 4f8e080b617..ae8cb0b673f 100644 --- a/src/main/java/org/jabref/gui/keyboard/KeyBindingCategory.java +++ b/src/main/java/org/jabref/gui/keyboard/KeyBindingCategory.java @@ -11,11 +11,12 @@ public enum KeyBindingCategory { VIEW(Localization.lang("View")), BIBTEX(BibDatabaseMode.BIBTEX.getFormattedName()), QUALITY(Localization.lang("Quality")), - TOOLS(Localization.lang("Tools")); + TOOLS(Localization.lang("Tools")), + EDITOR(Localization.lang("Text editor")); private final String name; - private KeyBindingCategory(String name) { + KeyBindingCategory(String name) { this.name = name; } diff --git a/src/main/java/org/jabref/gui/keyboard/KeyBindingViewModel.java b/src/main/java/org/jabref/gui/keyboard/KeyBindingViewModel.java index 9ece69df0dd..92ff3bb95f6 100644 --- a/src/main/java/org/jabref/gui/keyboard/KeyBindingViewModel.java +++ b/src/main/java/org/jabref/gui/keyboard/KeyBindingViewModel.java @@ -64,11 +64,11 @@ public String getBinding() { private void setBinding(String bind) { this.realBinding = bind; String[] parts = bind.split(" "); - String displayBind = ""; + StringBuilder displayBind = new StringBuilder(); for (String part : parts) { - displayBind += CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, part) + " "; + displayBind.append(CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_CAMEL, part)).append(" "); } - this.shownBinding.set(displayBind.trim().replace(" ", " + ")); + this.shownBinding.set(displayBind.toString().trim().replace(" ", " + ")); } private void setDisplayName() { @@ -135,7 +135,19 @@ public void resetToDefault() { } } - public Optional getIcon() { + public void clear() { + if (!isCategory()) { + String key = getKeyBinding().getConstant(); + keyBindingRepository.put(key, ""); + setBinding(keyBindingRepository.get(key)); + } + } + + public Optional getResetIcon() { + return isCategory() ? Optional.empty() : Optional.of(IconTheme.JabRefIcons.REFRESH); + } + + public Optional getClearIcon() { return isCategory() ? Optional.empty() : Optional.of(IconTheme.JabRefIcons.CLEANUP_ENTRIES); } } diff --git a/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialog.fxml b/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialog.fxml index 99983bb67aa..076e1465146 100644 --- a/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialog.fxml +++ b/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialog.fxml @@ -2,23 +2,29 @@ + + - + - - - + + + + + + + diff --git a/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialogView.java b/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialogView.java index 21170bad2f1..da62dd47d78 100644 --- a/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialogView.java +++ b/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialogView.java @@ -4,6 +4,8 @@ import javafx.fxml.FXML; import javafx.scene.control.ButtonType; +import javafx.scene.control.MenuButton; +import javafx.scene.control.MenuItem; import javafx.scene.control.SelectionMode; import javafx.scene.control.SelectionModel; import javafx.scene.control.TreeItem; @@ -12,6 +14,7 @@ import org.jabref.gui.DialogService; import org.jabref.gui.icon.JabRefIcon; +import org.jabref.gui.keyboard.presets.KeyBindingPreset; import org.jabref.gui.util.BaseDialog; import org.jabref.gui.util.ControlHelper; import org.jabref.gui.util.RecursiveTreeItem; @@ -30,6 +33,8 @@ public class KeyBindingsDialogView extends BaseDialog { @FXML private TreeTableColumn actionColumn; @FXML private TreeTableColumn shortcutColumn; @FXML private TreeTableColumn resetColumn; + @FXML private TreeTableColumn clearColumn; + @FXML private MenuButton presetsButton; @Inject private KeyBindingRepository keyBindingRepository; @Inject private DialogService dialogService; @@ -66,9 +71,21 @@ private void initialize() { actionColumn.setCellValueFactory(cellData -> cellData.getValue().getValue().nameProperty()); shortcutColumn.setCellValueFactory(cellData -> cellData.getValue().getValue().shownBindingProperty()); new ViewModelTreeTableCellFactory() - .withGraphic(keyBinding -> keyBinding.getIcon().map(JabRefIcon::getGraphicNode).orElse(null)) + .withGraphic(keyBinding -> keyBinding.getResetIcon().map(JabRefIcon::getGraphicNode).orElse(null)) .withOnMouseClickedEvent(keyBinding -> evt -> keyBinding.resetToDefault()) .install(resetColumn); + new ViewModelTreeTableCellFactory() + .withGraphic(keyBinding -> keyBinding.getClearIcon().map(JabRefIcon::getGraphicNode).orElse(null)) + .withOnMouseClickedEvent(keyBinding -> evt -> keyBinding.clear()) + .install(clearColumn); + + viewModel.keyBindingPresets().forEach(preset -> presetsButton.getItems().add(createMenuItem(preset))); + } + + private MenuItem createMenuItem(KeyBindingPreset preset) { + MenuItem item = new MenuItem(preset.getName()); + item.setOnAction((event) -> viewModel.loadPreset(preset)); + return item; } @FXML diff --git a/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialogViewModel.java b/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialogViewModel.java index 6125b01230a..e88c9c7e8ba 100644 --- a/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialogViewModel.java +++ b/src/main/java/org/jabref/gui/keyboard/KeyBindingsDialogViewModel.java @@ -3,8 +3,11 @@ import java.util.Objects; import java.util.Optional; +import javafx.beans.property.ListProperty; import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleListProperty; import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.FXCollections; import javafx.scene.control.Alert; import javafx.scene.control.ButtonBar; import javafx.scene.control.ButtonType; @@ -12,6 +15,8 @@ import org.jabref.gui.AbstractViewModel; import org.jabref.gui.DialogService; +import org.jabref.gui.keyboard.presets.BashKeyBindingPreset; +import org.jabref.gui.keyboard.presets.KeyBindingPreset; import org.jabref.gui.util.OptionalObjectProperty; import org.jabref.logic.l10n.Localization; import org.jabref.preferences.PreferencesService; @@ -22,6 +27,8 @@ public class KeyBindingsDialogViewModel extends AbstractViewModel { private final PreferencesService preferences; private final OptionalObjectProperty selectedKeyBinding = OptionalObjectProperty.empty(); private final ObjectProperty rootKeyBinding = new SimpleObjectProperty<>(); + private final ListProperty keyBindingPresets = new SimpleListProperty<>(FXCollections.observableArrayList()); + private final DialogService dialogService; public KeyBindingsDialogViewModel(KeyBindingRepository keyBindingRepository, DialogService dialogService, PreferencesService preferences) { @@ -29,6 +36,7 @@ public KeyBindingsDialogViewModel(KeyBindingRepository keyBindingRepository, Dia this.dialogService = Objects.requireNonNull(dialogService); this.preferences = Objects.requireNonNull(preferences); populateTable(); + keyBindingPresets.add(new BashKeyBindingPreset()); } public OptionalObjectProperty selectedKeyBindingProperty() { @@ -94,4 +102,17 @@ public void resetToDefault() { } }); } + + public void loadPreset(KeyBindingPreset preset) { + if (preset == null) { + return; + } + + preset.getKeyBindings().forEach(keyBindingRepository::put); + populateTable(); + } + + public ListProperty keyBindingPresets() { + return keyBindingPresets; + } } diff --git a/src/main/java/org/jabref/gui/keyboard/TextInputKeyBindings.java b/src/main/java/org/jabref/gui/keyboard/TextInputKeyBindings.java new file mode 100644 index 00000000000..d195f0bdef8 --- /dev/null +++ b/src/main/java/org/jabref/gui/keyboard/TextInputKeyBindings.java @@ -0,0 +1,97 @@ +package org.jabref.gui.keyboard; + +import javafx.scene.Scene; +import javafx.scene.control.TextInputControl; +import javafx.scene.input.KeyEvent; + +import org.jabref.gui.Globals; +import org.jabref.logic.util.strings.StringManipulator; +import org.jabref.model.util.ResultingStringState; + +public class TextInputKeyBindings { + + public static void call(Scene scene, KeyEvent event) { + if (scene.focusOwnerProperty().get() instanceof TextInputControl) { + KeyBindingRepository keyBindingRepository = Globals.getKeyPrefs(); + TextInputControl focusedTextField = (TextInputControl) scene.focusOwnerProperty().get(); + keyBindingRepository.mapToKeyBinding(event).ifPresent(binding -> { + switch (binding) { + case EDITOR_DELETE -> { + focusedTextField.deleteNextChar(); + event.consume(); + } + case EDITOR_BACKWARD -> { + focusedTextField.backward(); + event.consume(); + } + case EDITOR_FORWARD -> { + focusedTextField.forward(); + event.consume(); + } + case EDITOR_WORD_BACKWARD -> { + focusedTextField.previousWord(); + event.consume(); + } + case EDITOR_WORD_FORWARD -> { + focusedTextField.nextWord(); + event.consume(); + } + case EDITOR_BEGINNING, EDITOR_UP, EDITOR_BEGINNING_DOC -> { + focusedTextField.home(); + event.consume(); + } + case EDITOR_END, EDITOR_DOWN, EDITOR_END_DOC -> { + focusedTextField.end(); + event.consume(); + } + case EDITOR_CAPITALIZE -> { + int pos = focusedTextField.getCaretPosition(); + String text = focusedTextField.getText(0, focusedTextField.getText().length()); + ResultingStringState res = StringManipulator.capitalize(pos, text); + focusedTextField.setText(res.text); + focusedTextField.positionCaret(res.caretPosition); + event.consume(); + } + case EDITOR_LOWERCASE -> { + int pos = focusedTextField.getCaretPosition(); + String text = focusedTextField.getText(0, focusedTextField.getText().length()); + ResultingStringState res = StringManipulator.lowercase(pos, text); + focusedTextField.setText(res.text); + focusedTextField.positionCaret(res.caretPosition); + event.consume(); + } + case EDITOR_UPPERCASE -> { + int pos = focusedTextField.getCaretPosition(); + String text = focusedTextField.getText(0, focusedTextField.getText().length()); + ResultingStringState res = StringManipulator.uppercase(pos, text); + focusedTextField.setText(res.text); + focusedTextField.positionCaret(res.caretPosition); + event.consume(); + } + case EDITOR_KILL_LINE -> { + int pos = focusedTextField.getCaretPosition(); + focusedTextField.setText(focusedTextField.getText(0, pos)); + focusedTextField.positionCaret(pos); + event.consume(); + } + case EDITOR_KILL_WORD -> { + int pos = focusedTextField.getCaretPosition(); + String text = focusedTextField.getText(0, focusedTextField.getText().length()); + ResultingStringState res = StringManipulator.killWord(pos, text); + focusedTextField.setText(res.text); + focusedTextField.positionCaret(res.caretPosition); + event.consume(); + } + case EDITOR_KILL_WORD_BACKWARD -> { + int pos = focusedTextField.getCaretPosition(); + String text = focusedTextField.getText(0, focusedTextField.getText().length()); + ResultingStringState res = StringManipulator.backwardKillWord(pos, text); + focusedTextField.setText(res.text); + focusedTextField.positionCaret(res.caretPosition); + event.consume(); + } + } + }); + } + } +} diff --git a/src/main/java/org/jabref/gui/keyboard/presets/BashKeyBindingPreset.java b/src/main/java/org/jabref/gui/keyboard/presets/BashKeyBindingPreset.java new file mode 100644 index 00000000000..15528af1023 --- /dev/null +++ b/src/main/java/org/jabref/gui/keyboard/presets/BashKeyBindingPreset.java @@ -0,0 +1,42 @@ +package org.jabref.gui.keyboard.presets; + +import java.util.HashMap; +import java.util.Map; + +import org.jabref.gui.keyboard.KeyBinding; + +public class BashKeyBindingPreset implements KeyBindingPreset { + + private static final Map KEY_BINDINGS = new HashMap<>(); + + static { + KEY_BINDINGS.put(KeyBinding.EDITOR_DELETE, "ctrl+D"); + // DELETE BACKWARDS = Rubout + KEY_BINDINGS.put(KeyBinding.EDITOR_BACKWARD, "ctrl+B"); + KEY_BINDINGS.put(KeyBinding.EDITOR_FORWARD, "ctrl+F"); + KEY_BINDINGS.put(KeyBinding.EDITOR_WORD_BACKWARD, "alt+B"); + KEY_BINDINGS.put(KeyBinding.EDITOR_WORD_FORWARD, "alt+F"); + KEY_BINDINGS.put(KeyBinding.EDITOR_BEGINNING, "ctrl+A"); + KEY_BINDINGS.put(KeyBinding.EDITOR_END, "ctrl+E"); + KEY_BINDINGS.put(KeyBinding.EDITOR_BEGINNING_DOC, "alt+LESS"); + KEY_BINDINGS.put(KeyBinding.EDITOR_END_DOC, "alt+shift+LESS"); + KEY_BINDINGS.put(KeyBinding.EDITOR_UP, "ctrl+P"); + KEY_BINDINGS.put(KeyBinding.EDITOR_DOWN, "ctrl+N"); + KEY_BINDINGS.put(KeyBinding.EDITOR_CAPITALIZE, "alt+C"); + KEY_BINDINGS.put(KeyBinding.EDITOR_LOWERCASE, "alt+L"); + KEY_BINDINGS.put(KeyBinding.EDITOR_UPPERCASE, "alt+U"); + KEY_BINDINGS.put(KeyBinding.EDITOR_KILL_LINE, "ctrl+K"); + KEY_BINDINGS.put(KeyBinding.EDITOR_KILL_WORD, "alt+D"); + KEY_BINDINGS.put(KeyBinding.EDITOR_KILL_WORD_BACKWARD, "alt+DELETE"); + } + + @Override + public String getName() { + return "Bash"; + } + + @Override + public Map getKeyBindings() { + return KEY_BINDINGS; + } +} diff --git a/src/main/java/org/jabref/gui/keyboard/presets/KeyBindingPreset.java b/src/main/java/org/jabref/gui/keyboard/presets/KeyBindingPreset.java new file mode 100644 index 00000000000..abea89b1c16 --- /dev/null +++ b/src/main/java/org/jabref/gui/keyboard/presets/KeyBindingPreset.java @@ -0,0 +1,11 @@ +package org.jabref.gui.keyboard.presets; + +import java.util.Map; + +import org.jabref.gui.keyboard.KeyBinding; + +public interface KeyBindingPreset { + String getName(); + + Map getKeyBindings(); +} diff --git a/src/main/java/org/jabref/logic/util/strings/StringManipulator.java b/src/main/java/org/jabref/logic/util/strings/StringManipulator.java new file mode 100644 index 00000000000..cb1972de5e1 --- /dev/null +++ b/src/main/java/org/jabref/logic/util/strings/StringManipulator.java @@ -0,0 +1,174 @@ +package org.jabref.logic.util.strings; + +import org.jabref.logic.formatter.casechanger.CapitalizeFormatter; +import org.jabref.logic.formatter.casechanger.LowerCaseFormatter; +import org.jabref.logic.formatter.casechanger.UpperCaseFormatter; +import org.jabref.model.util.ResultingStringState; + +public class StringManipulator { + private enum LetterCase { + UPPER, + LOWER, + CAPITALIZED + } + + enum Direction { + NEXT(1), + PREVIOUS(-1); + + public final int OFFSET; + + Direction(int offset) { + this.OFFSET = offset; + } + } + + /** + * Change word casing in a string from the given position to the next word boundary. + * + * @param text The text to manipulate. + * @param caretPosition The index to start from. + * @param targetCase The case mode the string should be changed to. + * + * @return The resulting text and caret position. + */ + private static ResultingStringState setWordCase(String text, int caretPosition, LetterCase targetCase) { + int nextWordBoundary = getNextWordBoundary(caretPosition, text, Direction.NEXT); + + // Preserve whitespaces + int wordStartPosition = caretPosition; + while (wordStartPosition < nextWordBoundary && Character.isWhitespace(text.charAt(wordStartPosition))) { + wordStartPosition++; + } + + String result = switch (targetCase) { + case UPPER -> (new UpperCaseFormatter()).format(text.substring(wordStartPosition, nextWordBoundary)); + case LOWER -> (new LowerCaseFormatter()).format(text.substring(wordStartPosition, nextWordBoundary)); + case CAPITALIZED -> (new CapitalizeFormatter()).format(text.substring(wordStartPosition, nextWordBoundary)); + }; + + return new ResultingStringState( + nextWordBoundary, + text.substring(0, wordStartPosition) + result + text.substring(nextWordBoundary)); + } + + /** + * Delete all characters in a string from the given position to the next word boundary. + * + * @param caretPosition The index to start from. + * @param text The text to manipulate. + * @param direction The direction to search. + * + * @return The resulting text and caret position. + */ + static ResultingStringState deleteUntilWordBoundary(int caretPosition, String text, Direction direction) { + // Define cutout range + int nextWordBoundary = getNextWordBoundary(caretPosition, text, direction); + + // Construct new string without cutout + return switch (direction) { + case NEXT -> new ResultingStringState( + caretPosition, + text.substring(0, caretPosition) + text.substring(nextWordBoundary)); + case PREVIOUS -> new ResultingStringState( + nextWordBoundary, + text.substring(0, nextWordBoundary) + text.substring(caretPosition)); + }; + } + + /** + * Utility method to find the next whitespace position in string after text + * @param caretPosition The current caret Position + * @param text The string to search in + * @param direction The direction to move through string + * + * @return The position of the next whitespace after a word + */ + static int getNextWordBoundary(int caretPosition, String text, Direction direction) { + int i = caretPosition; + + if (direction == Direction.PREVIOUS) { + // Swallow whitespaces + while (i > 0 && Character.isWhitespace((text.charAt(i + direction.OFFSET)))) { + i += direction.OFFSET; + } + + // Read next word + while (i > 0 && !Character.isWhitespace(text.charAt(i + direction.OFFSET))) { + i += direction.OFFSET; + } + } else if (direction == Direction.NEXT) { + // Swallow whitespaces + while (i < text.length() && Character.isWhitespace(text.charAt(i))) { + i += direction.OFFSET; + } + + // Read next word + while (i < text.length() && !Character.isWhitespace((text.charAt(i)))) { + i += direction.OFFSET; + } + } + + return i; + } + + /** + * Capitalize the word on the right side of the cursor. + * + * @param caretPosition The position of the cursor + * @param text The string to manipulate + * + * @return String The resulting text and caret position. + */ + public static ResultingStringState capitalize(int caretPosition, String text) { + return setWordCase(text, caretPosition, LetterCase.CAPITALIZED); + } + + /** + * Make all characters in the word uppercase. + * + * @param caretPosition The position of the cursor + * @param text The string to manipulate + * + * @return String The resulting text and caret position. + */ + public static ResultingStringState uppercase(int caretPosition, String text) { + return setWordCase(text, caretPosition, LetterCase.UPPER); + } + + /** + * Make all characters in the word lowercase. + * + * @param caretPosition The position of the cursor + * @param text The string to manipulate + * + * @return String The resulting text and caret position. + */ + public static ResultingStringState lowercase(int caretPosition, String text) { + return setWordCase(text, caretPosition, LetterCase.LOWER); + } + + /** + * Remove the next word on the right side of the cursor. + * + * @param caretPosition The position of the cursor + * @param text The string to manipulate + * + * @return String The resulting text and caret position. + */ + public static ResultingStringState killWord(int caretPosition, String text) { + return deleteUntilWordBoundary(caretPosition, text, Direction.NEXT); + } + + /** + * Remove the previous word on the left side of the cursor. + * + * @param caretPosition The position of the cursor + * @param text The string to manipulate + * + * @return String The resulting text and caret position. + */ + public static ResultingStringState backwardKillWord(int caretPosition, String text) { + return deleteUntilWordBoundary(caretPosition, text, Direction.PREVIOUS); + } +} diff --git a/src/main/java/org/jabref/model/util/ResultingStringState.java b/src/main/java/org/jabref/model/util/ResultingStringState.java new file mode 100644 index 00000000000..fc248cf7f8e --- /dev/null +++ b/src/main/java/org/jabref/model/util/ResultingStringState.java @@ -0,0 +1,11 @@ +package org.jabref.model.util; + +public class ResultingStringState { + public final int caretPosition; + public final String text; + + public ResultingStringState(int caretPosition, String text) { + this.caretPosition = caretPosition; + this.text = text; + } +} diff --git a/src/main/java/org/jabref/preferences/JabRefPreferences.java b/src/main/java/org/jabref/preferences/JabRefPreferences.java index 2e908686c38..97ef0bce820 100644 --- a/src/main/java/org/jabref/preferences/JabRefPreferences.java +++ b/src/main/java/org/jabref/preferences/JabRefPreferences.java @@ -172,9 +172,7 @@ public class JabRefPreferences implements PreferencesService { public static final String IMPORT_WORKING_DIRECTORY = "importWorkingDirectory"; public static final String EXPORT_WORKING_DIRECTORY = "exportWorkingDirectory"; public static final String WORKING_DIRECTORY = "workingDirectory"; - public static final String EDITOR_EMACS_KEYBINDINGS = "editorEMACSkeyBindings"; - public static final String EDITOR_EMACS_KEYBINDINGS_REBIND_CA = "editorEMACSkeyBindingsRebindCA"; - public static final String EDITOR_EMACS_KEYBINDINGS_REBIND_CF = "editorEMACSkeyBindingsRebindCF"; + public static final String GROUPS_DEFAULT_FIELD = "groupsDefaultField"; public static final String KEYWORD_SEPARATOR = "groupKeywordSeparator"; public static final String AUTO_ASSIGN_GROUP = "autoAssignGroup"; @@ -524,9 +522,6 @@ private JabRefPreferences() { defaults.put(SEND_OS_DATA, Boolean.FALSE); defaults.put(SEND_TIMEZONE_DATA, Boolean.FALSE); defaults.put(VALIDATE_IN_ENTRY_EDITOR, Boolean.TRUE); - defaults.put(EDITOR_EMACS_KEYBINDINGS, Boolean.FALSE); - defaults.put(EDITOR_EMACS_KEYBINDINGS_REBIND_CA, Boolean.TRUE); - defaults.put(EDITOR_EMACS_KEYBINDINGS_REBIND_CF, Boolean.TRUE); defaults.put(AUTO_COMPLETE, Boolean.FALSE); defaults.put(AUTOCOMPLETER_FIRSTNAME_MODE, AutoCompleteFirstNameMode.BOTH.name()); defaults.put(AUTOCOMPLETER_FIRST_LAST, Boolean.FALSE); // "Autocomplete names in 'Firstname Lastname' format only" diff --git a/src/main/resources/l10n/JabRef_en.properties b/src/main/resources/l10n/JabRef_en.properties index bbb9e007210..61ed00ff4cb 100644 --- a/src/main/resources/l10n/JabRef_en.properties +++ b/src/main/resources/l10n/JabRef_en.properties @@ -2093,6 +2093,26 @@ Required=Required Entry\ type\ cannot\ be\ empty.\ Please\ enter\ a\ name.=Entry type cannot be empty. Please enter a name. Field\ cannot\ be\ empty.\ Please\ enter\ a\ name.=Field cannot be empty. Please enter a name. +Capitalize\ current\ word=Capitalize current word +Delete\ text=Delete text +Make\ current\ word\ lowercase=Make current word lowercase +Make\ current\ word\ uppercase=Make current word uppercase +Move\ caret\ left=Move caret left +Move\ caret\ right=Move caret right +Move\ caret\ to\ previous\ word=Move caret to previous word +Move\ caret\ to\ next\ word=Move caret to next word +Move\ caret\ to\ beginning\ of\ line=Move caret to beginning of line +Move\ caret\ to\ end\ of\ line=Move caret to end of line +Move\ the\ caret\ down=Move the caret down +Move\ the\ caret\ to\ the\ beginning\ of\ text=Move the caret to the beginning of text +Move\ the\ caret\ to\ the\ end\ of\ text=Move the caret to the end of text +Move\ the\ caret\ up=Move the caret up +Remove\ line\ after\ caret=Remove line after caret +Remove\ characters\ until\ next\ word=Remove characters until next word +Remove\ the\ current\ word\ backwards=Remove the current word backwards + +Text\ editor=Text editor + Search\ ShortScience=Search ShortScience Unable\ to\ open\ ShortScience.=Unable to open ShortScience. @@ -2271,6 +2291,8 @@ The\ query\ cannot\ contain\ a\ year\ and\ year-range\ field.=The query cannot c This\ query\ uses\ unsupported\ fields.=This query uses unsupported fields. This\ query\ uses\ unsupported\ syntax.=This query uses unsupported syntax. +Presets=Presets + Check\ Proxy\ Setting=Check Proxy Setting Check\ connection=Check connection Connection\ failed\!=Connection failed\! diff --git a/src/test/java/org/jabref/logic/util/strings/StringManipulatorTest.java b/src/test/java/org/jabref/logic/util/strings/StringManipulatorTest.java new file mode 100644 index 00000000000..fce0126c0b7 --- /dev/null +++ b/src/test/java/org/jabref/logic/util/strings/StringManipulatorTest.java @@ -0,0 +1,157 @@ +package org.jabref.logic.util.strings; + +import java.util.stream.Stream; + +import org.jabref.model.util.ResultingStringState; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class StringManipulatorTest { + + @Test + public void testCapitalizePreservesNewlines() { + int caretPosition = 5; // Position of the caret, between the two ll in the first hellO" + String input = "hello\n\nhELLO"; + String expectedResult = "hello\n\nHello"; + ResultingStringState textOutput = StringManipulator.capitalize(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + } + + @Test + public void testUppercasePreservesSpace() { + int caretPosition = 3; // Position of the caret, between the two ll in the first hello + String input = "hello hello"; + String expectedResult = "helLO hello"; + ResultingStringState textOutput = StringManipulator.uppercase(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + } + + @Test + public void testUppercasePreservesNewlines() { + int caretPosition = 3; // Position of the caret, between the two ll in the first hello + String input = "hello\nhello"; + String expectedResult = "helLO\nhello"; + ResultingStringState textOutput = StringManipulator.uppercase(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + } + + @Test + public void testUppercasePreservesTab() { + int caretPosition = 3; // Position of the caret, between the two ll in the first hello + String input = "hello\thello"; + String expectedResult = "helLO\thello"; + ResultingStringState textOutput = StringManipulator.uppercase(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + } + + @Test + public void testUppercasePreservesDoubleSpace() { + int caretPosition = 5; // Position of the caret, at the first space + String input = "hello hello"; + String expectedResult = "hello HELLO"; + ResultingStringState textOutput = StringManipulator.uppercase(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + } + + @Test + public void testUppercaseIgnoresTrailingWhitespace() { + int caretPosition = 5; // First space + String input = "hello "; + String expectedResult = "hello "; + ResultingStringState textOutput = StringManipulator.uppercase(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + // Expected caret position is right after the last space, which is index 7 + assertEquals(7, textOutput.caretPosition); + } + + @Test + public void testKillWordTrimsTrailingWhitespace() { + int caretPosition = 5; // First space + String input = "hello "; + String expectedResult = "hello"; + ResultingStringState textOutput = StringManipulator.killWord(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + assertEquals(caretPosition, textOutput.caretPosition); + } + + @Test + public void testBackwardsKillWordTrimsPreceedingWhitespace() { + int caretPosition = 1; // Second space + String input = " hello"; + // One space should be preserved since we are deleting everything preceding the second space. + String expectedResult = " hello"; + ResultingStringState textOutput = StringManipulator.backwardKillWord(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + // The caret should have been moved to the start. + assertEquals(0, textOutput.caretPosition); + } + + @Test + public void testUppercasePreservesMixedSpaceNewLineTab() { + int caretPosition = 5; // Position of the caret, after first hello + String input = "hello \n\thello"; + String expectedResult = "hello \n\tHELLO"; + ResultingStringState textOutput = StringManipulator.uppercase(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + } + + @Test + public void testLowercaseEditsTheNextWord() { + int caretPosition = 5; // Position of the caret, right at the space + String input = "hello HELLO"; + String expectedResult = "hello hello"; + ResultingStringState textOutput = StringManipulator.lowercase(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + } + + @Test + public void testKillWordRemovesFromPositionUpToNextWord() { + int caretPosition = 3; // Position of the caret, between the two "ll in the first hello" + String input = "hello hello"; + String expectedResult = "hel hello"; + ResultingStringState textOutput = StringManipulator.killWord(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + } + + @Test + public void testKillWordRemovesNextWordIfPositionIsInSpace() { + int caretPosition = 5; // Position of the caret, after the first hello" + String input = "hello person"; + String expectedResult = "hello"; + ResultingStringState textOutput = StringManipulator.killWord(caretPosition, input); + assertEquals(expectedResult, textOutput.text); + } + + @Test + public void testKillPreviousWord() { + int caretPosition = 8; + int expectedPosition = 6; + String input = "hello person"; + String expectedResult = "hello rson"; + ResultingStringState result = StringManipulator.backwardKillWord(caretPosition, input); + assertEquals(expectedResult, result.text); + assertEquals(expectedPosition, result.caretPosition); + } + + @ParameterizedTest + @MethodSource("wordBoundaryTestData") + void testGetNextWordBoundary(String text, int caretPosition, int expectedPosition, StringManipulator.Direction direction) { + int result = StringManipulator.getNextWordBoundary(caretPosition, text, direction); + assertEquals(expectedPosition, result); + } + + private static Stream wordBoundaryTestData() { + return Stream.of( + Arguments.of("hello person", 3, 0, StringManipulator.Direction.PREVIOUS), + Arguments.of("hello person", 12, 6, StringManipulator.Direction.PREVIOUS), + Arguments.of("hello person", 0, 0, StringManipulator.Direction.PREVIOUS), + Arguments.of("hello person", 0, 5, StringManipulator.Direction.NEXT), + Arguments.of("hello person", 5, 12, StringManipulator.Direction.NEXT), + Arguments.of("hello person", 12, 12, StringManipulator.Direction.NEXT)); + } +}