Reputation: 359
I'm trying to create a debounced Typeahead widget that populates with data from my database. When I try to down arrow through the query results, the highlighted result autoselects when I stop pressing the down arrow key.
I looked at the docs for the QCompleter and the return type of popup, QAbstractItemView. QAbstractItemView, says Ctrl+Arrow keys, "Changes the current item but does not select it.". However, when I try navigating with the Ctrl key held, the behavior is no different.
The code for my widget is below:
# Typeahead.h
from PyQt5.QtWidgets import QLineEdit
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QCompleter
from utk_apis.sql_engine import SQLEngine
from PyQt5.QtCore import QStringListModel
from PyQt5.QtCore import QTimer, QStringListModel
from PyQt5.QtWidgets import QLineEdit, QCompleter, QAbstractItemView
class Typeahead(QLineEdit):
def __init__(self, parent=None):
super(Typeahead, self).__init__(parent)
self.results = []
self.selected_result = None
self.engine = None
self.current_index = -1
self.ignore_text_change = False
self.completer = QCompleter(self)
self.completer.setWidget(self)
self.completer.setCompletionMode(QCompleter.PopupCompletion)
self.completer.popup().setSelectionMode(
QAbstractItemView.SingleSelection
) # Single item selection in popup
self.setCompleter(self.completer)
self.completer.popup().setAlternatingRowColors(True)
# Create a timer for debouncing
self.debounce_timer = QTimer(self)
self.debounce_timer.setSingleShot(True) # Only trigger once after interval
self.debounce_timer.setInterval(300) # 300 ms debounce interval
# Connect the timeout of the timer to the emit_text_changed method
self.debounce_timer.timeout.connect(self.emit_text_changed)
# Connect the textChanged signal to start the debounce timer
self.textChanged.connect(self.start_debounce)
# Connect the completer's activated signal to set the selected item only when Enter is pressed
self.completer.activated.connect(self.on_completer_activated)
def start_debounce(self):
"""Start the debounce timer when the text changes."""
self.debounce_timer.start() # This starts or restarts the debounce timer
def emit_text_changed(self):
"""Fetch results and update completer when text changes."""
print(f'Emitting text changed event: {self.text()} Ignore text change: {self.ignore_text_change}')
if self.ignore_text_change is True:
self.ignore_text_change = False
return
if self.engine is None:
self.engine = SQLEngine(env=self.property("env"))
# Run the query to fetch data from the database
data = self.engine.run_query(self._build_query(), results_as_dict=True)
# Convert data to the list of results. Store for sharing with other Typeahead instances
self.results = [
{
"text": f"{row['primary_key_1']} ({row['primary_key_2']}, {row['primary_key_3']})",
"primary_key_1": row["primary_key_1"],
"primary_key_2": row["primary_key_2"],
"primary_key_3": row["primary_key_3"],
}
for row in data
]
# Update the completer with the new results
self.update_completer()
def update_completer(self):
"""Update the QCompleter with the new results."""
completer_model = QStringListModel([result["text"] for result in self.results])
#self.completer.model().setStringList(completer_model)
self.completer.setModel(completer_model)
# Set the width of the popup based on the longest string to avoid truncation
longest_string = max(
[len(result["text"]) for result in self.results], default=0
)
self.completer.popup().setMinimumWidth(
longest_string * 15
) # Adjust 7 to fit font size
# Manually open the completer dropdown
if self.results:
self.completer.complete() # Force the dropdown to show up again
def on_completer_activated(self, text):
"""Handle what happens when an item in the dropdown is selected."""
# Only set the text when the user selects an item by pressing Enter
selected_item = next(
(result for result in self.results if result["text"] == text), None
)
if selected_item:
self.setText(selected_item["text"])
def _build_query(self):
"""Build the SQL query for fetching suggestions."""
query = f"SELECT {','.join(self.property('primary_keys'))} FROM {self.property('targetTable')} WHERE {self.property('targetField')} LIKE '%{self.text()}%'"
return query
Upvotes: 0
Views: 23
Reputation: 359
The corrected code from musicmante is below. The line self.completer.popup().selectionModel().selectionChanged.disconnect()
was added:
# Typeahead.h
from PyQt5.QtWidgets import QLineEdit
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import QCompleter
from utk_apis.sql_engine import SQLEngine
from PyQt5.QtCore import QStringListModel
from PyQt5.QtCore import QTimer, QStringListModel
from PyQt5.QtWidgets import QLineEdit, QCompleter, QAbstractItemView
class Typeahead(QLineEdit):
def __init__(self, parent=None):
super(Typeahead, self).__init__(parent)
self.results = []
self.selected_result = None
self.engine = None
self.current_index = -1
self.ignore_text_change = False
self.completer = QCompleter(self)
self.completer.setCompletionMode(QCompleter.PopupCompletion)
self.setCompleter(self.completer)
self.completer.popup().setAlternatingRowColors(True)
# Create a timer for debouncing
self.debounce_timer = QTimer(self)
self.debounce_timer.setSingleShot(True) # Only trigger once after interval
self.debounce_timer.setInterval(300) # 300 ms debounce interval
# Connect the timeout of the timer to the emit_text_changed method
self.debounce_timer.timeout.connect(self.emit_text_changed)
# Connect the textChanged signal to start the debounce timer
self.textChanged.connect(self.start_debounce)
# Connect the completer's activated signal to set the selected item only when Enter is pressed
self.completer.activated.connect(self.on_completer_activated)
def start_debounce(self):
"""Start the debounce timer when the text changes."""
self.debounce_timer.start() # This starts or restarts the debounce timer
def emit_text_changed(self):
"""Fetch results and update completer when text changes."""
print(f'Emitting text changed event: {self.text()} Ignore text change: {self.ignore_text_change}')
if self.ignore_text_change is True:
self.ignore_text_change = False
return
if self.engine is None:
self.engine = SQLEngine(env=self.property("env"))
# Run the query to fetch data from the database
data = self.engine.run_query(self._build_query(), results_as_dict=True)
# Convert data to the list of results
self.results = [
{
"text": f"{row['primary_key_1']} ({row['primary_key_2']}, {row['primary_key_3']})",
"primary_key_1": row["primary_key_1"],
"primary_key_2": row["primary_key_2"],
"primary_key_3": row["primary_key_3"],
}
for row in data
]
# Update the completer with the new results
self.update_completer()
def update_completer(self):
"""Update the QCompleter with the new results."""
completer_model = QStringListModel([result["text"] for result in self.results])
self.completer.setModel(completer_model)
# THIS LINE STOPS THE DROPDOWN FROM CLOSING WHEN DOWN ARROW IS PRESSED
self.completer.popup().selectionModel().selectionChanged.disconnect()
# Set the width of the popup based on the longest string to avoid truncation
longest_string = max(
[len(result["text"]) for result in self.results], default=0
)
self.completer.popup().setMinimumWidth(
longest_string * 15
) # Adjust 7 to fit font size
# Manually open the completer dropdown
if self.results:
self.completer.complete() # Force the dropdown to show up again
def on_completer_activated(self, text):
"""Handle what happens when an item in the dropdown is selected."""
# Only set the text when the user selects an item by pressing Enter
selected_item = next(
(result for result in self.results if result["text"] == text), None
)
if selected_item:
self.setText(selected_item["text"])
def _build_query(self):
"""Build the SQL query for fetching suggestions."""
query = f"SELECT {','.join(self.property('primary_keys'))} FROM {self.property('targetTable')} WHERE {self.property('targetField')} LIKE '%{self.text()}%'"
return query
Upvotes: 0