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. 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). 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. 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. 7. Save an overlay (`💾`) when ready.
## Project Layout ## Project Layout

View File

@ -24,9 +24,9 @@ class UIBuilderMixin:
("💾", self._t("toolbar.save_overlay"), self.save_overlay), ("💾", self._t("toolbar.save_overlay"), self.save_overlay),
("", self._t("toolbar.toggle_free_draw"), self.toggle_exclusion_mode), ("", self._t("toolbar.toggle_free_draw"), self.toggle_exclusion_mode),
("🧹", self._t("toolbar.clear_excludes"), self.clear_excludes), ("🧹", 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.undo_exclude"), self.undo_exclude),
("🔄", self._t("toolbar.reset_sliders"), self.reset_sliders), ("🔄", 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._t("toolbar.toggle_theme"), self.toggle_theme),
] ]
self._toolbar_buttons: list[dict[str, object]] = [] self._toolbar_buttons: list[dict[str, object]] = []

View File

@ -7,7 +7,7 @@
"toolbar.save_overlay" = "Overlay speichern" "toolbar.save_overlay" = "Overlay speichern"
"toolbar.clear_excludes" = "Ausschlüsse löschen" "toolbar.clear_excludes" = "Ausschlüsse löschen"
"toolbar.toggle_free_draw" = "Freihandmodus umschalten" "toolbar.toggle_free_draw" = "Freihandmodus umschalten"
"toolbar.cs2_tool" = "CS2 Muster laden" "toolbar.cs2_tool" = "Bilder abrufen"
"toolbar.undo_exclude" = "Letzten Ausschluss entfernen" "toolbar.undo_exclude" = "Letzten Ausschluss entfernen"
"toolbar.reset_sliders" = "Slider zurücksetzen" "toolbar.reset_sliders" = "Slider zurücksetzen"
"toolbar.toggle_theme" = "Theme umschalten" "toolbar.toggle_theme" = "Theme umschalten"

View File

@ -7,7 +7,7 @@
"toolbar.save_overlay" = "Save overlay" "toolbar.save_overlay" = "Save overlay"
"toolbar.clear_excludes" = "Clear exclusions" "toolbar.clear_excludes" = "Clear exclusions"
"toolbar.toggle_free_draw" = "Toggle free-draw" "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.undo_exclude" = "Undo last exclusion"
"toolbar.reset_sliders" = "Reset sliders" "toolbar.reset_sliders" = "Reset sliders"
"toolbar.toggle_theme" = "Toggle theme" "toolbar.toggle_theme" = "Toggle theme"

View File

@ -4,15 +4,20 @@ from __future__ import annotations
import json import json
import threading import threading
from importlib import resources
from pathlib import Path from pathlib import Path
from typing import Any, Iterable, Optional from typing import Any, Iterable, Optional
import tkinter as tk import tkinter as tk
import tkinter.font as tkfont
from tkinter import filedialog, messagebox, ttk from tkinter import filedialog, messagebox, ttk
import requests import requests
from PIL import Image, ImageTk
from urllib.parse import urlparse from urllib.parse import urlparse
from app.logic import IMAGES_DIR
class CS2PatternFetcher: class CS2PatternFetcher:
"""Fetch CS2 skin metadata and download pattern images.""" """Fetch CS2 skin metadata and download pattern images."""
@ -175,17 +180,20 @@ class CS2PatternTool(tk.Toplevel):
super().__init__(app.root) super().__init__(app.root)
self.app = app self.app = app
self.fetcher = CS2PatternFetcher() self.fetcher = CS2PatternFetcher()
self.theme = getattr(self.app, "theme", "light")
self.title(self._t("cs2.title")) self.title(self._t("cs2.title"))
self.geometry("520x320") self.geometry("560x380")
self.minsize(480, 300) self.minsize(520, 320)
self.configure(bg=self._background_colour())
self.resizable(True, True) 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.weapons_var = tk.StringVar()
self.patterns_var = tk.StringVar() self.patterns_var = tk.StringVar()
self.directory_var = tk.StringVar( self.directory_var = tk.StringVar(value=str(IMAGES_DIR.resolve()))
value=str((Path.cwd() / "images" / "cs2").resolve())
)
self.status_var = tk.StringVar(value=self._t("cs2.status_loading")) self.status_var = tk.StringVar(value=self._t("cs2.status_loading"))
self._init_widgets() self._init_widgets()
@ -194,12 +202,17 @@ class CS2PatternTool(tk.Toplevel):
self._start_loading() self._start_loading()
self.protocol("WM_DELETE_WINDOW", self._on_close) self.protocol("WM_DELETE_WINDOW", self._on_close)
self._bring_to_front()
# UI construction -------------------------------------------------- # UI construction --------------------------------------------------
def _init_widgets(self) -> None: def _init_widgets(self) -> None:
frame = ttk.Frame(self) frame = self.body
frame.pack(fill=tk.BOTH, expand=True, padx=16, pady=16)
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 = ttk.Frame(frame)
top.pack(fill=tk.X, pady=(0, 12)) 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 = ttk.Entry(dir_frame, textvariable=self.directory_var)
entry.grid(row=1, column=0, sticky="we", padx=(0, 8)) 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) 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( status_label = ttk.Label(
frame, textvariable=self.status_var, anchor="w", justify="left" frame, textvariable=self.status_var, anchor="w", justify="left"
) )
status_label.pack(fill=tk.X) 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 ----------------------------------------------------- # Data loading -----------------------------------------------------
@ -311,10 +381,8 @@ class CS2PatternTool(tk.Toplevel):
self.pattern_combo.configure(state="readonly", values=patterns) self.pattern_combo.configure(state="readonly", values=patterns)
if patterns: if patterns:
self.pattern_combo.set(patterns[0]) self.pattern_combo.set(patterns[0])
self.download_btn.configure(state="normal")
else: else:
self.pattern_combo.set("") self.pattern_combo.set("")
self.download_btn.configure(state="disabled")
# Event handlers --------------------------------------------------- # Event handlers ---------------------------------------------------
@ -324,8 +392,7 @@ class CS2PatternTool(tk.Toplevel):
self._populate_patterns(weapon) self._populate_patterns(weapon)
def _on_pattern_selected(self, event=None) -> None: # noqa: ANN001 def _on_pattern_selected(self, event=None) -> None: # noqa: ANN001
if self.patterns_var.get(): return
self.download_btn.configure(state="normal")
def _browse_directory(self) -> None: def _browse_directory(self) -> None:
directory = filedialog.askdirectory(parent=self, mustexist=True) directory = filedialog.askdirectory(parent=self, mustexist=True)
@ -376,8 +443,201 @@ class CS2PatternTool(tk.Toplevel):
parent=self, 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: 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: def _t(self, key: str) -> str:
translator = getattr(self.app, "translator", None) translator = getattr(self.app, "translator", None)
@ -407,4 +667,3 @@ def open_cs2_pattern_tool(app) -> CS2PatternTool:
window = CS2PatternTool(app) window = CS2PatternTool(app)
app._cs2_tool_window = window # type: ignore[attr-defined] app._cs2_tool_window = window # type: ignore[attr-defined]
return window return window