Built on top of David's solution:
I added functionality for the UP key, as well added functionality for the ESC key in order to hide the popup window.
In addition to that, you can specify a callback function when constructing a AutoSuggestor
object, which will be called when a suggestion from the list is selected.
import javax.swing.border.LineBorder
import java.util.ArrayList
import javax.swing.event.DocumentListener
import java.awt.*
import java.awt.event.*
import javax.swing.*
import javax.swing.event.DocumentEvent
/**
* Author of the original version: David @ https://stackoverflow.com/users/1133011/david-kroukamp
*/
class Test {
init {
val frame = JFrame()
frame.defaultCloseOperation = JFrame.EXIT_ON_CLOSE
val f = JTextField(10)
val autoSuggestor = object : AutoSuggestor(f, frame, ArrayList(), Color.WHITE.brighter(), Color.BLUE, Color.RED, 0.75f) {
override fun wordTyped(typedWord: String?): Boolean {
//create list for dictionary this in your case might be done via calling a method which queries db and returns results as arraylist
val words = ArrayList<String>()
words.add("hello")
words.add("heritage")
words.add("happiness")
words.add("goodbye")
words.add("cruel")
words.add("car")
words.add("war")
words.add("will")
words.add("world")
words.add("wall")
setDictionary(words)
//addToDictionary("bye");//adds a single word
return super.wordTyped(typedWord)//now call super to check for any matches against newest dictionary
}
}
val p = JPanel()
p.add(f)
frame.add(p)
frame.pack()
frame.isVisible = true
}
companion object {
@JvmStatic
fun main(args: Array<String>) {
SwingUtilities.invokeLater { Test() }
}
}
}
internal open class AutoSuggestor(val textField: JTextField, val container: Window, words: ArrayList<String>, popUpBackground: Color, private val suggestionsTextColor: Color, private val suggestionFocusedColor: Color, opacity: Float, private val callback: (String) -> Unit = {}) {
private val suggestionsPanel: JPanel
val autoSuggestionPopUpWindow: JWindow
private var typedWord: String? = null
private val dictionary = ArrayList<String>()
private var currentIndexOfSpace: Int = 0
private var tW: Int = 0
private var tH: Int = 0
private val documentListener = object : DocumentListener {
override fun insertUpdate(de: DocumentEvent) {
checkForAndShowSuggestions()
}
override fun removeUpdate(de: DocumentEvent) {
checkForAndShowSuggestions()
}
override fun changedUpdate(de: DocumentEvent) {
checkForAndShowSuggestions()
}
}
val addedSuggestionLabels: ArrayList<SuggestionLabel>
get() {
val sls = ArrayList<SuggestionLabel>()
for (i in 0 until suggestionsPanel.componentCount) {
if (suggestionsPanel.getComponent(i) is SuggestionLabel) {
val sl = suggestionsPanel.getComponent(i) as SuggestionLabel
sls.add(sl)
}
}
return sls
}
//get newest word after last white space if any or the first word if no white spaces
val currentlyTypedWord: String
get() {
val text = textField.text
var wordBeingTyped = ""
if (text.contains(" ")) {
val tmp = text.lastIndexOf(" ")
if (tmp >= currentIndexOfSpace) {
currentIndexOfSpace = tmp
wordBeingTyped = text.substring(text.lastIndexOf(" "))
}
} else {
wordBeingTyped = text
}
return wordBeingTyped.trim { it <= ' ' }
}
init {
this.textField.document.addDocumentListener(documentListener)
setDictionary(words)
typedWord = ""
currentIndexOfSpace = 0
tW = 0
tH = 0
autoSuggestionPopUpWindow = JWindow(container)
autoSuggestionPopUpWindow.opacity = opacity
suggestionsPanel = JPanel()
suggestionsPanel.layout = GridLayout(0, 1)
suggestionsPanel.background = popUpBackground
addFocusListenersToHandleVisibilityOfPopUpWindow()
addKeyBindingToRequestFocusInPopUpWindow()
}
private fun addFocusListenersToHandleVisibilityOfPopUpWindow() {
textField.addFocusListener(object:FocusListener {
override fun focusLost(e: FocusEvent?) {
var focusOnPopUp = false
for (i in 0 until suggestionsPanel.componentCount) {
if (suggestionsPanel.getComponent(i) is SuggestionLabel) {
val label = suggestionsPanel.getComponent(i) as SuggestionLabel
if (label.isFocused)
focusOnPopUp = true
}
}
if (!focusOnPopUp && !shouldShowPopUpWindow) {
autoSuggestionPopUpWindow.isVisible = false
}
}
override fun focusGained(e: FocusEvent?) {
shouldShowPopUpWindow = false
}
})
}
private fun addKeyBindingToRequestFocusInPopUpWindow() {
textField.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, true), "Escape released")
textField.actionMap.put("Escape released", object : AbstractAction() {
override fun actionPerformed(ae: ActionEvent) {// Hide the popwindow
shouldShowPopUpWindow = false
autoSuggestionPopUpWindow.isVisible = false
}
})
textField.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "Down released")
textField.actionMap.put("Down released", object : AbstractAction() {
override fun actionPerformed(ae: ActionEvent) {//focuses the first label on popwindow
for (i in 0 until suggestionsPanel.componentCount) {
if (suggestionsPanel.getComponent(i) is SuggestionLabel) {
(suggestionsPanel.getComponent(i) as SuggestionLabel).isFocused = true
autoSuggestionPopUpWindow.toFront()
autoSuggestionPopUpWindow.requestFocusInWindow()
suggestionsPanel.requestFocusInWindow()
suggestionsPanel.getComponent(i).requestFocusInWindow()
break
}
}
}
})
textField.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "Up released")
textField.actionMap.put("Up released", object : AbstractAction() {
override fun actionPerformed(ae: ActionEvent) {//focuses the last label on popwindow
for (i in 0 until suggestionsPanel.componentCount) {
val reverseIndex = suggestionsPanel.componentCount-1 - i
if (suggestionsPanel.getComponent(reverseIndex) is SuggestionLabel) {
(suggestionsPanel.getComponent(reverseIndex) as SuggestionLabel).isFocused = true
autoSuggestionPopUpWindow.toFront()
autoSuggestionPopUpWindow.requestFocusInWindow()
suggestionsPanel.requestFocusInWindow()
suggestionsPanel.getComponent(reverseIndex).requestFocusInWindow()
break
}
}
}
})
suggestionsPanel.getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, true), "Escape released")
suggestionsPanel.actionMap.put("Escape released", object : AbstractAction() {
override fun actionPerformed(ae: ActionEvent) {// Hide the popwindow
shouldShowPopUpWindow = false
autoSuggestionPopUpWindow.isVisible = false
}
})
suggestionsPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, true), "Up released")
suggestionsPanel.actionMap.put("Up released", object : AbstractAction() {
override fun actionPerformed(ae: ActionEvent) {//allows scrolling of labels in pop window (I know very hacky for now :))
val sls = addedSuggestionLabels
val max = sls.size
var indexOfFocusedSuggestion = -1
for (i in 0 until max) {
val sl = sls[i]
if ( sl.isFocused )
indexOfFocusedSuggestion = i
}
if (indexOfFocusedSuggestion - 1 < 0) {
sls[indexOfFocusedSuggestion].isFocused = false
autoSuggestionPopUpWindow.isVisible = false
setFocusToTextField()
checkForAndShowSuggestions()//fire method as if document listener change occured and fired it
}
else {
sls[indexOfFocusedSuggestion].isFocused = false
sls[indexOfFocusedSuggestion-1].isFocused = true
autoSuggestionPopUpWindow.toFront()
autoSuggestionPopUpWindow.requestFocusInWindow()
suggestionsPanel.requestFocusInWindow()
suggestionsPanel.getComponent(indexOfFocusedSuggestion-1).requestFocusInWindow()
}
}
})
suggestionsPanel.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, true), "Down released")
suggestionsPanel.actionMap.put("Down released", object : AbstractAction() {
override fun actionPerformed(ae: ActionEvent) {//allows scrolling of labels in pop window (I know very hacky for now :))
val sls = addedSuggestionLabels
val max = sls.size
var indexOfFocusedSuggestion = -1
for (i in 0 until max) {
val sl = sls[i]
if ( sl.isFocused )
indexOfFocusedSuggestion = i
}
if (indexOfFocusedSuggestion + 1 >= max) {
sls[indexOfFocusedSuggestion].isFocused = false
autoSuggestionPopUpWindow.isVisible = false
setFocusToTextField()
checkForAndShowSuggestions()//fire method as if document listener change occured and fired it
}
else {
sls[indexOfFocusedSuggestion].isFocused = false
sls[indexOfFocusedSuggestion+1].isFocused = true
autoSuggestionPopUpWindow.toFront()
autoSuggestionPopUpWindow.requestFocusInWindow()
suggestionsPanel.requestFocusInWindow()
suggestionsPanel.getComponent(indexOfFocusedSuggestion+1).requestFocusInWindow()
}
}
})
}
private fun setFocusToTextField() {
container.toFront()
container.requestFocusInWindow()
textField.requestFocusInWindow()
}
var shouldShowPopUpWindow = false
private fun checkForAndShowSuggestions() {
typedWord = currentlyTypedWord
suggestionsPanel.removeAll()//remove previos words/jlabels that were added
//used to calcualte size of JWindow as new Jlabels are added
tW = 0
tH = 0
val added = wordTyped(typedWord)
if (!added) {
if (autoSuggestionPopUpWindow.isVisible) {
autoSuggestionPopUpWindow.isVisible = false
}
} else {
shouldShowPopUpWindow = true
showPopUpWindow()
setFocusToTextField()
}
}
protected fun addWordToSuggestions(word: String) {
val suggestionLabel = SuggestionLabel(word, suggestionFocusedColor, suggestionsTextColor, this, callback)
calculatePopUpWindowSize(suggestionLabel)
suggestionsPanel.add(suggestionLabel)
}
private fun calculatePopUpWindowSize(label: JLabel) {
//so we can size the JWindow correctly
if (tW < label.preferredSize.width) {
tW = label.preferredSize.width
}
tH += label.preferredSize.height
}
private fun showPopUpWindow() {
autoSuggestionPopUpWindow.contentPane.add(suggestionsPanel)
autoSuggestionPopUpWindow.minimumSize = Dimension(textField.width, 30)
autoSuggestionPopUpWindow.setSize(tW, tH)
autoSuggestionPopUpWindow.isVisible = true
var windowX = 0
var windowY = 0
windowX = container.getX() + textField.x + 5
if (suggestionsPanel.height > autoSuggestionPopUpWindow.minimumSize.height) {
windowY = container.getY() + textField.y + textField.height + autoSuggestionPopUpWindow.minimumSize.height
} else {
windowY = container.getY() + textField.y + textField.height + autoSuggestionPopUpWindow.height
}
autoSuggestionPopUpWindow.setLocation(windowX, windowY)
autoSuggestionPopUpWindow.minimumSize = Dimension(textField.width, 30)
autoSuggestionPopUpWindow.revalidate()
autoSuggestionPopUpWindow.repaint()
}
fun setDictionary(words: ArrayList<String>?) {
dictionary.clear()
if (words == null) {
return //so we can call constructor with null value for dictionary without exception thrown
}
for (word in words) {
dictionary.add(word)
}
}
fun addToDictionary(word: String) {
dictionary.add(word)
}
open fun wordTyped(typedWord: String?): Boolean {
if (typedWord!!.isEmpty()) {
return false
}
var suggestionAdded = false
for (word in dictionary) {//get words in the dictionary which we added
var fullyMatches = word.length >= typedWord.length
for (i in 0 until typedWord.length) {//each string in the word
if (word.length > i && !typedWord.toLowerCase().startsWith(word.toLowerCase()[i].toString(), i)) {//check for match
fullyMatches = false
break
}
}
if (fullyMatches) {
addWordToSuggestions(word)
suggestionAdded = true
}
}
return suggestionAdded
}
}
internal class SuggestionLabel(string: String, private val suggestionBorderColor: Color, private val suggestionsTextColor: Color, private val autoSuggestor: AutoSuggestor, private val callback: (String) -> Unit) : JLabel(string) {
var isFocused = false
set(focused) {
if (focused) {
border = LineBorder(suggestionBorderColor)
} else {
border = null
}
repaint()
field = focused
}
private val autoSuggestionsPopUpWindow: JWindow
private val textField: JTextField
init {
this.textField = autoSuggestor.textField
this.autoSuggestionsPopUpWindow = autoSuggestor.autoSuggestionPopUpWindow
initComponent()
}
private fun initComponent() {
isFocusable = true
foreground = suggestionsTextColor
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(me: MouseEvent) {
super.mouseClicked(me)
replaceWithSuggestedText()
autoSuggestionsPopUpWindow.isVisible = false
}
})
getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, true), "Enter released")
actionMap.put("Enter released", object : AbstractAction() {
override fun actionPerformed(ae: ActionEvent) {
replaceWithSuggestedText()
autoSuggestionsPopUpWindow.isVisible = false
}
})
getInputMap(JComponent.WHEN_FOCUSED).put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0, true), "Escape released")
actionMap.put("Escape released", object : AbstractAction() {
override fun actionPerformed(ae: ActionEvent) {// Hide the popwindow
autoSuggestionsPopUpWindow.isVisible = false
}
})
}
private fun replaceWithSuggestedText() {
val suggestedWord = text
val text = textField.text
val typedWord = autoSuggestor.currentlyTypedWord
val t = text.substring(0, text.lastIndexOf(typedWord))
val tmp = t + text.substring(text.lastIndexOf(typedWord)).replace(typedWord, suggestedWord)
textField.text = tmp
callback(tmp)
}
}
Note: The above is written in Kotlin, but if you really want Java code, you can easily convert it back to Java.