actually works
Some checks failed
Gitea/kapitanbooru-uploader/pipeline/head There was a failure building this commit
Some checks failed
Gitea/kapitanbooru-uploader/pipeline/head There was a failure building this commit
Now with tagger Miejsce na zdjęcie Linki do wiki Zapis ustawień Tagger działa w tle Kolorujemy pliki po ratingu Tagger cache Tagi w bazie Pobranie implikacji tagów Autocomplete Podział na pliki i skrypty + nowe API Structure for package Version 0.1.0
This commit is contained in:
252
kapitanbooru_uploader/autocomplete.py
Normal file
252
kapitanbooru_uploader/autocomplete.py
Normal file
@@ -0,0 +1,252 @@
|
||||
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("<KeyRelease>", self.on_keyrelease)
|
||||
self.bind("<Down>", self.on_down)
|
||||
self.bind("<Up>", self.on_up)
|
||||
self.bind("<Return>", self.on_return)
|
||||
self.bind("<Tab>", self.on_return)
|
||||
self.bind("<FocusOut>", 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("<Button-1>", self.on_listbox_click)
|
||||
self.listbox.bind("<Motion>", 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, "<Button-1>", self.remove_tag)
|
||||
self.tags_display.tag_bind(tag_name, "<Button-3>", 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
|
Reference in New Issue
Block a user