Force keyboard focus to LineEdit QT

Viewed 171

I'm trying to develop an overlay PopUp for Windows that's triggered by a global keybind, which should capture focus into a QLineEdit once the keybind is pressed. The issue is that if it was focused once, but I click outside effectively removing focus, the focus can't be reacquired afterward.

This is a simplified version of the code I'm trying to use to force keyboard focus on the QLineEdit:

from PySide6 import QtCore, QtWidgets, QtGui
from pynput import keyboard

class MyWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()

        self.input = QtWidgets.QLineEdit()
        self.layout = QtWidgets.QVBoxLayout(self)
        self.layout.addWidget(self.input)

        self.input.setWindowModality(QtCore.Qt.ApplicationModal)
        self.setWindowState(QtCore.Qt.WindowActive)
        self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
    
    @QtCore.Slot()
    def toggle_visible(self):
        if self.isVisible():
            print("Hiding popup")
            self.hide()
        else:
            print("Showing popup")
            self.show()
            self.activateWindow()
            self.input.grabKeyboard()
            self.input.setFocus()

class KeybindPressed(QtCore.QObject):
    keybind_pressed = QtCore.Signal()

    def __call__(self):
        self.keybind_pressed.emit()


if __name__ == "__main__":
    app = QtWidgets.QApplication([])

    pressed = KeybindPressed()
    with keyboard.GlobalHotKeys({"<alt>+<space>": pressed}):

        widget = MyWidget()
        pressed.keybind_pressed.connect(widget.toggle_visible)
        widget.resize(800, 600)
        widget.show()

        app.exec()

This is a recording showing the undesired behaviour of the focus staying in the other app instead of returning to the window when showing.

enter image description here

1 Answers

Thanks to ekhumoro i was able to figure out how to force focus on windows. Below is the code needed to force focus:

imports and setup

windows = False
if os.name == "nt":
    import win32gui, win32con, win32process, win32api
    win32gui.SystemParametersInfo(win32con.SPI_SETFOREGROUNDLOCKTIMEOUT, 0, win32con.SPIF_SENDWININICHANGE | win32con.SPIF_UPDATEINIFILE)
    windows = True

code to actually force focus, this would be called when your window should get focus:

def force_focus(qt_widget_instance: QtWidgets.QWidget):
    if windows:
        fgwin = win32gui.GetForegroundWindow()
        fg = win32process.GetWindowThreadProcessId(fgwin)[0]
        current = win32api.GetCurrentThreadId()
        if current != fg:
            win32process.AttachThreadInput(fg, current, True)
            win32gui.SetForegroundWindow(qt_widget_instance.winId())
            win32process.AttachThreadInput(fg, win32api.GetCurrentThreadId(), False)

And here is the full example with this implemented:

from PySide6 import QtCore, QtWidgets, QtGui
from pynput import keyboard

import os

windows = False
if os.name == "nt":
    import win32gui, win32con, win32process, win32api
    win32gui.SystemParametersInfo(win32con.SPI_SETFOREGROUNDLOCKTIMEOUT, 0, win32con.SPIF_SENDWININICHANGE | win32con.SPIF_UPDATEINIFILE)
    windows = True

def force_focus(qt_widget_instance: QtWidgets.QWidget):
    if windows:
        fgwin = win32gui.GetForegroundWindow()
        fg = win32process.GetWindowThreadProcessId(fgwin)[0]
        current = win32api.GetCurrentThreadId()
        if current != fg:
            win32process.AttachThreadInput(fg, current, True)
            win32gui.SetForegroundWindow(qt_widget_instance.winId())
            win32process.AttachThreadInput(fg, win32api.GetCurrentThreadId(), False)

class MyWidget(QtWidgets.QWidget):
    def __init__(self):
        super().__init__()

        self.input = QtWidgets.QLineEdit()
        self.layout = QtWidgets.QVBoxLayout(self)
        self.layout.addWidget(self.input)

        self.input.setWindowModality(QtCore.Qt.ApplicationModal)
        self.setWindowState(QtCore.Qt.WindowActive)
        self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowStaysOnTopHint)
    
    @QtCore.Slot()
    def toggle_visible(self):
        if self.isVisible():
            print("Hiding popup")
            self.hide()
        else:
            print("Showing popup")
            self.show()
            force_focus(self)
            self.activateWindow()
            self.input.grabKeyboard()
            self.input.setFocus()



class KeybindPressed(QtCore.QObject):
    keybind_pressed = QtCore.Signal()

    def __call__(self):
        self.keybind_pressed.emit()


if __name__ == "__main__":
    app = QtWidgets.QApplication([])

    pressed = KeybindPressed()
    with keyboard.GlobalHotKeys({"<cmd>+<space>": pressed}):
    
        widget = MyWidget()
        pressed.keybind_pressed.connect(widget.toggle_visible)
        widget.resize(800, 600)
        widget.show()

        app.exec()
Related