Monday, July 6, 2026

Before dividing into multiple .py files and with color scheme mostly done

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 with open(filename, encoding="utf-8") as pgn_file: game = chess.pgn.read_game(pgn_file) if game is None: return game_data_tags = ["Event", "Site", "Date", "Round", "White", "Black", "FEN", "Result"] other_data_tags = [] headers = game.headers # 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) tags_list = list(headers.keys()) # 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] ).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 = [ "WhiteTitle", "BlackTitle", "WhiteElo", "BlackElo", "WhiteFideId", "BlackFideId", "EventDate", "EventType", ] other_data_vars = {} for tag in other_data_tags: other_data_vars[tag] = tk.BooleanVar(value=False) 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(): filename = filedialog.askopenfilename( title="Import PGN", filetypes=[("PGN Files", "*.pgn")] ) if not filename: return show_import_workspace(filename) # 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}" ) # ---------------------------- # 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() # Title title = ctk.CTkLabel( workspace, text="Opening Catalog", font=("Arial", 24, "bold") ) title.pack(anchor="w", padx=20, pady=(15, 10)) # ----- 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="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