Menus

File with implementation for all menus: MoveRecord, Options and Settings.

   1"""File with implementation for all menus: MoveRecord, Options and Settings.
   2"""
   3
   4from fontTools.ttLib import TTFont
   5from typing import Callable, Any
   6import customtkinter as ctk
   7import subprocess
   8import os
   9import re
  10import pywinstyles
  11
  12from tools import (
  13    get_from_config,
  14    change_config,
  15    load_menu_image,
  16    resource_path,
  17    change_color,
  18    update_error_log,
  19    create_save_file,
  20    delete_save_file,
  21    get_save_info
  22)
  23from properties import COLOR, STRING, SYSTEM
  24from notifications import Notification
  25from color_picker import ColorPicker
  26from piece import Piece, Knight
  27
  28class MovesRecord(ctk.CTkFrame):
  29    """Class handling recording the moves during playtime. Class stores both players moves in lists and displays notation in two boxes dedicated for each player.
  30
  31    Args:
  32        ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget. 
  33    """
  34    def __init__(self, master) -> None:
  35        """Constructor:
  36             - calls function create_frames
  37             - creates 2D vector to record moves
  38
  39        Args:
  40            master (Any): Parent widget
  41        """
  42        super().__init__(master, fg_color=COLOR.BACKGROUND)
  43        self.font: ctk.CTkFont = ctk.CTkFont(str(get_from_config('font_name')), int(int(get_from_config('size')) * 0.4))
  44        self.create_frames()
  45        self.moves_white: list[str] = []
  46        self.moves_black: list[str] = []
  47
  48    def record_move(self, moved_piece: Piece, previous_coords: tuple[int, int] | None=None, capture: bool=False,
  49                    castle: str | None=None, check: bool=False, checkmate: bool=False, promotion: str='') -> None:
  50        """Displays the chess notation of the move on the frame for specific player color.
  51        Simple if else logic with flags passed to the function is responsible of handling correctness of the notation.
  52
  53        Args:
  54            moved_piece (Piece): Figure which was moved
  55            previous_coords (tuple[int, int] | None, optional): Coordinates of position before moving the figure. Defaults to None.
  56            capture (bool, optional): Flag to check if figure captured another figure. Defaults to False.
  57            castle (str | None, optional): Flag to check if castle occurred. Defaults to None.
  58            check (bool, optional): Checks if move caused the check. Defaults to False.
  59            checkmate (bool, optional): Checks if move caused the checkmate. Defaults to False.
  60            promotion (str, optional): Checks if pawn was promoted. Defaults to '' which means the promotion didn't occurred.
  61        """
  62        y_axis: list[str] = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
  63        x, y = 8 - moved_piece.position[0], y_axis[moved_piece.position[1]]
  64        prev_x = 8 - previous_coords[0] if previous_coords else ''
  65        prev_y = y_axis[previous_coords[1]] if previous_coords else ''
  66        if not isinstance(moved_piece, Knight):
  67            piece_name = moved_piece.__class__.__name__[0] if not moved_piece.__class__.__name__ == 'Pawn' else ''
  68        else:
  69            piece_name = 'N'
  70        check_nota: str = '+' if check and not checkmate else ''
  71        checkmate_nota: str = '#' if checkmate else ''
  72        promotion_nota: str = promotion if promotion != 'K' else 'N'
  73        if not castle:
  74            notation = f' {check_nota}{checkmate_nota}{'x' if capture else ''}{piece_name}{prev_y}{prev_x}-{y}{x}{promotion_nota}'
  75        else:
  76            notation = f' {check_nota}{checkmate_nota}{'0-0-0' if castle == 'queenside' else '0-0'}'
  77        current_frame = self.white_scroll_frame if moved_piece.color == 'w' else self.black_scroll_frame
  78        self.moves_white.append(notation) if moved_piece.color == 'w' else self.moves_black.append(notation)
  79        ctk.CTkLabel(
  80            master = current_frame,
  81            text   = notation,
  82            font   = self.font
  83        ).pack(side=ctk.BOTTOM)
  84
  85    def load_notation_from_save(self, white_moves: list[str], black_moves: list[str]) -> None:
  86        """Loads notation from save file. Function gets already parsed json format to two lists and displays it using record_move() function.
  87
  88        Args:
  89            white_moves (list[str]): List of previous white moves.
  90            black_moves (list[str]): List of previous white moves.
  91        """
  92        for notation in white_moves:
  93            ctk.CTkLabel(
  94                master = self.white_scroll_frame,
  95                text   = notation,
  96                font   = self.font
  97            ).pack(side=ctk.BOTTOM)
  98        for notation in black_moves:
  99            ctk.CTkLabel(
 100                master = self.black_scroll_frame,
 101                text   = notation,
 102                font   = self.font
 103            ).pack(side=ctk.BOTTOM)
 104        self.moves_white[:] = white_moves
 105        self.moves_black[:] = black_moves
 106
 107    def create_frames(self) -> None:
 108        """Creates frames to reserve space on main app page for displaying move notations.
 109        """
 110        black_label: ctk.CTkLabel = ctk.CTkLabel(
 111            master     =  self,
 112            text       = 'Black',
 113            font       = self.font,
 114            text_color = COLOR.DARK_TEXT
 115        )
 116        black_label.pack(side=ctk.TOP, padx=1, pady=1)
 117        additional_frame: ctk.CTkFrame = ctk.CTkFrame(
 118            master        = self, 
 119            fg_color      = COLOR.TRANSPARENT, 
 120            corner_radius = 0,
 121            border_color  = COLOR.DARK_TEXT, 
 122            border_width  = 7
 123        )
 124        additional_frame.pack(side=ctk.TOP, padx=15, expand=True, fill=ctk.Y)
 125        self.black_scroll_frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(
 126            master                       = additional_frame, 
 127            scrollbar_button_color       = COLOR.NOTATION_BACKGROUND_B,
 128            fg_color                     = COLOR.NOTATION_BACKGROUND_B, 
 129            corner_radius                = 0,
 130            scrollbar_button_hover_color = COLOR.NOTATION_BACKGROUND_B
 131        )
 132        self.black_scroll_frame.pack(side=ctk.TOP, padx=6, pady=7, fill=ctk.Y, expand=True)
 133        white_label: ctk.CTkLabel = ctk.CTkLabel(
 134            master     = self, 
 135            text       = 'White', 
 136            font       = self.font, 
 137            text_color = COLOR.TEXT
 138        )
 139        white_label.pack(side=ctk.TOP, padx=0, pady=0)
 140        additional_frame = ctk.CTkFrame(
 141            master = self,
 142            fg_color=COLOR.TRANSPARENT,
 143            corner_radius=0,
 144            border_color=COLOR.DARK_TEXT,
 145            border_width=7
 146        )
 147        additional_frame.pack(side=ctk.TOP, padx=15, expand=True, fill=ctk.Y)
 148        self.white_scroll_frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(
 149            master                       = additional_frame,
 150            scrollbar_button_color       = COLOR.NOTATION_BACKGROUND_W,
 151            fg_color                     = COLOR.NOTATION_BACKGROUND_W,
 152            corner_radius                = 0,
 153            scrollbar_button_hover_color = COLOR.NOTATION_BACKGROUND_W)
 154        self.white_scroll_frame.pack(side=ctk.TOP, padx=6, pady=7, fill=ctk.Y, expand=True)
 155        space_label: ctk.CTkLabel = ctk.CTkLabel(
 156            master =self,
 157            text='\n'
 158        )
 159        space_label.pack()
 160
 161    def restart(self) -> None:
 162        """Destroys the old notated moves and clears the lists.
 163        """
 164        self.moves_white.clear()
 165        self.moves_black.clear()
 166        for child in self.white_scroll_frame.winfo_children():
 167            child.destroy()
 168        for child in self.black_scroll_frame.winfo_children():
 169            child.destroy()
 170
 171class Saves(ctk.CTkFrame):
 172    """Class handling saving, showing and loading saves in separate menu.
 173
 174    Args:
 175        ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget.
 176    """
 177    def __init__(self, master: Any, board) -> None:
 178        """Constructor:
 179         - loads fonts
 180         - calls function showing all saves
 181
 182        Args:
 183            master (Any): Parent widget.
 184            board (Board): Board object.
 185        """
 186        super().__init__(master, fg_color=COLOR.BACKGROUND)
 187        self.font_32 = ctk.CTkFont(get_from_config('font_name'), 32)
 188        self.font_26 = ctk.CTkFont(get_from_config('font_name'), 26)
 189        self.close_image: ctk.CTkImage | None = load_menu_image('close')
 190        self.show_all_saves(board)
 191        ctk.CTkLabel(
 192            master   = self,
 193            text     = '',
 194            height   = 18,
 195            fg_color = COLOR.BACKGROUND
 196        ).pack(padx=0, pady=0)
 197
 198    @staticmethod
 199    def save_game_to_file(board) -> bool:
 200        """Saves the current game state to the .json file in saves folder.
 201
 202        Args:
 203            board (Board): Board object.
 204
 205        Returns:
 206            bool: Returns True if save was created successfully, False otherwise.
 207        """
 208        save_info: dict[tuple[int, int] | str, tuple[str, str, bool] | list[str]] = dict()
 209        for row in board.board:
 210            for cell in row:
 211                if cell.figure:
 212                    figure: str = cell.figure.__class__.__name__
 213                    save_info[cell.position] = (figure, cell.figure.color, cell.figure.first_move)
 214        if not board.current_save_name:
 215            save_name: str | None | bool = SaveName().get_save_name()
 216            board.current_save_name = save_name
 217        else:
 218            save_name = board.current_save_name.strip('.json')
 219        moves_record: MovesRecord = board.moves_record
 220        if not isinstance(save_name, bool):
 221            create_save_file(
 222                save_info,
 223                board.current_turn,
 224                moves_record.moves_white,
 225                moves_record.moves_black,
 226                board.game_over,
 227                save_name
 228            )
 229            return True
 230        return False
 231
 232    def show_all_saves(self, board) -> None:
 233        """Displays all saves as clickable buttons in saves menu.
 234
 235        Args:
 236            board (Board): Board object.
 237        """
 238        top_frame: ctk.CTkFrame = ctk.CTkFrame(
 239            master   = self,
 240            fg_color = COLOR.TRANSPARENT
 241        )
 242        top_frame.pack(side=ctk.TOP, padx=0, pady=0, fill=ctk.X)
 243        settings_text = ctk.CTkLabel(
 244            master     = top_frame,
 245            text       = 'Saves',
 246            font       = ctk.CTkFont(str(get_from_config('font_name')), 38),
 247            text_color = COLOR.DARK_TEXT,
 248            anchor     = ctk.N
 249        )
 250        settings_text.pack(side=ctk.LEFT, padx=20, anchor=ctk.NW)
 251        close_button = ctk.CTkLabel(
 252            master = top_frame,
 253            text   = '',
 254            font   = ctk.CTkFont(str(get_from_config('font_name')), 24),
 255            image  = self.close_image,
 256            anchor = ctk.S
 257        )
 258        close_button.bind('<Button-1>', self.on_close)
 259        close_button.pack(side=ctk.RIGHT, anchor=ctk.NE, padx=10, pady=10)
 260        self.scrollable_frame = ctk.CTkScrollableFrame(
 261            master   = self,
 262            fg_color = COLOR.BACKGROUND,
 263            corner_radius = 0,
 264            scrollbar_button_color = COLOR.DARK_TEXT,
 265        )
 266        self.scrollable_frame.pack(side=ctk.TOP, padx=0, pady=0, expand=True, fill=ctk.BOTH)
 267        files: list[str] = [f for f in os.listdir(resource_path('saves'))]
 268        for file in files:
 269            self.create_file_button(self.scrollable_frame, file, board)
 270
 271    def create_file_button(self, frame: ctk.CTkFrame, file_name: str, board) -> None:
 272        """Helper function creating single button which will load specific save after clicking.
 273
 274        Args:
 275            frame (ctk.CTkFrame): Parent widget.
 276            file_name (str): Name of the file.
 277            board (Board): Board object.
 278        """
 279        helper_frame = ctk.CTkFrame(
 280            master        = frame,
 281            fg_color      = COLOR.TILE_1,
 282            corner_radius = 0
 283        )
 284        helper_frame.pack(side=ctk.TOP, padx=150, pady=10, fill=ctk.X)
 285        ctk.CTkLabel(
 286            master   = helper_frame,
 287            fg_color = COLOR.NOTATION_BACKGROUND_B,
 288            text     = '',
 289            width    = 20
 290        ).pack(side=ctk.LEFT, padx=0, pady=0, fill=ctk.Y)
 291        file_name_label = ctk.CTkLabel(
 292            master        = helper_frame,
 293            text          = f' {file_name.replace('.json', '')}',
 294            fg_color      = COLOR.TILE_1,
 295            font          = self.font_32,
 296            corner_radius = 0,
 297            anchor        = ctk.W
 298        )
 299        file_name_label.bind('<Button-1>', lambda e: self.load_save(e, board, file_name))
 300        file_name_label.pack(side=ctk.LEFT, padx=15, pady=0, fill=ctk.BOTH, expand=True)
 301        delete_button = ctk.CTkButton(
 302            master        = helper_frame,
 303            fg_color      = COLOR.CLOSE,
 304            hover_color   = COLOR.CLOSE_HOVER,
 305            command       = lambda: self.remove_save(file_name, helper_frame),
 306            text          = 'REMOVE',
 307            font          = self.font_26,
 308            corner_radius = 0
 309        )
 310        delete_button.pack(side=ctk.RIGHT, padx=10, pady=10, anchor=ctk.N)
 311
 312    def remove_save(self, file_name: str, frame: ctk.CTkFrame) -> None:
 313        """Deletes specific save. Button is part of the save button which makes it easier for user to determine which save is being deleted.
 314
 315        Args:
 316            file_name (str): Name of the file to be deleted.
 317            frame (ctk.CTkFrame): Parent widget.
 318        """
 319        if delete_save_file(file_name):
 320            frame.destroy()
 321            Notification(self.master, f'Save {file_name.replace('.json', '')} has been removed', 2, 'top')
 322        else:
 323            Notification(self.master, 'Couldn\'t remove the save', 2, 'top')
 324
 325    def load_save(self, event: Any, board, file_name: str) -> None:
 326        """Helper function calling all necessary functions to load the game from save. Notifications will indicate if it was successful or not.
 327
 328        Args:
 329            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
 330            board (Board): Board object.
 331            file_name (str): Name of the file from which the game will be loaded.
 332        """
 333        if board.load_board_from_file(get_save_info(file_name), file_name):
 334            Notification(self.master, 'Save loaded successfully', 3, 'top')
 335            self.master.after(201, self.on_close)
 336        else:
 337            Notification(self.master, 'Couldn\'t load save', 2, 'top')
 338
 339    def on_close(self, event: Any=None) -> None:
 340        """Custom close function handling slow fade out animation.
 341
 342        Args:
 343            event (Any, optional): Event type. Doesn't matter but is required parameter by customtkinter.. Defaults to None.
 344        """
 345        def update_opacity(i: int) -> None:
 346            if i >= 0:
 347                pywinstyles.set_opacity(self, value=i*0.005, color='#000001')
 348                self.master.after(1, lambda: update_opacity(i - 1))
 349            else:
 350                self.after(10, self.destroy)
 351        update_opacity(200)
 352
 353class Options(ctk.CTkFrame):
 354    """Class handling user interface with available options on main window frame:
 355     - customization settings
 356     - restarting game
 357     - saving game
 358     - loading game
 359
 360    Args:
 361        ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget.
 362    """
 363    def __init__(self, master, restart_func: Callable, update_assets_func: Callable, update_font_func: Callable, get_board_func: Callable):
 364        """Constructor:
 365             - places all options buttons
 366             - loads menu assets
 367             - calls all necessary setup functions
 368
 369        Args:
 370            master (Any): Parent widget.
 371            restart_func (Callable): Master function to restart the game.
 372            update_assets_func (Callable): Master function to update assets.
 373            update_font_func (Callable): Master function to update font.
 374        """
 375        super().__init__(master, fg_color=COLOR.BACKGROUND)
 376        self.restart_func: Callable = restart_func
 377        self.update_assets_func: Callable = update_assets_func
 378        self.update_font_func: Callable = update_font_func
 379        self.get_board_func: Callable = get_board_func
 380        self.setting_icon: ctk.CTkImage | None = load_menu_image('settings')
 381        self.replay_icon: ctk.CTkImage | None = load_menu_image('replay')
 382        self.saves_image: ctk.CTkImage | None = load_menu_image('saves')
 383        self.save_as_image: ctk.CTkImage | None = load_menu_image('save_as')
 384        self.settings: Settings | None = None
 385        self.saves: Saves | None = None
 386        self.setting_button()
 387        self.space_label()
 388        self.replay_button()
 389        self.space_label()
 390        self.save_button()
 391        self.space_label()
 392        self.load_saves_button()
 393
 394    def setting_button(self) -> None:
 395        """Setup of setting button.
 396        """
 397        self.s_icon_label: ctk.CTkLabel = ctk.CTkLabel(self, text='', image=self.setting_icon)
 398        self.s_icon_label.pack(side=ctk.TOP, padx=10, pady=5)
 399        self.s_icon_label.bind('<Button-1>', self.open_settings)
 400
 401    def replay_button(self) -> None:
 402        """Setup of replay button.
 403        """
 404        self.r_icon_label: ctk.CTkLabel = ctk.CTkLabel(
 405            master =  self,
 406            text   = '',
 407            image  = self.replay_icon)
 408        self.r_icon_label.pack(side=ctk.TOP, padx=10, pady=0)
 409        self.r_icon_label.bind('<Button-1>', self.replay)
 410
 411    def save_button(self) -> None:
 412        """Setup of save button.
 413        """
 414        self.save_icon_label: ctk.CTkLabel = ctk.CTkLabel(
 415            master = self,
 416            text   = '',
 417            image  = self.save_as_image
 418        )
 419        self.save_icon_label.pack(side=ctk.TOP, padx=10, pady=0)
 420        self.save_icon_label.bind('<Button-1>', self.save_game)
 421
 422    def load_saves_button(self) -> None:
 423        """Setup of button showing all saves.
 424        """
 425        self.load_icon_label: ctk.CTkLabel = ctk.CTkLabel(
 426            master = self,
 427            text   = '',
 428            image  = self.saves_image 
 429        )
 430        self.load_icon_label.pack(side=ctk.TOP, padx=10, pady=0)
 431        self.load_icon_label.bind('<Button-1>', self.load_saves)
 432
 433    def space_label(self) -> None:
 434        """Setups of space to maintain spacing between the button.
 435        """
 436        space: ctk.CTkLabel = ctk.CTkLabel(
 437            master = self,
 438            text   = '\n')
 439        space.pack(padx=2, pady=2)
 440
 441    def open_settings(self, event: Any) -> None:
 442        """Function opening settings menu. For optimizations the settings frame is not being destroyed, but is hidden,
 443        it has no impact on user experience as all changes are dynamic and app restart wont be required to see the changes.
 444
 445        Args:
 446            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
 447        """
 448        if self.settings:
 449            self.settings.place(relx=0, rely=0, relwidth=1, relheight=1)
 450        else:
 451            self.settings = Settings(self.master, self.restart_func, self.update_assets_func, self.update_font_func)
 452
 453    def replay(self, event: Any) -> None:
 454        """Function restarting the game. Calls function passed from Board to restart state of the game.
 455
 456        Args:
 457            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
 458        """
 459        self.after(1, self.restart_func)
 460
 461    def save_game(self, event: Any) -> None:
 462        """Saves game to .json file and displays notification if successful.
 463
 464        Args:
 465            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
 466        """
 467        if Saves.save_game_to_file(self.get_board_func()):
 468            Notification(self.master, 'Save was created successfully', 2, 'top')
 469
 470    def load_saves(self, event: Any) -> None:
 471        """Function opening saves menu. To always get all saves even these created during app runtime it has to be created every time from scratch to avoid bugs and unintended behavior.
 472
 473        Args:
 474            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
 475        """
 476        self.saves = Saves(self.master, self.get_board_func())
 477        self.saves.place(relx=0, rely=0, relwidth=1, relheight=1)
 478
 479class Settings(ctk.CTkFrame):
 480    """Class handling changes in setting such as fonts, assets and colors made by user.
 481    Handles saving and updating the changes during app runtime except for color changes as they could take too much time for smooth experience.
 482
 483    Args:
 484        ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget.
 485    """
 486    def __init__(self, master, restart_func: Callable, update_assets_func: Callable, update_font_func: Callable) -> None:
 487        """Constructor
 488         - places itself on the screen
 489         - calls all functions creating frames containing content
 490
 491        Args:
 492            master (Any): Parent widget.
 493            restart_func (Callable): Master function to restart the game.
 494            update_assets_func (Callable): Master function to update assets.
 495            update_font_func (Callable): Master function to update font.
 496        """
 497        super().__init__(master, fg_color=COLOR.BACKGROUND, corner_radius=0)
 498        self.place(relx=0, rely=0, relwidth=1, relheight=1)
 499        self.close_image: ctk.CTkImage | None = load_menu_image('close')
 500        self.color_picker_image: ctk.CTkImage | None = load_menu_image('colorpicker', resize=2)
 501        self.close_button()
 502        self.font_30: ctk.CTkFont = ctk.CTkFont(str(get_from_config('font_name')), 30)
 503        self.scrollable_frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(self, corner_radius=0, fg_color=COLOR.BACKGROUND,
 504                                                        scrollbar_button_color=COLOR.DARK_TEXT)
 505        self.scrollable_frame.pack(side=ctk.TOP, padx=0, pady=0, fill=ctk.BOTH, expand=True)
 506        self.font_name: str = str(get_from_config('font_name'))
 507        self.choose_theme()
 508        self.choose_font()
 509        self.open_assets_folder()
 510        self.change_colors()
 511        self.previous_theme: str | None = None
 512        self.choice: str | None = None
 513        self.restart_func: Callable = restart_func
 514        self.update_assets_func: Callable = update_assets_func
 515        self.update_font_func: Callable = update_font_func
 516        ctk.CTkLabel(
 517            master   = self,
 518            text     = '',
 519            height   = 18,
 520            fg_color = COLOR.BACKGROUND
 521        ).pack(padx=0, pady=0)
 522
 523    @staticmethod
 524    def list_directories_os(path: str) -> list[str]:
 525        """Lists all directories for given path.
 526
 527        Args:
 528            path (str): Desired path.
 529
 530        Returns:
 531            list[str]: List of all directories from path.
 532        """
 533        try:
 534            entries: list[str] = os.listdir(path)
 535            directories: list[str] = [
 536                entry for entry in entries
 537                if os.path.isdir(os.path.join(path, entry)) and os.listdir(os.path.join(path, entry))
 538            ]
 539            return directories
 540        except FileNotFoundError as e:
 541            update_error_log(e)
 542            return []
 543
 544    def close_button(self) -> None:
 545        """Setup of close button.
 546        """
 547        top_frame: ctk.CTkFrame = ctk.CTkFrame(
 548            master   = self,
 549            fg_color = COLOR.TRANSPARENT
 550        )
 551        top_frame.pack(side=ctk.TOP, padx=0, pady=0, fill=ctk.X)
 552        settings_text = ctk.CTkLabel(
 553            master     = top_frame,
 554            text       = 'Settings',
 555            font       = ctk.CTkFont(str(get_from_config('font_name')), 38),
 556            text_color = COLOR.DARK_TEXT,
 557            anchor     = ctk.N
 558        )
 559        settings_text.pack(side=ctk.LEFT, padx=20, anchor=ctk.NW)
 560        close_button = ctk.CTkLabel(
 561            master = top_frame,
 562            text   = '',
 563            font   = ctk.CTkFont(str(get_from_config('font_name')), 24),
 564            image  = self.close_image,
 565            anchor = ctk.S
 566        )
 567        close_button.bind('<Button-1>', self.on_close)
 568        close_button.pack(side=ctk.RIGHT, anchor=ctk.NE, padx=10, pady=10)
 569
 570    def create_theme_button(self, frame: ctk.CTkFrame, theme: str) -> None:
 571        """Setup of theme button.
 572
 573        Args:
 574            frame (ctk.CTkFrame): Frame in which button will be placed.
 575            theme (str): Style of Figures to choose.
 576        """ 
 577        current_theme = get_from_config('theme')
 578        theme_button: ctk.CTkButton = ctk.CTkButton(
 579            master        = frame,
 580            text          = theme,
 581            command       = lambda: self.select_theme(theme, theme_button),
 582            font          = self.font_30,
 583            corner_radius = 0,
 584            fg_color      = COLOR.TILE_1,
 585            hover_color   = COLOR.HIGH_TILE_2,
 586            text_color    = COLOR.TEXT,
 587        )
 588        theme_button.pack(side=ctk.LEFT, padx=4, pady=4, expand=True)
 589        if current_theme == theme:
 590            theme_button.configure(state=ctk.DISABLED)
 591
 592    def choose_theme(self) -> None:
 593        """Setup of theme chooser.
 594        """
 595        self.previous_theme = str(get_from_config('theme'))
 596        themes: list[str] = self.list_directories_os('assets')
 597        if not themes:
 598            return
 599        text: ctk.CTkLabel = ctk.CTkLabel(
 600            master     = self.scrollable_frame,
 601            text       = 'Themes: ',
 602            font       = ctk.CTkFont(str(get_from_config('font_name')), 32),
 603            text_color = COLOR.TEXT
 604        )
 605        text.pack(side=ctk.TOP, anchor=ctk.SW, padx=75, pady=0)
 606        themes.remove('menu') if 'menu' in themes else themes
 607        frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(
 608            master                 = self.scrollable_frame,
 609            fg_color               = COLOR.TILE_2,
 610            scrollbar_button_color = COLOR.DARK_TEXT,
 611            orientation            = ctk.HORIZONTAL,
 612            scrollbar_fg_color     = COLOR.DARK_TEXT,
 613            height                 = 70,
 614            corner_radius          = 0
 615        )
 616        frame.pack(side=ctk.TOP, padx=80, pady=5, anchor=ctk.W, fill=ctk.X)
 617        for theme in themes:
 618            self.create_theme_button(frame, theme)
 619        warning_text: ctk.CTkLabel = ctk.CTkLabel(
 620            master     = self.scrollable_frame,
 621            text       = STRING.ASSETS_WARNING,
 622            font       = ctk.CTkFont(str(get_from_config('font_name')), 18),
 623            text_color = COLOR.CLOSE
 624        )
 625        warning_text.pack(side=ctk.TOP, anchor=ctk.SW, padx=100, pady=0)
 626
 627    def select_theme(self, choice: str, button: ctk.CTkButton) -> None:
 628        """Helper function to save theme changes to config file.
 629
 630        Args:
 631            choice (str): Name of theme to save.
 632        """
 633        self.choice = choice
 634        theme = get_from_config('theme')
 635        for child in button.master.winfo_children():
 636            if isinstance(child, ctk.CTkButton) and child.cget('text') == theme:
 637                child.configure(state=ctk.NORMAL)
 638            elif isinstance(child, ctk.CTkButton) and child.cget('text') == choice:
 639                child.configure(state=ctk.DISABLED)
 640        change_config('theme', choice)
 641
 642    def on_close(self, event: Any) -> None:
 643        """Waits for close action to properly destroy the window with fade out animation.
 644
 645        Args:
 646            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
 647        """
 648        def update_opacity(i: int) -> None:
 649            if i >= 0:
 650                pywinstyles.set_opacity(self, value=i*0.005, color='#000001')
 651                self.master.after(1, lambda: update_opacity(i - 1))
 652            else:
 653                if not self.previous_theme and not self.choice:
 654                    self.place_forget()
 655                self.update_assets_func()
 656                self.place_forget()
 657                pywinstyles.set_opacity(self, value=1, color='#000001')
 658        update_opacity(200)
 659
 660    @staticmethod
 661    def open_file_explorer(path: str) -> None:
 662        """Opens file explorer with system call specific to user operating system.
 663
 664        Args:
 665            path (str): Path to open.
 666        """
 667        if SYSTEM == 'Windows':
 668            os.startfile(resource_path(path))
 669        elif SYSTEM == 'Darwin':
 670            subprocess.run(['open', resource_path(path)])
 671        elif SYSTEM == 'Linux':
 672            subprocess.run(['xdg-open', resource_path(path)])
 673
 674    @staticmethod
 675    def get_all_files(path: str) -> list[str]:
 676        """Gathers all files from directory. If error occurs after catching the exception empty list is returned.
 677
 678        Args:
 679            path (str): Path of the desired directory.
 680
 681        Returns:
 682            list[str]: List of all file names from path directory.
 683
 684        Exceptions:
 685            FileNotFoundError: If the directory does not exist.
 686            PermissionError: If access to the directory is denied.
 687            OSError: If an OS-related error occurs.
 688        """
 689        path = resource_path(path)
 690        try:
 691            all_files = [os.path.join((path), f) for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
 692            return all_files
 693        except (FileNotFoundError, PermissionError, OSError) as e:
 694            update_error_log(e)
 695            return []
 696
 697    @staticmethod
 698    def get_font_name(ttf_path: str) -> str | None:
 699        """Gets name of the font from font file.
 700
 701        Args:
 702            ttf_path (str): Path to .ttf font file name.
 703
 704        Returns:
 705            str | None: Returns font name on success otherwise None.
 706        """
 707        try:
 708            font: TTFont = TTFont(resource_path(ttf_path))
 709            name: str = ''
 710            for record in font['name'].names:
 711                if record.nameID == 4:
 712                    if b'\000' in record.string:
 713                        name = record.string.decode('utf-16-be')
 714                    else:
 715                        name = record.string.decode('utf-8')
 716                    break
 717            return name
 718        except Exception as e: # dont really know what kind of error might occur here
 719            update_error_log(e)
 720            return None
 721
 722    def open_assets_folder(self) -> None:
 723        """Setup of open assets button.
 724        """
 725        text_label = ctk.CTkLabel(
 726            master     = self.scrollable_frame,
 727            text       = 'Open assets folder',
 728            text_color = COLOR.TEXT,
 729            font       = ctk.CTkFont(str(get_from_config('font_name')), 32)
 730        )
 731        text_label.pack(side=ctk.TOP, padx=75, pady=4, anchor=ctk.NW)
 732        additional_frame = ctk.CTkFrame(
 733            master        = self.scrollable_frame,
 734            fg_color      = COLOR.TILE_2,
 735            corner_radius = 0
 736        )
 737        additional_frame.pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)
 738        open_button = ctk.CTkButton(
 739            master        = additional_frame,
 740            text          = 'OPEN',
 741            font          = ctk.CTkFont(str(get_from_config('font_name')), 20),
 742            text_color    = COLOR.TEXT,
 743            command       = lambda: self.open_file_explorer('assets'),
 744            fg_color      = COLOR.TILE_1,
 745            hover_color   = COLOR.HIGH_TILE_2,
 746            corner_radius = 0
 747        )
 748        open_button.pack(side=ctk.RIGHT, padx=10, pady=4, anchor=ctk.E)
 749        path_text = ctk.CTkLabel(
 750            master     = additional_frame, 
 751            text       = resource_path('assets'), 
 752            text_color = COLOR.DARK_TEXT,
 753            font       = ctk.CTkFont(str(get_from_config('font_name')), 18)
 754        )
 755        path_text.pack(side=ctk.LEFT, padx=15, pady=15)
 756        ctk.CTkLabel(
 757            master        = self.scrollable_frame,
 758            fg_color      = COLOR.DARK_TEXT,
 759            text          = '',
 760            corner_radius = 0,
 761            height        = 16
 762        ).pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)
 763
 764    def choose_font(self) -> None:
 765        """setup of choose font.
 766        """
 767        self.previous_font = str(get_from_config('font_file_name'))
 768        fonts = self.get_all_files('fonts')
 769        if not fonts:
 770            return
 771        text = ctk.CTkLabel(
 772            master     = self.scrollable_frame,
 773            text       = 'Fonts: ',
 774            font       = ctk.CTkFont(str(get_from_config('font_name')), 32),
 775            text_color = COLOR.TEXT
 776        )
 777        text.pack(side=ctk.TOP, anchor=ctk.SW, padx=75, pady=0)
 778        frame = ctk.CTkScrollableFrame(
 779            master                 = self.scrollable_frame,
 780            fg_color               = COLOR.TILE_2,
 781            scrollbar_button_color = COLOR.DARK_TEXT,
 782            orientation            = ctk.HORIZONTAL,
 783            height                 = 70,
 784            corner_radius          = 0,
 785            scrollbar_fg_color     = COLOR.DARK_TEXT
 786        )
 787        frame.pack(side=ctk.TOP, padx=80, pady=5, anchor=ctk.W, fill=ctk.X)
 788        for font in fonts:
 789            self.create_font_button(frame, font)
 790
 791    def create_font_button(self, frame: ctk.CTkFrame, font: str) -> None:
 792        """Setup of font button.
 793
 794        Args:
 795            frame (ctk.CTkFrame): Frame in which button will be placed.
 796            font (str): Font name.
 797        """
 798        current_font = get_from_config('font_name')
 799        font_name = self.get_font_name(font)
 800        font_button: ctk.CTkButton = ctk.CTkButton(
 801            master        = frame,
 802            text          = font_name,
 803            command       = lambda: self.select_font(font, font_button),
 804            font          = self.font_30,
 805            corner_radius = 0,
 806            fg_color      = COLOR.TILE_1,
 807            hover_color   = COLOR.HIGH_TILE_2,
 808            text_color    = COLOR.TEXT
 809        )
 810        font_button.pack(side=ctk.LEFT, padx=4, pady=4, expand=True)
 811        if current_font == font_name:
 812            font_button.configure(state=ctk.DISABLED)
 813
 814    def select_font(self, font: str, button: ctk.CTkButton) -> None:
 815        """Helper function to save change of font name and path to file to config file.
 816
 817        Args:
 818            font (str): Font path.
 819        """
 820        if os.path.basename(font) == self.previous_font:
 821            return
 822        new_font = self.get_font_name(font)
 823        for child in button.master.winfo_children():
 824            if isinstance(child, ctk.CTkButton) and child.cget('text') ==  get_from_config('font_name'):
 825                child.configure(state=ctk.NORMAL)
 826            elif isinstance(child, ctk.CTkButton) and child.cget('text') == new_font:
 827                child.configure(state=ctk.DISABLED)
 828        if new_font:
 829            change_config('font_name', new_font)
 830            change_config('font_file_name', os.path.basename(font))
 831            self.master.board.font_42 = ctk.CTkFont(get_from_config('font_name'), 42)
 832            self.master.board.board_font = ctk.CTkFont(get_from_config('font_name'), int(get_from_config('size'))//3)
 833            self.update_font_func()
 834            self.previous_font = str(get_from_config('font_file_name'))
 835
 836    @staticmethod
 837    def is_valid_color(color: str) -> bool:
 838        """Checks if user passed string is valid with hex color.
 839
 840        Args:
 841            color (str): User defined color.
 842
 843        Returns:
 844            bool: True if color passes regex pattern for hex color, False otherwise.
 845        """
 846        return bool(re.compile(r'^#[0-9a-fA-F]{6}$').match(color))
 847
 848    @staticmethod
 849    def validate_length(new_value: str) -> bool:
 850        """Validation function for color input.
 851
 852        Args:
 853            new_value (str): User input from color entry.
 854
 855        Returns:
 856            bool: True if length of the string is not longer than 7, False otherwise.
 857        """
 858        
 859        return bool(re.compile(r'^[#\w]{0,7}$').match(new_value))
 860
 861    def change_colors(self) -> None:
 862        """Function updating color preview in the theme changer.
 863        """
 864        text = ctk.CTkLabel(
 865            master     = self.scrollable_frame, 
 866            text       = 'Colors: ', 
 867            font       = ctk.CTkFont(str(get_from_config('font_name')), 32), 
 868            text_color = COLOR.TEXT
 869        )
 870        text.pack(side=ctk.TOP, anchor=ctk.SW, padx=75, pady=0)
 871        warning_text = ctk.CTkLabel(
 872            master     = self.scrollable_frame, 
 873            text       = STRING.COLORS_WARNING, 
 874            font       = ctk.CTkFont(str(get_from_config('font_name')), 18),
 875            text_color = COLOR.CLOSE
 876        )
 877        warning_text.pack(side=ctk.TOP, anchor=ctk.SW, padx=100, pady=0)
 878        frame = ctk.CTkFrame(
 879            master        = self.scrollable_frame,
 880            corner_radius = 0,
 881            fg_color      = COLOR.TILE_2
 882        )
 883        frame.pack(side=ctk.TOP, padx=80, pady=0, anchor=ctk.W, fill=ctk.X)
 884        ctk.CTkLabel(
 885            master = frame,
 886            text   = '',
 887            height = 2
 888        ).pack(padx=0, pady=0)
 889        for color in COLOR:
 890            self.color_label(frame, color) if color != 'transparent' else ...
 891        ctk.CTkLabel(
 892            master = frame,
 893            text   = '',
 894            height = 2
 895        ).pack(padx=0, pady=0)
 896        ctk.CTkLabel(
 897            master        = self.scrollable_frame, 
 898            fg_color      = COLOR.DARK_TEXT, 
 899            text          = '', 
 900            corner_radius = 0, 
 901            height        = 16
 902        ).pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)
 903        ctk.CTkLabel(
 904            master        = self.scrollable_frame, 
 905            fg_color      = COLOR.TRANSPARENT, 
 906            text          = '', 
 907            corner_radius = 0, 
 908            height        = 16
 909        ).pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)
 910
 911    def color_label(self, frame: ctk.CTkFrame, color: str) -> None:
 912        """Function creating color preview frame.
 913
 914        Args:
 915            frame (ctk.CTkFrame): Parent frame.
 916            color (str): New hex color string.
 917        """
 918        for color_name , color_str in COLOR.__members__.items():
 919            if color_str == color:
 920                name_of_color = color_name
 921                break
 922        color_frame = ctk.CTkFrame(
 923            master = frame,
 924            fg_color=COLOR.NOTATION_BACKGROUND_B,
 925            corner_radius=0
 926        )
 927        color_frame.pack(side=ctk.TOP, padx=10, pady=4, fill=ctk.X)
 928        vcmd = (self.register(self.validate_length), '%P')
 929        color_entry = ctk.CTkEntry(
 930            master          = color_frame, 
 931            border_width    = 0, 
 932            corner_radius   = 0, 
 933            fg_color        = color,
 934            font            = ctk.CTkFont(get_from_config('font_name'), 20),
 935            validate        = 'key',
 936            validatecommand = vcmd,
 937            text_color      = COLOR.TEXT if color != COLOR.TEXT else COLOR.DARK_TEXT
 938        )
 939        color_entry.insert(0, color)
 940        rgb_color = color.lstrip('#')
 941        r = int(rgb_color[0:2], 16)
 942        g = int(rgb_color[2:4], 16)
 943        b = int(rgb_color[4:6], 16)
 944        color_picker = ctk.CTkLabel(
 945            master = color_frame,
 946            text   = '',
 947            image  = self.color_picker_image
 948        )
 949        color_picker.pack(side=ctk.LEFT, padx=5, pady=4)
 950        color_picker.bind('<Button-1>', lambda e: self.ask_for_color(r, g, b, color_entry, color_name))
 951        color_entry.pack(side=ctk.LEFT, padx=10, pady=4)
 952        ok_button = ctk.CTkButton(
 953            master        = color_frame, 
 954            text          = 'OK', 
 955            font          = ctk.CTkFont(get_from_config('font_name'), 20),
 956            command       = lambda: self.save_color(color_name, color_entry, color_entry, color),
 957            width         = 50,
 958            corner_radius = 0,
 959            fg_color      = COLOR.TILE_1,
 960            hover_color   = COLOR.HIGH_TILE_1,
 961            text_color    = COLOR.TEXT
 962        )
 963        ok_button.pack(side=ctk.LEFT, padx=10, pady=4)
 964        cancel_button = ctk.CTkButton(
 965            master        = color_frame,
 966            text          = 'CANCEL',
 967            font          = ctk.CTkFont(get_from_config('font_name'), 20),
 968            command       = lambda: self.cancel(color_name, color_entry, color),
 969            width         = 50,
 970            corner_radius = 0,
 971            fg_color      = COLOR.CLOSE,
 972            hover_color   = COLOR.CLOSE_HOVER,
 973            text_color    = COLOR.TEXT
 974        )
 975        cancel_button.pack(side=ctk.LEFT, padx=10, pady=4)
 976        color_name_label = ctk.CTkLabel(
 977            master     = color_frame,
 978            text       = name_of_color,
 979            text_color = COLOR.TEXT,
 980            font       = ctk.CTkFont(get_from_config('font_name'), 22)
 981        )
 982        color_name_label.pack(side=ctk.RIGHT, padx=4, pady=4)
 983
 984    def save_color(self, color_name: str, entry: ctk.CTkEntry, color_label: ctk.CTkLabel, old_color: str) -> None:
 985        """Saves new color into config file.
 986
 987        Args:
 988            color_name (str): Name of the color to change.
 989            entry (ctk.CTkEntry): User input with color hex code.
 990            color_label (ctk.CTkLabel): Parent frame to update.
 991        """
 992        new_color = entry.get()
 993        if self.is_valid_color(new_color):
 994            change_color(color_name, new_color)
 995            color_label.configure(fg_color=new_color)
 996        else:
 997            entry.delete(0, ctk.END)
 998            entry.insert(0, old_color)
 999
