import tkinter as tk from tkinter import font from .TagsRepo import TagsRepo from .common import open_tag_wiki_url from .tag_processing import process_tag from .settings import Settings class AutocompleteEntry(tk.Entry): def __init__(self, master, tags_repo: TagsRepo, callback=None, *args, **kwargs): super().__init__(master, *args, **kwargs) self.tags_repo = tags_repo self.callback = callback self.listbox = None self.listbox_window = None self.suggestions = [] self.suggestion_map = {} self.search_after_id = None # Przechowuje ID opóźnionego wyszukiwania self.selection_index = -1 self.bind("", self.on_keyrelease) self.bind("", self.on_down) self.bind("", self.on_up) self.bind("", self.on_return) self.bind("", self.on_return) self.bind("", lambda e: self.hide_listbox()) def on_keyrelease(self, event): if event.keysym in ("Down", "Up", "Return", "Tab"): return if event.keysym == "Escape": self.hide_listbox() return if self.search_after_id: self.after_cancel(self.search_after_id) self.search_after_id = self.after(200, self.update_suggestions) def update_suggestions(self): self.search_after_id = None # Pobieramy cały tekst oraz indeks kursora full_text = self.get() # self.index(tk.INSERT) zwraca indeks w formacie "linia.kolumna", możemy go wykorzystać jako indeks znakowy text_before_cursor = full_text[: self.index(tk.INSERT)] # Jeżeli ostatni znak to spacja, to znaczy, że użytkownik zakończył ostatni tag – nie sugerujemy if text_before_cursor and text_before_cursor[-1].isspace(): self.hide_listbox() return # Podziel tekst przed kursorem na tokeny (oddzielone spacjami) tokens = text_before_cursor.split() prefix = tokens[-1] if tokens else "" if not prefix: self.hide_listbox() return # Pobieramy sugestie na podstawie prefixu self.suggestions = self.get_suggestions(prefix) if self.suggestions: self.show_listbox() else: self.hide_listbox() def on_return(self, event): if self.listbox and self.selection_index >= 0: selected_display = self.listbox.get(self.selection_index) # Pobieramy wartość do wstawienia z mapy (czyli bez liczby postów) suggestion = self.suggestion_map.get(selected_display, selected_display) tag = suggestion else: tag = self.get().strip() if tag and self.callback: self.callback(tag) self.delete(0, tk.END) self.hide_listbox() return "break" def get_suggestions(self, prefix): try: conn = self.tags_repo.get_conn() cursor = conn.cursor() query = """ SELECT name, category, post_count FROM tags WHERE name LIKE ? AND post_count >= 1 ORDER BY post_count DESC LIMIT 10 """ cursor.execute(query, (prefix + "%",)) results = cursor.fetchall() conn.close() # Mapowanie kategorii na prefiksy prefix_map = {1: "artist:", 3: "copyright:", 4: "character:", 5: "meta:"} suggestions = [] # Utwórz słownik mapujący tekst wyświetlany (z liczbą) na tekst do wstawienia (bez liczby) self.suggestion_map = {} for row in results: name, category, post_count = row tag_insert = prefix_map.get(category, "") + name display_text = f"{tag_insert} ({post_count})" suggestions.append(display_text) self.suggestion_map[display_text] = tag_insert return suggestions except Exception as e: print("Błąd przy pobieraniu sugestii:", e) return [] def show_listbox(self): if self.listbox_window: self.listbox_window.destroy() self.listbox_window = tk.Toplevel(self) self.listbox_window.wm_overrideredirect(True) self.listbox = tk.Listbox(self.listbox_window, height=6) self.listbox.bind("", self.on_listbox_click) self.listbox.bind("", self.on_listbox_motion) for suggestion in self.suggestions: self.listbox.insert(tk.END, suggestion) self.listbox.pack(fill=tk.BOTH, expand=True) # Pobierz czcionkę używaną w listboxie list_font = font.Font(font=self.listbox.cget("font")) # Oblicz maksymalną szerokość na podstawie najdłuższego elementu max_width = ( max(list_font.measure(item) for item in self.suggestions) + 20 ) # +20 dla marginesu # Ustaw szerokość okna na podstawie najszerszego elementu self.listbox_window.geometry(f"{max_width}x200") # 200 - wysokość okna # Umieszczamy okno poniżej pola autouzupełniania x = self.winfo_rootx() y = self.winfo_rooty() + self.winfo_height() self.listbox_window.geometry("+%d+%d" % (x, y)) self.listbox_window.deiconify() self.selection_index = -1 def hide_listbox(self): if self.listbox_window: self.listbox_window.destroy() self.listbox_window = None self.listbox = None self.selection_index = -1 def on_listbox_click(self, event): if self.listbox: index = self.listbox.curselection() if index: value = self.listbox.get(index) self.delete(0, tk.END) self.insert(tk.END, value) self.hide_listbox() return "break" def on_listbox_motion(self, event): if self.listbox: self.listbox.selection_clear(0, tk.END) index = self.listbox.nearest(event.y) self.listbox.selection_set(first=index) self.selection_index = index def on_down(self, event): if self.listbox: self.selection_index = (self.selection_index + 1) % self.listbox.size() self.listbox.selection_clear(0, tk.END) self.listbox.selection_set(self.selection_index) self.listbox.activate(self.selection_index) return "break" def on_up(self, event): if self.listbox: self.selection_index = (self.selection_index - 1) % self.listbox.size() self.listbox.selection_clear(0, tk.END) self.listbox.selection_set(self.selection_index) self.listbox.activate(self.selection_index) return "break" class TagManager(tk.Frame): """ This widget holds a tag input entry (with autocompletion) and a display area that shows the entered tags. In the display area, left-clicking on a tag removes it, and right-clicking opens its wiki URL. Tag appearance is adjusted (color/underline) based on custom logic. """ def __init__(self, master, settings: Settings, tags_repo: TagsRepo, *args, **kwargs): super().__init__(master, *args, **kwargs) self.tags_repo = tags_repo self.settings = settings self.manual_tags = [] # List to hold manually entered tags # Entry for new tags (with autocompletion) self.entry = AutocompleteEntry(self, callback=self.add_tag, tags_repo=self.tags_repo) self.entry.pack(fill=tk.X, padx=5, pady=5) # Text widget for displaying already entered tags self.tags_display = tk.Text(self, wrap="word", height=4) self.tags_display.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) self.tags_display.config(state="disabled") # (Optional: add a scrollbar if needed) def add_tag(self, tag): """Add a new tag if it is not already present.""" if tag and tag not in self.manual_tags: self.manual_tags.append(tag) self.update_tags_display() def update_tags_display(self): """Refresh the text widget to display all manual tags with styling and event bindings.""" self.tags_display.config(state="normal") self.tags_display.delete("1.0", tk.END) for tag in self.manual_tags: # Process tag to decide its style _, deprecated = process_tag(tag, self.tags_repo) if deprecated is True: color = "red" underline = 1 elif deprecated is None: color = "darkorange" underline = 1 else: color = "blue" underline = 0 start_index = self.tags_display.index(tk.INSERT) self.tags_display.insert(tk.INSERT, tag) end_index = self.tags_display.index(tk.INSERT) tag_name = "manual_" + tag self.tags_display.tag_add(tag_name, start_index, end_index) self.tags_display.tag_configure( tag_name, foreground=color, underline=underline ) # Left-click: remove tag; Right-click: open wiki URL self.tags_display.tag_bind(tag_name, "", self.remove_tag) self.tags_display.tag_bind(tag_name, "", self.open_tag_wiki_url) self.tags_display.insert(tk.INSERT, " ") self.tags_display.config(state="disabled") def remove_tag(self, event): """Remove the clicked tag from the list and update the display.""" index = self.tags_display.index("@%d,%d" % (event.x, event.y)) for t in self.tags_display.tag_names(index): if t.startswith("manual_"): tag = t[len("manual_") :] if tag in self.manual_tags: self.manual_tags.remove(tag) self.update_tags_display() break def open_tag_wiki_url(self, event): """Open a wiki URL for the clicked tag.""" index = self.tags_display.index("@%d,%d" % (event.x, event.y)) for t in self.tags_display.tag_names(index): if t.startswith("manual_"): tag = t[len("manual_") :] open_tag_wiki_url(tag, self.settings) break