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:
Gracious
2026-09-21 08:22:14 +01:00
parent dfe26919ad
commit 76e6732285
4 changed files with 112 additions and 45 deletions
+109 -45
View File
@@ -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: