Friday, July 10, 2026

Cleanup before creating modules / Start individual PGN reading layout

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 MAX_PLIES = 30 # ---------------------------- # 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", 20, "bold"), text_color = ("#dddddd") ) 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, import_button, catalog_button print(f"Opening {filename}") # Clear workspace for widget in workspace.winfo_children(): widget.destroy() label = ctk.CTkLabel( workspace, text=Path(filename).name, font=("Arial", 24) ) label.pack(pady=20) # 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=(1, 20) ) # Button frame button_frame = ctk.CTkFrame(workspace, fg_color="transparent") button_frame.pack(anchor="w", padx=20, pady=5) ACTIVE_BUTTON_COLOR = "#a51f3d" ACTIVE_BUTTON_HOVER = "#8c1933" # Import, Add to Catalog, Catalog buttons add_catalog_button = ctk.CTkButton( button_frame, text="Add to Catalog", command=catalog_pgns, #fg_color = "#a51f3d", hover_color = ACTIVE_BUTTON_HOVER, text_color = "white" ) add_catalog_button.pack(side="left", padx=(10, 0)) import_button = ctk.CTkButton( button_frame, text="Import", command=import_pgn, #fg_color = "#a51f3d", hover_color = ACTIVE_BUTTON_HOVER, text_color = "white" ) import_button.pack(side="left", padx=(10, 0)) catalog_button = ctk.CTkButton( button_frame, text="Catalog", command=show_catalog_workspace, #fg_color = "#a51f3d", hover_color = ACTIVE_BUTTON_HOVER, text_color = "white" ) catalog_button.pack(side="left", padx=(10, 0)) add_catalog_button.configure(state="normal") import_button.configure(state="disabled") catalog_button.configure(state="disabled") # 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="#68aa68", hover_color="#347434", border_color="#68aa68", checkmark_color="#346834", text_color="#aabbaa" ).pack(anchor="w", pady=4) # ---------- Column 2 ---------- ctk.CTkLabel( other_data_frame, text="", height=28 ).pack(anchor="w", pady=(0, 10)) # 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) # Amber if the tag is unknown if tag in unknown_tags: ctk.CTkCheckBox( other_data_frame, text=tag, variable=other_data_vars[tag], fg_color="#d98c00", hover_color="#b36f00", border_color="#d98c00", checkmark_color="#8a5600", text_color="#ffcc66" ).pack(anchor="w", pady=4) # Blue if the tag is recognized else: ctk.CTkCheckBox( other_data_frame, text=tag, variable=other_data_vars[tag], fg_color="#1f6aa5", hover_color="#2d5a90", border_color="#3d6ca8", checkmark_color="#214c7a", text_color="#82b8ff" ).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 pgn_item = sidebar.insert( pgn_node, "end", text=short_name, open=False ) sidebar.insert( pgn_item, "end", text="Players" ) sidebar.insert( pgn_item, "end", text="Event" ) sidebar.insert( pgn_item, "end", text="Result" ) sidebar.insert( pgn_item, "end", text="ECO" ) status.configure( text=f"Imported: {short_name}" ) status.configure( text=f"Imported: {short_name}" ) def import_fen(): print("Import FEN selected") #Showing the whole pgn file data def format_continuation(moves, start_ply=1): lines = [] move_number = (start_ply + 1) // 2 for i in range(0, len(moves), 2): white = moves[i] black = "" if i + 1 < len(moves): black = moves[i + 1] lines.append(f"{move_number}. {white} {black}".strip()) move_number += 1 return lines def show_pgn_workspace(filename): preview_lookup = {} # Clear workspace for widget in workspace.winfo_children(): widget.destroy() # Title title = ctk.CTkLabel( workspace, text=Path(filename).name, font=("Arial", 24) ) title.pack(pady=10) # Create PGN tree pgn_tree = ttk.Treeview( workspace, show="tree" ) pgn_tree.pack( fill="both", expand=True, padx=10, pady=10 ) #### After the tree moves_box = ctk.CTkTextbox( workspace, height=250, wrap="word" ) moves_box.pack( fill="x", padx=15, pady=(0, 15) ) def toggle_game(event): item = pgn_tree.identify_row(event.y) if not item: return text = pgn_tree.item(item, "text") # Only toggle game rows if text.startswith("□ "): new_text = text.replace("□ ", "■ ", 1) pgn_tree.item( item, text=new_text ) elif text.startswith("■ "): new_text = text.replace("■ ", "□ ", 1) pgn_tree.item( item, text=new_text ) pgn_tree.bind( "", toggle_game ) # Read PGN games with open(filename, encoding="utf-8") as pgn_file: game_number = 1 while True: game = chess.pgn.read_game(pgn_file) if game is None: break headers = game.headers white = headers.get("White", "?") black = headers.get("Black", "?") # Add information below game headers = game.headers game_node = pgn_tree.insert( "", "end", text=f"□ Game {game_number}: {white} - {black}" ) board = game.board() san_moves = [] for move in game.mainline_moves(): san_moves.append(board.san(move)) board.push(move) preview_moves = san_moves[:MAX_PLIES] preview_lines = format_continuation( preview_moves, 0 ) preview_text = " ".join(preview_lines) preview_lookup[game_node] = preview_text game_number += 1 # ---------------------------- # Menu Bar # ---------------------------- menu_bar = tk.Menu(app) app.config(menu=menu_bar) def catalog_pgns(): global add_catalog_button, import_button, catalog_button add_catalog_button.configure(text="Add to Catalog") add_catalog_button.configure(state="disabled") import_button.configure(state="normal") catalog_button.configure(state="normal") selected_games = set() 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)." ) 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(): global add_catalog_button, import_button, catalog_button # 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 ) # ---------------------------- # Sidebar # ---------------------------- 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 sidebar)---- style.configure( "Treeview", background="#172134", fieldbackground="#172134", #fix - Not field background, it is the background for the whole main body foreground="#88AAEE", 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", "#346934")], foreground=[("selected", "#dddddd")] ) # ---- 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) mixed_collections_node = sidebar.insert("", "end", text="Mixed Collections", 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") parent = sidebar.parent(item) # User clicked a PGN file if parent == pgn_node: show_pgn_workspace(text) return # User clicked one of the PGN's categories if sidebar.parent(parent) == pgn_node: return print("Item:", item) print("Text:", text) print("Parent:", parent) print("Clicked:", 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