Focus first result on typing
This commit is contained in:
1 parent
3b13e603d3
commit
4ba83b8b9e
1 file changed
+116
-2
+116
-2
@@ -4,12 +4,20 @@
|
||||
|
||||
package net.sourceforge.pmd.util.fxdesigner.util.autocomplete;
|
||||
|
||||
import static java.awt.event.KeyEvent.VK_DOWN;
|
||||
import static java.awt.event.KeyEvent.VK_UP;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.Robot;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.reflect.FieldUtils;
|
||||
import org.apache.commons.lang3.reflect.MethodUtils;
|
||||
import org.fxmisc.richtext.StyledTextArea;
|
||||
import org.reactfx.EventStream;
|
||||
import org.reactfx.EventStreams;
|
||||
@@ -19,6 +27,10 @@ import org.reactfx.util.Tuples;
|
||||
import net.sourceforge.pmd.util.fxdesigner.util.controls.ContextMenuWithNoArrows;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.ObservableSet;
|
||||
import javafx.css.PseudoClass;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.Parent;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.CustomMenuItem;
|
||||
import javafx.scene.control.Label;
|
||||
@@ -46,8 +58,6 @@ public final class CodeAreaAutocompleteProvider {
|
||||
|
||||
autoCompletePopup.getStyleClass().add("autocomplete-menu");
|
||||
autoCompletePopup.setHideOnEscape(true);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -106,6 +116,8 @@ public final class CodeAreaAutocompleteProvider {
|
||||
|
||||
private void showAutocompletePopup(int insertionIndex, String input) {
|
||||
|
||||
int curFocusIdx = getFocusIdx();
|
||||
|
||||
XPathSuggestionMaker suggestionMaker = mySuggestionProvider.get();
|
||||
|
||||
List<MenuItem> suggestions =
|
||||
@@ -127,6 +139,8 @@ public final class CodeAreaAutocompleteProvider {
|
||||
|
||||
myCodeArea.getCharacterBoundsOnScreen(insertionIndex, insertionIndex + input.length())
|
||||
.ifPresent(bounds -> autoCompletePopup.show(myCodeArea, bounds.getMinX(), bounds.getMaxY()));
|
||||
|
||||
Platform.runLater(() -> focusFirstItem(curFocusIdx));
|
||||
}
|
||||
|
||||
|
||||
@@ -135,4 +149,104 @@ public final class CodeAreaAutocompleteProvider {
|
||||
Platform.runLater(autoCompletePopup::hide);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Programmatically focuses the node at index 0. Doing it with key presses is
|
||||
* the only reliable way, because we're replacing the items.
|
||||
*
|
||||
* That allows using ENTER to insert the first completion without pressing DOWN first.
|
||||
*/
|
||||
private void focusFirstItem(int curFocusIdx) {
|
||||
|
||||
Robot r;
|
||||
try {
|
||||
r = new Robot();
|
||||
} catch (AWTException e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (curFocusIdx == 0) {
|
||||
// the first item was previously focused
|
||||
// For some reason there's a bug where we can't be sure that the 0th
|
||||
// idx will stay focused when changing items unless we do the following:
|
||||
|
||||
r.keyPress(VK_DOWN);
|
||||
r.keyRelease(VK_DOWN);
|
||||
r.keyPress(VK_UP);
|
||||
r.keyRelease(VK_UP);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
int diff = Math.abs(curFocusIdx); // so if curFocusIdx was -1 we have to go 1 down
|
||||
boolean goUp = curFocusIdx > 0;
|
||||
|
||||
while (diff-- > 0) {
|
||||
int key = goUp ? VK_UP : VK_DOWN;
|
||||
|
||||
r.keyPress(key);
|
||||
r.keyRelease(key);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Gets the index of the currently focused item. */
|
||||
private int getFocusIdx() {
|
||||
List<ObservableSet<PseudoClass>> collect =
|
||||
autoCompletePopup.getItems()
|
||||
.stream()
|
||||
.map(this::getStyleableNode)
|
||||
.filter(Objects::nonNull)
|
||||
.map(Node::getPseudoClassStates)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
for (int i = 0; i < collect.size(); i++) {
|
||||
if (collect.get(i).contains(PseudoClass.getPseudoClass("focused"))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
/** Gets the index of the node. */
|
||||
private Node getStyleableNode(MenuItem item) {
|
||||
|
||||
try {
|
||||
// Only since jdk 9 unfortunately
|
||||
return (Node) MethodUtils.invokeMethod(item, "getStyleableNode");
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
// then we're on jdk 8, in which case we do the work ourselves
|
||||
}
|
||||
|
||||
ContextMenu parentPopup = item.getParentPopup();
|
||||
if (parentPopup == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Parent nodes;
|
||||
try {
|
||||
nodes = (Parent) FieldUtils.readDeclaredField(parentPopup.getSkin().getNode(), "itemsContainer", true);
|
||||
} catch (IllegalAccessException e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
for (Node child : nodes.getChildrenUnmodifiable()) {
|
||||
|
||||
// we can't check for instanceof bc that would not be cross-jdk
|
||||
try {
|
||||
Object childItem = MethodUtils.invokeExactMethod(child, "getItem");
|
||||
if (item.equals(childItem)) {
|
||||
return child;
|
||||
}
|
||||
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user