1000    def ask_for_color(self, r: int, g: int, b: int, entry: ctk.CTkEntry, color_name: str) -> None:
1001        """Input dialog with custom color picker for easy use.
1002
1003        Args:
1004            r (int): Red color intensity.
1005            g (int): Green color intensity.
1006            b (int): Blue color intensity.
1007            entry (ctk.CTkEntry): Entry frame for user input.
1008            color_name (str): Color name from config file.
1009        """
1010        picker = ColorPicker(
1011            fg_color              = COLOR.BACKGROUND,
1012            r                     = r,
1013            g                     = g,
1014            b                     = b,
1015            font                  = ctk.CTkFont(self.font_name, 15),
1016            border_color          = COLOR.TILE_2,
1017            slider_button_color   = COLOR.TILE_2,
1018            slider_progress_color = COLOR.TEXT,
1019            slider_fg_color       = COLOR.DARK_TEXT,
1020            preview_border_color  = COLOR.DARK_TEXT,
1021            button_fg_color       = COLOR.NOTATION_BACKGROUND_B,
1022            button_hover_color    = COLOR.NOTATION_BACKGROUND_W,
1023            icon                  = resource_path(os.path.join('assets', 'logo.ico')),
1024            corner_radius         = 0
1025        )
1026        color = picker.get_color()
1027        if color:
1028            entry.delete(0, ctk.END)
1029            entry.insert(0, color)
1030            change_color(color_name, color)
1031            entry.configure(fg_color=color)
1032
1033    def cancel(self, color_name: str, entry: ctk.CTkEntry, color: str) -> None:
1034        """Helper function to close input dialog without changing any properties in config file.
1035
1036        Args:
1037            color_name (str): Color name from config file.
1038            entry (ctk.CTkEntry): Entry frame for user input.
1039            color (str): Color to keep.
1040        """
1041        entry.delete(0, ctk.END)
1042        entry.insert(0, color)
1043        change_color(color_name, color)
1044        entry.configure(fg_color=color)
1045
1046class SaveName(ctk.CTkToplevel):
1047    """Class for asking user for the save name in popup window.
1048
1049    Args:
1050        ctk.CTkTopLevel : Inheritance from customtkinter CTkFrame widget.
1051    """
1052    def __init__(self) -> None:
1053        """Constructor:
1054         - sets window to appear on top
1055         - loads fonts
1056         - calls all setup functions
1057         - centers window
1058        """
1059        super().__init__(fg_color=COLOR.BACKGROUND)
1060        if SYSTEM == 'Windows':
1061            self.grab_set()
1062        self.attributes('-topmost', True)
1063        self.title('Save')
1064        self.font_21 = ctk.CTkFont(get_from_config('font_name'), 21)
1065        self.font_28 = ctk.CTkFont(get_from_config('font_name'), 28)
1066        self.save_name: str | None | bool = None
1067        self.create_info()
1068        self.create_name_entry()
1069        self.create_save_button()
1070        self.resizable(False, False)
1071        self.protocol('WM_DELETE_WINDOW', self.on_close)
1072        self.center_window()
1073        self.after(201, lambda: self.iconbitmap(resource_path('assets\\logo.ico')))
1074
1075    def create_info(self) -> None:
1076        """Displays warning info.
1077        """
1078        self.info_label: ctk.CTkLabel = ctk.CTkLabel(
1079            master     = self,
1080            fg_color   = COLOR.BACKGROUND,
1081            text       = STRING.SAVES_WARNING,
1082            text_color = COLOR.CLOSE_HOVER,
1083            font       = self.font_21
1084        )
1085        self.info_label.pack(side=ctk.TOP, padx=15, pady=15, fill=ctk.X)
1086
1087    def create_name_entry(self) -> None:
1088        """Creates entry for name of the save.
1089        """
1090        helper_frame: ctk.CTkFrame = ctk.CTkFrame(
1091            master   = self,
1092            fg_color = COLOR.BACKGROUND,
1093
1094        )
1095        helper_frame.pack(side=ctk.TOP, padx=15, pady=15, fill=ctk.X)
1096        self.save_name_entry: ctk.CTkEntry = ctk.CTkEntry(
1097            master           = helper_frame,
1098            fg_color         = COLOR.BACKGROUND,
1099            text_color       = COLOR.TEXT,
1100            corner_radius    = 0,
1101            border_color     = COLOR.DARK_TEXT,
1102            font             = self.font_28,
1103            border_width     = 3,
1104            placeholder_text = 'Name'
1105        )
1106        self.save_name_entry.pack(side=ctk.LEFT, padx=1, pady=1, fill=ctk.X, expand=True)
1107
1108    def create_save_button(self) -> None:
1109        """Setups save button.
1110        """
1111        self.save_button: ctk.CTkButton = ctk.CTkButton(
1112            master        = self,
1113            fg_color      = COLOR.TILE_1,
1114            hover_color   = COLOR.HIGH_TILE_1,
1115            text          = 'SAVE',
1116            font          = self.font_21,
1117            command       = self.on_save_button,
1118            corner_radius = 0,
1119            width         = ctk.CTkFont.measure(self.font_21, 'SAVE') + 20,
1120        )
1121        self.save_button.pack(side=ctk.TOP, padx=15, pady=15, expand=True)
1122
1123    def center_window(self) -> None:
1124        """Function centering the TopLevel window. Screen size independent.
1125        """
1126        x: int = self.winfo_screenwidth()
1127        y: int = self.winfo_screenheight()
1128        app_width: int = self.winfo_width()
1129        app_height: int = self.winfo_height()
1130        self.geometry(f'+{(x//2)-(app_width)}+{(y//2)-(app_height)}')
1131
1132    def get_save_name(self) -> str | None | bool:
1133        """Getter for user input from the entry widget.
1134
1135        Returns:
1136            str | None | bool: String if name is valid, None if user decides to keep default save name and bool if canceled with closing window with ❌.
1137        """
1138        self.master.wait_window(self)
1139        return self.save_name
1140
1141    def on_save_button(self) -> None:
1142        """Function checking if user entry is valid after clicking save button.
1143        """
1144        self.save_name = self.save_name_entry.get()
1145        files: list[str] = [f for f in os.listdir(resource_path('saves'))]
1146        if f'{self.save_name}.json' in files:
1147            self.save_name = None
1148        if isinstance(self.save_name, str) and len(self.save_name) < 1:
1149            self.save_name = None
1150        if isinstance(self.save_name, str) and self.save_name.startswith('chess_game_'):
1151            self.save_name = None
1152        self.destroy()
1153
1154    def on_close(self) -> None:
1155        """Custom closing function ensuring proper closing of the window. Sets save_name to False to cancel saving.
1156        """
1157        self.save_name = False
1158        self.grab_release()
1159        self.destroy()
class MovesRecord(customtkinter.windows.widgets.ctk_frame.CTkFrame):
 29class MovesRecord(ctk.CTkFrame):
 30    """Class handling recording the moves during playtime. Class stores both players moves in lists and displays notation in two boxes dedicated for each player.
 31
 32    Args:
 33        ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget. 
 34    """
 35    def __init__(self, master) -> None:
 36        """Constructor:
 37             - calls function create_frames
 38             - creates 2D vector to record moves
 39
 40        Args:
 41            master (Any): Parent widget
 42        """
 43        super().__init__(master, fg_color=COLOR.BACKGROUND)
 44        self.font: ctk.CTkFont = ctk.CTkFont(str(get_from_config('font_name')), int(int(get_from_config('size')) * 0.4))
 45        self.create_frames()
 46        self.moves_white: list[str] = []
 47        self.moves_black: list[str] = []
 48
 49    def record_move(self, moved_piece: Piece, previous_coords: tuple[int, int] | None=None, capture: bool=False,
 50                    castle: str | None=None, check: bool=False, checkmate: bool=False, promotion: str='') -> None:
 51        """Displays the chess notation of the move on the frame for specific player color.
 52        Simple if else logic with flags passed to the function is responsible of handling correctness of the notation.
 53
 54        Args:
 55            moved_piece (Piece): Figure which was moved
 56            previous_coords (tuple[int, int] | None, optional): Coordinates of position before moving the figure. Defaults to None.
 57            capture (bool, optional): Flag to check if figure captured another figure. Defaults to False.
 58            castle (str | None, optional): Flag to check if castle occurred. Defaults to None.
 59            check (bool, optional): Checks if move caused the check. Defaults to False.
 60            checkmate (bool, optional): Checks if move caused the checkmate. Defaults to False.
 61            promotion (str, optional): Checks if pawn was promoted. Defaults to '' which means the promotion didn't occurred.
 62        """
 63        y_axis: list[str] = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
 64        x, y = 8 - moved_piece.position[0], y_axis[moved_piece.position[1]]
 65        prev_x = 8 - previous_coords[0] if previous_coords else ''
 66        prev_y = y_axis[previous_coords[1]] if previous_coords else ''
 67        if not isinstance(moved_piece, Knight):
 68            piece_name = moved_piece.__class__.__name__[0] if not moved_piece.__class__.__name__ == 'Pawn' else ''
 69        else:
 70            piece_name = 'N'
 71        check_nota: str = '+' if check and not checkmate else ''
 72        checkmate_nota: str = '#' if checkmate else ''
 73        promotion_nota: str = promotion if promotion != 'K' else 'N'
 74        if not castle:
 75            notation = f' {check_nota}{checkmate_nota}{'x' if capture else ''}{piece_name}{prev_y}{prev_x}-{y}{x}{promotion_nota}'
 76        else:
 77            notation = f' {check_nota}{checkmate_nota}{'0-0-0' if castle == 'queenside' else '0-0'}'
 78        current_frame = self.white_scroll_frame if moved_piece.color == 'w' else self.black_scroll_frame
 79        self.moves_white.append(notation) if moved_piece.color == 'w' else self.moves_black.append(notation)
 80        ctk.CTkLabel(
 81            master = current_frame,
 82            text   = notation,
 83            font   = self.font
 84        ).pack(side=ctk.BOTTOM)
 85
 86    def load_notation_from_save(self, white_moves: list[str], black_moves: list[str]) -> None:
 87        """Loads notation from save file. Function gets already parsed json format to two lists and displays it using record_move() function.
 88
 89        Args:
 90            white_moves (list[str]): List of previous white moves.
 91            black_moves (list[str]): List of previous white moves.
 92        """
 93        for notation in white_moves:
 94            ctk.CTkLabel(
 95                master = self.white_scroll_frame,
 96                text   = notation,
 97                font   = self.font
 98            ).pack(side=ctk.BOTTOM)
 99        for notation in black_moves:
100            ctk.CTkLabel(
101                master = self.black_scroll_frame,
102                text   = notation,
103                font   = self.font
104            ).pack(side=ctk.BOTTOM)
105        self.moves_white[:] = white_moves
106        self.moves_black[:] = black_moves
107
108    def create_frames(self) -> None:
109        """Creates frames to reserve space on main app page for displaying move notations.
110        """
111        black_label: ctk.CTkLabel = ctk.CTkLabel(
112            master     =  self,
113            text       = 'Black',
114            font       = self.font,
115            text_color = COLOR.DARK_TEXT
116        )
117        black_label.pack(side=ctk.TOP, padx=1, pady=1)
118        additional_frame: ctk.CTkFrame = ctk.CTkFrame(
119            master        = self, 
120            fg_color      = COLOR.TRANSPARENT, 
121            corner_radius = 0,
122            border_color  = COLOR.DARK_TEXT, 
123            border_width  = 7
124        )
125        additional_frame.pack(side=ctk.TOP, padx=15, expand=True, fill=ctk.Y)
126        self.black_scroll_frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(
127            master                       = additional_frame, 
128            scrollbar_button_color       = COLOR.NOTATION_BACKGROUND_B,
129            fg_color                     = COLOR.NOTATION_BACKGROUND_B, 
130            corner_radius                = 0,
131            scrollbar_button_hover_color = COLOR.NOTATION_BACKGROUND_B
132        )
133        self.black_scroll_frame.pack(side=ctk.TOP, padx=6, pady=7, fill=ctk.Y, expand=True)
134        white_label: ctk.CTkLabel = ctk.CTkLabel(
135            master     = self, 
136            text       = 'White', 
137            font       = self.font, 
138            text_color = COLOR.TEXT
139        )
140        white_label.pack(side=ctk.TOP, padx=0, pady=0)
141        additional_frame = ctk.CTkFrame(
142            master = self,
143            fg_color=COLOR.TRANSPARENT,
144            corner_radius=0,
145            border_color=COLOR.DARK_TEXT,
146            border_width=7
147        )
148        additional_frame.pack(side=ctk.TOP, padx=15, expand=True, fill=ctk.Y)
149        self.white_scroll_frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(
150            master                       = additional_frame,
151            scrollbar_button_color       = COLOR.NOTATION_BACKGROUND_W,
152            fg_color                     = COLOR.NOTATION_BACKGROUND_W,
153            corner_radius                = 0,
154            scrollbar_button_hover_color = COLOR.NOTATION_BACKGROUND_W)
155        self.white_scroll_frame.pack(side=ctk.TOP, padx=6, pady=7, fill=ctk.Y, expand=True)
156        space_label: ctk.CTkLabel = ctk.CTkLabel(
157            master =self,
158            text='\n'
159        )
160        space_label.pack()
161
162    def restart(self) -> None:
163        """Destroys the old notated moves and clears the lists.
164        """
165        self.moves_white.clear()
166        self.moves_black.clear()
167        for child in self.white_scroll_frame.winfo_children():
168            child.destroy()
169        for child in self.black_scroll_frame.winfo_children():
170            child.destroy()

Class handling recording the moves during playtime. Class stores both players moves in lists and displays notation in two boxes dedicated for each player.

Arguments:
  • ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget.
MovesRecord(master)
35    def __init__(self, master) -> None:
36        """Constructor:
37             - calls function create_frames
38             - creates 2D vector to record moves
39
40        Args:
41            master (Any): Parent widget
42        """
43        super().__init__(master, fg_color=COLOR.BACKGROUND)
44        self.font: ctk.CTkFont = ctk.CTkFont(str(get_from_config('font_name')), int(int(get_from_config('size')) * 0.4))
45        self.create_frames()
46        self.moves_white: list[str] = []
47        self.moves_black: list[str] = []
Constructor:
  • calls function create_frames
  • creates 2D vector to record moves
Arguments:
  • master (Any): Parent widget
font: customtkinter.windows.widgets.font.ctk_font.CTkFont
moves_white: list[str]
moves_black: list[str]
def record_move( self, moved_piece: piece.Piece, previous_coords: tuple[int, int] | None = None, capture: bool = False, castle: str | None = None, check: bool = False, checkmate: bool = False, promotion: str = '') -> None:
49    def record_move(self, moved_piece: Piece, previous_coords: tuple[int, int] | None=None, capture: bool=False,
50                    castle: str | None=None, check: bool=False, checkmate: bool=False, promotion: str='') -> None:
51        """Displays the chess notation of the move on the frame for specific player color.
52        Simple if else logic with flags passed to the function is responsible of handling correctness of the notation.
53
54        Args:
55            moved_piece (Piece): Figure which was moved
56            previous_coords (tuple[int, int] | None, optional): Coordinates of position before moving the figure. Defaults to None.
57            capture (bool, optional): Flag to check if figure captured another figure. Defaults to False.
58            castle (str | None, optional): Flag to check if castle occurred. Defaults to None.
59            check (bool, optional): Checks if move caused the check. Defaults to False.
60            checkmate (bool, optional): Checks if move caused the checkmate. Defaults to False.
61            promotion (str, optional): Checks if pawn was promoted. Defaults to '' which means the promotion didn't occurred.
62        """
63        y_axis: list[str] = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
64        x, y = 8 - moved_piece.position[0], y_axis[moved_piece.position[1]]
65        prev_x = 8 - previous_coords[0] if previous_coords else ''
66        prev_y = y_axis[previous_coords[1]] if previous_coords else ''
67        if not isinstance(moved_piece, Knight):
68            piece_name = moved_piece.__class__.__name__[0] if not moved_piece.__class__.__name__ == 'Pawn' else ''
69        else:
70            piece_name = 'N'
71        check_nota: str = '+' if check and not checkmate else ''
72        checkmate_nota: str = '#' if checkmate else ''
73        promotion_nota: str = promotion if promotion != 'K' else 'N'
74        if not castle:
75            notation = f' {check_nota}{checkmate_nota}{'x' if capture else ''}{piece_name}{prev_y}{prev_x}-{y}{x}{promotion_nota}'
76        else:
77            notation = f' {check_nota}{checkmate_nota}{'0-0-0' if castle == 'queenside' else '0-0'}'
78        current_frame = self.white_scroll_frame if moved_piece.color == 'w' else self.black_scroll_frame
79        self.moves_white.append(notation) if moved_piece.color == 'w' else self.moves_black.append(notation)
80        ctk.CTkLabel(
81            master = current_frame,
82            text   = notation,
83            font   = self.font
84        ).pack(side=ctk.BOTTOM)

Displays the chess notation of the move on the frame for specific player color. Simple if else logic with flags passed to the function is responsible of handling correctness of the notation.

Arguments:
  • moved_piece (Piece): Figure which was moved
  • previous_coords (tuple[int, int] | None, optional): Coordinates of position before moving the figure. Defaults to None.
  • capture (bool, optional): Flag to check if figure captured another figure. Defaults to False.
  • castle (str | None, optional): Flag to check if castle occurred. Defaults to None.
  • check (bool, optional): Checks if move caused the check. Defaults to False.
  • checkmate (bool, optional): Checks if move caused the checkmate. Defaults to False.
  • promotion (str, optional): Checks if pawn was promoted. Defaults to '' which means the promotion didn't occurred.
def load_notation_from_save(self, white_moves: list[str], black_moves: list[str]) -> None:
 86    def load_notation_from_save(self, white_moves: list[str], black_moves: list[str]) -> None:
 87        """Loads notation from save file. Function gets already parsed json format to two lists and displays it using record_move() function.
 88
 89        Args:
 90            white_moves (list[str]): List of previous white moves.
 91            black_moves (list[str]): List of previous white moves.
 92        """
 93        for notation in white_moves:
 94            ctk.CTkLabel(
 95                master = self.white_scroll_frame,
 96                text   = notation,
 97                font   = self.font
 98            ).pack(side=ctk.BOTTOM)
 99        for notation in black_moves:
100            ctk.CTkLabel(
101                master = self.black_scroll_frame,
102                text   = notation,
103                font   = self.font
104            ).pack(side=ctk.BOTTOM)
105        self.moves_white[:] = white_moves
106        self.moves_black[:] = black_moves

Loads notation from save file. Function gets already parsed json format to two lists and displays it using record_move() function.

Arguments:
  • white_moves (list[str]): List of previous white moves.
  • black_moves (list[str]): List of previous white moves.
def create_frames(self) -> None:
108    def create_frames(self) -> None:
109        """Creates frames to reserve space on main app page for displaying move notations.
110        """
111        black_label: ctk.CTkLabel = ctk.CTkLabel(
112            master     =  self,
113            text       = 'Black',
114            font       = self.font,
115            text_color = COLOR.DARK_TEXT
116        )
117        black_label.pack(side=ctk.TOP, padx=1, pady=1)
118        additional_frame: ctk.CTkFrame = ctk.CTkFrame(
119            master        = self, 
120            fg_color      = COLOR.TRANSPARENT, 
121            corner_radius = 0,
122            border_color  = COLOR.DARK_TEXT, 
123            border_width  = 7
124        )
125        additional_frame.pack(side=ctk.TOP, padx=15, expand=True, fill=ctk.Y)
126        self.black_scroll_frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(
127            master                       = additional_frame, 
128            scrollbar_button_color       = COLOR.NOTATION_BACKGROUND_B,
129            fg_color                     = COLOR.NOTATION_BACKGROUND_B, 
130            corner_radius                = 0,
131            scrollbar_button_hover_color = COLOR.NOTATION_BACKGROUND_B
132        )
133        self.black_scroll_frame.pack(side=ctk.TOP, padx=6, pady=7, fill=ctk.Y, expand=True)
134        white_label: ctk.CTkLabel = ctk.CTkLabel(
135            master     = self, 
136            text       = 'White', 
137            font       = self.font, 
138            text_color = COLOR.TEXT
139        )
140        white_label.pack(side=ctk.TOP, padx=0, pady=0)
141        additional_frame = ctk.CTkFrame(
142            master = self,
143            fg_color=COLOR.TRANSPARENT,
144            corner_radius=0,
145            border_color=COLOR.DARK_TEXT,
146            border_width=7
147        )
148        additional_frame.pack(side=ctk.TOP, padx=15, expand=True, fill=ctk.Y)
149        self.white_scroll_frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(
150            master                       = additional_frame,
151            scrollbar_button_color       = COLOR.NOTATION_BACKGROUND_W,
152            fg_color                     = COLOR.NOTATION_BACKGROUND_W,
153            corner_radius                = 0,
154            scrollbar_button_hover_color = COLOR.NOTATION_BACKGROUND_W)
155        self.white_scroll_frame.pack(side=ctk.TOP, padx=6, pady=7, fill=ctk.Y, expand=True)
156        space_label: ctk.CTkLabel = ctk.CTkLabel(
157            master =self,
158            text='\n'
159        )
160        space_label.pack()

Creates frames to reserve space on main app page for displaying move notations.

def restart(self) -> None:
162    def restart(self) -> None:
163        """Destroys the old notated moves and clears the lists.
164        """
165        self.moves_white.clear()
166        self.moves_black.clear()
167        for child in self.white_scroll_frame.winfo_children():
168            child.destroy()
169        for child in self.black_scroll_frame.winfo_children():
170            child.destroy()

Destroys the old notated moves and clears the lists.

class Saves(customtkinter.windows.widgets.ctk_frame.CTkFrame):
172class Saves(ctk.CTkFrame):
173    """Class handling saving, showing and loading saves in separate menu.
174
175    Args:
176        ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget.
177    """
178    def __init__(self, master: Any, board) -> None:
179        """Constructor:
180         - loads fonts
181         - calls function showing all saves
182
183        Args:
184            master (Any): Parent widget.
185            board (Board): Board object.
186        """
187        super().__init__(master, fg_color=COLOR.BACKGROUND)
188        self.font_32 = ctk.CTkFont(get_from_config('font_name'), 32)
189        self.font_26 = ctk.CTkFont(get_from_config('font_name'), 26)
190        self.close_image: ctk.CTkImage | None = load_menu_image('close')
191        self.show_all_saves(board)
192        ctk.CTkLabel(
193            master   = self,
194            text     = '',
195            height   = 18,
196            fg_color = COLOR.BACKGROUND
197        ).pack(padx=0, pady=0)
198
199    @staticmethod
200    def save_game_to_file(board) -> bool:
201        """Saves the current game state to the .json file in saves folder.
202
203        Args:
204            board (Board): Board object.
205
206        Returns:
207            bool: Returns True if save was created successfully, False otherwise.
208        """
209        save_info: dict[tuple[int, int] | str, tuple[str, str, bool] | list[str]] = dict()
210        for row in board.board:
211            for cell in row:
212                if cell.figure:
213                    figure: str = cell.figure.__class__.__name__
214                    save_info[cell.position] = (figure, cell.figure.color, cell.figure.first_move)
215        if not board.current_save_name:
216            save_name: str | None | bool = SaveName().get_save_name()
217            board.current_save_name = save_name
218        else:
219            save_name = board.current_save_name.strip('.json')
220        moves_record: MovesRecord = board.moves_record
221        if not isinstance(save_name, bool):
222            create_save_file(
223                save_info,
224                board.current_turn,
225                moves_record.moves_white,
226                moves_record.moves_black,
227                board.game_over,
228                save_name
229            )
230            return True
231        return False
232
233    def show_all_saves(self, board) -> None:
234        """Displays all saves as clickable buttons in saves menu.
235
236        Args:
237            board (Board): Board object.
238        """
239        top_frame: ctk.CTkFrame = ctk.CTkFrame(
240            master   = self,
241            fg_color = COLOR.TRANSPARENT
242        )
243        top_frame.pack(side=ctk.TOP, padx=0, pady=0, fill=ctk.X)
244        settings_text = ctk.CTkLabel(
245            master     = top_frame,
246            text       = 'Saves',
247            font       = ctk.CTkFont(str(get_from_config('font_name')), 38),
248            text_color = COLOR.DARK_TEXT,
249            anchor     = ctk.N
250        )
251        settings_text.pack(side=ctk.LEFT, padx=20, anchor=ctk.NW)
252        close_button = ctk.CTkLabel(
253            master = top_frame,
254            text   = '',
255            font   = ctk.CTkFont(str(get_from_config('font_name')), 24),
256            image  = self.close_image,
257            anchor = ctk.S
258        )
259        close_button.bind('<Button-1>', self.on_close)
260        close_button.pack(side=ctk.RIGHT, anchor=ctk.NE, padx=10, pady=10)
261        self.scrollable_frame = ctk.CTkScrollableFrame(
262            master   = self,
263            fg_color = COLOR.BACKGROUND,
264            corner_radius = 0,
265            scrollbar_button_color = COLOR.DARK_TEXT,
266        )
267        self.scrollable_frame.pack(side=ctk.TOP, padx=0, pady=0, expand=True, fill=ctk.BOTH)
268        files: list[str] = [f for f in os.listdir(resource_path('saves'))]
269        for file in files:
270            self.create_file_button(self.scrollable_frame, file, board)
271
272    def create_file_button(self, frame: ctk.CTkFrame, file_name: str, board) -> None:
273        """Helper function creating single button which will load specific save after clicking.
274
275        Args:
276            frame (ctk.CTkFrame): Parent widget.
277            file_name (str): Name of the file.
278            board (Board): Board object.
279        """
280        helper_frame = ctk.CTkFrame(
281            master        = frame,
282            fg_color      = COLOR.TILE_1,
283            corner_radius = 0
284        )
285        helper_frame.pack(side=ctk.TOP, padx=150, pady=10, fill=ctk.X)
286        ctk.CTkLabel(
287            master   = helper_frame,
288            fg_color = COLOR.NOTATION_BACKGROUND_B,
289            text     = '',
290            width    = 20
291        ).pack(side=ctk.LEFT, padx=0, pady=0, fill=ctk.Y)
292        file_name_label = ctk.CTkLabel(
293            master        = helper_frame,
294            text          = f' {file_name.replace('.json', '')}',
295            fg_color      = COLOR.TILE_1,
296            font          = self.font_32,
297            corner_radius = 0,
298            anchor        = ctk.W
299        )
300        file_name_label.bind('<Button-1>', lambda e: self.load_save(e, board, file_name))
301        file_name_label.pack(side=ctk.LEFT, padx=15, pady=0, fill=ctk.BOTH, expand=True)
302        delete_button = ctk.CTkButton(
303            master        = helper_frame,
304            fg_color      = COLOR.CLOSE,
305            hover_color   = COLOR.CLOSE_HOVER,
306            command       = lambda: self.remove_save(file_name, helper_frame),
307            text          = 'REMOVE',
308            font          = self.font_26,
309            corner_radius = 0
310        )
311        delete_button.pack(side=ctk.RIGHT, padx=10, pady=10, anchor=ctk.N)
312
313    def remove_save(self, file_name: str, frame: ctk.CTkFrame) -> None:
314        """Deletes specific save. Button is part of the save button which makes it easier for user to determine which save is being deleted.
315
316        Args:
317            file_name (str): Name of the file to be deleted.
318            frame (ctk.CTkFrame): Parent widget.
319        """
320        if delete_save_file(file_name):
321            frame.destroy()
322            Notification(self.master, f'Save {file_name.replace('.json', '')} has been removed', 2, 'top')
323        else:
324            Notification(self.master, 'Couldn\'t remove the save', 2, 'top')
325
326    def load_save(self, event: Any, board, file_name: str) -> None:
327        """Helper function calling all necessary functions to load the game from save. Notifications will indicate if it was successful or not.
328
329        Args:
330            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
331            board (Board): Board object.
332            file_name (str): Name of the file from which the game will be loaded.
333        """
334        if board.load_board_from_file(get_save_info(file_name), file_name):
335            Notification(self.master, 'Save loaded successfully', 3, 'top')
336            self.master.after(201, self.on_close)
337        else:
338            Notification(self.master, 'Couldn\'t load save', 2, 'top')
339
340    def on_close(self, event: Any=None) -> None:
341        """Custom close function handling slow fade out animation.
342
343        Args:
344            event (Any, optional): Event type. Doesn't matter but is required parameter by customtkinter.. Defaults to None.
345        """
346        def update_opacity(i: int) -> None:
347            if i >= 0:
348                pywinstyles.set_opacity(self, value=i*0.005, color='#000001')
349                self.master.after(1, lambda: update_opacity(i - 1))
350            else:
351                self.after(10, self.destroy)
352        update_opacity(200)

Class handling saving, showing and loading saves in separate menu.

Arguments:
  • ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget.
Saves(master: Any, board)
178    def __init__(self, master: Any, board) -> None:
179        """Constructor:
180         - loads fonts
181         - calls function showing all saves
182
183        Args:
184            master (Any): Parent widget.
185            board (Board): Board object.
186        """
187        super().__init__(master, fg_color=COLOR.BACKGROUND)
188        self.font_32 = ctk.CTkFont(get_from_config('font_name'), 32)
189        self.font_26 = ctk.CTkFont(get_from_config('font_name'), 26)
190        self.close_image: ctk.CTkImage | None = load_menu_image('close')
191        self.show_all_saves(board)
192        ctk.CTkLabel(
193            master   = self,
194            text     = '',
195            height   = 18,
196            fg_color = COLOR.BACKGROUND
197        ).pack(padx=0, pady=0)
Constructor:
  • loads fonts
  • calls function showing all saves
Arguments:
  • master (Any): Parent widget.
  • board (Board): Board object.
font_32
font_26
close_image: customtkinter.windows.widgets.image.ctk_image.CTkImage | None
@staticmethod
def save_game_to_file(board) -> bool:
199    @staticmethod
200    def save_game_to_file(board) -> bool:
201        """Saves the current game state to the .json file in saves folder.
202
203        Args:
204            board (Board): Board object.
205
206        Returns:
207            bool: Returns True if save was created successfully, False otherwise.
208        """
209        save_info: dict[tuple[int, int] | str, tuple[str, str, bool] | list[str]] = dict()
210        for row in board.board:
211            for cell in row:
212                if cell.figure:
213                    figure: str = cell.figure.__class__.__name__
214                    save_info[cell.position] = (figure, cell.figure.color, cell.figure.first_move)
215        if not board.current_save_name:
216            save_name: str | None | bool = SaveName().get_save_name()
217            board.current_save_name = save_name
218        else:
219            save_name = board.current_save_name.strip('.json')
220        moves_record: MovesRecord = board.moves_record
221        if not isinstance(save_name, bool):
222            create_save_file(
223                save_info,
224                board.current_turn,
225                moves_record.moves_white,
226                moves_record.moves_black,
227                board.game_over,
228                save_name
229            )
230            return True
231        return False

Saves the current game state to the .json file in saves folder.

Arguments:
  • board (Board): Board object.
Returns:

bool: Returns True if save was created successfully, False otherwise.

def show_all_saves(self, board) -> None:
233    def show_all_saves(self, board) -> None:
234        """Displays all saves as clickable buttons in saves menu.
235
236        Args:
237            board (Board): Board object.
238        """
239        top_frame: ctk.CTkFrame = ctk.CTkFrame(
240            master   = self,
241            fg_color = COLOR.TRANSPARENT
242        )
243        top_frame.pack(side=ctk.TOP, padx=0, pady=0, fill=ctk.X)
244        settings_text = ctk.CTkLabel(
245            master     = top_frame,
246            text       = 'Saves',
247            font       = ctk.CTkFont(str(get_from_config('font_name')), 38),
248            text_color = COLOR.DARK_TEXT,
249            anchor     = ctk.N
250        )
251        settings_text.pack(side=ctk.LEFT, padx=20, anchor=ctk.NW)
252        close_button = ctk.CTkLabel(
253            master = top_frame,
254            text   = '',
255            font   = ctk.CTkFont(str(get_from_config('font_name')), 24),
256            image  = self.close_image,
257            anchor = ctk.S
258        )
259        close_button.bind('<Button-1>', self.on_close)
260        close_button.pack(side=ctk.RIGHT, anchor=ctk.NE, padx=10, pady=10)
261        self.scrollable_frame = ctk.CTkScrollableFrame(
262            master   = self,
263            fg_color = COLOR.BACKGROUND,
264            corner_radius = 0,
265            scrollbar_button_color = COLOR.DARK_TEXT,
266        )
267        self.scrollable_frame.pack(side=ctk.TOP, padx=0, pady=0, expand=True, fill=ctk.BOTH)
268        files: list[str] = [f for f in os.listdir(resource_path('saves'))]
269        for file in files:
270            self.create_file_button(self.scrollable_frame, file, board)

Displays all saves as clickable buttons in saves menu.

Arguments:
  • board (Board): Board object.
def create_file_button( self, frame: customtkinter.windows.widgets.ctk_frame.CTkFrame, file_name: str, board) -> None:
272    def create_file_button(self, frame: ctk.CTkFrame, file_name: str, board) -> None:
273        """Helper function creating single button which will load specific save after clicking.
274
275        Args:
276            frame (ctk.CTkFrame): Parent widget.
277            file_name (str): Name of the file.
278            board (Board): Board object.
279        """
280        helper_frame = ctk.CTkFrame(
281            master        = frame,
282            fg_color      = COLOR.TILE_1,
283            corner_radius = 0
284        )
285        helper_frame.pack(side=ctk.TOP, padx=150, pady=10, fill=ctk.X)
286        ctk.CTkLabel(
287            master   = helper_frame,
288            fg_color = COLOR.NOTATION_BACKGROUND_B,
289            text     = '',
290            width    = 20
291        ).pack(side=ctk.LEFT, padx=0, pady=0, fill=ctk.Y)
292        file_name_label = ctk.CTkLabel(
293            master        = helper_frame,
294            text          = f' {file_name.replace('.json', '')}',
295            fg_color      = COLOR.TILE_1,
296            font          = self.font_32,
297            corner_radius = 0,
298            anchor        = ctk.W
299        )
300        file_name_label.bind('<Button-1>', lambda e: self.load_save(e, board, file_name))
301        file_name_label.pack(side=ctk.LEFT, padx=15, pady=0, fill=ctk.BOTH, expand=True)
302        delete_button = ctk.CTkButton(
303            master        = helper_frame,
304            fg_color      = COLOR.CLOSE,
305            hover_color   = COLOR.CLOSE_HOVER,
306            command       = lambda: self.remove_save(file_name, helper_frame),
307            text          = 'REMOVE',
308            font          = self.font_26,
309            corner_radius = 0
310        )
311        delete_button.pack(side=ctk.RIGHT, padx=10, pady=10, anchor=ctk.N)

Helper function creating single button which will load specific save after clicking.

Arguments:
  • frame (ctk.CTkFrame): Parent widget.
  • file_name (str): Name of the file.
  • board (Board): Board object.
def remove_save( self, file_name: str, frame: customtkinter.windows.widgets.ctk_frame.CTkFrame) -> None:
313    def remove_save(self, file_name: str, frame: ctk.CTkFrame) -> None:
314        """Deletes specific save. Button is part of the save button which makes it easier for user to determine which save is being deleted.
315
316        Args:
317            file_name (str): Name of the file to be deleted.
318            frame (ctk.CTkFrame): Parent widget.
319        """
320        if delete_save_file(file_name):
321            frame.destroy()
322            Notification(self.master, f'Save {file_name.replace('.json', '')} has been removed', 2, 'top')
323        else:
324            Notification(self.master, 'Couldn\'t remove the save', 2, 'top')

Deletes specific save. Button is part of the save button which makes it easier for user to determine which save is being deleted.

Arguments:
  • file_name (str): Name of the file to be deleted.
  • frame (ctk.CTkFrame): Parent widget.
def load_save(self, event: Any, board, file_name: str) -> None:
326    def load_save(self, event: Any, board, file_name: str) -> None:
327        """Helper function calling all necessary functions to load the game from save. Notifications will indicate if it was successful or not.
328
329        Args:
330            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
331            board (Board): Board object.
332            file_name (str): Name of the file from which the game will be loaded.
333        """
334        if board.load_board_from_file(get_save_info(file_name), file_name):
335            Notification(self.master, 'Save loaded successfully', 3, 'top')
336            self.master.after(201, self.on_close)
337        else:
338            Notification(self.master, 'Couldn\'t load save', 2, 'top')

Helper function calling all necessary functions to load the game from save. Notifications will indicate if it was successful or not.

Arguments:
  • event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
  • board (Board): Board object.
  • file_name (str): Name of the file from which the game will be loaded.
def on_close(self, event: Any = None) -> None:
340    def on_close(self, event: Any=None) -> None:
341        """Custom close function handling slow fade out animation.
342
343        Args:
344            event (Any, optional): Event type. Doesn't matter but is required parameter by customtkinter.. Defaults to None.
345        """
346        def update_opacity(i: int) -> None:
347            if i >= 0:
348                pywinstyles.set_opacity(self, value=i*0.005, color='#000001')
349                self.master.after(1, lambda: update_opacity(i - 1))
350            else:
351                self.after(10, self.destroy)
352        update_opacity(200)

Custom close function handling slow fade out animation.

Arguments:
  • event (Any, optional): Event type. Doesn't matter but is required parameter by customtkinter.. Defaults to None.
class Options(customtkinter.windows.widgets.ctk_frame.CTkFrame):
354class Options(ctk.CTkFrame):
355    """Class handling user interface with available options on main window frame:
356     - customization settings
357     - restarting game
358     - saving game
359     - loading game
360
361    Args:
362        ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget.
363    """
364    def __init__(self, master, restart_func: Callable, update_assets_func: Callable, update_font_func: Callable, get_board_func: Callable):
365        """Constructor:
366             - places all options buttons
367             - loads menu assets
368             - calls all necessary setup functions
369
370        Args:
371            master (Any): Parent widget.
372            restart_func (Callable): Master function to restart the game.
373            update_assets_func (Callable): Master function to update assets.
374            update_font_func (Callable): Master function to update font.
375        """
376        super().__init__(master, fg_color=COLOR.BACKGROUND)
377        self.restart_func: Callable = restart_func
378        self.update_assets_func: Callable = update_assets_func
379        self.update_font_func: Callable = update_font_func
380        self.get_board_func: Callable = get_board_func
381        self.setting_icon: ctk.CTkImage | None = load_menu_image('settings')
382        self.replay_icon: ctk.CTkImage | None = load_menu_image('replay')
383        self.saves_image: ctk.CTkImage | None = load_menu_image('saves')
384        self.save_as_image: ctk.CTkImage | None = load_menu_image('save_as')
385        self.settings: Settings | None = None
386        self.saves: Saves | None = None
387        self.setting_button()
388        self.space_label()
389        self.replay_button()
390        self.space_label()
391        self.save_button()
392        self.space_label()
393        self.load_saves_button()
394
395    def setting_button(self) -> None:
396        """Setup of setting button.
397        """
398        self.s_icon_label: ctk.CTkLabel = ctk.CTkLabel(self, text='', image=self.setting_icon)
399        self.s_icon_label.pack(side=ctk.TOP, padx=10, pady=5)
400        self.s_icon_label.bind('<Button-1>', self.open_settings)
401
402    def replay_button(self) -> None:
403        """Setup of replay button.
404        """
405        self.r_icon_label: ctk.CTkLabel = ctk.CTkLabel(
406            master =  self,
407            text   = '',
408            image  = self.replay_icon)
409        self.r_icon_label.pack(side=ctk.TOP, padx=10, pady=0)
410        self.r_icon_label.bind('<Button-1>', self.replay)
411
412    def save_button(self) -> None:
413        """Setup of save button.
414        """
415        self.save_icon_label: ctk.CTkLabel = ctk.CTkLabel(
416            master = self,
417            text   = '',
418            image  = self.save_as_image
419        )
420        self.save_icon_label.pack(side=ctk.TOP, padx=10, pady=0)
421        self.save_icon_label.bind('<Button-1>', self.save_game)
422
423    def load_saves_button(self) -> None:
424        """Setup of button showing all saves.
425        """
426        self.load_icon_label: ctk.CTkLabel = ctk.CTkLabel(
427            master = self,
428            text   = '',
429            image  = self.saves_image 
430        )
431        self.load_icon_label.pack(side=ctk.TOP, padx=10, pady=0)
432        self.load_icon_label.bind('<Button-1>', self.load_saves)
433
434    def space_label(self) -> None:
435        """Setups of space to maintain spacing between the button.
436        """
437        space: ctk.CTkLabel = ctk.CTkLabel(
438            master = self,
439            text   = '\n')
440        space.pack(padx=2, pady=2)
441
442    def open_settings(self, event: Any) -> None:
443        """Function opening settings menu. For optimizations the settings frame is not being destroyed, but is hidden,
444        it has no impact on user experience as all changes are dynamic and app restart wont be required to see the changes.
445
446        Args:
447            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
448        """
449        if self.settings:
450            self.settings.place(relx=0, rely=0, relwidth=1, relheight=1)
451        else:
452            self.settings = Settings(self.master, self.restart_func, self.update_assets_func, self.update_font_func)
453
454    def replay(self, event: Any) -> None:
455        """Function restarting the game. Calls function passed from Board to restart state of the game.
456
457        Args:
458            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
459        """
460        self.after(1, self.restart_func)
461
462    def save_game(self, event: Any) -> None:
463        """Saves game to .json file and displays notification if successful.
464
465        Args:
466            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
467        """
468        if Saves.save_game_to_file(self.get_board_func()):
469            Notification(self.master, 'Save was created successfully', 2, 'top')
470
471    def load_saves(self, event: Any) -> None:
472        """Function opening saves menu. To always get all saves even these created during app runtime it has to be created every time from scratch to avoid bugs and unintended behavior.
473
474        Args:
475            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
476        """
477        self.saves = Saves(self.master, self.get_board_func())
478        self.saves.place(relx=0, rely=0, relwidth=1, relheight=1)
Class handling user interface with available options on main window frame:
  • customization settings
  • restarting game
  • saving game
  • loading game
Arguments:
  • ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget.
Options( master, restart_func: Callable, update_assets_func: Callable, update_font_func: Callable, get_board_func: Callable)
364    def __init__(self, master, restart_func: Callable, update_assets_func: Callable, update_font_func: Callable, get_board_func: Callable):
365        """Constructor:
366             - places all options buttons
367             - loads menu assets
368             - calls all necessary setup functions
369
370        Args:
371            master (Any): Parent widget.
372            restart_func (Callable): Master function to restart the game.
373            update_assets_func (Callable): Master function to update assets.
374            update_font_func (Callable): Master function to update font.
375        """
376        super().__init__(master, fg_color=COLOR.BACKGROUND)
377        self.restart_func: Callable = restart_func
378        self.update_assets_func: Callable = update_assets_func
379        self.update_font_func: Callable = update_font_func
380        self.get_board_func: Callable = get_board_func
381        self.setting_icon: ctk.CTkImage | None = load_menu_image('settings')
382        self.replay_icon: ctk.CTkImage | None = load_menu_image('replay')
383        self.saves_image: ctk.CTkImage | None = load_menu_image('saves')
384        self.save_as_image: ctk.CTkImage | None = load_menu_image('save_as')
385        self.settings: Settings | None = None
386        self.saves: Saves | None = None
387        self.setting_button()
388        self.space_label()
389        self.replay_button()
390        self.space_label()
391        self.save_button()
392        self.space_label()
393        self.load_saves_button()
Constructor:
  • places all options buttons
  • loads menu assets
  • calls all necessary setup functions
Arguments:
  • master (Any): Parent widget.
  • restart_func (Callable): Master function to restart the game.
  • update_assets_func (Callable): Master function to update assets.
  • update_font_func (Callable): Master function to update font.
restart_func: Callable
update_assets_func: Callable
update_font_func: Callable
get_board_func: Callable
setting_icon: customtkinter.windows.widgets.image.ctk_image.CTkImage | None
replay_icon: customtkinter.windows.widgets.image.ctk_image.CTkImage | None
saves_image: customtkinter.windows.widgets.image.ctk_image.CTkImage | None
save_as_image: customtkinter.windows.widgets.image.ctk_image.CTkImage | None
settings: Settings | None
saves: Saves | None
def setting_button(self) -> None:
395    def setting_button(self) -> None:
396        """Setup of setting button.
397        """
398        self.s_icon_label: ctk.CTkLabel = ctk.CTkLabel(self, text='', image=self.setting_icon)
399        self.s_icon_label.pack(side=ctk.TOP, padx=10, pady=5)
400        self.s_icon_label.bind('<Button-1>', self.open_settings)

Setup of setting button.

def replay_button(self) -> None:
402    def replay_button(self) -> None:
403        """Setup of replay button.
404        """
405        self.r_icon_label: ctk.CTkLabel = ctk.CTkLabel(
406            master =  self,
407            text   = '',
408            image  = self.replay_icon)
409        self.r_icon_label.pack(side=ctk.TOP, padx=10, pady=0)
410        self.r_icon_label.bind('<Button-1>', self.replay)

Setup of replay button.

def save_button(self) -> None:
412    def save_button(self) -> None:
413        """Setup of save button.
414        """
415        self.save_icon_label: ctk.CTkLabel = ctk.CTkLabel(
416            master = self,
417            text   = '',
418            image  = self.save_as_image
419        )
420        self.save_icon_label.pack(side=ctk.TOP, padx=10, pady=0)
421        self.save_icon_label.bind('<Button-1>', self.save_game)

Setup of save button.

def load_saves_button(self) -> None:
423    def load_saves_button(self) -> None:
424        """Setup of button showing all saves.
425        """
426        self.load_icon_label: ctk.CTkLabel = ctk.CTkLabel(
427            master = self,
428            text   = '',
429            image  = self.saves_image 
430        )
431        self.load_icon_label.pack(side=ctk.TOP, padx=10, pady=0)
432        self.load_icon_label.bind('<Button-1>', self.load_saves)

Setup of button showing all saves.

def space_label(self) -> None:
434    def space_label(self) -> None:
435        """Setups of space to maintain spacing between the button.
436        """
437        space: ctk.CTkLabel = ctk.CTkLabel(
438            master = self,
439            text   = '\n')
440        space.pack(padx=2, pady=2)

Setups of space to maintain spacing between the button.

def open_settings(self, event: Any) -> None:
442    def open_settings(self, event: Any) -> None:
443        """Function opening settings menu. For optimizations the settings frame is not being destroyed, but is hidden,
444        it has no impact on user experience as all changes are dynamic and app restart wont be required to see the changes.
445
446        Args:
447            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
448        """
449        if self.settings:
450            self.settings.place(relx=0, rely=0, relwidth=1, relheight=1)
451        else:
452            self.settings = Settings(self.master, self.restart_func, self.update_assets_func, self.update_font_func)

Function opening settings menu. For optimizations the settings frame is not being destroyed, but is hidden, it has no impact on user experience as all changes are dynamic and app restart wont be required to see the changes.

Arguments:
  • event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
def replay(self, event: Any) -> None:
454    def replay(self, event: Any) -> None:
455        """Function restarting the game. Calls function passed from Board to restart state of the game.
456
457        Args:
458            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
459        """
460        self.after(1, self.restart_func)

Function restarting the game. Calls function passed from Board to restart state of the game.

Arguments:
  • event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
def save_game(self, event: Any) -> None:
462    def save_game(self, event: Any) -> None:
463        """Saves game to .json file and displays notification if successful.
464
465        Args:
466            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
467        """
468        if Saves.save_game_to_file(self.get_board_func()):
469            Notification(self.master, 'Save was created successfully', 2, 'top')

Saves game to .json file and displays notification if successful.

Arguments:
  • event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
def load_saves(self, event: Any) -> None:
471    def load_saves(self, event: Any) -> None:
472        """Function opening saves menu. To always get all saves even these created during app runtime it has to be created every time from scratch to avoid bugs and unintended behavior.
473
474        Args:
475            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
476        """
477        self.saves = Saves(self.master, self.get_board_func())
478        self.saves.place(relx=0, rely=0, relwidth=1, relheight=1)

Function opening saves menu. To always get all saves even these created during app runtime it has to be created every time from scratch to avoid bugs and unintended behavior.

Arguments:
  • event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
class Settings(customtkinter.windows.widgets.ctk_frame.CTkFrame):
 480class Settings(ctk.CTkFrame):
 481    """Class handling changes in setting such as fonts, assets and colors made by user.
 482    Handles saving and updating the changes during app runtime except for color changes as they could take too much time for smooth experience.
 483
 484    Args:
 485        ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget.
 486    """
 487    def __init__(self, master, restart_func: Callable, update_assets_func: Callable, update_font_func: Callable) -> None:
 488        """Constructor
 489         - places itself on the screen
 490         - calls all functions creating frames containing content
 491
 492        Args:
 493            master (Any): Parent widget.
 494            restart_func (Callable): Master function to restart the game.
 495            update_assets_func (Callable): Master function to update assets.
 496            update_font_func (Callable): Master function to update font.
 497        """
 498        super().__init__(master, fg_color=COLOR.BACKGROUND, corner_radius=0)
 499        self.place(relx=0, rely=0, relwidth=1, relheight=1)
 500        self.close_image: ctk.CTkImage | None = load_menu_image('close')
 501        self.color_picker_image: ctk.CTkImage | None = load_menu_image('colorpicker', resize=2)
 502        self.close_button()
 503        self.font_30: ctk.CTkFont = ctk.CTkFont(str(get_from_config('font_name')), 30)
 504        self.scrollable_frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(self, corner_radius=0, fg_color=COLOR.BACKGROUND,
 505                                                        scrollbar_button_color=COLOR.DARK_TEXT)
 506        self.scrollable_frame.pack(side=ctk.TOP, padx=0, pady=0, fill=ctk.BOTH, expand=True)
 507        self.font_name: str = str(get_from_config('font_name'))
 508        self.choose_theme()
 509        self.choose_font()
 510        self.open_assets_folder()
 511        self.change_colors()
 512        self.previous_theme: str | None = None
 513        self.choice: str | None = None
 514        self.restart_func: Callable = restart_func
 515        self.update_assets_func: Callable = update_assets_func
 516        self.update_font_func: Callable = update_font_func
 517        ctk.CTkLabel(
 518            master   = self,
 519            text     = '',
 520            height   = 18,
 521            fg_color = COLOR.BACKGROUND
 522        ).pack(padx=0, pady=0)
 523
 524    @staticmethod
 525    def list_directories_os(path: str) -> list[str]:
 526        """Lists all directories for given path.
 527
 528        Args:
 529            path (str): Desired path.
 530
 531        Returns:
 532            list[str]: List of all directories from path.
 533        """
 534        try:
 535            entries: list[str] = os.listdir(path)
 536            directories: list[str] = [
 537                entry for entry in entries
 538                if os.path.isdir(os.path.join(path, entry)) and os.listdir(os.path.join(path, entry))
 539            ]
 540            return directories
 541        except FileNotFoundError as e:
 542            update_error_log(e)
 543            return []
 544
 545    def close_button(self) -> None:
 546        """Setup of close button.
 547        """
 548        top_frame: ctk.CTkFrame = ctk.CTkFrame(
 549            master   = self,
 550            fg_color = COLOR.TRANSPARENT
 551        )
 552        top_frame.pack(side=ctk.TOP, padx=0, pady=0, fill=ctk.X)
 553        settings_text = ctk.CTkLabel(
 554            master     = top_frame,
 555            text       = 'Settings',
 556            font       = ctk.CTkFont(str(get_from_config('font_name')), 38),
 557            text_color = COLOR.DARK_TEXT,
 558            anchor     = ctk.N
 559        )
 560        settings_text.pack(side=ctk.LEFT, padx=20, anchor=ctk.NW)
 561        close_button = ctk.CTkLabel(
 562            master = top_frame,
 563            text   = '',
 564            font   = ctk.CTkFont(str(get_from_config('font_name')), 24),
 565            image  = self.close_image,
 566            anchor = ctk.S
 567        )
 568        close_button.bind('<Button-1>', self.on_close)
 569        close_button.pack(side=ctk.RIGHT, anchor=ctk.NE, padx=10, pady=10)
 570
 571    def create_theme_button(self, frame: ctk.CTkFrame, theme: str) -> None:
 572        """Setup of theme button.
 573
 574        Args:
 575            frame (ctk.CTkFrame): Frame in which button will be placed.
 576            theme (str): Style of Figures to choose.
 577        """ 
 578        current_theme = get_from_config('theme')
 579        theme_button: ctk.CTkButton = ctk.CTkButton(
 580            master        = frame,
 581            text          = theme,
 582            command       = lambda: self.select_theme(theme, theme_button),
 583            font          = self.font_30,
 584            corner_radius = 0,
 585            fg_color      = COLOR.TILE_1,
 586            hover_color   = COLOR.HIGH_TILE_2,
 587            text_color    = COLOR.TEXT,
 588        )
 589        theme_button.pack(side=ctk.LEFT, padx=4, pady=4, expand=True)
 590        if current_theme == theme:
 591            theme_button.configure(state=ctk.DISABLED)
 592
 593    def choose_theme(self) -> None:
 594        """Setup of theme chooser.
 595        """
 596        self.previous_theme = str(get_from_config('theme'))
 597        themes: list[str] = self.list_directories_os('assets')
 598        if not themes:
 599            return
 600        text: ctk.CTkLabel = ctk.CTkLabel(
 601            master     = self.scrollable_frame,
 602            text       = 'Themes: ',
 603            font       = ctk.CTkFont(str(get_from_config('font_name')), 32),
 604            text_color = COLOR.TEXT
 605        )
 606        text.pack(side=ctk.TOP, anchor=ctk.SW, padx=75, pady=0)
 607        themes.remove('menu') if 'menu' in themes else themes
 608        frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(
 609            master                 = self.scrollable_frame,
 610            fg_color               = COLOR.TILE_2,
 611            scrollbar_button_color = COLOR.DARK_TEXT,
 612            orientation            = ctk.HORIZONTAL,
 613            scrollbar_fg_color     = COLOR.DARK_TEXT,
 614            height                 = 70,
 615            corner_radius          = 0
 616        )
 617        frame.pack(side=ctk.TOP, padx=80, pady=5, anchor=ctk.W, fill=ctk.X)
 618        for theme in themes:
 619            self.create_theme_button(frame, theme)
 620        warning_text: ctk.CTkLabel = ctk.CTkLabel(
 621            master     = self.scrollable_frame,
 622            text       = STRING.ASSETS_WARNING,
 623            font       = ctk.CTkFont(str(get_from_config('font_name')), 18),
 624            text_color = COLOR.CLOSE
 625        )
 626        warning_text.pack(side=ctk.TOP, anchor=ctk.SW, padx=100, pady=0)
 627
 628    def select_theme(self, choice: str, button: ctk.CTkButton) -> None:
 629        """Helper function to save theme changes to config file.
 630
 631        Args:
 632            choice (str): Name of theme to save.
 633        """
 634        self.choice = choice
 635        theme = get_from_config('theme')
 636        for child in button.master.winfo_children():
 637            if isinstance(child, ctk.CTkButton) and child.cget('text') == theme:
 638                child.configure(state=ctk.NORMAL)
 639            elif isinstance(child, ctk.CTkButton) and child.cget('text') == choice:
 640                child.configure(state=ctk.DISABLED)
 641        change_config('theme', choice)
 642
 643    def on_close(self, event: Any) -> None:
 644        """Waits for close action to properly destroy the window with fade out animation.
 645
 646        Args:
 647            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
 648        """
 649        def update_opacity(i: int) -> None:
 650            if i >= 0:
 651                pywinstyles.set_opacity(self, value=i*0.005, color='#000001')
 652                self.master.after(1, lambda: update_opacity(i - 1))
 653            else:
 654                if not self.previous_theme and not self.choice:
 655                    self.place_forget()
 656                self.update_assets_func()
 657                self.place_forget()
 658                pywinstyles.set_opacity(self, value=1, color='#000001')
 659        update_opacity(200)
 660
 661    @staticmethod
 662    def open_file_explorer(path: str) -> None:
 663        """Opens file explorer with system call specific to user operating system.
 664
 665        Args:
 666            path (str): Path to open.
 667        """
 668        if SYSTEM == 'Windows':
 669            os.startfile(resource_path(path))
 670        elif SYSTEM == 'Darwin':
 671            subprocess.run(['open', resource_path(path)])
 672        elif SYSTEM == 'Linux':
 673            subprocess.run(['xdg-open', resource_path(path)])
 674
 675    @staticmethod
 676    def get_all_files(path: str) -> list[str]:
 677        """Gathers all files from directory. If error occurs after catching the exception empty list is returned.
 678
 679        Args:
 680            path (str): Path of the desired directory.
 681
 682        Returns:
 683            list[str]: List of all file names from path directory.
 684
 685        Exceptions:
 686            FileNotFoundError: If the directory does not exist.
 687            PermissionError: If access to the directory is denied.
 688            OSError: If an OS-related error occurs.
 689        """
 690        path = resource_path(path)
 691        try:
 692            all_files = [os.path.join((path), f) for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
 693            return all_files
 694        except (FileNotFoundError, PermissionError, OSError) as e:
 695            update_error_log(e)
 696            return []
 697
 698    @staticmethod
 699    def get_font_name(ttf_path: str) -> str | None:
 700        """Gets name of the font from font file.
 701
 702        Args:
 703            ttf_path (str): Path to .ttf font file name.
 704
 705        Returns:
 706            str | None: Returns font name on success otherwise None.
 707        """
 708        try:
 709            font: TTFont = TTFont(resource_path(ttf_path))
 710            name: str = ''
 711            for record in font['name'].names:
 712                if record.nameID == 4:
 713                    if b'\000' in record.string:
 714                        name = record.string.decode('utf-16-be')
 715                    else:
 716                        name = record.string.decode('utf-8')
 717                    break
 718            return name
 719        except Exception as e: # dont really know what kind of error might occur here
 720            update_error_log(e)
 721            return None
 722
 723    def open_assets_folder(self) -> None:
 724        """Setup of open assets button.
 725        """
 726        text_label = ctk.CTkLabel(
 727            master     = self.scrollable_frame,
 728            text       = 'Open assets folder',
 729            text_color = COLOR.TEXT,
 730            font       = ctk.CTkFont(str(get_from_config('font_name')), 32)
 731        )
 732        text_label.pack(side=ctk.TOP, padx=75, pady=4, anchor=ctk.NW)
 733        additional_frame = ctk.CTkFrame(
 734            master        = self.scrollable_frame,
 735            fg_color      = COLOR.TILE_2,
 736            corner_radius = 0
 737        )
 738        additional_frame.pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)
 739        open_button = ctk.CTkButton(
 740            master        = additional_frame,
 741            text          = 'OPEN',
 742            font          = ctk.CTkFont(str(get_from_config('font_name')), 20),
 743            text_color    = COLOR.TEXT,
 744            command       = lambda: self.open_file_explorer('assets'),
 745            fg_color      = COLOR.TILE_1,
 746            hover_color   = COLOR.HIGH_TILE_2,
 747            corner_radius = 0
 748        )
 749        open_button.pack(side=ctk.RIGHT, padx=10, pady=4, anchor=ctk.E)
 750        path_text = ctk.CTkLabel(
 751            master     = additional_frame, 
 752            text       = resource_path('assets'), 
 753            text_color = COLOR.DARK_TEXT,
 754            font       = ctk.CTkFont(str(get_from_config('font_name')), 18)
 755        )
 756        path_text.pack(side=ctk.LEFT, padx=15, pady=15)
 757        ctk.CTkLabel(
 758            master        = self.scrollable_frame,
 759            fg_color      = COLOR.DARK_TEXT,
 760            text          = '',
 761            corner_radius = 0,
 762            height        = 16
 763        ).pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)
 764
 765    def choose_font(self) -> None:
 766        """setup of choose font.
 767        """
 768        self.previous_font = str(get_from_config('font_file_name'))
 769        fonts = self.get_all_files('fonts')
 770        if not fonts:
 771            return
 772        text = ctk.CTkLabel(
 773            master     = self.scrollable_frame,
 774            text       = 'Fonts: ',
 775            font       = ctk.CTkFont(str(get_from_config('font_name')), 32),
 776            text_color = COLOR.TEXT
 777        )
 778        text.pack(side=ctk.TOP, anchor=ctk.SW, padx=75, pady=0)
 779        frame = ctk.CTkScrollableFrame(
 780            master                 = self.scrollable_frame,
 781            fg_color               = COLOR.TILE_2,
 782            scrollbar_button_color = COLOR.DARK_TEXT,
 783            orientation            = ctk.HORIZONTAL,
 784            height                 = 70,
 785            corner_radius          = 0,
 786            scrollbar_fg_color     = COLOR.DARK_TEXT
 787        )
 788        frame.pack(side=ctk.TOP, padx=80, pady=5, anchor=ctk.W, fill=ctk.X)
 789        for font in fonts:
 790            self.create_font_button(frame, font)
 791
 792    def create_font_button(self, frame: ctk.CTkFrame, font: str) -> None:
 793        """Setup of font button.
 794
 795        Args:
 796            frame (ctk.CTkFrame): Frame in which button will be placed.
 797            font (str): Font name.
 798        """
 799        current_font = get_from_config('font_name')
 800        font_name = self.get_font_name(font)
 801        font_button: ctk.CTkButton = ctk.CTkButton(
 802            master        = frame,
 803            text          = font_name,
 804            command       = lambda: self.select_font(font, font_button),
 805            font          = self.font_30,
 806            corner_radius = 0,
 807            fg_color      = COLOR.TILE_1,
 808            hover_color   = COLOR.HIGH_TILE_2,
 809            text_color    = COLOR.TEXT
 810        )
 811        font_button.pack(side=ctk.LEFT, padx=4, pady=4, expand=True)
 812        if current_font == font_name:
 813            font_button.configure(state=ctk.DISABLED)
 814
 815    def select_font(self, font: str, button: ctk.CTkButton) -> None:
 816        """Helper function to save change of font name and path to file to config file.
 817
 818        Args:
 819            font (str): Font path.
 820        """
 821        if os.path.basename(font) == self.previous_font:
 822            return
 823        new_font = self.get_font_name(font)
 824        for child in button.master.winfo_children():
 825            if isinstance(child, ctk.CTkButton) and child.cget('text') ==  get_from_config('font_name'):
 826                child.configure(state=ctk.NORMAL)
 827            elif isinstance(child, ctk.CTkButton) and child.cget('text') == new_font:
 828                child.configure(state=ctk.DISABLED)
 829        if new_font:
 830            change_config('font_name', new_font)
 831            change_config('font_file_name', os.path.basename(font))
 832            self.master.board.font_42 = ctk.CTkFont(get_from_config('font_name'), 42)
 833            self.master.board.board_font = ctk.CTkFont(get_from_config('font_name'), int(get_from_config('size'))//3)
 834            self.update_font_func()
 835            self.previous_font = str(get_from_config('font_file_name'))
 836
 837    @staticmethod
 838    def is_valid_color(color: str) -> bool:
 839        """Checks if user passed string is valid with hex color.
 840
 841        Args:
 842            color (str): User defined color.
 843
 844        Returns:
 845            bool: True if color passes regex pattern for hex color, False otherwise.
 846        """
 847        return bool(re.compile(r'^#[0-9a-fA-F]{6}$').match(color))
 848
 849    @staticmethod
 850    def validate_length(new_value: str) -> bool:
 851        """Validation function for color input.
 852
 853        Args:
 854            new_value (str): User input from color entry.
 855
 856        Returns:
 857            bool: True if length of the string is not longer than 7, False otherwise.
 858        """
 859        
 860        return bool(re.compile(r'^[#\w]{0,7}$').match(new_value))
 861
 862    def change_colors(self) -> None:
 863        """Function updating color preview in the theme changer.
 864        """
 865        text = ctk.CTkLabel(
 866            master     = self.scrollable_frame, 
 867            text       = 'Colors: ', 
 868            font       = ctk.CTkFont(str(get_from_config('font_name')), 32), 
 869            text_color = COLOR.TEXT
 870        )
 871        text.pack(side=ctk.TOP, anchor=ctk.SW, padx=75, pady=0)
 872        warning_text = ctk.CTkLabel(
 873            master     = self.scrollable_frame, 
 874            text       = STRING.COLORS_WARNING, 
 875            font       = ctk.CTkFont(str(get_from_config('font_name')), 18),
 876            text_color = COLOR.CLOSE
 877        )
 878        warning_text.pack(side=ctk.TOP, anchor=ctk.SW, padx=100, pady=0)
 879        frame = ctk.CTkFrame(
 880            master        = self.scrollable_frame,
 881            corner_radius = 0,
 882            fg_color      = COLOR.TILE_2
 883        )
 884        frame.pack(side=ctk.TOP, padx=80, pady=0, anchor=ctk.W, fill=ctk.X)
 885        ctk.CTkLabel(
 886            master = frame,
 887            text   = '',
 888            height = 2
 889        ).pack(padx=0, pady=0)
 890        for color in COLOR:
 891            self.color_label(frame, color) if color != 'transparent' else ...
 892        ctk.CTkLabel(
 893            master = frame,
 894            text   = '',
 895            height = 2
 896        ).pack(padx=0, pady=0)
 897        ctk.CTkLabel(
 898            master        = self.scrollable_frame, 
 899            fg_color      = COLOR.DARK_TEXT, 
 900            text          = '', 
 901            corner_radius = 0, 
 902            height        = 16
 903        ).pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)
 904        ctk.CTkLabel(
 905            master        = self.scrollable_frame, 
 906            fg_color      = COLOR.TRANSPARENT, 
 907            text          = '', 
 908            corner_radius = 0, 
 909            height        = 16
 910        ).pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)
 911
 912    def color_label(self, frame: ctk.CTkFrame, color: str) -> None:
 913        """Function creating color preview frame.
 914
 915        Args:
 916            frame (ctk.CTkFrame): Parent frame.
 917            color (str): New hex color string.
 918        """
 919        for color_name , color_str in COLOR.__members__.items():
 920            if color_str == color:
 921                name_of_color = color_name
 922                break
 923        color_frame = ctk.CTkFrame(
 924            master = frame,
 925            fg_color=COLOR.NOTATION_BACKGROUND_B,
 926            corner_radius=0
 927        )
 928        color_frame.pack(side=ctk.TOP, padx=10, pady=4, fill=ctk.X)
 929        vcmd = (self.register(self.validate_length), '%P')
 930        color_entry = ctk.CTkEntry(
 931            master          = color_frame, 
 932            border_width    = 0, 
 933            corner_radius   = 0, 
 934            fg_color        = color,
 935            font            = ctk.CTkFont(get_from_config('font_name'), 20),
 936            validate        = 'key',
 937            validatecommand = vcmd,
 938            text_color      = COLOR.TEXT if color != COLOR.TEXT else COLOR.DARK_TEXT
 939        )
 940        color_entry.insert(0, color)
 941        rgb_color = color.lstrip('#')
 942        r = int(rgb_color[0:2], 16)
 943        g = int(rgb_color[2:4], 16)
 944        b = int(rgb_color[4:6], 16)
 945        color_picker = ctk.CTkLabel(
 946            master = color_frame,
 947            text   = '',
 948            image  = self.color_picker_image
 949        )
 950        color_picker.pack(side=ctk.LEFT, padx=5, pady=4)
 951        color_picker.bind('<Button-1>', lambda e: self.ask_for_color(r, g, b, color_entry, color_name))
 952        color_entry.pack(side=ctk.LEFT, padx=10, pady=4)
 953        ok_button = ctk.CTkButton(
 954            master        = color_frame, 
 955            text          = 'OK', 
 956            font          = ctk.CTkFont(get_from_config('font_name'), 20),
 957            command       = lambda: self.save_color(color_name, color_entry, color_entry, color),
 958            width         = 50,
 959            corner_radius = 0,
 960            fg_color      = COLOR.TILE_1,
 961            hover_color   = COLOR.HIGH_TILE_1,
 962            text_color    = COLOR.TEXT
 963        )
 964        ok_button.pack(side=ctk.LEFT, padx=10, pady=4)
 965        cancel_button = ctk.CTkButton(
 966            master        = color_frame,
 967            text          = 'CANCEL',
 968            font          = ctk.CTkFont(get_from_config('font_name'), 20),
 969            command       = lambda: self.cancel(color_name, color_entry, color),
 970            width         = 50,
 971            corner_radius = 0,
 972            fg_color      = COLOR.CLOSE,
 973            hover_color   = COLOR.CLOSE_HOVER,
 974            text_color    = COLOR.TEXT
 975        )
 976        cancel_button.pack(side=ctk.LEFT, padx=10, pady=4)
 977        color_name_label = ctk.CTkLabel(
 978            master     = color_frame,
 979            text       = name_of_color,
 980            text_color = COLOR.TEXT,
 981            font       = ctk.CTkFont(get_from_config('font_name'), 22)
 982        )
 983        color_name_label.pack(side=ctk.RIGHT, padx=4, pady=4)
 984
 985    def save_color(self, color_name: str, entry: ctk.CTkEntry, color_label: ctk.CTkLabel, old_color: str) -> None:
 986        """Saves new color into config file.
 987
 988        Args:
 989            color_name (str): Name of the color to change.
 990            entry (ctk.CTkEntry): User input with color hex code.
 991            color_label (ctk.CTkLabel): Parent frame to update.
 992        """
 993        new_color = entry.get()
 994        if self.is_valid_color(new_color):
 995            change_color(color_name, new_color)
 996            color_label.configure(fg_color=new_color)
 997        else:
 998            entry.delete(0, ctk.END)
 999            entry.insert(0, old_color)
1000
1001    def ask_for_color(self, r: int, g: int, b: int, entry: ctk.CTkEntry, color_name: str) -> None:
1002        """Input dialog with custom color picker for easy use.
1003
1004        Args:
1005            r (int): Red color intensity.
1006            g (int): Green color intensity.
1007            b (int): Blue color intensity.
1008            entry (ctk.CTkEntry): Entry frame for user input.
1009            color_name (str): Color name from config file.
1010        """
1011        picker = ColorPicker(
1012            fg_color              = COLOR.BACKGROUND,
1013            r                     = r,
1014            g                     = g,
1015            b                     = b,
1016            font                  = ctk.CTkFont(self.font_name, 15),
1017            border_color          = COLOR.TILE_2,
1018            slider_button_color   = COLOR.TILE_2,
1019            slider_progress_color = COLOR.TEXT,
1020            slider_fg_color       = COLOR.DARK_TEXT,
1021            preview_border_color  = COLOR.DARK_TEXT,
1022            button_fg_color       = COLOR.NOTATION_BACKGROUND_B,
1023            button_hover_color    = COLOR.NOTATION_BACKGROUND_W,
1024            icon                  = resource_path(os.path.join('assets', 'logo.ico')),
1025            corner_radius         = 0
1026        )
1027        color = picker.get_color()
1028        if color:
1029            entry.delete(0, ctk.END)
1030            entry.insert(0, color)
1031            change_color(color_name, color)
1032            entry.configure(fg_color=color)
1033
1034    def cancel(self, color_name: str, entry: ctk.CTkEntry, color: str) -> None:
1035        """Helper function to close input dialog without changing any properties in config file.
1036
1037        Args:
1038            color_name (str): Color name from config file.
1039            entry (ctk.CTkEntry): Entry frame for user input.
1040            color (str): Color to keep.
1041        """
1042        entry.delete(0, ctk.END)
1043        entry.insert(0, color)
1044        change_color(color_name, color)
1045        entry.configure(fg_color=color)

Class handling changes in setting such as fonts, assets and colors made by user. Handles saving and updating the changes during app runtime except for color changes as they could take too much time for smooth experience.

Arguments:
  • ctk.CTkFrame : Inheritance from customtkinter CTkFrame widget.
Settings( master, restart_func: Callable, update_assets_func: Callable, update_font_func: Callable)
487    def __init__(self, master, restart_func: Callable, update_assets_func: Callable, update_font_func: Callable) -> None:
488        """Constructor
489         - places itself on the screen
490         - calls all functions creating frames containing content
491
492        Args:
493            master (Any): Parent widget.
494            restart_func (Callable): Master function to restart the game.
495            update_assets_func (Callable): Master function to update assets.
496            update_font_func (Callable): Master function to update font.
497        """
498        super().__init__(master, fg_color=COLOR.BACKGROUND, corner_radius=0)
499        self.place(relx=0, rely=0, relwidth=1, relheight=1)
500        self.close_image: ctk.CTkImage | None = load_menu_image('close')
501        self.color_picker_image: ctk.CTkImage | None = load_menu_image('colorpicker', resize=2)
502        self.close_button()
503        self.font_30: ctk.CTkFont = ctk.CTkFont(str(get_from_config('font_name')), 30)
504        self.scrollable_frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(self, corner_radius=0, fg_color=COLOR.BACKGROUND,
505                                                        scrollbar_button_color=COLOR.DARK_TEXT)
506        self.scrollable_frame.pack(side=ctk.TOP, padx=0, pady=0, fill=ctk.BOTH, expand=True)
507        self.font_name: str = str(get_from_config('font_name'))
508        self.choose_theme()
509        self.choose_font()
510        self.open_assets_folder()
511        self.change_colors()
512        self.previous_theme: str | None = None
513        self.choice: str | None = None
514        self.restart_func: Callable = restart_func
515        self.update_assets_func: Callable = update_assets_func
516        self.update_font_func: Callable = update_font_func
517        ctk.CTkLabel(
518            master   = self,
519            text     = '',
520            height   = 18,
521            fg_color = COLOR.BACKGROUND
522        ).pack(padx=0, pady=0)

Constructor

  • places itself on the screen
  • calls all functions creating frames containing content
Arguments:
  • master (Any): Parent widget.
  • restart_func (Callable): Master function to restart the game.
  • update_assets_func (Callable): Master function to update assets.
  • update_font_func (Callable): Master function to update font.
close_image: customtkinter.windows.widgets.image.ctk_image.CTkImage | None
color_picker_image: customtkinter.windows.widgets.image.ctk_image.CTkImage | None
font_30: customtkinter.windows.widgets.font.ctk_font.CTkFont
scrollable_frame: customtkinter.windows.widgets.ctk_scrollable_frame.CTkScrollableFrame
font_name: str
previous_theme: str | None
choice: str | None
restart_func: Callable
update_assets_func: Callable
update_font_func: Callable
@staticmethod
def list_directories_os(path: str) -> list[str]:
524    @staticmethod
525    def list_directories_os(path: str) -> list[str]:
526        """Lists all directories for given path.
527
528        Args:
529            path (str): Desired path.
530
531        Returns:
532            list[str]: List of all directories from path.
533        """
534        try:
535            entries: list[str] = os.listdir(path)
536            directories: list[str] = [
537                entry for entry in entries
538                if os.path.isdir(os.path.join(path, entry)) and os.listdir(os.path.join(path, entry))
539            ]
540            return directories
541        except FileNotFoundError as e:
542            update_error_log(e)
543            return []

Lists all directories for given path.

Arguments:
  • path (str): Desired path.
Returns:

list[str]: List of all directories from path.

def close_button(self) -> None:
545    def close_button(self) -> None:
546        """Setup of close button.
547        """
548        top_frame: ctk.CTkFrame = ctk.CTkFrame(
549            master   = self,
550            fg_color = COLOR.TRANSPARENT
551        )
552        top_frame.pack(side=ctk.TOP, padx=0, pady=0, fill=ctk.X)
553        settings_text = ctk.CTkLabel(
554            master     = top_frame,
555            text       = 'Settings',
556            font       = ctk.CTkFont(str(get_from_config('font_name')), 38),
557            text_color = COLOR.DARK_TEXT,
558            anchor     = ctk.N
559        )
560        settings_text.pack(side=ctk.LEFT, padx=20, anchor=ctk.NW)
561        close_button = ctk.CTkLabel(
562            master = top_frame,
563            text   = '',
564            font   = ctk.CTkFont(str(get_from_config('font_name')), 24),
565            image  = self.close_image,
566            anchor = ctk.S
567        )
568        close_button.bind('<Button-1>', self.on_close)
569        close_button.pack(side=ctk.RIGHT, anchor=ctk.NE, padx=10, pady=10)

Setup of close button.

def create_theme_button( self, frame: customtkinter.windows.widgets.ctk_frame.CTkFrame, theme: str) -> None:
571    def create_theme_button(self, frame: ctk.CTkFrame, theme: str) -> None:
572        """Setup of theme button.
573
574        Args:
575            frame (ctk.CTkFrame): Frame in which button will be placed.
576            theme (str): Style of Figures to choose.
577        """ 
578        current_theme = get_from_config('theme')
579        theme_button: ctk.CTkButton = ctk.CTkButton(
580            master        = frame,
581            text          = theme,
582            command       = lambda: self.select_theme(theme, theme_button),
583            font          = self.font_30,
584            corner_radius = 0,
585            fg_color      = COLOR.TILE_1,
586            hover_color   = COLOR.HIGH_TILE_2,
587            text_color    = COLOR.TEXT,
588        )
589        theme_button.pack(side=ctk.LEFT, padx=4, pady=4, expand=True)
590        if current_theme == theme:
591            theme_button.configure(state=ctk.DISABLED)

Setup of theme button.

Arguments:
  • frame (ctk.CTkFrame): Frame in which button will be placed.
  • theme (str): Style of Figures to choose.
def choose_theme(self) -> None:
593    def choose_theme(self) -> None:
594        """Setup of theme chooser.
595        """
596        self.previous_theme = str(get_from_config('theme'))
597        themes: list[str] = self.list_directories_os('assets')
598        if not themes:
599            return
600        text: ctk.CTkLabel = ctk.CTkLabel(
601            master     = self.scrollable_frame,
602            text       = 'Themes: ',
603            font       = ctk.CTkFont(str(get_from_config('font_name')), 32),
604            text_color = COLOR.TEXT
605        )
606        text.pack(side=ctk.TOP, anchor=ctk.SW, padx=75, pady=0)
607        themes.remove('menu') if 'menu' in themes else themes
608        frame: ctk.CTkScrollableFrame = ctk.CTkScrollableFrame(
609            master                 = self.scrollable_frame,
610            fg_color               = COLOR.TILE_2,
611            scrollbar_button_color = COLOR.DARK_TEXT,
612            orientation            = ctk.HORIZONTAL,
613            scrollbar_fg_color     = COLOR.DARK_TEXT,
614            height                 = 70,
615            corner_radius          = 0
616        )
617        frame.pack(side=ctk.TOP, padx=80, pady=5, anchor=ctk.W, fill=ctk.X)
618        for theme in themes:
619            self.create_theme_button(frame, theme)
620        warning_text: ctk.CTkLabel = ctk.CTkLabel(
621            master     = self.scrollable_frame,
622            text       = STRING.ASSETS_WARNING,
623            font       = ctk.CTkFont(str(get_from_config('font_name')), 18),
624            text_color = COLOR.CLOSE
625        )
626        warning_text.pack(side=ctk.TOP, anchor=ctk.SW, padx=100, pady=0)

Setup of theme chooser.

def select_theme( self, choice: str, button: customtkinter.windows.widgets.ctk_button.CTkButton) -> None:
628    def select_theme(self, choice: str, button: ctk.CTkButton) -> None:
629        """Helper function to save theme changes to config file.
630
631        Args:
632            choice (str): Name of theme to save.
633        """
634        self.choice = choice
635        theme = get_from_config('theme')
636        for child in button.master.winfo_children():
637            if isinstance(child, ctk.CTkButton) and child.cget('text') == theme:
638                child.configure(state=ctk.NORMAL)
639            elif isinstance(child, ctk.CTkButton) and child.cget('text') == choice:
640                child.configure(state=ctk.DISABLED)
641        change_config('theme', choice)

Helper function to save theme changes to config file.

Arguments:
  • choice (str): Name of theme to save.
def on_close(self, event: Any) -> None:
643    def on_close(self, event: Any) -> None:
644        """Waits for close action to properly destroy the window with fade out animation.
645
646        Args:
647            event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
648        """
649        def update_opacity(i: int) -> None:
650            if i >= 0:
651                pywinstyles.set_opacity(self, value=i*0.005, color='#000001')
652                self.master.after(1, lambda: update_opacity(i - 1))
653            else:
654                if not self.previous_theme and not self.choice:
655                    self.place_forget()
656                self.update_assets_func()
657                self.place_forget()
658                pywinstyles.set_opacity(self, value=1, color='#000001')
659        update_opacity(200)

Waits for close action to properly destroy the window with fade out animation.

Arguments:
  • event (Any): Event type. Doesn't matter but is required parameter by customtkinter.
@staticmethod
def open_file_explorer(path: str) -> None:
661    @staticmethod
662    def open_file_explorer(path: str) -> None:
663        """Opens file explorer with system call specific to user operating system.
664
665        Args:
666            path (str): Path to open.
667        """
668        if SYSTEM == 'Windows':
669            os.startfile(resource_path(path))
670        elif SYSTEM == 'Darwin':
671            subprocess.run(['open', resource_path(path)])
672        elif SYSTEM == 'Linux':
673            subprocess.run(['xdg-open', resource_path(path)])

Opens file explorer with system call specific to user operating system.

Arguments:
  • path (str): Path to open.
@staticmethod
def get_all_files(path: str) -> list[str]:
675    @staticmethod
676    def get_all_files(path: str) -> list[str]:
677        """Gathers all files from directory. If error occurs after catching the exception empty list is returned.
678
679        Args:
680            path (str): Path of the desired directory.
681
682        Returns:
683            list[str]: List of all file names from path directory.
684
685        Exceptions:
686            FileNotFoundError: If the directory does not exist.
687            PermissionError: If access to the directory is denied.
688            OSError: If an OS-related error occurs.
689        """
690        path = resource_path(path)
691        try:
692            all_files = [os.path.join((path), f) for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
693            return all_files
694        except (FileNotFoundError, PermissionError, OSError) as e:
695            update_error_log(e)
696            return []

Gathers all files from directory. If error occurs after catching the exception empty list is returned.

Arguments:
  • path (str): Path of the desired directory.
Returns:

list[str]: List of all file names from path directory.

Exceptions:

FileNotFoundError: If the directory does not exist. PermissionError: If access to the directory is denied. OSError: If an OS-related error occurs.

@staticmethod
def get_font_name(ttf_path: str) -> str | None:
698    @staticmethod
699    def get_font_name(ttf_path: str) -> str | None:
700        """Gets name of the font from font file.
701
702        Args:
703            ttf_path (str): Path to .ttf font file name.
704
705        Returns:
706            str | None: Returns font name on success otherwise None.
707        """
708        try:
709            font: TTFont = TTFont(resource_path(ttf_path))
710            name: str = ''
711            for record in font['name'].names:
712                if record.nameID == 4:
713                    if b'\000' in record.string:
714                        name = record.string.decode('utf-16-be')
715                    else:
716                        name = record.string.decode('utf-8')
717                    break
718            return name
719        except Exception as e: # dont really know what kind of error might occur here
720            update_error_log(e)
721            return None

Gets name of the font from font file.

Arguments:
  • ttf_path (str): Path to .ttf font file name.
Returns:

str | None: Returns font name on success otherwise None.

def open_assets_folder(self) -> None:
723    def open_assets_folder(self) -> None:
724        """Setup of open assets button.
725        """
726        text_label = ctk.CTkLabel(
727            master     = self.scrollable_frame,
728            text       = 'Open assets folder',
729            text_color = COLOR.TEXT,
730            font       = ctk.CTkFont(str(get_from_config('font_name')), 32)
731        )
732        text_label.pack(side=ctk.TOP, padx=75, pady=4, anchor=ctk.NW)
733        additional_frame = ctk.CTkFrame(
734            master        = self.scrollable_frame,
735            fg_color      = COLOR.TILE_2,
736            corner_radius = 0
737        )
738        additional_frame.pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)
739        open_button = ctk.CTkButton(
740            master        = additional_frame,
741            text          = 'OPEN',
742            font          = ctk.CTkFont(str(get_from_config('font_name')), 20),
743            text_color    = COLOR.TEXT,
744            command       = lambda: self.open_file_explorer('assets'),
745            fg_color      = COLOR.TILE_1,
746            hover_color   = COLOR.HIGH_TILE_2,
747            corner_radius = 0
748        )
749        open_button.pack(side=ctk.RIGHT, padx=10, pady=4, anchor=ctk.E)
750        path_text = ctk.CTkLabel(
751            master     = additional_frame, 
752            text       = resource_path('assets'), 
753            text_color = COLOR.DARK_TEXT,
754            font       = ctk.CTkFont(str(get_from_config('font_name')), 18)
755        )
756        path_text.pack(side=ctk.LEFT, padx=15, pady=15)
757        ctk.CTkLabel(
758            master        = self.scrollable_frame,
759            fg_color      = COLOR.DARK_TEXT,
760            text          = '',
761            corner_radius = 0,
762            height        = 16
763        ).pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)

Setup of open assets button.

def choose_font(self) -> None:
765    def choose_font(self) -> None:
766        """setup of choose font.
767        """
768        self.previous_font = str(get_from_config('font_file_name'))
769        fonts = self.get_all_files('fonts')
770        if not fonts:
771            return
772        text = ctk.CTkLabel(
773            master     = self.scrollable_frame,
774            text       = 'Fonts: ',
775            font       = ctk.CTkFont(str(get_from_config('font_name')), 32),
776            text_color = COLOR.TEXT
777        )
778        text.pack(side=ctk.TOP, anchor=ctk.SW, padx=75, pady=0)
779        frame = ctk.CTkScrollableFrame(
780            master                 = self.scrollable_frame,
781            fg_color               = COLOR.TILE_2,
782            scrollbar_button_color = COLOR.DARK_TEXT,
783            orientation            = ctk.HORIZONTAL,
784            height                 = 70,
785            corner_radius          = 0,
786            scrollbar_fg_color     = COLOR.DARK_TEXT
787        )
788        frame.pack(side=ctk.TOP, padx=80, pady=5, anchor=ctk.W, fill=ctk.X)
789        for font in fonts:
790            self.create_font_button(frame, font)

setup of choose font.

def create_font_button( self, frame: customtkinter.windows.widgets.ctk_frame.CTkFrame, font: str) -> None:
792    def create_font_button(self, frame: ctk.CTkFrame, font: str) -> None:
793        """Setup of font button.
794
795        Args:
796            frame (ctk.CTkFrame): Frame in which button will be placed.
797            font (str): Font name.
798        """
799        current_font = get_from_config('font_name')
800        font_name = self.get_font_name(font)
801        font_button: ctk.CTkButton = ctk.CTkButton(
802            master        = frame,
803            text          = font_name,
804            command       = lambda: self.select_font(font, font_button),
805            font          = self.font_30,
806            corner_radius = 0,
807            fg_color      = COLOR.TILE_1,
808            hover_color   = COLOR.HIGH_TILE_2,
809            text_color    = COLOR.TEXT
810        )
811        font_button.pack(side=ctk.LEFT, padx=4, pady=4, expand=True)
812        if current_font == font_name:
813            font_button.configure(state=ctk.DISABLED)

Setup of font button.

Arguments:
  • frame (ctk.CTkFrame): Frame in which button will be placed.
  • font (str): Font name.
def select_font( self, font: str, button: customtkinter.windows.widgets.ctk_button.CTkButton) -> None:
815    def select_font(self, font: str, button: ctk.CTkButton) -> None:
816        """Helper function to save change of font name and path to file to config file.
817
818        Args:
819            font (str): Font path.
820        """
821        if os.path.basename(font) == self.previous_font:
822            return
823        new_font = self.get_font_name(font)
824        for child in button.master.winfo_children():
825            if isinstance(child, ctk.CTkButton) and child.cget('text') ==  get_from_config('font_name'):
826                child.configure(state=ctk.NORMAL)
827            elif isinstance(child, ctk.CTkButton) and child.cget('text') == new_font:
828                child.configure(state=ctk.DISABLED)
829        if new_font:
830            change_config('font_name', new_font)
831            change_config('font_file_name', os.path.basename(font))
832            self.master.board.font_42 = ctk.CTkFont(get_from_config('font_name'), 42)
833            self.master.board.board_font = ctk.CTkFont(get_from_config('font_name'), int(get_from_config('size'))//3)
834            self.update_font_func()
835            self.previous_font = str(get_from_config('font_file_name'))

Helper function to save change of font name and path to file to config file.

Arguments:
  • font (str): Font path.
@staticmethod
def is_valid_color(color: str) -> bool:
837    @staticmethod
838    def is_valid_color(color: str) -> bool:
839        """Checks if user passed string is valid with hex color.
840
841        Args:
842            color (str): User defined color.
843
844        Returns:
845            bool: True if color passes regex pattern for hex color, False otherwise.
846        """
847        return bool(re.compile(r'^#[0-9a-fA-F]{6}$').match(color))

Checks if user passed string is valid with hex color.

Arguments:
  • color (str): User defined color.
Returns:

bool: True if color passes regex pattern for hex color, False otherwise.

@staticmethod
def validate_length(new_value: str) -> bool:
849    @staticmethod
850    def validate_length(new_value: str) -> bool:
851        """Validation function for color input.
852
853        Args:
854            new_value (str): User input from color entry.
855
856        Returns:
857            bool: True if length of the string is not longer than 7, False otherwise.
858        """
859        
860        return bool(re.compile(r'^[#\w]{0,7}$').match(new_value))

Validation function for color input.

Arguments:
  • new_value (str): User input from color entry.
Returns:

bool: True if length of the string is not longer than 7, False otherwise.

def change_colors(self) -> None:
862    def change_colors(self) -> None:
863        """Function updating color preview in the theme changer.
864        """
865        text = ctk.CTkLabel(
866            master     = self.scrollable_frame, 
867            text       = 'Colors: ', 
868            font       = ctk.CTkFont(str(get_from_config('font_name')), 32), 
869            text_color = COLOR.TEXT
870        )
871        text.pack(side=ctk.TOP, anchor=ctk.SW, padx=75, pady=0)
872        warning_text = ctk.CTkLabel(
873            master     = self.scrollable_frame, 
874            text       = STRING.COLORS_WARNING, 
875            font       = ctk.CTkFont(str(get_from_config('font_name')), 18),
876            text_color = COLOR.CLOSE
877        )
878        warning_text.pack(side=ctk.TOP, anchor=ctk.SW, padx=100, pady=0)
879        frame = ctk.CTkFrame(
880            master        = self.scrollable_frame,
881            corner_radius = 0,
882            fg_color      = COLOR.TILE_2
883        )
884        frame.pack(side=ctk.TOP, padx=80, pady=0, anchor=ctk.W, fill=ctk.X)
885        ctk.CTkLabel(
886            master = frame,
887            text   = '',
888            height = 2
889        ).pack(padx=0, pady=0)
890        for color in COLOR:
891            self.color_label(frame, color) if color != 'transparent' else ...
892        ctk.CTkLabel(
893            master = frame,
894            text   = '',
895            height = 2
896        ).pack(padx=0, pady=0)
897        ctk.CTkLabel(
898            master        = self.scrollable_frame, 
899            fg_color      = COLOR.DARK_TEXT, 
900            text          = '', 
901            corner_radius = 0, 
902            height        = 16
903        ).pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)
904        ctk.CTkLabel(
905            master        = self.scrollable_frame, 
906            fg_color      = COLOR.TRANSPARENT, 
907            text          = '', 
908            corner_radius = 0, 
909            height        = 16
910        ).pack(side=ctk.TOP, padx=80, pady=0, fill=ctk.X)

