weed-sim / inventory_gui.py
tostido's picture
WEED-SIM: evolutionary genetics sandbox with embedded Observer Bus
ae853c1
Raw
History Blame Contribute Delete
16.2 kB
import tkinter as tk
from tkinter import ttk
from PIL import Image, ImageTk
from icon_loader import generate_seed_image
from seed_profile import GrowthStage
LIME = "#39FF14"
BLACK = "#000000"
class Tooltip:
def __init__(self, widget, text):
self.widget = widget
self.text = text
self.tip_window = None
widget.bind("<Enter>", self.show)
widget.bind("<Leave>", self.hide)
def show(self, event=None):
if self.tip_window:
return
x = self.widget.winfo_pointerx() + 10
y = self.widget.winfo_pointery() + 10
self.tip_window = tw = tk.Toplevel(self.widget)
tw.wm_overrideredirect(True)
tw.configure(bg=BLACK)
label = tk.Label(
tw,
text=self.text,
bg=BLACK,
fg=LIME,
justify=tk.LEFT,
relief=tk.SOLID,
borderwidth=1,
highlightbackground=LIME,
highlightthickness=1,
padx=4,
pady=2,
font=("Consolas", 8)
)
label.pack()
tw.wm_geometry(f"+{x}+{y}")
def hide(self, event=None):
if self.tip_window:
self.tip_window.destroy()
self.tip_window = None
class ScrollableFrame(ttk.Frame):
def __init__(self, container, *args, **kwargs):
super().__init__(container, *args, **kwargs)
# Create a canvas and a vertical scrollbar for scrolling it
self.canvas = tk.Canvas(self, borderwidth=0, highlightthickness=0)
self.vscrollbar = ttk.Scrollbar(self, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.vscrollbar.set)
self.vscrollbar.pack(side="right", fill="y")
self.canvas.pack(side="left", fill="both", expand=True)
# This frame will be scrolled inside the canvas
self.scrollable_frame = ttk.Frame(self.canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: self.canvas.configure(
scrollregion=self.canvas.bbox("all")
)
)
# Create a window inside the canvas to hold the scrollable frame
self.canvas_frame = self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
# Bind events to make width resize correctly
self.scrollable_frame.bind("<Configure>", self._on_frame_configure)
self.canvas.bind("<Configure>", self._on_canvas_configure)
# Optional: bind mouse wheel to scroll
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
def _on_frame_configure(self, event):
# Update scrollregion and match width of the canvas window to the frame
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
self.canvas.itemconfig(self.canvas_frame, width=self.canvas.winfo_width())
def _on_canvas_configure(self, event):
# Resize the inner frame's width to match canvas width
self.canvas.itemconfig(self.canvas_frame, width=event.width)
def _on_mousewheel(self, event):
# Scroll vertically
self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
class InventoryGUI:
def __init__(self, root, catalog, breeder, starter_loader):
self.root = root
self.catalog = catalog
self.breeder = breeder
self.starter_loader = starter_loader
self.selected_seeds = []
self.image_cache = {}
self._configure_styles()
self.root.title("Seed Inventory")
self.root.configure(bg=BLACK)
self._build_controls()
self._build_selected_panel()
self._build_inventory_grid()
self.refresh_inventory()
# Dynamic panel height for breeding pit
self._update_breeding_pit_height()
self.root.bind("<Configure>", lambda e: self._update_breeding_pit_height())
def _configure_styles(self):
style = ttk.Style()
style.theme_use('default')
style.configure("TFrame", background=BLACK)
style.configure("TLabel", background=BLACK, foreground=LIME)
style.configure("TButton", background=BLACK, foreground=LIME, borderwidth=1)
style.map("TButton", background=[('active', BLACK)], foreground=[('active', LIME)])
style.configure("ParentA.TFrame", background="#003300") # Dark green
style.configure("ParentB.TFrame", background="#003366") # Dark blue-green
def _build_controls(self):
control_frame = ttk.Frame(self.root)
control_frame.pack(fill=tk.X, pady=4)
ttk.Button(control_frame, text="Add Starter Seeds", command=self.add_starter_seeds).pack(side=tk.LEFT, padx=4)
ttk.Button(control_frame, text="Clear Inventory", command=self.clear_inventory).pack(side=tk.LEFT, padx=4)
self.breed_btn = ttk.Button(control_frame, text="Breed Selected", command=self.breed_selected)
self.breed_btn.pack(side=tk.LEFT, padx=4)
self.breed_btn.state(["disabled"])
self.clone_btn = ttk.Button(control_frame, text="Clone Selected", command=self.clone_selected)
self.clone_btn.pack(side=tk.LEFT, padx=4)
self.clone_btn.state(["disabled"])
def _build_selected_panel(self):
self.selected_frame = ttk.Frame(self.root)
self.selected_frame.pack(fill=tk.BOTH, expand=False, pady=4)
self.parent_a_panel = ttk.Frame(self.selected_frame, padding=6, relief=tk.RIDGE, borderwidth=2)
self.parent_b_panel = ttk.Frame(self.selected_frame, padding=6, relief=tk.RIDGE, borderwidth=2)
self.parent_a_panel.pack(side=tk.LEFT, expand=True, fill=tk.BOTH, padx=10)
self.parent_b_panel.pack(side=tk.LEFT, expand=True, fill=tk.BOTH, padx=10)
self.parent_a_panel.pack_propagate(False)
self.parent_b_panel.pack_propagate(False)
def _build_inventory_grid(self):
self.canvas = tk.Canvas(self.root, bg=BLACK, highlightthickness=0)
self.scrollbar = ttk.Scrollbar(self.root, orient="vertical", command=self.canvas.yview)
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.grid_frame = ttk.Frame(self.canvas)
self.canvas.create_window((0, 0), window=self.grid_frame, anchor="nw")
self.grid_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox("all")))
self.canvas.bind_all("<MouseWheel>", self._on_mousewheel)
def _on_mousewheel(self, event):
self.canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
def _update_breeding_pit_height(self):
total_height = self.root.winfo_height()
if total_height <= 1:
total_height = 800 # fallback
desired_height = int(total_height * 0.5) # 50%
self.parent_a_panel.config(height=desired_height)
self.parent_b_panel.config(height=desired_height)
def refresh_inventory(self):
for widget in self.grid_frame.winfo_children():
widget.destroy()
seeds = self.catalog.get_all_seeds()
cols = 5
for idx, seed in enumerate(seeds):
self._render_card(seed).grid(row=idx // cols, column=idx % cols, padx=8, pady=8)
self._refresh_selected_panel()
# Enable/disable buttons based on attempts and selection
can_breed = len(self.selected_seeds) == 2 and all(seed.can_attempt() for seed in self.selected_seeds)
can_clone = len(self.selected_seeds) == 1 and self.selected_seeds[0].can_attempt()
self.breed_btn.state(["!disabled"] if can_breed else ["disabled"])
self.clone_btn.state(["!disabled"] if can_clone else ["disabled"])
def _render_card(self, seed):
frame = ttk.Frame(self.grid_frame, relief=tk.SOLID, borderwidth=2)
if seed in self.selected_seeds:
idx = self.selected_seeds.index(seed)
frame.config(style="ParentA.TFrame" if idx == 0 else "ParentB.TFrame")
else:
frame.config(style="TFrame")
cache_key = (
seed.seed_id,
seed.growth_stage.name,
seed.color_rgb,
tuple(sorted(seed.traits.items()))
)
if cache_key not in self.image_cache:
self.image_cache[cache_key] = generate_seed_image(seed, seed.growth_stage.name).resize((96, 96), resample=Image.NEAREST)
pil_img = self.image_cache[cache_key]
tk_img = ImageTk.PhotoImage(pil_img)
label = tk.Label(frame, image=tk_img, bg=BLACK)
label.image = tk_img
label.pack()
label.bind("<Button-1>", lambda e, s=seed: self.toggle_seed_selection(s))
tooltip_text = f"{seed.strain_name}\nType: {seed.traits.get('Type', 'Unknown')}\n"
for trait in ["THC", "CBD", "Yield", "GrowTime", "ColorScheme"]:
val = seed.traits.get(trait, "-")
stab = seed.stabilities.get(trait, "-")
tooltip_text += f"{trait}: {val} (Stability: {stab})\n"
tooltip_text += "\nAlleles:\n"
for trait in ["THC", "CBD", "Yield", "GrowTime", "ColorScheme"]:
alleles = seed.alleles.get(trait)
if alleles:
tooltip_text += f"{trait}: {alleles}\n"
Tooltip(label, tooltip_text)
name_label = ttk.Label(frame, text=seed.strain_name, font=("Consolas", 9))
name_label.pack()
if seed.growth_stage != GrowthStage.MATURE:
grow_btn = ttk.Button(frame, text="Grow", command=lambda s=seed: self.advance_stage(s))
grow_btn.pack(pady=2)
else:
status = ttk.Label(frame, text="MATURE")
status.pack(pady=2)
return frame
def _refresh_selected_panel(self):
for panel in [self.parent_a_panel, self.parent_b_panel]:
# Clear previous content
for widget in panel.winfo_children():
widget.destroy()
if len(self.selected_seeds) == 0:
return
panels = [self.parent_a_panel, self.parent_b_panel]
for idx, seed in enumerate(self.selected_seeds):
if idx >= len(panels):
break
panel = panels[idx]
# Create scrollable frame inside parent panel
scrollable_container = ScrollableFrame(panel)
scrollable_container.pack(fill="both", expand=True, padx=10, pady=10)
# Now add your info panel inside scrollable_container.scrollable_frame
self._display_seed_info_panel(scrollable_container.scrollable_frame, seed)
self._draw_lineage_tree_canvas(scrollable_container.scrollable_frame, seed)
def _display_seed_info_panel(self, panel, seed):
container = ttk.Frame(panel)
container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
info_frame = ttk.Frame(container)
info_frame.pack(side=tk.LEFT, fill=tk.Y)
size = 96
cache_key = (seed.seed_id, GrowthStage.MATURE.name, size)
if cache_key not in self.image_cache:
pil_img = generate_seed_image(seed, GrowthStage.MATURE.name).resize((size, size), resample=Image.NEAREST)
self.image_cache[cache_key] = pil_img
else:
pil_img = self.image_cache[cache_key]
tk_img = ImageTk.PhotoImage(pil_img)
img_label = tk.Label(info_frame, image=tk_img, bg=BLACK)
img_label.image = tk_img
img_label.pack(pady=5)
ttk.Label(info_frame, text=seed.strain_name, font=("Consolas", 10, "bold")).pack(anchor=tk.W, pady=(0, 5))
for trait in ["Type", "THC", "CBD", "Yield", "GrowTime"]:
val = seed.traits.get(trait, "-")
stab = seed.stabilities.get(trait, "-")
ttk.Label(info_frame, text=f"{trait}: {val} (Stability: {stab})", font=("Consolas", 9)).pack(anchor=tk.W, pady=1)
ttk.Label(info_frame, text="Alleles:", font=("Consolas", 9, "underline")).pack(anchor=tk.W, pady=(6, 3))
for trait in ["THC", "CBD", "Yield", "GrowTime"]:
alleles = seed.alleles.get(trait)
if alleles:
ttk.Label(info_frame, text=f"{trait}: {alleles}", font=("Consolas", 9)).pack(anchor=tk.W, pady=1)
def _draw_lineage_tree_canvas(self, panel, seed):
if hasattr(panel, '_lineage_canvas'):
panel._lineage_canvas.destroy()
canvas_width = 250
canvas_height = 160
canvas = tk.Canvas(panel, width=canvas_width, height=canvas_height, bg=BLACK, highlightthickness=0)
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10)
panel._lineage_canvas = canvas
parent_coords = [(60, 30), (180, 30)]
grandparent_coords = [(20, 110), (100, 110), (160, 110), (240, 110)]
lineage_ids = seed.lineage if seed.lineage else [None, None]
parents = [self.catalog.get_seed_by_id(pid) for pid in lineage_ids]
for i, p in enumerate(parents):
if p is None:
continue
self._draw_icon_on_canvas(canvas, p, parent_coords[i], 48)
grandparents = []
for p in parents:
if p and p.lineage:
grandparents.extend([self.catalog.get_seed_by_id(pid) for pid in p.lineage])
for i, gp in enumerate(grandparents):
if gp is None:
continue
self._draw_icon_on_canvas(canvas, gp, grandparent_coords[i], 32)
canvas.create_line(parent_coords[0][0]+24, parent_coords[0][1]+48, grandparent_coords[0][0]+16, grandparent_coords[0][1], fill=LIME, width=2)
canvas.create_line(parent_coords[0][0]+24, parent_coords[0][1]+48, grandparent_coords[1][0]+16, grandparent_coords[1][1], fill=LIME, width=2)
canvas.create_line(parent_coords[1][0]+24, parent_coords[1][1]+48, grandparent_coords[2][0]+16, grandparent_coords[2][1], fill=LIME, width=2)
canvas.create_line(parent_coords[1][0]+24, parent_coords[1][1]+48, grandparent_coords[3][0]+16, grandparent_coords[3][1], fill=LIME, width=2)
def _draw_icon_on_canvas(self, canvas, seed, position, size):
cache_key = (seed.seed_id, GrowthStage.MATURE.name, size)
if cache_key not in self.image_cache:
pil_img = generate_seed_image(seed, GrowthStage.MATURE.name).resize((size, size), resample=Image.NEAREST)
self.image_cache[cache_key] = pil_img
else:
pil_img = self.image_cache[cache_key]
tk_img = ImageTk.PhotoImage(pil_img)
if not hasattr(canvas, '_image_refs'):
canvas._image_refs = []
canvas._image_refs.append(tk_img)
canvas.create_image(position[0], position[1], image=tk_img, anchor='nw')
def toggle_seed_selection(self, seed):
if seed in self.selected_seeds:
self.selected_seeds.remove(seed)
elif len(self.selected_seeds) < 2:
self.selected_seeds.append(seed)
self.refresh_inventory()
def advance_stage(self, seed):
seed.advance_growth_stage()
self.refresh_inventory()
def breed_selected(self):
if len(self.selected_seeds) == 2:
try:
offspring = self.breeder.breed(self.selected_seeds[0], self.selected_seeds[1], self.starter_loader.name_generator)
self.catalog.add_seed(offspring)
self.selected_seeds.clear()
self.refresh_inventory()
except Exception as e:
print(f"Breeding failed: {e}")
def clone_selected(self):
if len(self.selected_seeds) != 1:
return
try:
clone_seed = self.breeder.clone(self.selected_seeds[0], self.starter_loader.name_generator)
self.catalog.add_seed(clone_seed)
self.refresh_inventory()
except Exception as e:
print(f"Clone failed: {e}")
def add_starter_seeds(self):
self.starter_loader.load_starter_seeds()
self.refresh_inventory()
def clear_inventory(self):
self.catalog.clear()
self.selected_seeds.clear()
self.refresh_inventory()