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()
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