Function updating color preview in the theme changer.

def color_label( self, frame: customtkinter.windows.widgets.ctk_frame.CTkFrame, color: str) -> None:
912    def color_label(self, frame: ctk.CTkFrame, color: str) -> None:
913        """Function creating color preview frame.
914
915        Args:
916            frame (ctk.CTkFrame): Parent frame.
917            color (str): New hex color string.
918        """
919        for color_name , color_str in COLOR.__members__.items():
920            if color_str == color:
921                name_of_color = color_name
922                break
923        color_frame = ctk.CTkFrame(
924            master = frame,
925            fg_color=COLOR.NOTATION_BACKGROUND_B,
926            corner_radius=0
927        )
928        color_frame.pack(side=ctk.TOP, padx=10, pady=4, fill=ctk.X)
929        vcmd = (self.register(self.validate_length), '%P')
930        color_entry = ctk.CTkEntry(
931            master          = color_frame, 
932            border_width    = 0, 
933            corner_radius   = 0, 
934            fg_color        = color,
935            font            = ctk.CTkFont(get_from_config('font_name'), 20),
936            validate        = 'key',
937            validatecommand = vcmd,
938            text_color      = COLOR.TEXT if color != COLOR.TEXT else COLOR.DARK_TEXT
939        )
940        color_entry.insert(0, color)
941        rgb_color = color.lstrip('#')
942        r = int(rgb_color[0:2], 16)
943        g = int(rgb_color[2:4], 16)
944        b = int(rgb_color[4:6], 16)
945        color_picker = ctk.CTkLabel(
946            master = color_frame,
947            text   = '',
948            image  = self.color_picker_image
949        )
950        color_picker.pack(side=ctk.LEFT, padx=5, pady=4)
951        color_picker.bind('<Button-1>', lambda e: self.ask_for_color(r, g, b, color_entry, color_name))
952        color_entry.pack(side=ctk.LEFT, padx=10, pady=4)
953        ok_button = ctk.CTkButton(
954            master        = color_frame, 
955            text          = 'OK', 
956            font          = ctk.CTkFont(get_from_config('font_name'), 20),
957            command       = lambda: self.save_color(color_name, color_entry, color_entry, color),
958            width         = 50,
959            corner_radius = 0,
960            fg_color      = COLOR.TILE_1,
961            hover_color   = COLOR.HIGH_TILE_1,
962            text_color    = COLOR.TEXT
963        )
964        ok_button.pack(side=ctk.LEFT, padx=10, pady=4)
965        cancel_button = ctk.CTkButton(
966            master        = color_frame,
967            text          = 'CANCEL',
968            font          = ctk.CTkFont(get_from_config('font_name'), 20),
969            command       = lambda: self.cancel(color_name, color_entry, color),
970            width         = 50,
971            corner_radius = 0,
972            fg_color      = COLOR.CLOSE,
973            hover_color   = COLOR.CLOSE_HOVER,
974            text_color    = COLOR.TEXT
975        )
976        cancel_button.pack(side=ctk.LEFT, padx=10, pady=4)
977        color_name_label = ctk.CTkLabel(
978            master     = color_frame,
979            text       = name_of_color,
980            text_color = COLOR.TEXT,
981            font       = ctk.CTkFont(get_from_config('font_name'), 22)
982        )
983        color_name_label.pack(side=ctk.RIGHT, padx=4, pady=4)

Function creating color preview frame.

Arguments:
  • frame (ctk.CTkFrame): Parent frame.
  • color (str): New hex color string.
def save_color( self, color_name: str, entry: customtkinter.windows.widgets.ctk_entry.CTkEntry, color_label: customtkinter.windows.widgets.ctk_label.CTkLabel, old_color: str) -> None:
985    def save_color(self, color_name: str, entry: ctk.CTkEntry, color_label: ctk.CTkLabel, old_color: str) -> None:
986        """Saves new color into config file.
987
988        Args:
989            color_name (str): Name of the color to change.
990            entry (ctk.CTkEntry): User input with color hex code.
991            color_label (ctk.CTkLabel): Parent frame to update.
992        """
993        new_color = entry.get()
994        if self.is_valid_color(new_color):
995            change_color(color_name, new_color)
996            color_label.configure(fg_color=new_color)
997        else:
998            entry.delete(0, ctk.END)
999            entry.insert(0, old_color)

Saves new color into config file.

Arguments:
  • color_name (str): Name of the color to change.
  • entry (ctk.CTkEntry): User input with color hex code.
  • color_label (ctk.CTkLabel): Parent frame to update.
def ask_for_color( self, r: int, g: int, b: int, entry: customtkinter.windows.widgets.ctk_entry.CTkEntry, color_name: str) -> None:
1001    def ask_for_color(self, r: int, g: int, b: int, entry: ctk.CTkEntry, color_name: str) -> None:
1002        """Input dialog with custom color picker for easy use.
1003
1004        Args:
1005            r (int): Red color intensity.
1006            g (int): Green color intensity.
1007            b (int): Blue color intensity.
1008            entry (ctk.CTkEntry): Entry frame for user input.
1009            color_name (str): Color name from config file.
1010        """
1011        picker = ColorPicker(
1012            fg_color              = COLOR.BACKGROUND,
1013            r                     = r,
1014            g                     = g,
1015            b                     = b,
1016            font                  = ctk.CTkFont(self.font_name, 15),
1017            border_color          = COLOR.TILE_2,
1018            slider_button_color   = COLOR.TILE_2,
1019            slider_progress_color = COLOR.TEXT,
1020            slider_fg_color       = COLOR.DARK_TEXT,
1021            preview_border_color  = COLOR.DARK_TEXT,
1022            button_fg_color       = COLOR.NOTATION_BACKGROUND_B,
1023            button_hover_color    = COLOR.NOTATION_BACKGROUND_W,
1024            icon                  = resource_path(os.path.join('assets', 'logo.ico')),
1025            corner_radius         = 0
1026        )
1027        color = picker.get_color()
1028        if color:
1029            entry.delete(0, ctk.END)
1030            entry.insert(0, color)
1031            change_color(color_name, color)
1032            entry.configure(fg_color=color)

Input dialog with custom color picker for easy use.

Arguments:
  • r (int): Red color intensity.
  • g (int): Green color intensity.
  • b (int): Blue color intensity.
  • entry (ctk.CTkEntry): Entry frame for user input.
  • color_name (str): Color name from config file.
def cancel( self, color_name: str, entry: customtkinter.windows.widgets.ctk_entry.CTkEntry, color: str) -> None:
1034    def cancel(self, color_name: str, entry: ctk.CTkEntry, color: str) -> None:
1035        """Helper function to close input dialog without changing any properties in config file.
1036
1037        Args:
1038            color_name (str): Color name from config file.
1039            entry (ctk.CTkEntry): Entry frame for user input.
1040            color (str): Color to keep.
1041        """
1042        entry.delete(0, ctk.END)
1043        entry.insert(0, color)
1044        change_color(color_name, color)
1045        entry.configure(fg_color=color)

Helper function to close input dialog without changing any properties in config file.

Arguments:
  • color_name (str): Color name from config file.
  • entry (ctk.CTkEntry): Entry frame for user input.
  • color (str): Color to keep.
class SaveName(customtkinter.windows.ctk_toplevel.CTkToplevel):
1047class SaveName(ctk.CTkToplevel):
1048    """Class for asking user for the save name in popup window.
1049
1050    Args:
1051        ctk.CTkTopLevel : Inheritance from customtkinter CTkFrame widget.
1052    """
1053    def __init__(self) -> None:
1054        """Constructor:
1055         - sets window to appear on top
1056         - loads fonts
1057         - calls all setup functions
1058         - centers window
1059        """
1060        super().__init__(fg_color=COLOR.BACKGROUND)
1061        if SYSTEM == 'Windows':
1062            self.grab_set()
1063        self.attributes('-topmost', True)
1064        self.title('Save')
1065        self.font_21 = ctk.CTkFont(get_from_config('font_name'), 21)
1066        self.font_28 = ctk.CTkFont(get_from_config('font_name'), 28)
1067        self.save_name: str | None | bool = None
1068        self.create_info()
1069        self.create_name_entry()
1070        self.create_save_button()
1071        self.resizable(False, False)
1072        self.protocol('WM_DELETE_WINDOW', self.on_close)
1073        self.center_window()
1074        self.after(201, lambda: self.iconbitmap(resource_path('assets\\logo.ico')))
1075
1076    def create_info(self) -> None:
1077        """Displays warning info.
1078        """
1079        self.info_label: ctk.CTkLabel = ctk.CTkLabel(
1080            master     = self,
1081            fg_color   = COLOR.BACKGROUND,
1082            text       = STRING.SAVES_WARNING,
1083            text_color = COLOR.CLOSE_HOVER,
1084            font       = self.font_21
1085        )
1086        self.info_label.pack(side=ctk.TOP, padx=15, pady=15, fill=ctk.X)
1087
1088    def create_name_entry(self) -> None:
1089        """Creates entry for name of the save.
1090        """
1091        helper_frame: ctk.CTkFrame = ctk.CTkFrame(
1092            master   = self,
1093            fg_color = COLOR.BACKGROUND,
1094
1095        )
1096        helper_frame.pack(side=ctk.TOP, padx=15, pady=15, fill=ctk.X)
1097        self.save_name_entry: ctk.CTkEntry = ctk.CTkEntry(
1098            master           = helper_frame,
1099            fg_color         = COLOR.BACKGROUND,
1100            text_color       = COLOR.TEXT,
1101            corner_radius    = 0,
1102            border_color     = COLOR.DARK_TEXT,
1103            font             = self.font_28,
1104            border_width     = 3,
1105            placeholder_text = 'Name'
1106        )
1107        self.save_name_entry.pack(side=ctk.LEFT, padx=1, pady=1, fill=ctk.X, expand=True)
1108
1109    def create_save_button(self) -> None:
1110        """Setups save button.
1111        """
1112        self.save_button: ctk.CTkButton = ctk.CTkButton(
1113            master        = self,
1114            fg_color      = COLOR.TILE_1,
1115            hover_color   = COLOR.HIGH_TILE_1,
1116            text          = 'SAVE',
1117            font          = self.font_21,
1118            command       = self.on_save_button,
1119            corner_radius = 0,
1120            width         = ctk.CTkFont.measure(self.font_21, 'SAVE') + 20,
1121        )
1122        self.save_button.pack(side=ctk.TOP, padx=15, pady=15, expand=True)
1123
1124    def center_window(self) -> None:
1125        """Function centering the TopLevel window. Screen size independent.
1126        """
1127        x: int = self.winfo_screenwidth()
1128        y: int = self.winfo_screenheight()
1129        app_width: int = self.winfo_width()
1130        app_height: int = self.winfo_height()
1131        self.geometry(f'+{(x//2)-(app_width)}+{(y//2)-(app_height)}')
1132
1133    def get_save_name(self) -> str | None | bool:
1134        """Getter for user input from the entry widget.
1135
1136        Returns:
1137            str | None | bool: String if name is valid, None if user decides to keep default save name and bool if canceled with closing window with ❌.
1138        """
1139        self.master.wait_window(self)
1140        return self.save_name
1141
1142    def on_save_button(self) -> None:
1143        """Function checking if user entry is valid after clicking save button.
1144        """
1145        self.save_name = self.save_name_entry.get()
1146        files: list[str] = [f for f in os.listdir(resource_path('saves'))]
1147        if f'{self.save_name}.json' in files:
1148            self.save_name = None
1149        if isinstance(self.save_name, str) and len(self.save_name) < 1:
1150            self.save_name = None
1151        if isinstance(self.save_name, str) and self.save_name.startswith('chess_game_'):
1152            self.save_name = None
1153        self.destroy()
1154
1155    def on_close(self) -> None:
1156        """Custom closing function ensuring proper closing of the window. Sets save_name to False to cancel saving.
1157        """
1158        self.save_name = False
1159        self.grab_release()
1160        self.destroy()

Class for asking user for the save name in popup window.

Arguments:
  • ctk.CTkTopLevel : Inheritance from customtkinter CTkFrame widget.
SaveName()
1053    def __init__(self) -> None:
1054        """Constructor:
1055         - sets window to appear on top
1056         - loads fonts
1057         - calls all setup functions
1058         - centers window
1059        """
1060        super().__init__(fg_color=COLOR.BACKGROUND)
1061        if SYSTEM == 'Windows':
1062            self.grab_set()
1063        self.attributes('-topmost', True)
1064        self.title('Save')
1065        self.font_21 = ctk.CTkFont(get_from_config('font_name'), 21)
1066        self.font_28 = ctk.CTkFont(get_from_config('font_name'), 28)
1067        self.save_name: str | None | bool = None
1068        self.create_info()
1069        self.create_name_entry()
1070        self.create_save_button()
1071        self.resizable(False, False)
1072        self.protocol('WM_DELETE_WINDOW', self.on_close)
1073        self.center_window()
1074        self.after(201, lambda: self.iconbitmap(resource_path('assets\\logo.ico')))

Constructor:

  • sets window to appear on top
  • loads fonts
  • calls all setup functions
  • centers window
font_21
font_28
save_name: str | None | bool
def create_info(self) -> None:
1076    def create_info(self) -> None:
1077        """Displays warning info.
1078        """
1079        self.info_label: ctk.CTkLabel = ctk.CTkLabel(
1080            master     = self,
1081            fg_color   = COLOR.BACKGROUND,
1082            text       = STRING.SAVES_WARNING,
1083            text_color = COLOR.CLOSE_HOVER,
1084            font       = self.font_21
1085        )
1086        self.info_label.pack(side=ctk.TOP, padx=15, pady=15, fill=ctk.X)

Displays warning info.

def create_name_entry(self) -> None:
1088    def create_name_entry(self) -> None:
1089        """Creates entry for name of the save.
1090        """
1091        helper_frame: ctk.CTkFrame = ctk.CTkFrame(
1092            master   = self,
1093            fg_color = COLOR.BACKGROUND,
1094
1095        )
1096        helper_frame.pack(side=ctk.TOP, padx=15, pady=15, fill=ctk.X)
1097        self.save_name_entry: ctk.CTkEntry = ctk.CTkEntry(
1098            master           = helper_frame,
1099            fg_color         = COLOR.BACKGROUND,
1100            text_color       = COLOR.TEXT,
1101            corner_radius    = 0,
1102            border_color     = COLOR.DARK_TEXT,
1103            font             = self.font_28,
1104            border_width     = 3,
1105            placeholder_text = 'Name'
1106        )
1107        self.save_name_entry.pack(side=ctk.LEFT, padx=1, pady=1, fill=ctk.X, expand=True)

Creates entry for name of the save.

def create_save_button(self) -> None:
1109    def create_save_button(self) -> None:
1110        """Setups save button.
1111        """
1112        self.save_button: ctk.CTkButton = ctk.CTkButton(
1113            master        = self,
1114            fg_color      = COLOR.TILE_1,
1115            hover_color   = COLOR.HIGH_TILE_1,
1116            text          = 'SAVE',
1117            font          = self.font_21,
1118            command       = self.on_save_button,
1119            corner_radius = 0,
1120            width         = ctk.CTkFont.measure(self.font_21, 'SAVE') + 20,
1121        )
1122        self.save_button.pack(side=ctk.TOP, padx=15, pady=15, expand=True)

Setups save button.

def center_window(self) -> None:
1124    def center_window(self) -> None:
1125        """Function centering the TopLevel window. Screen size independent.
1126        """
1127        x: int = self.winfo_screenwidth()
1128        y: int = self.winfo_screenheight()
1129        app_width: int = self.winfo_width()
1130        app_height: int = self.winfo_height()
1131        self.geometry(f'+{(x//2)-(app_width)}+{(y//2)-(app_height)}')

Function centering the TopLevel window. Screen size independent.

def get_save_name(self) -> str | None | bool:
1133    def get_save_name(self) -> str | None | bool:
1134        """Getter for user input from the entry widget.
1135
1136        Returns:
1137            str | None | bool: String if name is valid, None if user decides to keep default save name and bool if canceled with closing window with ❌.
1138        """
1139        self.master.wait_window(self)
1140        return self.save_name

Getter for user input from the entry widget.

Returns:

str | None | bool: String if name is valid, None if user decides to keep default save name and bool if canceled with closing window with ❌.

def on_save_button(self) -> None:
1142    def on_save_button(self) -> None:
1143        """Function checking if user entry is valid after clicking save button.
1144        """
1145        self.save_name = self.save_name_entry.get()
1146        files: list[str] = [f for f in os.listdir(resource_path('saves'))]
1147        if f'{self.save_name}.json' in files:
1148            self.save_name = None
1149        if isinstance(self.save_name, str) and len(self.save_name) < 1:
1150            self.save_name = None
1151        if isinstance(self.save_name, str) and self.save_name.startswith('chess_game_'):
1152            self.save_name = None
1153        self.destroy()

Function checking if user entry is valid after clicking save button.

def on_close(self) -> None:
1155    def on_close(self) -> None:
1156        """Custom closing function ensuring proper closing of the window. Sets save_name to False to cancel saving.
1157        """
1158        self.save_name = False
1159        self.grab_release()
1160        self.destroy()

Custom closing function ensuring proper closing of the window. Sets save_name to False to cancel saving.