Compare commits
3 Commits
ff66aeb3c3
...
717ac72f36
| Author | SHA1 | Date |
|---|---|---|
|
|
717ac72f36 | |
|
|
5bfdd83e90 | |
|
|
07983f292d |
|
|
@ -39,7 +39,7 @@ On macOS/Linux activate with `source .venv/bin/activate` instead.
|
|||
3. Fine‑tune sliders; watch the overlay update on the right.
|
||||
4. Toggle freehand mode (`△`) or stick with rectangles and mark areas to exclude (right mouse drag).
|
||||
5. Move through folder images with `⬅️` / `➡️`; exclusions stay put unless you opt into automatic resets.
|
||||
6. Open the CS2 pattern tool (`🎯`) to pull skin artwork when you need visual references.
|
||||
6. Fetch CS2 pattern images (`⭳`) whenever you need additional references.
|
||||
7. Save an overlay (`💾`) when ready.
|
||||
|
||||
## Project Layout
|
||||
|
|
|
|||
|
|
@ -24,9 +24,9 @@ class UIBuilderMixin:
|
|||
("💾", self._t("toolbar.save_overlay"), self.save_overlay),
|
||||
("△", self._t("toolbar.toggle_free_draw"), self.toggle_exclusion_mode),
|
||||
("🧹", self._t("toolbar.clear_excludes"), self.clear_excludes),
|
||||
("🎯", self._t("toolbar.cs2_tool"), self.open_cs2_pattern_tool),
|
||||
("↩", self._t("toolbar.undo_exclude"), self.undo_exclude),
|
||||
("🔄", self._t("toolbar.reset_sliders"), self.reset_sliders),
|
||||
("⭳", self._t("toolbar.cs2_tool"), self.open_cs2_pattern_tool),
|
||||
("🌓", self._t("toolbar.toggle_theme"), self.toggle_theme),
|
||||
]
|
||||
self._toolbar_buttons: list[dict[str, object]] = []
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"toolbar.save_overlay" = "Overlay speichern"
|
||||
"toolbar.clear_excludes" = "Ausschlüsse löschen"
|
||||
"toolbar.toggle_free_draw" = "Freihandmodus umschalten"
|
||||
"toolbar.cs2_tool" = "CS2 Muster laden"
|
||||
"toolbar.cs2_tool" = "Bilder abrufen"
|
||||
"toolbar.undo_exclude" = "Letzten Ausschluss entfernen"
|
||||
"toolbar.reset_sliders" = "Slider zurücksetzen"
|
||||
"toolbar.toggle_theme" = "Theme umschalten"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
"toolbar.save_overlay" = "Save overlay"
|
||||
"toolbar.clear_excludes" = "Clear exclusions"
|
||||
"toolbar.toggle_free_draw" = "Toggle free-draw"
|
||||
"toolbar.cs2_tool" = "CS2 pattern fetcher"
|
||||
"toolbar.cs2_tool" = "Fetch Images"
|
||||
"toolbar.undo_exclude" = "Undo last exclusion"
|
||||
"toolbar.reset_sliders" = "Reset sliders"
|
||||
"toolbar.toggle_theme" = "Toggle theme"
|
||||
|
|
|
|||
|
|
@ -4,15 +4,20 @@ from __future__ import annotations
|
|||
|
||||
import json
|
||||
import threading
|
||||
from importlib import resources
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Optional
|
||||
|
||||
import tkinter as tk
|
||||
import tkinter.font as tkfont
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
|
||||
import requests
|
||||
from PIL import Image, ImageTk
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from app.logic import IMAGES_DIR
|
||||
|
||||
|
||||
class CS2PatternFetcher:
|
||||
"""Fetch CS2 skin metadata and download pattern images."""
|
||||
|
|
@ -175,17 +180,20 @@ class CS2PatternTool(tk.Toplevel):
|
|||
super().__init__(app.root)
|
||||
self.app = app
|
||||
self.fetcher = CS2PatternFetcher()
|
||||
self.theme = getattr(self.app, "theme", "light")
|
||||
self.title(self._t("cs2.title"))
|
||||
self.geometry("520x320")
|
||||
self.minsize(480, 300)
|
||||
self.configure(bg=self._background_colour())
|
||||
self.geometry("560x380")
|
||||
self.minsize(520, 320)
|
||||
self.resizable(True, True)
|
||||
self._drag_offset: tuple[int, int] | None = None
|
||||
self._setup_window()
|
||||
self.body = ttk.Frame(self)
|
||||
self.body.pack(fill=tk.BOTH, expand=True, padx=16, pady=(12, 16))
|
||||
self._toolbar_buttons: list[dict[str, Any]] = []
|
||||
|
||||
self.weapons_var = tk.StringVar()
|
||||
self.patterns_var = tk.StringVar()
|
||||
self.directory_var = tk.StringVar(
|
||||
value=str((Path.cwd() / "images" / "cs2").resolve())
|
||||
)
|
||||
self.directory_var = tk.StringVar(value=str(IMAGES_DIR.resolve()))
|
||||
self.status_var = tk.StringVar(value=self._t("cs2.status_loading"))
|
||||
|
||||
self._init_widgets()
|
||||
|
|
@ -194,12 +202,17 @@ class CS2PatternTool(tk.Toplevel):
|
|||
self._start_loading()
|
||||
|
||||
self.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
self._bring_to_front()
|
||||
|
||||
# UI construction --------------------------------------------------
|
||||
|
||||
def _init_widgets(self) -> None:
|
||||
frame = ttk.Frame(self)
|
||||
frame.pack(fill=tk.BOTH, expand=True, padx=16, pady=16)
|
||||
frame = self.body
|
||||
|
||||
toolbar = ttk.Frame(frame)
|
||||
toolbar.pack(fill=tk.X, pady=(0, 16))
|
||||
for icon, label, command in self._toolbar_button_defs():
|
||||
self._add_toolbar_button(toolbar, icon, label, command)
|
||||
|
||||
top = ttk.Frame(frame)
|
||||
top.pack(fill=tk.X, pady=(0, 12))
|
||||
|
|
@ -228,28 +241,85 @@ class CS2PatternTool(tk.Toplevel):
|
|||
)
|
||||
entry = ttk.Entry(dir_frame, textvariable=self.directory_var)
|
||||
entry.grid(row=1, column=0, sticky="we", padx=(0, 8))
|
||||
ttk.Button(
|
||||
dir_frame, text=self._t("cs2.browse_button"), command=self._browse_directory
|
||||
).grid(row=1, column=1, sticky="e")
|
||||
dir_frame.columnconfigure(0, weight=1)
|
||||
|
||||
buttons = ttk.Frame(frame)
|
||||
buttons.pack(fill=tk.X, pady=(0, 12))
|
||||
ttk.Button(
|
||||
buttons, text=self._t("cs2.refresh_button"), command=self._refresh_data
|
||||
).pack(side=tk.LEFT)
|
||||
self.download_btn = ttk.Button(
|
||||
buttons,
|
||||
text=self._t("cs2.download_button"),
|
||||
command=self._download_selected,
|
||||
state="disabled",
|
||||
)
|
||||
self.download_btn.pack(side=tk.RIGHT)
|
||||
|
||||
status_label = ttk.Label(
|
||||
frame, textvariable=self.status_var, anchor="w", justify="left"
|
||||
)
|
||||
status_label.pack(fill=tk.X)
|
||||
self._refresh_toolbar_buttons_theme()
|
||||
|
||||
def _setup_window(self) -> None:
|
||||
self.overrideredirect(True)
|
||||
self.configure(bg=self._background_colour())
|
||||
self._create_titlebar()
|
||||
|
||||
def _create_titlebar(self) -> None:
|
||||
bar_bg = "#1f1f1f"
|
||||
title_bar = tk.Frame(self, bg=bar_bg, relief="flat", height=34)
|
||||
title_bar.pack(fill=tk.X, side=tk.TOP)
|
||||
title_bar.pack_propagate(False)
|
||||
|
||||
logo = None
|
||||
try:
|
||||
logo_resource = resources.files("app.assets").joinpath("logo.png")
|
||||
with resources.as_file(logo_resource) as logo_path:
|
||||
image = Image.open(logo_path).convert("RGBA")
|
||||
image.thumbnail((26, 26))
|
||||
logo = ImageTk.PhotoImage(image)
|
||||
except Exception: # noqa: BLE001
|
||||
logo = None
|
||||
|
||||
if logo is not None:
|
||||
logo_label = tk.Label(title_bar, image=logo, bg=bar_bg)
|
||||
logo_label.image = logo
|
||||
logo_label.pack(side=tk.LEFT, padx=(10, 6), pady=4)
|
||||
else:
|
||||
logo_label = None
|
||||
|
||||
title_label = tk.Label(
|
||||
title_bar,
|
||||
text=self._t("cs2.title"),
|
||||
bg=bar_bg,
|
||||
fg="#f5f5f5",
|
||||
font=("Segoe UI", 11, "bold"),
|
||||
anchor="w",
|
||||
)
|
||||
title_label.pack(side=tk.LEFT, padx=6)
|
||||
|
||||
close_btn = tk.Button(
|
||||
title_bar,
|
||||
text="✕",
|
||||
command=self._on_close,
|
||||
bg=bar_bg,
|
||||
fg="#f5f5f5",
|
||||
activebackground="#ff3b30",
|
||||
activeforeground="#ffffff",
|
||||
borderwidth=0,
|
||||
highlightthickness=0,
|
||||
relief="flat",
|
||||
font=("Segoe UI", 10, "bold"),
|
||||
cursor="hand2",
|
||||
width=3,
|
||||
)
|
||||
close_btn.pack(side=tk.RIGHT, padx=8, pady=4)
|
||||
close_btn.bind("<Enter>", lambda _e: close_btn.configure(bg="#cf212f"))
|
||||
close_btn.bind("<Leave>", lambda _e: close_btn.configure(bg=bar_bg))
|
||||
|
||||
bind_targets = [title_bar, title_label]
|
||||
if logo_label is not None:
|
||||
bind_targets.append(logo_label)
|
||||
for widget in bind_targets:
|
||||
widget.bind("<ButtonPress-1>", self._start_window_drag)
|
||||
widget.bind("<B1-Motion>", self._perform_window_drag)
|
||||
|
||||
def _bring_to_front(self) -> None:
|
||||
try:
|
||||
self.transient(self.app.root)
|
||||
self.lift()
|
||||
self.focus_force()
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
# Data loading -----------------------------------------------------
|
||||
|
||||
|
|
@ -311,10 +381,8 @@ class CS2PatternTool(tk.Toplevel):
|
|||
self.pattern_combo.configure(state="readonly", values=patterns)
|
||||
if patterns:
|
||||
self.pattern_combo.set(patterns[0])
|
||||
self.download_btn.configure(state="normal")
|
||||
else:
|
||||
self.pattern_combo.set("")
|
||||
self.download_btn.configure(state="disabled")
|
||||
|
||||
# Event handlers ---------------------------------------------------
|
||||
|
||||
|
|
@ -324,8 +392,7 @@ class CS2PatternTool(tk.Toplevel):
|
|||
self._populate_patterns(weapon)
|
||||
|
||||
def _on_pattern_selected(self, event=None) -> None: # noqa: ANN001
|
||||
if self.patterns_var.get():
|
||||
self.download_btn.configure(state="normal")
|
||||
return
|
||||
|
||||
def _browse_directory(self) -> None:
|
||||
directory = filedialog.askdirectory(parent=self, mustexist=True)
|
||||
|
|
@ -376,8 +443,201 @@ class CS2PatternTool(tk.Toplevel):
|
|||
parent=self,
|
||||
)
|
||||
|
||||
def _start_window_drag(self, event) -> None: # noqa: ANN001
|
||||
self._drag_offset = (
|
||||
event.x_root - self.winfo_rootx(),
|
||||
event.y_root - self.winfo_rooty(),
|
||||
)
|
||||
|
||||
def _perform_window_drag(self, event) -> None: # noqa: ANN001
|
||||
offset = getattr(self, "_drag_offset", None)
|
||||
if offset is None:
|
||||
return
|
||||
x = event.x_root - offset[0]
|
||||
y = event.y_root - offset[1]
|
||||
self.geometry(f"+{x}+{y}")
|
||||
|
||||
def _toolbar_button_defs(self) -> list[tuple[str, str, Any]]:
|
||||
return [
|
||||
("🔄", self._t("cs2.refresh_button"), self._refresh_data),
|
||||
("⭳", self._t("cs2.download_button"), self._download_selected),
|
||||
("📁", self._t("cs2.browse_button"), self._browse_directory),
|
||||
]
|
||||
|
||||
def _toolbar_palette(self) -> dict[str, str]:
|
||||
if getattr(self, "theme", "light") == "dark":
|
||||
return {
|
||||
"normal": "#2f2f35",
|
||||
"hover": "#3a3a40",
|
||||
"active": "#1f1f25",
|
||||
"outline": "#4d4d50",
|
||||
"outline_focus": "#7c7c88",
|
||||
"text": "#f1f1f5",
|
||||
}
|
||||
return {
|
||||
"normal": "#ffffff",
|
||||
"hover": "#ededf4",
|
||||
"active": "#dcdce6",
|
||||
"outline": "#d0d0d8",
|
||||
"outline_focus": "#a9a9b2",
|
||||
"text": "#1f1f1f",
|
||||
}
|
||||
|
||||
def _add_toolbar_button(self, parent, icon: str, label: str, command) -> None:
|
||||
font = tkfont.Font(root=self, family="Segoe UI", size=9)
|
||||
padding_x = 12
|
||||
gap = font.measure(" ")
|
||||
icon_width = font.measure(icon) or font.measure(" ")
|
||||
label_width = font.measure(label)
|
||||
width = padding_x * 2 + icon_width + gap + label_width
|
||||
height = 28
|
||||
radius = 9
|
||||
bg = self._background_colour()
|
||||
canvas = tk.Canvas(
|
||||
parent,
|
||||
width=width,
|
||||
height=height,
|
||||
bd=0,
|
||||
highlightthickness=0,
|
||||
bg=bg,
|
||||
relief="flat",
|
||||
cursor="hand2",
|
||||
takefocus=1,
|
||||
)
|
||||
canvas.pack(side=tk.LEFT, padx=4, pady=1)
|
||||
|
||||
palette = self._toolbar_palette()
|
||||
rect_id = self._create_round_rect(
|
||||
canvas,
|
||||
1,
|
||||
1,
|
||||
width - 1,
|
||||
height - 1,
|
||||
radius,
|
||||
fill=palette["normal"],
|
||||
outline=palette["outline"],
|
||||
width=1,
|
||||
)
|
||||
icon_id = canvas.create_text(
|
||||
padding_x,
|
||||
height / 2,
|
||||
text=icon,
|
||||
font=font,
|
||||
fill=palette["text"],
|
||||
anchor="w",
|
||||
)
|
||||
label_id = canvas.create_text(
|
||||
padding_x + icon_width + gap,
|
||||
height / 2,
|
||||
text=label,
|
||||
font=font,
|
||||
fill=palette["text"],
|
||||
anchor="w",
|
||||
)
|
||||
|
||||
button_data = {
|
||||
"canvas": canvas,
|
||||
"rect": rect_id,
|
||||
"text_ids": (icon_id, label_id),
|
||||
"command": command,
|
||||
"palette": palette.copy(),
|
||||
}
|
||||
self._toolbar_buttons.append(button_data)
|
||||
|
||||
def set_fill(state: str) -> None:
|
||||
pal = button_data["palette"]
|
||||
canvas.itemconfigure(rect_id, fill=pal[state])
|
||||
|
||||
def execute():
|
||||
command()
|
||||
|
||||
def on_press(_event=None):
|
||||
set_fill("active")
|
||||
|
||||
def on_release(event=None):
|
||||
if event is not None and (
|
||||
event.x < 0 or event.y < 0 or event.x > width or event.y > height
|
||||
):
|
||||
set_fill("normal")
|
||||
return
|
||||
set_fill("hover")
|
||||
canvas.after_idle(execute)
|
||||
|
||||
def on_enter(_event):
|
||||
set_fill("hover")
|
||||
|
||||
def on_leave(_event):
|
||||
set_fill("normal")
|
||||
|
||||
def on_focus_in(_event):
|
||||
pal = button_data["palette"]
|
||||
canvas.itemconfigure(rect_id, outline=pal["outline_focus"])
|
||||
|
||||
def on_focus_out(_event):
|
||||
pal = button_data["palette"]
|
||||
canvas.itemconfigure(rect_id, outline=pal["outline"])
|
||||
|
||||
def invoke_keyboard(_event=None):
|
||||
set_fill("active")
|
||||
canvas.after(120, lambda: set_fill("hover"))
|
||||
canvas.after_idle(execute)
|
||||
|
||||
canvas.bind("<ButtonPress-1>", on_press)
|
||||
canvas.bind("<ButtonRelease-1>", on_release)
|
||||
canvas.bind("<Enter>", on_enter)
|
||||
canvas.bind("<Leave>", on_leave)
|
||||
canvas.bind("<FocusIn>", on_focus_in)
|
||||
canvas.bind("<FocusOut>", on_focus_out)
|
||||
canvas.bind("<space>", invoke_keyboard)
|
||||
canvas.bind("<Return>", invoke_keyboard)
|
||||
|
||||
def _refresh_toolbar_buttons_theme(self) -> None:
|
||||
if not self._toolbar_buttons:
|
||||
return
|
||||
palette = self._toolbar_palette()
|
||||
bg = self._background_colour()
|
||||
for data in self._toolbar_buttons:
|
||||
canvas = data["canvas"]
|
||||
rect = data["rect"]
|
||||
text_ids = data["text_ids"]
|
||||
data["palette"] = palette.copy()
|
||||
canvas.configure(bg=bg)
|
||||
canvas.itemconfigure(rect, fill=palette["normal"], outline=palette["outline"])
|
||||
for text_id in text_ids:
|
||||
canvas.itemconfigure(text_id, fill=palette["text"])
|
||||
|
||||
@staticmethod
|
||||
def _create_round_rect(canvas: tk.Canvas, x1, y1, x2, y2, radius, **kwargs):
|
||||
points = [
|
||||
x1 + radius,
|
||||
y1,
|
||||
x2 - radius,
|
||||
y1,
|
||||
x2,
|
||||
y1,
|
||||
x2,
|
||||
y1 + radius,
|
||||
x2,
|
||||
y2 - radius,
|
||||
x2,
|
||||
y2,
|
||||
x2 - radius,
|
||||
y2,
|
||||
x1 + radius,
|
||||
y2,
|
||||
x1,
|
||||
y2,
|
||||
x1,
|
||||
y2 - radius,
|
||||
x1,
|
||||
y1 + radius,
|
||||
x1,
|
||||
y1,
|
||||
]
|
||||
return canvas.create_polygon(points, smooth=True, splinesteps=24, **kwargs)
|
||||
|
||||
def _background_colour(self) -> str:
|
||||
return "#0f0f10" if getattr(self.app, "theme", "light") == "dark" else "#ffffff"
|
||||
return "#0f0f10" if getattr(self, "theme", "light") == "dark" else "#ffffff"
|
||||
|
||||
def _t(self, key: str) -> str:
|
||||
translator = getattr(self.app, "translator", None)
|
||||
|
|
@ -407,4 +667,3 @@ def open_cs2_pattern_tool(app) -> CS2PatternTool:
|
|||
window = CS2PatternTool(app)
|
||||
app._cs2_tool_window = window # type: ignore[attr-defined]
|
||||
return window
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue