Wednesday, July 8, 2026
Green, Blue, Amber "inspection" criteria
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="#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="#3d6ca8",
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
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)."
)
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", "#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
)
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()
Subscribe to:
Post Comments (Atom)
-
**See 6/22 post for adjustments.** Code can be tested at https://www.programiz.com/python-programming/online-compiler/ (Remove the 3 lines ...
-
The code below starts the bidding process. It's possible now to add graphics, that might be next as well as continuing with picking up. ...
No comments:
Post a Comment