Skip to content

Commit

Permalink
Merge 7d15e4c into 79b88f8
Browse files Browse the repository at this point in the history
  • Loading branch information
tsoonjin committed Mar 28, 2016
2 parents 79b88f8 + 7d15e4c commit 4dcb223
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 116 deletions.
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ allprojects {
compile "org.ocpsoft.prettytime:prettytime:$prettytimeVersion"
compile "org.seleniumhq.selenium:selenium-java:$seleniumJavaVersion"
compile "org.seleniumhq.selenium:selenium-server:$seleniumServerVersion"
compile group: 'org.fxmisc.richtext', name: 'richtextfx', version: '0.6.10'

testCompile "junit:junit:$junitVersion"
testCompile "org.loadui:testFx:$testFxVersion"
Expand Down
177 changes: 87 additions & 90 deletions src/main/java/ui/components/FilterTextField.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package ui.components;

import javafx.application.Platform;
import javafx.scene.control.IndexRange;
import filter.expression.QualifierType;
import javafx.event.ActionEvent;
import javafx.geometry.Side;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
Expand All @@ -14,6 +15,7 @@
import java.util.function.Predicate;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static ui.components.KeyboardShortcuts.SHOW_DOCS;

Expand All @@ -23,6 +25,8 @@
*/
public class FilterTextField extends TextField {

private static final int MAX_SUGGESTIONS = 10;

// A character class that indicates the characters considered to be word boundaries.
// Specialised to HubTurbo's filter syntax.
private static final String WORD_BOUNDARY_REGEX = "[ (:)]";
Expand All @@ -32,6 +36,10 @@ public class FilterTextField extends TextField {
public static final String VALID_FILTER_STYLE = "-fx-control-inner-background: white";
public static final String INVALID_FILTER_STYLE = "-fx-control-inner-background: #EE8993";

// For on-the-fly parsing and checking
private final ValidationSupport validationSupport = new ValidationSupport();
private final SuggestionMenu suggestion;

// Callback functions
private Runnable onCancel = () -> {};
private Runnable onShowDocs = () -> {};
Expand All @@ -40,14 +48,16 @@ public class FilterTextField extends TextField {
// For reverting edits
private String previousText = "";

// Shows that is navigating the suggestion menu
private boolean isNavigating = false;

// The list of keywords which will be used in completion
private List<String> keywords = new ArrayList<>();
private List<String> keywords = new ArrayList<>(QualifierType.getCompletionKeywords());

// For on-the-fly parsing and checking
private final ValidationSupport validationSupport = new ValidationSupport();

public FilterTextField(Predicate<String> validation) {
super("");
suggestion = setupSuggestion();
setup(validation);
}

Expand All @@ -70,15 +80,15 @@ private void setup(Predicate<String> validation) {
if (key == null || key.isEmpty() || isModifierKeyPress) {
return;
}
char typed = e.getCharacter().charAt(0);
// \b will allow us to detect deletion, but we can't find out the characters deleted
if (typed == '\t') {
e.consume();
if (!getSelectedText().isEmpty()) {
confirmCompletion();
}
} else if (Character.isAlphabetic(typed) && shouldStartCompletion()) {
startCompletion(e);

char typed = key.charAt(0);

if (typed == '\t' && suggestion.isShowing()) {
suggestion.hide();
suggestion.getSelectedContent().ifPresent(this::completeWord);
} else {
suggestion.loadSuggestions(getMatchingKeywords(getCurrentWord()));
suggestion.show(this, Side.BOTTOM, 0, 0);
}
});
setOnKeyPressed(e -> {
Expand All @@ -89,14 +99,15 @@ private void setup(Predicate<String> validation) {
});
setOnKeyReleased(e -> {
e.consume();
if (e.getCode() == KeyCode.ENTER) {
confirmEdit();
} else if (e.getCode() == KeyCode.ESCAPE) {
if (getText().equals(previousText)) {
onCancel.run();
} else {
revertEdit();
}

if (e.getCode() == KeyCode.ENTER) handleOnEnter();

if (e.getCode() == KeyCode.ESCAPE) handleOnEscape();

isNavigating = e.getCode() == KeyCode.UP || e.getCode() == KeyCode.DOWN;

if (suggestion.isShowing() && !isNavigating) {
suggestion.loadSuggestions(getMatchingKeywords(getCurrentWord()));
}
});
addEventHandler(KeyEvent.KEY_PRESSED, event -> {
Expand All @@ -106,6 +117,24 @@ private void setup(Predicate<String> validation) {
});
}

private void handleOnEnter() {
if (isNavigating) {
suggestion.hide();
suggestion.getSelectedContent().ifPresent(this::completeWord);
} else {
confirmEdit();
}
}

private void handleOnEscape() {
if (getText().equals(previousText)) {
onCancel.run();
} else {
revertEdit();
}
}


/**
* Sets the style of FilterTextField to the style for valid filter
*/
Expand All @@ -121,77 +150,32 @@ public final void setStyleForInvalidFilter() {
}

/**
* Completion is only started when there's effectively nothing (only whitespace)
* after the caret, or if there is selected text (indicating either that we are in
* the midst of completion, or that the user does not want the selected text and is
* typing to replace it)
* @return true if completion should be started
* @param query
* @return suggested keyword that contains a given query
*/
private boolean shouldStartCompletion() {
return getCharAfterCaret().trim().isEmpty() || !getSelectedText().isEmpty();
}

/**
* Determines if the word being edited begins a registered completion word.
* If so, performs completion.
*/
private void startCompletion(KeyEvent e) {
String editedWord = getCurrentWord() + e.getCharacter();
for (String candidateWord : keywords) {
if (candidateWord.startsWith(editedWord)) {
performCompletionOfWord(e, editedWord, candidateWord);
break;
}
}
private List<String> getMatchingKeywords(String query) {
return keywords.stream().filter(keyword -> keyword.contains(query)).collect(Collectors.toList());
}

/**
* Low-level manipulation of field selection to implement completion
*/
private void performCompletionOfWord(KeyEvent e, String word, String candidateWord) {
e.consume();
int caret = getCaretPosition();
if (getSelectedText().isEmpty()) {
String before = getText().substring(0, caret);
String insertion = e.getCharacter();
String after = getText().substring(caret, getText().length());
String addition = candidateWord.substring(word.length());
setText(before + insertion + addition + after);
Platform.runLater(() -> selectRange(
before.length() + insertion.length() + addition.length(),
before.length() + insertion.length()));
} else {
IndexRange sel = getSelection();
int start = Math.min(sel.getStart(), sel.getEnd());
int end = Math.max(sel.getStart(), sel.getEnd());
String before = getText().substring(0, start);
String after = getText().substring(end, getText().length());
String insertion = e.getCharacter();
String addition = candidateWord.substring(word.length());
setText(before + insertion + addition + after);
Platform.runLater(() -> selectRange(
before.length() + insertion.length() + addition.length(),
before.length() + insertion.length()));
}
}

/**
* Confirms a completion by moving to the extreme right side of the selection
* Determines the word currently being edited.
*/
private void confirmCompletion() {
positionCaret(Math.max(getSelection().getStart(), getSelection().getEnd()));
private String getCurrentWord() {
int caret = Math.min(getSelection().getStart(), getSelection().getEnd());
return getText().substring(getInitialCaretPosition(caret), caret);
}

/**
* Determines the word currently being edited.
* @param caret
* @return index before first char in current word
*/
private String getCurrentWord() {
int caret = Math.min(getSelection().getStart(), getSelection().getEnd());
private int getInitialCaretPosition(int caret) {
int pos = regexLastIndexOf(getText().substring(0, caret), WORD_BOUNDARY_REGEX);
if (pos == -1) {
pos = 0;
}
return getText().substring(pos > 0 ? pos + 1 : pos, caret);
return pos > 0 ? pos + 1 : pos;
}

/**
Expand All @@ -210,17 +194,6 @@ private static int regexLastIndexOf(String inString, String charClassRegex) {
}
}

/**
* Returns the character following the caret as a string. The string returned
* is guaranteed to be of length 1, unless the caret is at the end of the field,
* in which case it is empty.
*/
private String getCharAfterCaret() {
if (getCaretPosition() < getText().length()) {
return getText().substring(getCaretPosition(), getCaretPosition() + 1);
}
return "";
}

/**
* Reverts the contents of the field to its last confirmed value.
Expand All @@ -242,6 +215,30 @@ private void confirmEdit() {
positionCaret(caretPosition);
}

// SuggestionMenu

private final SuggestionMenu setupSuggestion() {
SuggestionMenu suggestion = new SuggestionMenu(MAX_SUGGESTIONS).setActionHandler(this::menuItemHandler);
suggestion.loadSuggestions(keywords);
return suggestion;
}

/**
* Replaces current word with suggested word
* @param suggestedWord
*/
private void completeWord(String suggestedWord) {
int caret = getCaretPosition();
selectRange(getInitialCaretPosition(caret), caret);
replaceSelection(suggestedWord);
}


private void menuItemHandler(ActionEvent event) {
SuggestionMenu.getTextOnAction(event).ifPresent(this::completeWord);
suggestion.hide();
}

/**
* Sets the contents of the field and acts as if it was confirmed by the user.
*/
Expand Down
80 changes: 80 additions & 0 deletions src/main/java/ui/components/SuggestionMenu.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package ui.components;

import java.util.List;
import java.util.Optional;

import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;

/**
* Pop-up menu that appears with suggestions for auto-completion
*
*/
public class SuggestionMenu extends ContextMenu {

private final int maxEntries;

// Selected menu item's content
private Optional<String> selected = Optional.empty();

// Callback functions
private EventHandler<ActionEvent> actionHandler = (event) -> defaultActionHandler(event);

public SuggestionMenu(int maxEntries) {
this.maxEntries = maxEntries;
}

/**
* Load search result in menu
* @param searchResult
*/
public void loadSuggestions(List<String> searchResult) {
getItems().clear();
searchResult.stream().limit(maxEntries).forEach(this::addMenuItem);
selected = searchResult.stream().findFirst();
}

/**
* Get content of selected menu item
*/
public Optional<String> getSelectedContent() {
return selected;
}

/**
* Wraps a label inside a menu item.
*/
private void addMenuItem(String content) {
Label label = new Label(content);
CustomMenuItem item = new CustomMenuItem(label, false);
item.setText(content);
getItems().add(item);
item.setOnAction(this.actionHandler);
assert getItems().size() <= maxEntries;
}

/**
* Sets selected content to event's source and hide the menu
* @param event
*/
private void defaultActionHandler(ActionEvent event) {
selected = getTextOnAction(event);
hide();
}

/**
* @param event
* @return text content from an event's source
*/
public static Optional<String> getTextOnAction(ActionEvent event) {
return Optional.of(((CustomMenuItem) event.getSource()).getText());
}

public SuggestionMenu setActionHandler(EventHandler<ActionEvent> actionHandler) {
this.actionHandler = actionHandler;
return this;
}
}
6 changes: 1 addition & 5 deletions src/main/java/ui/issuepanel/FilterPanel.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import ui.GUIController;
import ui.GuiElement;
import ui.components.PanelMenuBar;
import backend.resource.TurboUser;
import filter.FilterException;
import filter.Parser;
import filter.expression.FilterExpression;
Expand All @@ -31,7 +30,7 @@
import prefs.PanelInfo;

import java.util.*;
import java.util.stream.Collectors;
import java.util.List;

/**
* A FilterPanel is an AbstractPanel meant for containing issues and an accompanying filter text field,
Expand Down Expand Up @@ -113,9 +112,6 @@ private void setUpEventHandler() {

// Update keywords
List<String> all = new ArrayList<>(QualifierType.getCompletionKeywords());
all.addAll(e.users.stream()
.map(TurboUser::getLoginName)
.collect(Collectors.toList()));

// Ensure that completions appear in lexicographical order
Collections.sort(all);
Expand Down
Loading

0 comments on commit 4dcb223

Please sign in to comment.