Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Emacs key bindings #6037

Merged
merged 20 commits into from
Nov 6, 2020
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions src/main/java/org/jabref/gui/JabRefGUI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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.EditorKeyBindings;
import org.jabref.gui.shared.SharedDatabaseUIManager;
import org.jabref.logic.autosaveandbackup.BackupManager;
import org.jabref.logic.importer.OpenDatabase;
Expand Down Expand Up @@ -86,6 +88,10 @@ private void openWindow(Stage mainStage) {

Scene scene = new Scene(root, 800, 800);
Globals.prefs.getTheme().installCss(scene);

// Handle Emacs key bindings
scene.addEventFilter(KeyEvent.KEY_PRESSED, event -> EditorKeyBindings.call(scene, event, Globals.prefs));

mainStage.setTitle(JabRefFrame.FRAME_TITLE);
mainStage.getIcons().addAll(IconTheme.getLogoSetFX());
mainStage.setScene(scene);
Expand Down Expand Up @@ -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);
}
}

Expand Down
100 changes: 100 additions & 0 deletions src/main/java/org/jabref/gui/keyboard/EditorKeyBindings.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.jabref.gui.keyboard;

import java.util.Optional;

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;
import org.jabref.preferences.PreferencesService;

public class EditorKeyBindings {

public static void call(Scene scene, KeyEvent event, PreferencesService preferencesService) {
if (scene.focusOwnerProperty().get() instanceof TextInputControl) {

KeyBindingRepository keyBindingRepository = Globals.getKeyPrefs();
TextInputControl focusedTextField = (TextInputControl) scene.focusOwnerProperty().get();
Optional<KeyBinding> keyBinding = keyBindingRepository.mapToKeyBinding(event);
keyBinding.ifPresent(binding -> {
if (binding.equals(KeyBinding.EDITOR_DELETE)) {
calixtus marked this conversation as resolved.
Show resolved Hide resolved
focusedTextField.deleteNextChar();
event.consume();
} else if (binding == KeyBinding.EDITOR_BACKWARD) {
focusedTextField.backward();
event.consume();
} else if (binding == KeyBinding.EDITOR_FORWARD) {
focusedTextField.forward();
event.consume();
} else if (binding == KeyBinding.EDITOR_WORD_BACKWARD) {
focusedTextField.previousWord();
event.consume();
} else if (binding == KeyBinding.EDITOR_WORD_FORWARD) {
focusedTextField.nextWord();
event.consume();
} else if (binding == KeyBinding.EDITOR_BEGINNING) {
focusedTextField.home();
event.consume();
} else if (binding == KeyBinding.EDITOR_END) {
focusedTextField.end();
event.consume();
} else if (binding == KeyBinding.EDITOR_BEGINNING_DOC) {
focusedTextField.home();
event.consume();
} else if (binding == KeyBinding.EDITOR_END_DOC) {
focusedTextField.end();
event.consume();
} else if (binding == KeyBinding.EDITOR_UP) {
focusedTextField.home();
event.consume();
} else if (binding == KeyBinding.EDITOR_DOWN) {
focusedTextField.end();
event.consume();
} else if (binding == KeyBinding.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();
} else if (binding == KeyBinding.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();
} else if (binding == KeyBinding.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();
} else if (binding == KeyBinding.EDITOR_KILL_LINE) {
int pos = focusedTextField.getCaretPosition();
focusedTextField.setText(focusedTextField.getText(0, pos));
focusedTextField.positionCaret(pos);
event.consume();
} else if (binding == KeyBinding.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();
} else if (binding == KeyBinding.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();
}
});
}
}
}
18 changes: 18 additions & 0 deletions src/main/java/org/jabref/gui/keyboard/KeyBinding.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,24 @@
import org.jabref.logic.l10n.Localization;

public enum KeyBinding {
EDITOR_DELETE("Delete", Localization.lang("Delete text"), "ctrl+D", KeyBindingCategory.EDITOR),
// DELETE BACKWARDS = Rubout
EDITOR_BACKWARD("Move caret left", Localization.lang("Move caret left"), "ctrl+B", KeyBindingCategory.EDITOR),
EDITOR_FORWARD("Move caret right", Localization.lang("Move caret right"), "ctrl+F", KeyBindingCategory.EDITOR),
EDITOR_WORD_BACKWARD("Move caret to previous word", Localization.lang("Move caret to previous word"), "alt+B", KeyBindingCategory.EDITOR),
EDITOR_WORD_FORWARD("Move caret to next word", Localization.lang("Move caret to next word"), "alt+F", KeyBindingCategory.EDITOR),
EDITOR_BEGINNING("Move caret to beginning of line", Localization.lang("Move caret to beginning of line"), "ctrl+A", KeyBindingCategory.EDITOR),
EDITOR_END("Move caret to of line", Localization.lang("Move caret to end of line"), "ctrl+E", KeyBindingCategory.EDITOR),
EDITOR_BEGINNING_DOC("Move caret to beginning of text", Localization.lang("Move the caret to the beginning of text"), "alt+LESS", KeyBindingCategory.EDITOR),
EDITOR_END_DOC("Move caret to end of text", Localization.lang("Move the caret to the end of text"), "alt+shift+LESS", KeyBindingCategory.EDITOR),
EDITOR_UP("Move caret up", Localization.lang("Move the caret up"), "ctrl+P", KeyBindingCategory.EDITOR),
EDITOR_DOWN("Move caret down", Localization.lang("Move the caret down"), "ctrl+N", KeyBindingCategory.EDITOR),
EDITOR_CAPITALIZE("Capitalize word", Localization.lang("Capitalize current word"), "alt+C", KeyBindingCategory.EDITOR),
EDITOR_LOWERCASE("Lowercase word", Localization.lang("Make current word lowercase"), "alt+L", KeyBindingCategory.EDITOR),
EDITOR_UPPERCASE("Uppercase word", Localization.lang("Make current word uppercase"), "alt+U", KeyBindingCategory.EDITOR),
EDITOR_KILL_LINE("Remove all characters caret to end of line", Localization.lang("Remove line after caret"), "ctrl+K", KeyBindingCategory.EDITOR),
EDITOR_KILL_WORD("Remove characters until next word", Localization.lang("Remove characters until next word"), "alt+D", KeyBindingCategory.EDITOR),
EDITOR_KILL_WORD_BACKWARD("Characters until previous word", Localization.lang("Remove the current word backwards"), "alt+DELETE", 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),
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/org/jabref/gui/keyboard/KeyBindingCategory.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
174 changes: 174 additions & 0 deletions src/main/java/org/jabref/logic/util/strings/StringManipulator.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
11 changes: 11 additions & 0 deletions src/main/java/org/jabref/model/util/ResultingStringState.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 1 addition & 6 deletions src/main/java/org/jabref/preferences/JabRefPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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"
Expand Down
Loading