604 lines
22 KiB
Python
604 lines
22 KiB
Python
"""UI helpers and reusable Tk callbacks."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import colorsys
|
|
import tkinter as tk
|
|
import tkinter.font as tkfont
|
|
from tkinter import ttk
|
|
|
|
|
|
class UIBuilderMixin:
|
|
"""Constructs the Tkinter UI and common widgets."""
|
|
|
|
def setup_ui(self) -> None:
|
|
self._create_titlebar()
|
|
|
|
toolbar = ttk.Frame(self.root)
|
|
toolbar.pack(fill=tk.X, padx=12, pady=(4, 2))
|
|
buttons = [
|
|
("๐", self._t("toolbar.open_image"), self.load_image),
|
|
("๐", self._t("toolbar.open_folder"), self.load_folder),
|
|
("๐จ", self._t("toolbar.choose_color"), self.choose_color),
|
|
("๐ฑ", self._t("toolbar.pick_from_image"), self.enable_pick_mode),
|
|
("๐พ", self._t("toolbar.save_overlay"), self.save_overlay),
|
|
("๐งน", self._t("toolbar.clear_excludes"), self.clear_excludes),
|
|
("โฉ", self._t("toolbar.undo_exclude"), self.undo_exclude),
|
|
("๐", self._t("toolbar.reset_sliders"), self.reset_sliders),
|
|
("๐", self._t("toolbar.toggle_theme"), self.toggle_theme),
|
|
]
|
|
self._toolbar_buttons: list[dict[str, object]] = []
|
|
self._nav_buttons: list[tk.Button] = []
|
|
|
|
buttons_frame = ttk.Frame(toolbar)
|
|
buttons_frame.pack(side=tk.LEFT)
|
|
for icon, label, command in buttons:
|
|
self._add_toolbar_button(buttons_frame, icon, label, command)
|
|
|
|
status_container = ttk.Frame(toolbar)
|
|
status_container.pack(side=tk.RIGHT, expand=True, fill=tk.X)
|
|
self.status = ttk.Label(
|
|
status_container,
|
|
text=self._t("status.no_file"),
|
|
anchor="e",
|
|
foreground="#efefef",
|
|
)
|
|
self.status.pack(fill=tk.X)
|
|
self._attach_copy_menu(self.status)
|
|
self.status_default_text = self.status.cget("text")
|
|
self._status_palette = {"fg": self.status.cget("foreground")}
|
|
|
|
palette_frame = ttk.Frame(self.root)
|
|
palette_frame.pack(fill=tk.X, padx=12, pady=(6, 8))
|
|
default_colour = self._default_colour_hex()
|
|
|
|
current_frame = ttk.Frame(palette_frame)
|
|
current_frame.pack(side=tk.LEFT, padx=(0, 16))
|
|
ttk.Label(current_frame, text=self._t("palette.current")).pack(side=tk.LEFT, padx=(0, 6))
|
|
self.current_colour_sw = tk.Canvas(
|
|
current_frame,
|
|
width=24,
|
|
height=24,
|
|
highlightthickness=0,
|
|
background=default_colour,
|
|
bd=0,
|
|
)
|
|
self.current_colour_sw.pack(side=tk.LEFT, pady=2)
|
|
self.current_colour_label = ttk.Label(current_frame, text=f"({default_colour})")
|
|
self.current_colour_label.pack(side=tk.LEFT, padx=(6, 0))
|
|
|
|
ttk.Label(palette_frame, text=self._t("palette.more")).pack(side=tk.LEFT, padx=(0, 8))
|
|
swatch_container = ttk.Frame(palette_frame)
|
|
swatch_container.pack(side=tk.LEFT)
|
|
for name, hex_code in self._preset_colours():
|
|
self._add_palette_swatch(swatch_container, name, hex_code)
|
|
|
|
sliders_frame = ttk.Frame(self.root)
|
|
sliders_frame.pack(fill=tk.X, padx=12, pady=4)
|
|
sliders = [
|
|
(self._t("sliders.hue_min"), self.hue_min, 0, 360),
|
|
(self._t("sliders.hue_max"), self.hue_max, 0, 360),
|
|
(self._t("sliders.sat_min"), self.sat_min, 0, 100),
|
|
(self._t("sliders.val_min"), self.val_min, 0, 100),
|
|
(self._t("sliders.val_max"), self.val_max, 0, 100),
|
|
(self._t("sliders.alpha"), self.alpha, 0, 255),
|
|
]
|
|
for index, (label, variable, minimum, maximum) in enumerate(sliders):
|
|
self.add_slider_with_value(sliders_frame, label, variable, minimum, maximum, column=index)
|
|
sliders_frame.grid_columnconfigure(index, weight=1)
|
|
|
|
main = ttk.Frame(self.root)
|
|
main.pack(fill=tk.BOTH, expand=True, padx=12, pady=12)
|
|
|
|
left_column = ttk.Frame(main)
|
|
left_column.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=(0, 6))
|
|
left_column.grid_columnconfigure(1, weight=1)
|
|
left_column.grid_rowconfigure(0, weight=1)
|
|
|
|
self._create_navigation_button(left_column, "โ", self.show_previous_image, column=0)
|
|
|
|
self.canvas_orig = tk.Canvas(
|
|
left_column,
|
|
bg=self._canvas_background_colour(),
|
|
highlightthickness=0,
|
|
relief="flat",
|
|
)
|
|
self.canvas_orig.grid(row=0, column=1, sticky="nsew")
|
|
self.canvas_orig.bind("<Button-1>", self.on_canvas_click)
|
|
self.canvas_orig.bind("<ButtonPress-3>", self._exclude_start)
|
|
self.canvas_orig.bind("<B3-Motion>", self._exclude_drag)
|
|
self.canvas_orig.bind("<ButtonRelease-3>", self._exclude_end)
|
|
|
|
right_column = ttk.Frame(main)
|
|
right_column.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(6, 0))
|
|
right_column.grid_columnconfigure(0, weight=1)
|
|
right_column.grid_rowconfigure(0, weight=1)
|
|
|
|
self.canvas_overlay = tk.Canvas(
|
|
right_column,
|
|
bg=self._canvas_background_colour(),
|
|
highlightthickness=0,
|
|
relief="flat",
|
|
)
|
|
self.canvas_overlay.grid(row=0, column=0, sticky="nsew")
|
|
self._create_navigation_button(right_column, "โถ", self.show_next_image, column=1)
|
|
|
|
|
|
info_frame = ttk.Frame(self.root)
|
|
info_frame.pack(fill=tk.X, padx=12, pady=(0, 12))
|
|
self.filename_label = ttk.Label(
|
|
info_frame,
|
|
text="โ",
|
|
font=("Segoe UI", 10, "bold"),
|
|
anchor="center",
|
|
justify="center",
|
|
)
|
|
self.filename_label.pack(anchor="center")
|
|
self._attach_copy_menu(self.filename_label)
|
|
|
|
self.ratio_label = ttk.Label(
|
|
info_frame,
|
|
text=self._t("stats.placeholder"),
|
|
font=("Segoe UI", 10, "bold"),
|
|
anchor="center",
|
|
justify="center",
|
|
)
|
|
self.ratio_label.pack(anchor="center", pady=(4, 0))
|
|
self._attach_copy_menu(self.ratio_label)
|
|
|
|
self.root.bind("<Escape>", self.disable_pick_mode)
|
|
self.root.bind("<ButtonPress-1>", self._maybe_focus_window)
|
|
|
|
def add_slider_with_value(self, parent, text, var, minimum, maximum, column=0):
|
|
cell = ttk.Frame(parent)
|
|
cell.grid(row=0, column=column, sticky="we", padx=6)
|
|
header = ttk.Frame(cell)
|
|
header.pack(fill="x")
|
|
name_lbl = ttk.Label(header, text=text)
|
|
name_lbl.pack(side="left")
|
|
self._attach_copy_menu(name_lbl)
|
|
val_lbl = ttk.Label(header, text=f"{float(var.get()):.0f}")
|
|
val_lbl.pack(side="right")
|
|
self._attach_copy_menu(val_lbl)
|
|
style_name = getattr(self, "scale_style", "Horizontal.TScale")
|
|
ttk.Scale(
|
|
cell,
|
|
from_=minimum,
|
|
to=maximum,
|
|
orient="horizontal",
|
|
variable=var,
|
|
style=style_name,
|
|
command=self.on_slider_change,
|
|
).pack(fill="x", pady=(2, 8))
|
|
|
|
def on_var_change(*_):
|
|
val_lbl.config(text=f"{float(var.get()):.0f}")
|
|
|
|
try:
|
|
var.trace_add("write", on_var_change)
|
|
except Exception:
|
|
var.trace("w", lambda *_: on_var_change()) # type: ignore[attr-defined]
|
|
|
|
def on_slider_change(self, *_):
|
|
if self._update_job is not None:
|
|
try:
|
|
self.root.after_cancel(self._update_job)
|
|
except Exception:
|
|
pass
|
|
self._update_job = self.root.after(self.update_delay_ms, self.update_preview)
|
|
|
|
def _preset_colours(self):
|
|
return [
|
|
(self._t("palette.swatch.red"), "#ff3b30"),
|
|
(self._t("palette.swatch.orange"), "#ff9500"),
|
|
(self._t("palette.swatch.yellow"), "#ffd60a"),
|
|
(self._t("palette.swatch.green"), "#34c759"),
|
|
(self._t("palette.swatch.teal"), "#5ac8fa"),
|
|
(self._t("palette.swatch.blue"), "#0a84ff"),
|
|
(self._t("palette.swatch.violet"), "#af52de"),
|
|
(self._t("palette.swatch.magenta"), "#ff2d55"),
|
|
(self._t("palette.swatch.white"), "#ffffff"),
|
|
(self._t("palette.swatch.grey"), "#8e8e93"),
|
|
(self._t("palette.swatch.black"), "#000000"),
|
|
]
|
|
|
|
def _add_palette_swatch(self, parent, name: str, hex_code: str) -> None:
|
|
swatch = tk.Canvas(
|
|
parent,
|
|
width=24,
|
|
height=24,
|
|
highlightthickness=0,
|
|
background=hex_code,
|
|
bd=0,
|
|
relief="flat",
|
|
takefocus=1,
|
|
cursor="hand2",
|
|
)
|
|
swatch.pack(side=tk.LEFT, padx=4, pady=2)
|
|
|
|
def trigger(_event=None, colour=hex_code, label=name):
|
|
self.apply_sample_colour(colour, label)
|
|
|
|
swatch.bind("<Button-1>", trigger)
|
|
swatch.bind("<space>", trigger)
|
|
swatch.bind("<Return>", trigger)
|
|
swatch.bind("<Enter>", lambda _e: swatch.configure(cursor="hand2"))
|
|
swatch.bind("<Leave>", lambda _e: swatch.configure(cursor="arrow"))
|
|
|
|
def _add_toolbar_button(self, parent, icon: str, label: str, command) -> None:
|
|
font = tkfont.Font(root=self.root, family="Segoe UI", size=9)
|
|
padding_x = 12
|
|
gap = font.measure(" ")
|
|
icon_width = font.measure(icon)
|
|
label_width = font.measure(label)
|
|
width = padding_x * 2 + icon_width + gap + label_width
|
|
height = 28
|
|
radius = 9
|
|
bg = self.root.cget("bg") if hasattr(self.root, "cget") else "#f2f2f7"
|
|
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(),
|
|
"dimensions": (width, height, radius),
|
|
}
|
|
self._toolbar_buttons.append(button_data)
|
|
|
|
def set_fill(state: str) -> None:
|
|
pal: dict[str, str] = button_data["palette"] # type: ignore[index]
|
|
canvas.itemconfigure(rect_id, fill=pal[state]) # type: ignore[index]
|
|
|
|
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")
|
|
self.root.after_idle(execute)
|
|
|
|
def on_enter(_event):
|
|
set_fill("hover")
|
|
|
|
def on_leave(_event):
|
|
set_fill("normal")
|
|
|
|
def on_focus_in(_event):
|
|
pal: dict[str, str] = button_data["palette"] # type: ignore[index]
|
|
canvas.itemconfigure(rect_id, outline=pal["outline_focus"]) # type: ignore[index]
|
|
|
|
def on_focus_out(_event):
|
|
pal: dict[str, str] = button_data["palette"] # type: ignore[index]
|
|
canvas.itemconfigure(rect_id, outline=pal["outline"]) # type: ignore[index]
|
|
|
|
def invoke_keyboard(_event=None):
|
|
set_fill("active")
|
|
canvas.after(120, lambda: set_fill("hover"))
|
|
self.root.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)
|
|
|
|
@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 _create_navigation_button(self, container, symbol: str, command, *, column: int) -> None:
|
|
palette = self._navigation_palette()
|
|
bg = palette["bg"]
|
|
fg = palette["fg"]
|
|
container.grid_rowconfigure(0, weight=1)
|
|
btn = tk.Button(
|
|
container,
|
|
text=symbol,
|
|
command=command,
|
|
font=("Segoe UI", 26, "bold"),
|
|
relief="flat",
|
|
borderwidth=0,
|
|
background=bg,
|
|
activebackground=bg,
|
|
highlightthickness=0,
|
|
fg=fg,
|
|
activeforeground=fg,
|
|
cursor="hand2",
|
|
width=2,
|
|
)
|
|
btn.grid(row=0, column=column, sticky="ns", padx=6)
|
|
self._nav_buttons.append(btn)
|
|
|
|
def _create_titlebar(self) -> None:
|
|
bar_bg = "#1f1f1f"
|
|
title_bar = tk.Frame(self.root, bg=bar_bg, relief="flat", height=34)
|
|
title_bar.pack(fill=tk.X, side=tk.TOP)
|
|
title_bar.pack_propagate(False)
|
|
|
|
logo = None
|
|
try:
|
|
from PIL import Image, ImageTk # type: ignore
|
|
from importlib import resources
|
|
|
|
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:
|
|
logo = None
|
|
|
|
if logo is not None:
|
|
logo_label = tk.Label(title_bar, image=logo, bg=bar_bg)
|
|
logo_label.image = logo # keep reference
|
|
logo_label.pack(side=tk.LEFT, padx=(10, 6), pady=4)
|
|
|
|
title_label = tk.Label(
|
|
title_bar,
|
|
text=self._t("app.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._close_app,
|
|
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))
|
|
|
|
for widget in (title_bar, title_label):
|
|
widget.bind("<ButtonPress-1>", self._start_window_drag)
|
|
widget.bind("<B1-Motion>", self._perform_window_drag)
|
|
|
|
def _close_app(self) -> None:
|
|
try:
|
|
self.root.destroy()
|
|
except Exception:
|
|
pass
|
|
|
|
def _start_window_drag(self, event) -> None:
|
|
self._drag_offset = (event.x_root - self.root.winfo_rootx(), event.y_root - self.root.winfo_rooty())
|
|
|
|
def _perform_window_drag(self, event) -> None:
|
|
offset = getattr(self, "_drag_offset", None)
|
|
if offset is None:
|
|
return
|
|
x = event.x_root - offset[0]
|
|
y = event.y_root - offset[1]
|
|
self.root.geometry(f"+{x}+{y}")
|
|
|
|
def _maybe_focus_window(self, _event) -> None:
|
|
try:
|
|
self.root.focus_set()
|
|
except Exception:
|
|
pass
|
|
|
|
def _toolbar_palette(self) -> dict[str, str]:
|
|
is_dark = getattr(self, "theme", "light") == "dark"
|
|
if is_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 _navigation_palette(self) -> dict[str, str]:
|
|
is_dark = getattr(self, "theme", "light") == "dark"
|
|
default_bg = "#0f0f10" if is_dark else "#ededf2"
|
|
bg = self.root.cget("bg") if hasattr(self.root, "cget") else default_bg
|
|
fg = "#f5f5f5" if is_dark else "#1f1f1f"
|
|
return {"bg": bg, "fg": fg}
|
|
|
|
def _refresh_toolbar_buttons_theme(self) -> None:
|
|
if not getattr(self, "_toolbar_buttons", None):
|
|
return
|
|
bg = self.root.cget("bg") if hasattr(self.root, "cget") else "#f2f2f7"
|
|
palette = self._toolbar_palette()
|
|
for data in self._toolbar_buttons:
|
|
canvas = data["canvas"] # type: ignore[index]
|
|
rect_id = data["rect"] # type: ignore[index]
|
|
text_ids = data["text_ids"] # type: ignore[index]
|
|
data["palette"] = palette.copy()
|
|
canvas.configure(bg=bg)
|
|
canvas.itemconfigure(rect_id, fill=palette["normal"], outline=palette["outline"])
|
|
for text_id in text_ids:
|
|
canvas.itemconfigure(text_id, fill=palette["text"])
|
|
|
|
def _refresh_navigation_buttons_theme(self) -> None:
|
|
if not getattr(self, "_nav_buttons", None):
|
|
return
|
|
palette = self._navigation_palette()
|
|
for btn in self._nav_buttons:
|
|
btn.configure(
|
|
background=palette["bg"],
|
|
activebackground=palette["bg"],
|
|
fg=palette["fg"],
|
|
activeforeground=palette["fg"],
|
|
)
|
|
|
|
def _canvas_background_colour(self) -> str:
|
|
return "#0f0f10" if getattr(self, "theme", "light") == "dark" else "#ffffff"
|
|
|
|
def _refresh_canvas_backgrounds(self) -> None:
|
|
bg = self._canvas_background_colour()
|
|
for attr in ("canvas_orig", "canvas_overlay"):
|
|
canvas = getattr(self, attr, None)
|
|
if canvas is not None:
|
|
try:
|
|
canvas.configure(bg=bg)
|
|
except Exception:
|
|
pass
|
|
|
|
def _refresh_status_palette(self, fg: str) -> None:
|
|
self.status.configure(foreground=fg)
|
|
self._status_palette["fg"] = fg
|
|
|
|
def _refresh_accent_labels(self, colour: str) -> None:
|
|
try:
|
|
self.filename_label.configure(foreground=colour)
|
|
self.ratio_label.configure(foreground=colour)
|
|
except Exception:
|
|
pass
|
|
|
|
def _default_colour_hex(self) -> str:
|
|
defaults = getattr(self, "DEFAULTS", {})
|
|
hue_min = float(defaults.get("hue_min", 0.0))
|
|
hue_max = float(defaults.get("hue_max", hue_min))
|
|
if hue_min <= hue_max:
|
|
hue = (hue_min + hue_max) / 2.0
|
|
else:
|
|
span = ((hue_max + 360.0) - hue_min) / 2.0
|
|
hue = (hue_min + span) % 360.0
|
|
|
|
sat_min = float(defaults.get("sat_min", 0.0))
|
|
saturation = (sat_min + 100.0) / 2.0
|
|
|
|
val_min = float(defaults.get("val_min", 0.0))
|
|
val_max = float(defaults.get("val_max", 100.0))
|
|
value = (val_min + val_max) / 2.0
|
|
|
|
r, g, b = colorsys.hsv_to_rgb(hue / 360.0, saturation / 100.0, value / 100.0)
|
|
return f"#{int(r * 255):02x}{int(g * 255):02x}{int(b * 255):02x}"
|
|
|
|
def _init_copy_menu(self):
|
|
self._copy_target = None
|
|
self.copy_menu = tk.Menu(self.root, tearoff=0)
|
|
label = self._t("menu.copy") if hasattr(self, "_t") else "Copy"
|
|
self.copy_menu.add_command(label=label, command=self._copy_current_label)
|
|
|
|
def _attach_copy_menu(self, widget):
|
|
widget.bind("<Button-3>", lambda event, w=widget: self._show_copy_menu(event, w))
|
|
widget.bind("<Control-c>", lambda event, w=widget: self._copy_widget_text(w))
|
|
|
|
def _show_copy_menu(self, event, widget):
|
|
self._copy_target = widget
|
|
try:
|
|
self.copy_menu.tk_popup(event.x_root, event.y_root)
|
|
finally:
|
|
self.copy_menu.grab_release()
|
|
|
|
def _copy_current_label(self):
|
|
if self._copy_target is not None:
|
|
self._copy_widget_text(self._copy_target)
|
|
|
|
def _copy_widget_text(self, widget):
|
|
try:
|
|
text = widget.cget("text")
|
|
except Exception:
|
|
text = ""
|
|
if not text:
|
|
return
|
|
try:
|
|
self.root.clipboard_clear()
|
|
self.root.clipboard_append(text)
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
__all__ = ["UIBuilderMixin"]
|