| 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) |
|
|
| |
| 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) |
|
|
| |
| self.scrollable_frame = ttk.Frame(self.canvas) |
| self.scrollable_frame.bind( |
| "<Configure>", |
| lambda e: self.canvas.configure( |
| scrollregion=self.canvas.bbox("all") |
| ) |
| ) |
|
|
| |
| self.canvas_frame = self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") |
|
|
| |
| self.scrollable_frame.bind("<Configure>", self._on_frame_configure) |
| self.canvas.bind("<Configure>", self._on_canvas_configure) |
|
|
| |
| self.canvas.bind_all("<MouseWheel>", self._on_mousewheel) |
|
|
| def _on_frame_configure(self, event): |
| |
| 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): |
| |
| self.canvas.itemconfig(self.canvas_frame, width=event.width) |
|
|
| def _on_mousewheel(self, event): |
| |
| 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() |
|
|
| |
| 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") |
| style.configure("ParentB.TFrame", background="#003366") |
|
|
| 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 |
| desired_height = int(total_height * 0.5) |
| 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() |
|
|
| |
| 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]: |
| |
| 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] |
|
|
| |
| scrollable_container = ScrollableFrame(panel) |
| scrollable_container.pack(fill="both", expand=True, padx=10, pady=10) |
|
|
| |
| 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() |
|
|