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