Python Forum
[Tkinter] Tkinter popup no grabbing the selected keys - Event propagation
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] Tkinter popup no grabbing the selected keys - Event propagation
#1
Hi guys,

I am creating a popup that, while user is typing, shows some options for the user to select, if tab, enter or arrow keys are pressed, or if none is pressed, it keeps typing the text in the current window and the popup updates itself. However, when the popup show and is visible (but has no focus - if the user chose to keeps typing) the keys work in the popup (only in the fist time it shows), but are propagating to the text, for instance, if I press tab, it will write the selection, but only after a tab is inserted into te text. I tried many things, but cannot solve this issue? Any help??

import os
import time
import tkinter as tk
from tkinter import Listbox, font
import keyboard
from popup_utils import get_caret_position
from queue_manager import gui_queue


class PopupAutoComplete:
    def __init__(self):
        self.popup_auto = None
        self.listbox = None
        self.prevent_typing = False

    def show_autocomplete_popup(self, suggestions, position, current_word, last_word):
        global word_at_caret, last_word_at_caret
        word_at_caret = current_word  # Atualiza a variável global
        last_word_at_caret = last_word  # Atualiza a variável global

        if not self.popup_auto:
            self.popup_auto = tk.Toplevel()
            self.popup_auto.wm_overrideredirect(True)
            self.popup_auto.attributes("-topmost", True)
            self.popup_auto.attributes("-alpha", 0.87)
            self.popup_auto.configure(bg="#303030")

            frame = tk.Frame(self.popup_auto, bg="#303030")
            frame.pack(fill=tk.BOTH, expand=True)

            self.listbox = Listbox(
                frame,
                font=("Work Sans", 11),
                bg="#303030",
                fg="white",
                selectbackground="orange",
                selectforeground="black",
                activestyle="none",
            )
            self.listbox.pack(fill=tk.BOTH, expand=True)

            self.popup_auto.bind_all("<Return>", self.suppress_key)
            self.popup_auto.bind_all("<Tab>", self.suppress_key)
            self.popup_auto.bind_all("<Up>", self.suppress_key)
            self.popup_auto.bind_all("<Down>", self.suppress_key)
            self.popup_auto.bind_all("<Left>", self.suppress_key)
            self.popup_auto.bind_all("<Right>", self.suppress_key)

            self.listbox.bind("<<ListboxSelect>>", self.on_suggestion_select)
            self.listbox.bind("<Return>", self.on_suggestion_select)
            self.listbox.bind("<Motion>", self.on_mouse_motion)
            self.popup_auto.bind("<FocusOut>", self.on_focus_out)

            self.popup_auto.focus_set()
        else:
            self.popup_auto.deiconify()

        suggestion_height = 20
        total_height = suggestion_height * len(suggestions)

        listbox_font = font.Font(font=self.listbox.cget("font"))
        max_width = (
            max(listbox_font.measure(suggestion) for suggestion in suggestions) + 70
        )

        screen_width = self.popup_auto.winfo_screenwidth()
        screen_height = self.popup_auto.winfo_screenheight()

        x_position = position[0] + 10
        y_position = position[1] + 35

        if x_position + max_width > screen_width:
            x_position = screen_width - max_width - 20
        elif x_position < 0:
            x_position = 20

        if y_position + total_height > screen_height:
            y_position = position[1] - total_height - 20
        elif y_position < 0:
            y_position = 20

        self.popup_auto.geometry(
            f"{max_width}x{total_height}+{x_position}+{y_position}"
        )

        self.listbox.delete(0, tk.END)
        for i, suggestion in enumerate(suggestions, 1):
            self.listbox.insert(
                tk.END, f"{i}. {suggestion.encode('utf-8').decode('utf-8')}"
            )

        if suggestions:
            self.listbox.selection_set(0)
            self.listbox.activate(0)

    def suppress_key(self, event):
        return "break"

    def hide_popup_auto(self):
        if self.popup_auto:
            self.popup_auto.withdraw()

    def on_focus_out(self, event):
        self.hide_popup_auto()

    def get_selected_suggestion(self):
        if self.listbox:
            try:
                return self.listbox.get(self.listbox.curselection())
            except tk.TclError:
                return None
        return None

    def on_mouse_motion(self, event):
        widget = event.widget
        index = widget.nearest(event.y)
        widget.selection_clear(0, tk.END)
        widget.selection_set(index)
        widget.activate(index)
        widget.configure(cursor="hand2")

    
    ################################################################
    def on_suggestion_select(self, event=None):
        global word_at_caret, last_word_at_caret

        # Se o evento for None, use a listbox diretamente
        if event is None:
            widget = self.listbox  # Assume que a listbox está armazenada como um atributo
            selection = widget.curselection()
        else:
            widget = event.widget
            selection = widget.curselection()

        if isinstance(selection, int):
            index = selection
        elif selection:
            index = selection[0]
        else:
            return

        value = widget.get(index)[3:]
        print(f"Selected suggestion: {value}")
        self.selected_suggestion = value
        self.popup_auto.withdraw()

        # Compara word_at_caret com value e adiciona apenas os caracteres que faltam
        if word_at_caret:
            common_prefix = os.path.commonprefix([word_at_caret.lower(), value.lower()])
            chars_to_add = value[len(common_prefix):]
            
            print(f"Palavra atual: {word_at_caret}")
            print(f"Sugestão selecionada: {value}")
            print(f"Prefixo comum: {common_prefix}")
            print(f"Caracteres a adicionar: {chars_to_add}")

            # Adiciona um pequeno atraso antes de escrever os novos caracteres
            time.sleep(0.05)

            # Escreve apenas os caracteres que faltam
            if last_word_at_caret.endswith("."):
                keyboard.write(chars_to_add.capitalize())
            else:
                keyboard.write(chars_to_add)
        else:
            # Se word_at_caret estiver vazio, escreve a sugestão completa
            if last_word_at_caret.endswith("."):
                keyboard.write(value.capitalize())
            else:
                keyboard.write(value)

        # Adiciona um pequeno atraso após escrever a sugestão
        time.sleep(0.05)

        self.hide_popup_auto()

        # Previne a propagação do evento, se houver
        if event:
            return "break"

    ################################################################
    def keys_on_popup_AUTO(self, key):
        global word_at_caret, last_word_at_caret

        if key.isdigit():
            # Imprime os valores das variáveis globais para depuração
            print(f"word_at_caret (global): {word_at_caret}")
            print(f"last_word_at_caret (global): {last_word_at_caret}")

            index = int(key) - 1
            if 0 <= index < self.listbox.size():
                value = self.listbox.get(index)[3:]
                print(f"Selected suggestion by key press: {value}")
                self.selected_suggestion = value
                self.popup_auto.withdraw()

                # Verifica e apaga a palavra na posição do cursor
                if word_at_caret:
                    print(f"Apagando a palavra no cursor: {word_at_caret}")
                    for _ in range(len(word_at_caret) +1):
                        keyboard.send("backspace")

                # Escreve o valor selecionado
                if last_word_at_caret.endswith("."):
                    keyboard.write(value.capitalize())
                else:
                    keyboard.write(value)

                self.hide_popup_auto()
                self.prevent_typing = False
                return

        elif key in ["tab", "enter"]:
            self.suppress_key(None)  # Previne a propagação do evento

            # Adiciona a letra "a" a word_at_caret
            if word_at_caret:
                # print(f"Adicionando 'a' ao cursor.")
                # keyboard.write("a")
                # Atualiza word_at_caret para refletir a adição do caractere
                word_at_caret += "a"

            self.on_suggestion_select(event=None)
            return "break"  # Impede qualquer processamento adicional do evento

        elif key in ["down", "right"]:
            selection = self.listbox.curselection()
            if selection:
                next_index = selection[0] + 1
                if next_index < self.listbox.size():
                    self.listbox.selection_clear(0, tk.END)
                    self.listbox.selection_set(next_index)
                    self.listbox.activate(next_index)
                    print(
                        f"Selected next suggestion: {self.listbox.get(next_index)[3:]}"
                    )

        elif key in ["up", "left"]:
            selection = self.listbox.curselection()
            if selection:
                prev_index = selection[0] - 1
                if prev_index >= 0:
                    self.listbox.selection_clear(0, tk.END)
                    self.listbox.selection_set(prev_index)
                    self.listbox.activate(prev_index)
