Restyle label designer with ttkbootstrap (flatly/darkly themes)
Modern flat widgets, coloured primary/danger/success buttons, themed canvas/text backgrounds, and a light/dark toggle remembered in settings.
This commit is contained in:
+109
-45
@@ -10,10 +10,12 @@ import threading
|
||||
import tkinter as tk
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
from tkinter import filedialog, messagebox, ttk
|
||||
from tkinter import filedialog
|
||||
|
||||
import emoji_data_python
|
||||
import ttkbootstrap as ttk
|
||||
from PIL import Image, ImageDraw, ImageFont, ImageOps, ImageTk
|
||||
from ttkbootstrap.dialogs import Messagebox
|
||||
|
||||
from niimprint import (
|
||||
MODELS,
|
||||
@@ -37,6 +39,8 @@ EMOJI_FONT_SIZE = 109 # Noto Color Emoji is a bitmap font; only this size loads
|
||||
EMOJI_GAMMA = 3.0 # >1 darkens emoji so they survive 1-bit dithering
|
||||
EMOJI_SCALE = 1.15 # emoji height relative to text size
|
||||
PICKER_PX = 26 # thumbnail size in the emoji picker
|
||||
THEMES = {"light": "flatly", "dark": "darkly"}
|
||||
UI_FONT = ("DejaVu Sans", 10)
|
||||
|
||||
# (name, width mm, length mm) — width is the short side (tape width)
|
||||
LABEL_PRESETS = [
|
||||
@@ -271,12 +275,16 @@ class Design:
|
||||
# --------------------------------------------------------------------------- app
|
||||
|
||||
|
||||
class LabelApp(tk.Tk):
|
||||
class LabelApp(ttk.Window):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.title("Niimbot Label Designer")
|
||||
self.minsize(1100, 640)
|
||||
self.settings = self._load_settings()
|
||||
self.theme_mode = self.settings.get("theme", "light")
|
||||
super().__init__(
|
||||
title="Niimbot Label Designer",
|
||||
themename=THEMES.get(self.theme_mode, "flatly"),
|
||||
minsize=(1100, 680),
|
||||
)
|
||||
self.option_add("*Font", UI_FONT)
|
||||
self.design = Design.from_dict(self.settings.get("design", {}))
|
||||
if not self.design.items and "text" in self.settings: # migrate old settings
|
||||
w, h = self.design.size_px
|
||||
@@ -312,6 +320,7 @@ class LabelApp(tk.Tk):
|
||||
self.p_y = tk.IntVar(value=0)
|
||||
|
||||
self._build_ui()
|
||||
self._apply_theme_colors()
|
||||
self.preset.trace_add("write", lambda *_: self._apply_preset())
|
||||
for var in (self.width_mm, self.length_mm):
|
||||
var.trace_add("write", lambda *_: self._size_changed())
|
||||
@@ -331,13 +340,13 @@ class LabelApp(tk.Tk):
|
||||
# ---- UI construction
|
||||
|
||||
def _build_ui(self):
|
||||
root = ttk.Frame(self, padding=8)
|
||||
root = ttk.Frame(self, padding=12)
|
||||
root.pack(fill="both", expand=True)
|
||||
root.columnconfigure(1, weight=1)
|
||||
root.rowconfigure(0, weight=1)
|
||||
|
||||
left = ttk.Frame(root)
|
||||
left.grid(row=0, column=0, sticky="nsw", padx=(0, 8))
|
||||
left.grid(row=0, column=0, sticky="nsw", padx=(0, 12))
|
||||
right = ttk.Frame(root)
|
||||
right.grid(row=0, column=1, sticky="nsew")
|
||||
right.columnconfigure(0, weight=1)
|
||||
@@ -352,7 +361,7 @@ class LabelApp(tk.Tk):
|
||||
self._build_actions(right)
|
||||
|
||||
def _build_size_box(self, parent):
|
||||
box = ttk.LabelFrame(parent, text="Label", padding=6)
|
||||
box = ttk.Labelframe(parent, text="Label size", padding=10)
|
||||
box.pack(fill="x")
|
||||
ttk.Combobox(
|
||||
box,
|
||||
@@ -373,16 +382,16 @@ class LabelApp(tk.Tk):
|
||||
self.length_spin.grid(row=0, column=5)
|
||||
|
||||
def _build_emoji_picker(self, parent):
|
||||
box = ttk.LabelFrame(parent, text="Emoji — click to add to label", padding=4)
|
||||
box.pack(fill="both", expand=True, pady=(8, 0))
|
||||
box = ttk.Labelframe(parent, text="Emoji — click to add", padding=8)
|
||||
box.pack(fill="both", expand=True, pady=(10, 0))
|
||||
top = ttk.Frame(box)
|
||||
top.pack(fill="x")
|
||||
ttk.Label(top, text="Search").pack(side="left")
|
||||
ttk.Entry(top, textvariable=self.search, width=24).pack(
|
||||
side="left", fill="x", expand=True, padx=(4, 0)
|
||||
)
|
||||
self.hover_name = ttk.Label(box, text=" ", foreground="#555")
|
||||
self.hover_name.pack(fill="x")
|
||||
self.hover_name = ttk.Label(box, text=" ", bootstyle="secondary")
|
||||
self.hover_name.pack(fill="x", pady=(4, 2))
|
||||
|
||||
self.nb = ttk.Notebook(box, width=420, height=260)
|
||||
self.nb.pack(fill="both", expand=True)
|
||||
@@ -461,8 +470,8 @@ class LabelApp(tk.Tk):
|
||||
self.nb.select(self.search_tab.master)
|
||||
|
||||
def _build_printer_box(self, parent):
|
||||
box = ttk.LabelFrame(parent, text="Printer", padding=6)
|
||||
box.pack(fill="x", pady=(8, 0))
|
||||
box = ttk.Labelframe(parent, text="Printer", padding=10)
|
||||
box.pack(fill="x", pady=(10, 0))
|
||||
ttk.Label(box, text="Model").grid(row=0, column=0, sticky="w")
|
||||
ttk.Combobox(
|
||||
box, textvariable=self.model, values=list(MODELS), state="readonly", width=6
|
||||
@@ -492,15 +501,17 @@ class LabelApp(tk.Tk):
|
||||
bar = ttk.Frame(parent)
|
||||
bar.grid(row=0, column=0, sticky="we")
|
||||
|
||||
def btn(text, cmd, **kw):
|
||||
ttk.Button(bar, text=text, command=cmd, **kw).pack(side="left", padx=(0, 4))
|
||||
def btn(text, cmd, style="secondary-outline", **kw):
|
||||
ttk.Button(bar, text=text, command=cmd, bootstyle=style, **kw).pack(
|
||||
side="left", padx=(0, 4)
|
||||
)
|
||||
|
||||
def sep():
|
||||
ttk.Separator(bar, orient="vertical").pack(side="left", fill="y", padx=6)
|
||||
ttk.Separator(bar, orient="vertical").pack(side="left", fill="y", padx=8)
|
||||
|
||||
btn("+ Text", lambda: self.add_text("Text"))
|
||||
btn("+ Text", lambda: self.add_text("Text"), "primary")
|
||||
btn("Duplicate", self.duplicate_selected)
|
||||
btn("Delete", self.delete_selected)
|
||||
btn("Delete", self.delete_selected, "danger-outline")
|
||||
sep()
|
||||
btn("⟲ 90°", lambda: self.rotate(-90), width=6)
|
||||
btn("⟳ 90°", lambda: self.rotate(90), width=6)
|
||||
@@ -509,19 +520,27 @@ class LabelApp(tk.Tk):
|
||||
btn("Center V", lambda: self.center("v"))
|
||||
btn("Fit", self.fit_selected)
|
||||
sep()
|
||||
btn("Open…", self.open_design)
|
||||
btn("Save…", self.save_design)
|
||||
btn("New", self.new_design)
|
||||
btn("New", self.new_design, "link")
|
||||
btn("Open…", self.open_design, "link")
|
||||
btn("Save…", self.save_design, "link")
|
||||
self.theme_btn = ttk.Button(
|
||||
bar,
|
||||
text="☾",
|
||||
width=3,
|
||||
bootstyle="secondary-link",
|
||||
command=self.toggle_theme,
|
||||
)
|
||||
self.theme_btn.pack(side="right")
|
||||
|
||||
def _build_canvas(self, parent):
|
||||
ttk.Label(
|
||||
parent,
|
||||
text="Drag items to move · arrow keys nudge (Shift = 10px) · Delete removes",
|
||||
foreground="#555",
|
||||
).grid(row=1, column=0, sticky="w", pady=(6, 2))
|
||||
holder = tk.Frame(parent, bg="#777")
|
||||
holder.grid(row=2, column=0, sticky="nsew")
|
||||
self.canvas = tk.Canvas(holder, bg="#777", highlightthickness=0)
|
||||
text="Drag items to move · arrow keys nudge (Shift = 10 px) · Delete removes",
|
||||
bootstyle="secondary",
|
||||
).grid(row=1, column=0, sticky="w", pady=(10, 4))
|
||||
self.holder = tk.Frame(parent)
|
||||
self.holder.grid(row=2, column=0, sticky="nsew")
|
||||
self.canvas = tk.Canvas(self.holder, highlightthickness=0)
|
||||
self.canvas.place(relx=0.5, rely=0.5, anchor="center")
|
||||
self.canvas.bind("<ButtonPress-1>", self._canvas_press)
|
||||
self.canvas.bind("<B1-Motion>", self._canvas_drag)
|
||||
@@ -538,14 +557,22 @@ class LabelApp(tk.Tk):
|
||||
)
|
||||
self.canvas.bind("<Delete>", lambda e: self.delete_selected())
|
||||
self.canvas.bind("<BackSpace>", lambda e: self.delete_selected())
|
||||
self.size_label = ttk.Label(parent, text="")
|
||||
self.size_label.grid(row=3, column=0, sticky="w")
|
||||
self.size_label = ttk.Label(parent, text="", bootstyle="secondary")
|
||||
self.size_label.grid(row=3, column=0, sticky="w", pady=(4, 0))
|
||||
|
||||
def _build_props(self, parent):
|
||||
box = ttk.LabelFrame(parent, text="Selected item", padding=6)
|
||||
box.grid(row=4, column=0, sticky="we", pady=(6, 0))
|
||||
box = ttk.Labelframe(parent, text="Selected item", padding=10)
|
||||
box.grid(row=4, column=0, sticky="we", pady=(10, 0))
|
||||
box.columnconfigure(0, weight=1)
|
||||
self.prop_text = tk.Text(box, height=3, width=40, font=("DejaVu Sans", 12))
|
||||
self.prop_text = tk.Text(
|
||||
box,
|
||||
height=3,
|
||||
width=40,
|
||||
font=("DejaVu Sans", 12),
|
||||
relief="flat",
|
||||
padx=6,
|
||||
pady=4,
|
||||
)
|
||||
self.prop_text.grid(row=0, column=0, sticky="we")
|
||||
self.prop_text.bind("<<Modified>>", self._prop_text_modified)
|
||||
|
||||
@@ -579,13 +606,48 @@ class LabelApp(tk.Tk):
|
||||
|
||||
def _build_actions(self, parent):
|
||||
bar = ttk.Frame(parent)
|
||||
bar.grid(row=5, column=0, sticky="we", pady=(8, 0))
|
||||
self.print_btn = ttk.Button(bar, text="Print", command=self.print_label)
|
||||
self.print_btn.pack(side="left")
|
||||
ttk.Button(bar, text="Save PNG…", command=self.save_png).pack(
|
||||
side="left", padx=(6, 0)
|
||||
bar.grid(row=5, column=0, sticky="we", pady=(12, 0))
|
||||
self.print_btn = ttk.Button(
|
||||
bar,
|
||||
text="🖨 Print",
|
||||
command=self.print_label,
|
||||
bootstyle="success",
|
||||
width=12,
|
||||
)
|
||||
ttk.Label(bar, textvariable=self.status).pack(side="right")
|
||||
self.print_btn.pack(side="left", ipady=4)
|
||||
ttk.Button(
|
||||
bar, text="Save PNG…", command=self.save_png, bootstyle="secondary-outline"
|
||||
).pack(side="left", padx=(8, 0), ipady=4)
|
||||
ttk.Label(bar, textvariable=self.status, bootstyle="secondary").pack(
|
||||
side="right"
|
||||
)
|
||||
|
||||
# ---- theme
|
||||
|
||||
def toggle_theme(self):
|
||||
self.theme_mode = "dark" if self.theme_mode == "light" else "light"
|
||||
self.style.theme_use(THEMES[self.theme_mode])
|
||||
self._apply_theme_colors()
|
||||
self.render_canvas()
|
||||
|
||||
def _apply_theme_colors(self):
|
||||
"""Colour the plain-Tk widgets (Text, Canvas) to match the ttk theme."""
|
||||
c = self.style.colors
|
||||
dark = self.theme_mode == "dark"
|
||||
self.theme_btn.configure(text="☀" if dark else "☾")
|
||||
canvas_bg = c.dark if dark else c.light
|
||||
self.holder.configure(bg=canvas_bg)
|
||||
self.canvas.configure(bg=canvas_bg)
|
||||
text_opts = dict(
|
||||
bg=c.inputbg,
|
||||
fg=c.inputfg,
|
||||
insertbackground=c.inputfg,
|
||||
selectbackground=c.primary,
|
||||
)
|
||||
self.prop_text.configure(**text_opts)
|
||||
for txt in self.emoji_index:
|
||||
txt.configure(**text_opts)
|
||||
self._sel_color = c.info
|
||||
|
||||
# ---- label size
|
||||
|
||||
@@ -768,7 +830,7 @@ class LabelApp(tk.Tk):
|
||||
x0, y0, x1, y1 = self._boxes[self.design.items.index(self.selected)]
|
||||
self.canvas.create_rectangle(
|
||||
x0 * z - 2, y0 * z - 2, x1 * z + 2, y1 * z + 2,
|
||||
outline="#1e88e5", width=2, dash=(4, 3),
|
||||
outline=self._sel_color, width=2, dash=(4, 3),
|
||||
) # fmt: skip
|
||||
w, h = img.size
|
||||
model = MODELS[self.model.get()]
|
||||
@@ -783,8 +845,9 @@ class LabelApp(tk.Tk):
|
||||
# ---- designs
|
||||
|
||||
def new_design(self):
|
||||
if self.design.items and not messagebox.askyesno(
|
||||
"New label", "Clear all items?"
|
||||
if (
|
||||
self.design.items
|
||||
and Messagebox.yesno("Clear all items?", "New label", parent=self) != "Yes"
|
||||
):
|
||||
return
|
||||
self.design.items.clear()
|
||||
@@ -805,7 +868,7 @@ class LabelApp(tk.Tk):
|
||||
try:
|
||||
self.design = Design.from_dict(json.loads(Path(path).read_text()))
|
||||
except (OSError, ValueError, TypeError) as exc:
|
||||
messagebox.showerror("Cannot open", str(exc))
|
||||
Messagebox.show_error(str(exc), "Cannot open", parent=self)
|
||||
return
|
||||
self.preset.set(self.design.preset)
|
||||
self.width_mm.set(self.design.width_mm)
|
||||
@@ -844,7 +907,7 @@ class LabelApp(tk.Tk):
|
||||
self.model.get(), image, self.density.get(), rotate=90
|
||||
)
|
||||
except (ValueError, tk.TclError) as exc:
|
||||
messagebox.showerror("Cannot print", str(exc))
|
||||
Messagebox.show_error(str(exc), "Cannot print", parent=self)
|
||||
return
|
||||
copies = max(1, self.copies.get())
|
||||
self.print_btn.configure(state="disabled")
|
||||
@@ -872,7 +935,7 @@ class LabelApp(tk.Tk):
|
||||
self.status.set("Print failed")
|
||||
is_prn = isinstance(exc, PrinterError)
|
||||
kind = "Printer error" if is_prn else type(exc).__name__
|
||||
messagebox.showerror(kind, str(exc) or repr(exc))
|
||||
Messagebox.show_error(str(exc) or repr(exc), kind, parent=self)
|
||||
|
||||
# ---- settings
|
||||
|
||||
@@ -888,6 +951,7 @@ class LabelApp(tk.Tk):
|
||||
"conn": self.conn.get(),
|
||||
"addr": self.addr.get(),
|
||||
"density": self.density.get(),
|
||||
"theme": self.theme_mode,
|
||||
"design": self.design.to_dict(),
|
||||
}
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user