Compare commits

...

3 Commits

Author SHA1 Message Date
lm 717ac72f36 Tweak CS2 fetch defaults
Use a downward arrow icon for Fetch Images and default downloads to the project's images directory, updating subtool buttons and docs accordingly.
2025-10-18 14:53:55 +02:00
lm 5bfdd83e90 Restyle CS2 tool buttons
Replace ttk actions with custom rounded buttons matching the main toolbar and adjust the window layout accordingly.
2025-10-18 14:52:39 +02:00
lm 07983f292d Polish CS2 fetcher UI
Rename the toolbar action to “Fetch Images”, give the subtool a custom title bar matching the main window, and refresh docs/translations.
2025-10-18 14:49:20 +02:00
5 changed files with 293 additions and 34 deletions

View File

@ -39,7 +39,7 @@ On macOS/Linux activate with `source .venv/bin/activate` instead.
3. Finetune 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

View File

@ -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]] = []

View File

@ -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"

View File

@ -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"

View File

@ -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