Reply
#2
Please post a short example of the popup being used. I'm really confused why your class uses global variables and hope an example clears that up.

I think the problem is that you should not care when the popup loses focus, but when the widget that draws the popup loses focus. I also think the popup should be a child window of the widget that pops it up. This would force making a new window instead of trying to reuse the existing popup.
Reply
#3
(Aug-10-2024, 10:40 AM)deanhystad Wrote: Please post a short example of the popup being used. I'm really confused why your class uses global variables and hope an example clears that up.

I think the problem is that you should not care when the popup loses focus, but when the widget that draws the popup loses focus. I also think the popup should be a child window of the widget that pops it up. This would force making a new window instead of trying to reuse the existing popup.

For sure, some information. Sorry I am stating with programming:

0. A link to a short video I did to show the popup working: https://youtu.be/YRjBst2yPnk

1. My main window is a pywebview window. In the background, in another thread, I have a global keyboard listener, using keyboard library, that capture all keys pressed. I am using tkinter just to create the popup for autocomplete words. As the user types, the popup updates itself. Everything is working fine, except for this arrow keys, tab and enter on keyboard I am trying to achieve. Guess because that popup never get focus, to enable user keeps typing if he wants. Here the start of my on key press method on my listener.py:

################################################################
    def on_key_press(self, event):
        global pause_app  # Referencia a variável global
        if pause_app.is_set():  # Verifica se o estado de pausa está ativo
            print("O aplicativo está pausado.")
            return

        key = event.name
        print(f"Key pressed: {key}")

        # Verificação para seleção de sugestões do popup_auto
        if (
            self.popup_auto_complete.popup_auto
            and self.popup_auto_complete.popup_auto.winfo_ismapped()
        ):
            print("Sugestões do popup_auto detectadas-----------------")
            # Passa os valores atuais de word_at_caret e last_word_at_caret
            self.popup_auto_complete.keys_on_popup_AUTO(key)
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  [PyQt] Popup window not coming RamanSMann 2 791 Jan-02-2025, 02:18 AM
Last Post: lyly19
  popup, text input with two readable buttons ethompson 7 1,923 Aug-28-2024, 03:40 AM
Last Post: deanhystad
  [PyQt] Hover over highlighted text and open popup window DrakeSoft 2 2,884 Oct-29-2022, 04:30 PM
Last Post: DrakeSoft
  [TKINTER] Problems creating directories in a selected path Vulera 2 3,915 Aug-10-2021, 06:38 PM
Last Post: Vulera
  [PyQt] Avoid clicked event from button when button is not physically selected and clicked mart79 2 3,147 May-05-2020, 12:54 PM
Last Post: mart79
  POPUP on widget Entry taratata2020 4 5,116 Mar-10-2020, 05:04 PM
Last Post: taratata2020
  [Tkinter] Mouse click event not working on multiple tkinter window evrydaywannabe 2 4,738 Dec-16-2019, 04:47 AM
Last Post: woooee
  What is the purpose of "None" in event=None with tkinter ? alan9979 2 8,124 Jul-10-2019, 08:50 PM
Last Post: alan9979
  Tkinter error for a scheduled event Ceegen 5 7,461 Jan-14-2019, 09:24 PM
Last Post: Ceegen
  [Tkinter] Is there any way to bind keys to a gui window in tkinter? Nwb 1 3,846 Jun-21-2018, 06:04 PM
Last Post: Larz60+

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020