J.spenc
J.spenc

Reputation: 359

Debounced Completer autoselects highlighted option when I stop arrowing through results?

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

Answers (1)

J.spenc
J.spenc

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

Related Questions