Tuesday, July 7, 2026

Inspection vs. Import (Colorize things)

Just when you thought you had it, you didn't. Column 1 good, 2 contentious area, and 3 waiting. import customtkinter as ctk import tkinter as tk from tkinter import ttk, filedialog, messagebox from pathlib import Path import chess import chess.pgn import json import os # ---------------------------- # Main Window # ---------------------------- ctk.set_appearance_mode("dark") ctk.set_default_color_theme("blue") app = ctk.CTk() style = ttk.Style() style.theme_use("alt") print(style.theme_names()) reopen_btn = ctk.CTkLabel( app, text="🮟", font=("Arial", 16, "bold"), text_color = ("#515985") ) reopen_btn.place(x=2, rely=1.0, y=0, anchor="sw") app.configure(fg_color="#172134") app.title("🩒 Chess Catalog 🩒") app.geometry("800x700") #change default width later to be longer # ---------------------------- # Configure Grid # ---------------------------- app.grid_rowconfigure(1, weight=1) app.grid_columnconfigure(1, weight=1) # ---------------------------- # PGN Collection # ---------------------------- imported_files = [] cataloged_files = [] sidebar_visible = True # ---------------------------- # Import PGN # ---------------------------- def show_import_workspace(filename): global add_catalog_button # Scan the entire PGN and collect every unique header tag all_tags = set() with open(filename, encoding="utf-8") as pgn_file: while True: game = chess.pgn.read_game(pgn_file) if game is None: break all_tags.update(game.headers.keys()) # No games found if not all_tags: return print("all_tags =", all_tags) print("Number of unique tags =", len(all_tags)) game_data_tags = [] # Event if "Event" in all_tags: game_data_tags.append("Event") # Round if "Round" in all_tags: game_data_tags.append("Round") # Players (White and/or Black) if "White" in all_tags or "Black" in all_tags: game_data_tags.append("Players") # Result if "Result" in all_tags: game_data_tags.append("Result") # Title (WhiteTitle and/or BlackTitle) if "WhiteTitle" in all_tags or "BlackTitle" in all_tags: game_data_tags.append("Title") other_data_tags = [] column1_groups = { "Event", "Site", "Date", "Round", "Players", "Result", "Title", } # Clear the workspace for widget in workspace.winfo_children(): widget.destroy() # Title title = ctk.CTkLabel( workspace, text=f"{Path(filename).name}", font=("Arial", 24, "bold") ) title.pack( anchor="w", padx=20, pady=(15, 20) ) # Button frame button_frame = ctk.CTkFrame(workspace, fg_color="transparent") button_frame.pack(anchor="w", padx=20, pady=5) # Import, Add to Catalog, Catalog buttons add_catalog_button = ctk.CTkButton( button_frame, text="Add to Catalog", command=catalog_pgns ) add_catalog_button.pack(side="left", padx=(10, 0)) ctk.CTkButton( button_frame, text="Import", command=import_pgn ).pack(side="left", padx=(10, 0)) ctk.CTkButton( button_frame, text="Catalog", command=show_catalog_workspace ).pack(side="left", padx=(10, 0)) # Main container options_frame = ctk.CTkFrame(workspace, fg_color="#283858" )#game_data_panel options_frame.pack(fill="x", padx=20, pady=10) # Three columns game_data_frame = ctk.CTkFrame(options_frame, #fg_color="#34426e" fg_color="#00ff00" fg_color="#283858") #column color over game_data_panel_background other_data_frame = ctk.CTkFrame(options_frame, fg_color="#283858") #same depth_rating = ctk.CTkFrame(options_frame, fg_color="#283858") #same # Pack the columns for col in (game_data_frame, other_data_frame, depth_rating): col.pack(side="left", anchor="n", padx=(10, 30), pady=10) # ---------- Column 1 ---------- ctk.CTkLabel( game_data_frame, text="Game Data", font=("Arial", 16, "bold") ).pack(anchor="w", pady=(0, 10)) game_data_vars = {} for tag in game_data_tags: game_data_vars[tag] = tk.BooleanVar(value=False) ctk.CTkCheckBox( game_data_frame, text=tag, variable=game_data_vars[tag], fg_color="#4CAF50", hover_color="#5ECF63", border_color="#4CAF50", checkmark_color="white", text_color="#8EE68E" ).pack(anchor="w", pady=4) # ---------- Column 2 ---------- ctk.CTkLabel( other_data_frame, text="", font=("Arial", 16, "bold") ).pack(anchor="w", pady=(0, 10)) other_data_tags = [] # Elo (WhiteElo and/or BlackElo) if "WhiteElo" in all_tags or "BlackElo" in all_tags: other_data_tags.append("Elo") # FideId (WhiteFideId and/or BlackFideId) if "WhiteFideId" in all_tags or "BlackFideId" in all_tags: other_data_tags.append("FideId") # EventDate if "EventDate" in all_tags: other_data_tags.append("EventDate") # EventType if "EventType" in all_tags: other_data_tags.append("EventType") other_data_vars = {} print(other_data_tags) print(len(other_data_tags)) print("other_data_tags =", other_data_tags) print("Number of displayed tags =", len(other_data_tags)) # Remove tags that are already represented in Columns 1 and 2 known_tags = { "Fen", "White", "Black", "Result", "ECO", "Opening", "Variation", "WhiteTitle", "BlackTitle", "WhiteElo", "BlackElo", "WhiteFideId", "BlackFideId", "EventDate", "EventType" } # Standard tags that belong in Column 2 recognized_other_tags = [ tag for tag in ( "WhiteFideId", "BlackFideId", "EventDate", "EventType" ) if tag in all_tags ] # Unknown or misspelled tags unknown_tags = sorted( tag for tag in all_tags if tag not in known_tags ) # Show recognized tags first other_data_tags.extend(recognized_other_tags) # Then show unknown tags other_data_tags.extend(unknown_tags) other_data_tags = [ tag for tag in other_data_tags if not tag.startswith(("White", "Black")) ] other_data_tags = [ tag for tag in other_data_tags if tag not in column1_groups ] for tag in other_data_tags: other_data_vars[tag] = tk.BooleanVar(value=False) print("Creating checkbox:", tag) ctk.CTkCheckBox( other_data_frame, text=tag, variable=other_data_vars[tag] ).pack(anchor="w", pady=4) # ---------- Column 3 ---------- ctk.CTkLabel( depth_rating, text="Depth Rating" ).pack(anchor="w", pady=(35, 0)) depth_menu = ctk.CTkOptionMenu( depth_rating, values=[ "15 Moves", "20 Moves", "25 Moves", "30 Moves", "All Moves" ] ) depth_menu.pack(anchor="w") depth_menu.set("15 Moves") ctk.CTkLabel( depth_rating, text="Rating Filter" ).pack(anchor="w", pady=(20, 0)) rating_filter = ctk.CTkOptionMenu( depth_rating, values=[ "2100+", "2400+", "2600+", "All" ] ) rating_filter.pack(anchor="w") rating_filter.set("2100+") def import_pgn(): print("1 - import_pgn started") filename = filedialog.askopenfilename( title="Import PGN", filetypes=[("PGN Files", "*.pgn")] ) print("2 - file dialog returned") if not filename: return print(f"3 - Selected: {filename}") show_import_workspace(filename) print("4 - show_import_workspace finished") # Don't allow the same file twice if filename in imported_files: status.configure( text=f"{Path(filename).name} is already imported." ) return imported_files.append(filename) short_name = Path(filename).name sidebar.insert( pgn_node, "end", text=short_name ) status.configure( text=f"Imported: {short_name}" ) status.configure( text=f"Imported: {short_name}" ) def import_fen(): print("Import FEN selected") # ---------------------------- # Menu Bar # ---------------------------- menu_bar = tk.Menu(app) app.config(menu=menu_bar) def catalog_pgns(): global add_catalog_button if not imported_files: status.configure( text="No PGNs imported to catalog." ) return add_catalog_button.configure(text="Please Wait...") workspace.update() new_files = [] total_games = 0 if os.path.exists("personal_catalog.json"): with open("personal_catalog.json", "r", encoding="utf-8") as f: catalog = json.load(f) cataloged_files = catalog.get("cataloged_files", []) else: catalog = { "cataloged_files": [], "A": {}, "B": {}, "C": {}, "D": {}, "E": {} } cataloged_files = [] for filename in imported_files: if filename in cataloged_files: continue with open(filename, encoding="utf-8") as pgn: while True: game = chess.pgn.read_game(pgn) if game is None: break total_games += 1 eco = game.headers.get("ECO", "").strip() opening = game.headers.get("Opening", "").strip() variation = game.headers.get("Variation", "").strip() if eco and opening: group = eco[0] if eco not in catalog[group]: catalog[group][eco] = { "opening": opening, "variation": variation, "frequency": 0 } catalog[group][eco]["frequency"] += 1 cataloged_files.append(filename) new_files.append(filename) if not new_files: add_catalog_button.configure(text="Add to Catalog") status.configure( text="PGNs already cataloged. Import new games to catalog more." ) return print(catalog) catalog["cataloged_files"] = cataloged_files with open("personal_catalog.json", "w", encoding="utf-8") as f: json.dump(catalog, f, indent=4, sort_keys=True) print("JSON written.") status.configure( text=f"Cataloged {total_games} game(s) from {len(new_files)} PGN(s)." ) status.configure( text=f"Cataloged {total_games} game(s) from {len(new_files)} PGN(s)." ) add_catalog_button.configure(text="Add to Catalog") def clear_catalog(): answer = messagebox.askyesno( "Clear Catalog", "Delete the entire opening catalog?" ) if not answer: return if os.path.exists("personal_catalog.json"): os.remove("personal_catalog.json") cataloged_files.clear() status.configure( text="Catalog cleared." ) show_catalog_workspace() def show_catalog_workspace(): # Clear the workspace for widget in workspace.winfo_children(): widget.destroy() print("A") # Title title = ctk.CTkLabel( workspace, text="Opening Catalog", font=("Arial", 24, "bold") ) title.pack(anchor="w", padx=20, pady=(15, 10)) print("B") # ----- Table Frame ----- table_frame = ctk.CTkFrame(workspace) table_frame.pack(fill="both", anchor="w", expand=True, padx=(0, 0), pady=(0, 0)) columns = ( "Select", "Games", "ECO", "Opening", "Variation", ) catalog_table = ttk.Treeview( table_frame, columns=columns, show="headings" ) # Headings catalog_table.heading("Select", anchor="center", text=" ☐") catalog_table.heading("Games", anchor="center", text=" Games") catalog_table.heading("ECO", anchor="w", text=" ECO") catalog_table.heading("Opening", anchor="w", text=" Opening") catalog_table.heading("Variation", anchor="w", text=" Variation") # Column widths catalog_table.column("Select", width=45, anchor="center", stretch=False) catalog_table.column("Games", width=70, anchor="center", stretch=False) catalog_table.column("ECO", width=50, anchor="w", stretch=False) catalog_table.column("Opening", width=150, anchor ="w", stretch=False) catalog_table.column("Variation", width=200, anchor="w", stretch=True) # Scrollbar scrollbar = ttk.Scrollbar( table_frame, orient="vertical", command=catalog_table.yview ) catalog_table.configure(yscrollcommand=scrollbar.set) catalog_table.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") # ------------------------- # Load catalog from JSON # ------------------------- if not os.path.exists("personal_catalog.json"): return with open("personal_catalog.json", "r", encoding="utf-8") as f: catalog = json.load(f) rows = [] total_games = 0 for group in catalog: if group == "cataloged_files": continue for eco, entry in catalog[group].items(): opening = entry.get("opening", "") variation = entry.get("variation", "") games = entry.get("frequency", 0) total_games += games rows.append(( games, eco, opening, variation )) # Highest frequency first rows.sort(key=lambda row: row[0], reverse=True) # Insert into table for games, eco, opening, variation in rows: catalog_table.insert( "", "end", values=( "☐", games, eco, opening, variation ) ) # Total games in catalog catalog_table.heading( "Select", text=str(total_games) ) reopen_btn.bind("", lambda event: toggle_sidebar()) def toggle_sidebar(): global sidebar_visible if sidebar_visible: left_frame.grid_remove() sidebar_footer.grid_remove() reopen_btn.configure(text="🮝") else: left_frame.grid() sidebar_footer.grid() reopen_btn.configure(text="🮟") reopen_btn.place(x=2, rely=1.0, y=0, anchor="sw") reopen_btn.lift() sidebar_visible = not sidebar_visible # ---------------------------- # File Menu # ---------------------------- file_menu = tk.Menu(menu_bar, tearoff=0) menu_bar.add_cascade( label="File", menu=file_menu ) file_menu.add_command( label="Import PGN...", command=import_pgn ) file_menu.add_command( label="Import FEN...", command=import_fen ) file_menu.add_command(label="Export PGN") file_menu.add_separator() file_menu.add_command( label="Clear Catalog", command=clear_catalog ) file_menu.add_separator() file_menu.add_command( label="Exit", command=app.quit ) # ---------------------------- # Edit Menu # ---------------------------- edit_menu = tk.Menu(menu_bar, tearoff=0) menu_bar.add_cascade( label="Edit", menu=edit_menu ) # ---------------------------- # View Menu # ---------------------------- view_menu = tk.Menu(menu_bar, tearoff=0) menu_bar.add_cascade( label="View", menu=view_menu ) view_menu.add_command( label="Show / Hide Sidebar", command=toggle_sidebar ) # ---------------------------- # Tools Menu # ---------------------------- tools_menu = tk.Menu(menu_bar, tearoff=0) menu_bar.add_cascade( label="Tools", menu=tools_menu ) tools_menu.add_command( label="Catalog", command=show_catalog_workspace ) tools_menu.add_command(label="Sort") tools_menu.add_command(label="Analyze") # ---------------------------- # Help Menu # ---------------------------- help_menu = tk.Menu(menu_bar, tearoff=0) menu_bar.add_cascade( label="Help", menu=help_menu ) # ---------------------------- # Left Navigation # ---------------------------- left_frame = ctk.CTkFrame( app, width=250, fg_color="#283858" #sidebar right and bottom padding color ) left_frame.grid( row=1, column=0, sticky="ns" ) left_frame.grid_propagate(False) # ---------------------------- # Tree Style # ---------------------------- style = ttk.Style() style.theme_use("default") # ---- Treeview body (for menu bar)---- style.configure( "Treeview", background="#172134", fieldbackground="#172134", #fix - Not field background, it is the background for the whole main body foreground="#FFFFFF", relief="flat", borderwidth=0, rowheight=24 ) # ---- Treeview headers (for the catalog) ---- style.configure( "Treeview.Heading", relief="solid", borderwidth=0, padding="2", background="#1f6aa5", foreground="#eeeeee" ) style.map( "Treeview", background=[("selected", "#c5c5c5")], foreground=[("selected", "#172134")] ) # ---- Disable ALL header interaction visuals ---- style.map( "Treeview.Heading", background=[("active", "#1f6aa5"), ("pressed", "#1f6aa5")], foreground=[("active", "#eeeeee"), ("pressed", "#eeeeee")] ) sidebar = ttk.Treeview( left_frame, style="Treeview", show="tree" ) sidebar.pack( fill="both", expand=True, padx=(0, 4), pady=(0, 4) ) sidebar_footer = ctk.CTkFrame( app, height=28, fg_color="#172134" #where the collapse area is in the footer ) sidebar_footer.grid( row=2, column=0, sticky="ew" ) # Root Items pgn_node = sidebar.insert( "", "end", text="PGN Collection", open=True ) catalog_node = sidebar.insert( "", "end", text="Catalog", open=True ) notes_node = sidebar.insert( "", "end", text="Notes", open=True ) def save_notes(): global notes_box print("Saving notes...") try: with open("notes.txt", "w") as f: f.write(notes_box.get("1.0", "end-1c")) except Exception as e: print(e) def on_tree_select(event): global notes_box item = sidebar.focus() text = sidebar.item(item, "text") if text != "Notes" and "notes_box" in globals(): save_notes() # Clear the workspace for widget in workspace.winfo_children(): widget.destroy() if text == "Notes": notes_box = ctk.CTkTextbox(workspace) notes_box.pack(fill="both", expand=True, padx=10, pady=10) try: with open("notes.txt", "r") as f: notes_box.insert("1.0", f.read()) except FileNotFoundError: pass elif text == "PGN Collection": label = ctk.CTkLabel( workspace, text="PGN Collection", font=("Arial", 24) ) label.pack(expand=True) elif text == "Catalog": show_catalog_workspace() sidebar.bind("<>", on_tree_select) # ---------------------------- # Workspace # ---------------------------- workspace = ctk.CTkFrame( app, fg_color="#172134" #main_workspace_background ) workspace.grid( row=1, column=1, sticky="nsew", padx=1, pady=1 ) label = ctk.CTkLabel( workspace, text="Workspace", font=("Arial", 24) ) label.pack( expand=True ) # ---------------------------- # Status Bar # ---------------------------- status = ctk.CTkLabel( app, text="Import PGN files ", anchor="e", height=26 ) status.grid( row=2, column=1, sticky="ew", padx=10 ) reopen_btn.lift() app.mainloop()

No comments:

Post a Comment