try:
 from PyQt6 import QtWidgets, QtGui, QtCore
 from PyQt6.QtGui import QAction
 from PyQt6 import uic
 import PyQt6.QtSvgWidgets
 import PyQt6.QtCharts
except:
 try:
  from PyQt5 import QtWidgets, QtGui, QtCore
  from PyQt5.QtWidgets import QAction
  from PyQt5 import uic
  import PyQt5.QtSvg
  import PyQt5.QtChart
 except:
  raise ModuleNotFoundError('Neither the required PyQt6 nor PyQt5 modules completely installed')
import sys
# We must create the QtWidgets.QApplication here to avoid Sphinx issues
qApp = QtWidgets.QApplication(sys.argv)
'''
Main Window of the Chess GUI
================================
|MainWindow| 
The main Window consists of 4 parts:
 * On the left, the *game board* with the *material* and *next turn* labels. The *next turn* may 4 colors ``white``, ``black``, ``red`` (mate), ``yellow`` (draw)
 * On the right, the TAB widget with the
    
    * *Game* tree widget displaying the body of the actual game
    * *Headers* list widget displaying the header of the actual game
    * *Database* list widget displaying all loaded games
    * *Score* plot displaying engine and material scores
    * *Log* edit displaying communication with the engine in use
    
 * On the bottom, the status bar with the labels
    * *info/error* displaying information or errors
    * *engine* displaying the engine in use
    * *hint/score* displaying the hint and score computed by the engine
    * *ECO code* displaying the eco code of the current opening, the hint shows the full opening string
    * *square* displaying the square under the cursor within the board
Main Window Menu
================================
On the top, the menu with
 * *File* menu handling files in the Portable Game Notation (`PGN`_) and pickled PGN (*PPNG*) formats
    * *Encoding* sub-menu to set encoding for opening/saveing PNG-format
    * *Open DB ...* action to open a PPGN- or PGN-file and replace the current database
    * *Recent* sub-menu with the recent PPGN- or PGN-files
    * *Append to DB ...* action to open a PPGN- or PGN-file and append to the current database
    * *Save DB ...* action to save the whole database as a PPGN- or PGN-file 
    * *Save Game ...* action to save the actual game as a PGN-file 
    * *Exit* action to terminate the GUI
 * *Edit* menu with obvious functionality with the exception of
 
    * *Copy Game*, i.e. the actual game PGN is copied to the clipboard
    * *Copy FEN*, i.e. the actual position is copied to the clipboard
    * *Promote/Demote Variant* promote/demote the variant selected in the Game TAB
    * *Promote Variant to Main* promote the variant selected in the Game TAB to mainline
    * *Delete Variant* delete the variant selected in the Game TAB
    * *Undo Current Action* deletes the end move of a sequence (main line or variant)
 * *Database* menu with obvious functionality with the exception of
 
    * *Remove Games* removes selected games in the *Database* TAB
    * *Move Games>Up/Down*  moves a selected game up and down in the *Database*
    * *Paste Game*, i.e. the actual game PGN from the clipboard and added to the *Database*
    * *Paste FEN*, i.e. the actual position is pasted from the clipboard and added to the *Database*
 * *Game* menu with obvious functionality with the exception of
 
    * *Select Header Elements ...* opens a dialog to add/remove header elements according to the `PGN`_ standard
    * *Show Move Options* shows by left-button selecting a square the weighted options 
    * *Warn of Danger* shows squares attacked by the opponent 
 * *Engine* menu for handling Universal Chess Interface (`UCI`_) engines with
 
    * *Select Engine* sub-menu to select the engine for hints/scores and annotations
    * *Search Depth* sub-menu to set the search depth of the selected engine
    * *Score Current Move* annotates the move leading to actual position
    * *Annotate All* annotates the actual game or variant
    * *# Annotations* defines the number of variants to be suggested in case of a blunder
    * *Blunder Limit* defines the limit to add a variant
    * *Annotate Variants* defines the number of half moves (PLY) to be shown in variants 
    * *Show Scores* toggle actions enables the *score* part of the *hint/score* label of the status bar
    * *Show Hints* toggle actions enables the *hint* part of the *hint/score* label of the status bar
    * *Configure ...* opens a dialog to add/remove/configure engines
    * *Debug* logs the communication with engine in the *Log* TAB
Keyboard and Mouse Contol
================================
To allow for a user-friendly usage, the game can be controlled by keyboard and/or mouse:
.. csv-table:: Game Control by Keyboard/Mouse/Mouse Wheel
   :header: "Key/Wheel", "Board", "Game-TAB", "Description"
   :widths: 30, 10, 10, 50
   
   :kbd:`up` | :kbd:`scroll-up`       , :kbd:`✔`, :kbd:`✔`, goto last move
   :kbd:`down` | :kbd:`scroll-down`, :kbd:`✔`, :kbd:`✔`, goto next move
   :kbd:`left`                               ,              , :kbd:`✔`, step into variant
   :kbd:`right`                             ,              , :kbd:`✔`, step out of a variant
   :kbd:`home`                            ,              , :kbd:`✔`, goto initial move
   :kbd:`end`                              ,              , :kbd:`✔`, goto end-of-game
   :kbd:`mouse-left-press`            , :kbd:`✔`,              , begin move
   :kbd:`mouse-left-release`          , :kbd:`✔`,              , end move
   :kbd:`Control-P`                      ,              , :kbd:`✔`, promote variant
   :kbd:`Control-D`                      ,              , :kbd:`✔`, demote variant
   :kbd:`Control-R`                      ,              , :kbd:`✔`, remove variant
   :kbd:`Control-M`                      ,              , :kbd:`✔`, promote variant to mainline
   :kbd:`Control-W`                      ,              , :kbd:`✔`, toggle warn of danger
In addition, several *standard* key strokes are supported:
.. csv-table:: Other Key-Codes
   :header: "Key", "Description"
   :widths: 30, 50
   :kbd:`Control-Z`                       , undo last move
   :kbd:`Control-F`                       , flip board
   :kbd:`Control-C`                       , copy game (PGN) to clipboard
   :kbd:`Control-V`                       , paste game (PGN) from clipboard
   :kbd:`Control-Shift-C`               , copy position (FEN) to clipboard
   :kbd:`Control-Shift-V`               , paste position (FEN) from clipboard
.. |MainWindow| image:: mainWindow.png
  :width: 800
  :alt: Main Window
.. _PGN: https://github.com/fsmosca/PGN-Standard
.. _UCI: http://wbec-ridderkerk.nl/html/UCIProtocol.html
'''
from typing import Optional, Callable, Dict, List, Tuple, Union, Any
import configparser
import os, os.path
import copy
import platform
import pickle
import io
import re
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import chess, chess.pgn
import MzChess
from MzChess import read_game
import AboutDialog
[docs]class ChessMainWindow(QtWidgets.QMainWindow):
 logSignal = QtCore.pyqtSignal(str)
 notifySignal = QtCore.pyqtSignal(str)
 notifyGameSelectedSignal = QtCore.pyqtSignal(int)
 notifyGameListHeaderChangedSignal = QtCore.pyqtSignal(list)
 notifyGameListChangedSignal = QtCore.pyqtSignal()
 notifyGameHeadersChangedSignal = QtCore.pyqtSignal(chess.pgn.Headers)
 notifyGameNodeSelectedSignal = QtCore.pyqtSignal(chess.pgn.GameNode)
 notifyGameNodeChangedSignal = QtCore.pyqtSignal(chess.pgn.GameNode, tuple)
 notifyGameChangedSignal = QtCore.pyqtSignal(chess.pgn.Game)
 notifyNewGameNodeSignal = QtCore.pyqtSignal(chess.pgn.GameNode)
 hintDict = { 'None' :  '0',  'White' : '1',  'Black' : '2',  'All' : '3' }
 hintList = [ 'None', 'White',  'Black',  'All' ]
 fileDialogOptions = QtWidgets.QFileDialog.Option.DontUseNativeDialog
 fileDirectory = os.path.dirname(os.path.abspath(__file__))
 intRe = re.compile(r"^[+-]?[1-9][0-9]*$")
 boolRe = re.compile(r"^(True|False)$")
 def __init__(self, parent = None) -> None:
  super(ChessMainWindow, self).__init__(parent)
  MzChess.installLeipFont()
  uic.loadUi(os.path.join(self.fileDirectory, 'chessMainWindow.ui'), self)
  # self.setupUi(self)
  icon = QtGui.QIcon()
  icon.addPixmap(QtGui.QPixmap(os.path.join(self.fileDirectory,'schach.png')), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off)
  self.setWindowIcon(icon)
  
  self.pgm = MzChess.__name__
  self.version = MzChess.__version__
  self.dateString = MzChess.__date__
  
  # self.helpIndex = QtCore.QUrl.fromLocalFile(os.path.join(os.path.dirname(self.fileDirectory), 'doc_build', 'html', 'index.html'))
  self.helpIndex = QtCore.QUrl('https://reinhardm-dev.github.io/MzChess')
  self.ecoDB = MzChess.ECODatabase()
  self.ecoFen2IdDict = self.ecoDB.fen2Id()
  self.toolBar = QtWidgets.QToolBar()
  self._setIcon(self.actionSaveDB, QtWidgets.QStyle.StandardPixmap.SP_DialogSaveButton)
  self.toolBar.addSeparator()
  self._setIcon(self.actionAddGame, QtWidgets.QStyle.StandardPixmap.SP_FileIcon)
  self._setIcon(self.actionFlipBoard, QtWidgets.QStyle.StandardPixmap.SP_BrowserReload)
  self.toolBar.addSeparator()
  self._setIcon(self.actionNextMove, QtWidgets.QStyle.StandardPixmap.SP_ArrowDown)
  self._setIcon(self.actionPreviousMove, QtWidgets.QStyle.StandardPixmap.SP_ArrowUp)
  self._setIcon(self.actionNextVariant, QtWidgets.QStyle.StandardPixmap.SP_ArrowForward)
  self._setIcon(self.actionPreviousVariant, QtWidgets.QStyle.StandardPixmap.SP_ArrowBack)
  self._setIcon(self.actionPromoteVariant, QtWidgets.QStyle.StandardPixmap.SP_MediaSeekBackward)
  self._setIcon(self.actionDemoteVariant, QtWidgets.QStyle.StandardPixmap.SP_MediaSeekForward)
  self._setIcon(self.actionPromoteVariant2Main, QtWidgets.QStyle.StandardPixmap.SP_MediaSkipBackward)
  self._setIcon(self.actionDeleteVariant, QtWidgets.QStyle.StandardPixmap.SP_DialogCloseButton)
  self.toolBar.addSeparator()
  self._setIcon(self.actionUndo, QtWidgets.QStyle.StandardPixmap.SP_BrowserStop)
  self.addToolBar(self.toolBar)
  
  self.gameOptions = { 
   'showOptions' : (self.actionShowOptions, False), 
   'warnOfDanger' : (self.actionWarnOfDanger, False), 
   'encoding' : (self.menuEncoding, 'UTF-8')
  }
  self.encodingDict = { 'UTF-8' : 'utf-8-sig', 'ISO 8859/1' : 'iso-8859-1',  'ASCII' : 'ascii'}
  
  self.engineOptions = {
   'selectedEngine' : (self.menuSelectEngine, None), 
   'searchDepth' : (self.menuSearchDepth, 15), 
   'numberOfAnnotations' : (self.menuNumberOfAnnotations, 1), 
   'annotateVariants' : (self.menuAnnotateVariants, None), 
   'showScores' : (self.actionShowScores, False), 
   }
  
  #  'blunderLimit' : (self.menuBlunderLimit, -float('inf')), 
  self.gameListHeaders = ['Date',  'White',  'Black',  'Result']
  
  self.engineDict = dict()
  self.recentPGN = dict()
  self.eventList = list()
  self.siteList = list()
  self.playerList = list()
  self.optionalHeaderItems = list()
  self.hintEngine = None
  self.mateScore = 100
  self.debugEngine = False
  
  if platform.system() == 'Windows':
   self.settingsDir = os.path.join(os.path.expanduser('~'), 'AppData', 'Roaming', 'MzChess')
  else:
   self.settingsDir = os.path.join(os.path.expanduser('~'), '.config', 'MzChess')
  if not os.path.isdir(self.settingsDir):
   os.mkdir(self.settingsDir, 0o755)
  self.settingsFile = os.path.join(self.settingsDir, 'settings.ini')
  self.recoverFile = os.path.join(self.settingsDir, 'recover.ppgn')
  self.settings = configparser.ConfigParser(delimiters=['='], allow_no_value=True)
  self.settings.optionxform = str
  self.recentPGN = dict()
  self.engineDict = dict()
  self.eventList = list()
  self.siteList = list()
  self.playerList = list()
  self.optionalHeaderItems = list()
  if os.path.isfile(self.settingsFile):
   self.settings.read(self.settingsFile, encoding = 'utf-8')
   self.engineDict = MzChess.loadEngineSettings(self.settings)
   if 'Recent' in self.settings.sections():
    for recentDB, enc in dict(self.settings['Recent']).items():
     if os.path.exists(recentDB):
      self.recentPGN[recentDB] = enc
   if 'GameListHeaders' in self.settings.sections():
    self.gameListHeaders = self.settings.options('GameListHeaders')
   if 'Events' in self.settings.sections():
    self.eventList = self.settings.options('Events')
   if 'Sites' in self.settings.sections():
    self.siteList = self.settings.options('Sites')
   if 'Players' in self.settings.sections():
    self.playerList = self.settings.options('Players')
   if 'OptionalHeaderItems' in self.settings.sections():
    self.optionalHeaderItems = list(self.settings['OptionalHeaderItems'])
   if self.settings['Menu/Engine']['selectedEngine'] not in self.engineDict:
    self.settings['Menu/Engine']['selectedEngine'] = None
    self.settings['Menu/Engine']['showHints'] = '0'
    self.settings['Menu/Engine']['showScores'] = False
   
  self.menuRecentDB.clear()
  if self.recentPGN is None:
   self.recentPGN = dict()
  else:
   actDir = os.path.expanduser('~')
   for rItem in self.recentPGN.keys():
    if rItem is not None and os.path.isfile(rItem):
     if actDir == os.path.expanduser('~'):
      actDir = os.path.dirname(rItem)
     self.menuRecentDB.addAction(rItem)
  
  os.chdir(actDir)
  
  self.aboutDialog = AboutDialog.AboutDialog()
  self.aboutDialog.setup(
   pgm = self.pgm, 
   version = self.version, 
   dateString = self.dateString)
  sbText = self.statusBar().font()
  sbText.setPointSize(12)
  
  self.moveLabel = QtWidgets.QLabel()
  self.moveLabel.setFont(sbText)
  self.moveLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
  self.moveLabel.setToolTip("Total Moves/Half-moves since the last capture or pawn move")
  self.statusBar().addWidget(self.moveLabel, 20)
  self.infoLabel = QtWidgets.QLabel()
  self.infoLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignLeft)
  self.infoLabel.setFont(sbText)
  self.statusBar().addWidget(self.infoLabel, 130)
  self.engineLabel = QtWidgets.QLabel()
  self.engineLabel.setFont(sbText)
  self.engineLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
  self.engineLabel.setToolTip("Engine in use")
  self.statusBar().addWidget(self.engineLabel, 50)
  self.hintLabel = QtWidgets.QLabel()
  self.hintLabel.setFont(sbText)
  self.hintLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
  self.hintLabel.setToolTip("Best move/Score[Pawns]")
  self.statusBar().addWidget(self.hintLabel, 30)
  self.ecoLabel = QtWidgets.QLabel()
  self.ecoLabel.setFont(sbText)
  self.ecoLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
  self.statusBar().addWidget(self.ecoLabel, 10)
  self.squareLabel = QtWidgets.QLabel()
  self.squareLabel.setFont(sbText)
  self.squareLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
  self.squareLabel.setToolTip("Position")
  self.statusBar().addWidget(self.squareLabel, 10)
  self.itemSelector = MzChess.ItemSelector('Header Elements (without 7-tag roster)...', pointSize = 10)
  
  self.loadOptionDict('Menu/Game', self.gameOptions)
  self.resetSelectEngine()
  self.loadOptionDict('Menu/Engine', self.engineOptions)
  blunderLimit = float(self.settings['Menu/Engine'].get('blunderLimit', '-inf'))
  for action in self.menuBlunderLimit.actions():
   tList = action.text().split(' ')
   action.setChecked((len(tList) == 1 and blunderLimit == -float('inf')) \
                        
or  (len(tList) == 2 and blunderLimit == -float(tList[0])))
  hintValue = int(self.settings['Menu/Engine'].get('showHints', 0))
  for actAction in self.menuShowHints.actions():
   if actAction.text() == self.hintList[hintValue]:
    actAction.setChecked(True)
  self.gameListTableView.setup(notifyDoubleClickSignal = self.notifyGameSelectedSignal, 
                                          notifyHeaderChangedSignal = self.notifyGameListHeaderChangedSignal, 
                                          notifyListChangedSignal = self.notifyGameListChangedSignal)
  self.gameListTableView.gameHeaderKeys = self.gameListHeaders
  self.notifyGameSelectedSignal.connect(self.gameSelected)
  self.notifyGameListHeaderChangedSignal.connect(self.gameListHeaderChanged)
  self.notifyGameListChangedSignal.connect(self.gameListChanged)
  self.pgnFile = None
  self.gameListFile = ''
  self.gameList = list()
  self.gameListChanged = False
  self.gameFile = ''
  self.game = chess.pgn.Game()
  self.gameNode = self.game
  self.gameID = None
  self.undoListList = list()
  self.redoListList = list()
  self._addGame(game = None, isNew = True)
  self.setChessWindowTitle()
  self.gameSelected(0)
 
  self.gameHeaderTableView.setup(
       notifyGameHeadersChangedSignal = self.notifyGameHeadersChangedSignal, 
       eventList = self.eventList, siteList = self.siteList, playerList = self.playerList)
  self.notifyGameHeadersChangedSignal.connect(self.gameHeadersChanged)
  self.boardGraphicsView.setup(
   notifyNewGameNodeSignal = self.notifyNewGameNodeSignal, notifyGameNodeSelectedSignal =self.notifyGameNodeSelectedSignal, 
   materialLabel = self.materialLabel, squareLabel = self.squareLabel, 
   turnFrame= self.turnFrame, hintLabel = self.hintLabel, 
   flipped = False)
  self.gameTreeViewWidget.setup(self.notifyGameNodeSelectedSignal, self.notifyGameNodeChangedSignal)
  self.scorePlotGraphicsView.setup(self.notifyGameNodeSelectedSignal)
  self.notifyGameNodeSelectedSignal.connect(self.gameNodeSelected)
  self.notifyGameNodeChangedSignal.connect(self.gameNodeChanged)
  self.notifyNewGameNodeSignal.connect(self.newGameNode)
  self.notifyGameChangedSignal.connect(self.gameChanged)
  self.notifySignal.connect(self.notify)
  self.logSignal.connect(self.toLog)
  self.show_HintsScores()
 def setup(self) -> None:
  sizes = self.splitter.sizes()
  self.splitter.setSizes([self.boardGraphicsView.height(), sizes[0] + sizes[1] - self.boardGraphicsView.height()])
  self.boardGraphicsView.setDrawOptions(self.actionShowOptions.isChecked())
  self.boardGraphicsView.setGameNode(self.game)
  self.gameTreeViewWidget.setGame(self.game)
  self.gameListTableView.resetDB()
  self.gameHeaderTableView.resetGame()
 def loadOptionDict(self, name : str, n2oDict : Dict[str, Union[QAction, Tuple[QtWidgets.QMenu, Any]]]) -> Dict[str, Union[int, float]]:
  if name not in self.settings:
   self.settings[name] = dict()
  optionsDict = dict()
  for opt, o2v in n2oDict.items():
   obj, defValue = o2v
   optionsDict[opt] = defValue
   optValue = self.settings[name].get(opt, defValue)
   if isinstance(obj, QAction):
    optionsDict[opt] = (optValue == 'True')
    obj.setChecked(optionsDict[opt])
   elif isinstance(obj, QtWidgets.QMenu):
    for action in obj.actions():
     tListString = action.text()
     if len(tListString) == 0:
      continue
     tList = tListString.split(' ')
     if self.intRe.match(tList[0]) is not None:
      actionValue = int(tList[0])
     elif tList[0] == 'All':
      actionValue = 2^32 - 1
     elif tList[0] == 'None':
      actionValue = None
     else:
      try:
       actionValue = float(tList[0])
      except:
       actionValue = tListString
     if str(actionValue) == optValue or '{}.0'.format(actionValue) == optValue:
      action.setChecked(True)
      optionsDict[opt] = actionValue
     else:
      action.setChecked(False)
  for tag, value in optionsDict.items():
   self.settings[name][tag] = str(value)
   
 def resetSelectEngine(self) -> None:
  self.menuSelectEngine.clear()
  action = self.menuSelectEngine.addAction('None')
  action.setCheckable(True)
  if 'Menu/Engine' in self.settings.sections():
   if self.settings['Menu/Engine']['selectedEngine'] is None or self.settings['Menu/Engine']['selectedEngine'] not in self.engineDict:
    action.setChecked(True)
   self.menuSelectEngine.addSeparator()
   for eItem in self.engineDict:
    action = self.menuSelectEngine.addAction(eItem)
    action.setCheckable(True)
    if self.settings['Menu/Engine']['selectedEngine'] == eItem:
     action.setChecked(True)
 def updateSettingsList(self, section : str, valueList : List[Union[str, Tuple[str, str]]] = list(), firstValue : Union[str, Tuple[str, str], None] = None) -> None:
  if section not in self.settings.sections():
   self.settings.add_section(section)
  newDict = dict()
  if len(valueList) > 0:
   newDict = dict(self.settings[section])
  if isinstance(firstValue, tuple):
   key = firstValue[0]
   value = firstValue[1]
  else:
   key = firstValue
   value = None
  keys = list()
  if key is not None:
   newDict[key] = value
   self.settings.remove_option(section, key)
   keys = [key]
  if len(valueList) == 0:
   keys += self.settings.options(section)
  self.settings.remove_section(section)
  self.settings.add_section(section)
  for key in keys:
   if key in newDict:
    value = newDict[key]
   self.settings[section][key] = value
  for kv in valueList:
   if isinstance(kv, tuple):
    self.settings[section][kv[0]] = kv[1]
   else:
    self.settings[section][kv] = None
  
 def saveSettings(self) -> None:
  with open(self.settingsFile, 'w', encoding = 'utf-8') as f:
   self.settings.write(f)
 # --------------------------------------------------------------------------------------------------
 
 @QtCore.pyqtSlot(str)
 def notifyError(self, str : str) -> None:
  msgBox = QtWidgets.QMessageBox()
  msgBox.setIcon(QtWidgets.QMessageBox.Icon.Critical)
  msgBox.setText(str)
  msgBox.setWindowTitle("Error ...")
  msgBox.exec()
 def setInfoLabel(self, setupGameID = False):
  if setupGameID:
   if self.game in self.gameList:
    self.gameID = self.gameList.index(self.game)
   else:
    if self.gameID > 0:
     self.gameID = self.gameID - 1
    self.game = self.gameList[self.game]
  self.infoLabel.setText('game #{} of {} ({} moves): {} = {}'.format(self.gameID, len(self.gameList), 
   self.gameList[self.gameID].end().board().fullmove_number, self.gameListHeaders[0], self.game.headers[self.gameListHeaders[0]]))
  self.infoLabel.update()
  QtWidgets.QApplication.processEvents()
 def setMoveLabel(self, board):
  self.moveLabel.setText('{}/{}'.format(board.fullmove_number, board.halfmove_clock))
  self.moveLabel.update()
  QtWidgets.QApplication.processEvents()
 def notify(self, str):
  self.statusBar().showMessage(str, 3000)
  
 @QtCore.pyqtSlot(str)
 def toLog(self, msg):
  self.uciTextEdit.moveCursor (QtGui.QTextCursor.MoveOperation.End)
  self.uciTextEdit.insertPlainText (msg)
  self.uciTextEdit.moveCursor(QtGui.QTextCursor.MoveOperation.End)
 def _setIcon(self, action, stdIcon, toToolbar = True):
  action.setIcon(self.style().standardIcon(stdIcon))
  if toToolbar:
   self.toolBar.addAction(action)
 def _showEcoCode(self, gameNode, fromBeginning = False):
  if fromBeginning:
   actNode = gameNode.game().next()
   self.ecoCode = '---'
   self.ecoDescription = ''
  else:
   actNode = gameNode
  id = -1
  while actNode is not None:
   fen = ' '.join(actNode.board().fen().split(' ')[:-2])
   if fen in self.ecoFen2IdDict:
    id = self.ecoFen2IdDict[fen]
   actNode = actNode.next()
  if id >= 0:
   self.ecoCode = self.ecoDB[id][0]
   self.ecoDescription = self.ecoDB[id][1]
   gameNode.game().headers['Opening'] = self.ecoDescription
   gameNode.game().headers['ECO'] = self.ecoCode
   self.ecoLabel.setText(self.ecoCode)
   self.ecoLabel.setToolTip(self.ecoDescription)
 def _allowNewGameList(self):
  if self.gameListChanged:
   rc = QtWidgets.QMessageBox.warning(self, 
    'Game database not saved ...', 'Do you like to close anyway ?', 
     QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, 
     QtWidgets.QMessageBox.StandardButton.No)
   if rc == QtWidgets.QMessageBox.StandardButton.No:
    return False
  self.gameListChanged = False
  self.undoListList = list()
  self.redoListList = list()
  self.gameList = list()
  self.pgnFile = None
  self._addGame(game = None, isNew = True)
  return True
 def _addGame(self, game = None, isNew = False):
  if game is not None:
   self.game = game
  else:
   self.game = chess.pgn.Game()
   self.game.headers = self._setGameHeader(self.optionalHeaderItems)
  self.gameNode = self.game
  self.gameID = len(self.gameList)
  self.gameList.append(self.game)
  self.undoListList.append(list())
  self.redoListList.append(list())
  self.gameListTableView.setGameList(self.gameList)
  self._showEcoCode(self.game, fromBeginning = True)
  self.gameSelected(self.gameID)
  if not isNew:
   self.setChessWindowTitle()
 def _setGameHeader(self, optionalHeaderItems : List[str]) -> chess.pgn.Headers:
  selectedItems =  list(chess.pgn.Headers()) + optionalHeaderItems
  newHeaders = chess.pgn.Headers()
  for el in selectedItems:
   if el in self.game.headers:
    newHeaders[el] = self.game.headers[el]
   else:
    newHeaders[el] = self.gameHeaderTableView.stdKey2ValueTypeDict[el][0]
  self.optionalHeaderItems = optionalHeaderItems
  return newHeaders
 # ---------------------------------------------------------------------------
 def openPGN(self, pgnFile : str, encoding : str = None):
  if not self._allowNewGameList():
   return
  self.notify('Opening {} ...'.format(os.path.basename(pgnFile)))
  self.gameListFile = os.path.split(pgnFile)[1]
  _,  ext = os.path.splitext(pgnFile)
  if ext == '.ppgn':
   try:
    with open(pgnFile, mode = 'rb') as f:
     self.gameList = pickle.load(f)
     encoding = None
   except:
    self.notifyError('Cannot open PPGN file {}'.format(pgnFile))
    return
  elif ext == '.pgn':
   try:
    pgn = open(pgnFile, mode = 'r', encoding = encoding)
   except:
    self.notifyError('Cannot open PGN file {}'.format(pgnFile))
    return
   n = 1
   self.gameList = list()
   while True:
    actGame = read_game(pgn)
    if actGame is None:
     break
    if len(actGame.errors) == 0:
     self.gameList.append(actGame)
    else:
     self.notifyError('Failed to load game #{}'.format(n))
     return
    n += 1
  else:
   self.notifyError('Cannot handle file with extension "{}"'.format(ext))
   return
  if len(self.gameList) < 1:   
   self.notifyError('No game loaded')
   return
  with open(self.recoverFile, mode = 'wb') as f:
   pickle.dump(self.gameList, f)
  self.gameListTableView.setGameList(self.gameList)
  self.gameSelected(0)
  self.settings['Recent'] = {pgnFile : encoding}
  newRecentPGN = {pgnFile : encoding}
  for file, enc in self.recentPGN.items():
   if file != pgnFile:
    newRecentPGN[file] = enc
    self.settings['Recent'][file] = enc
  self.recentPGN = newRecentPGN
  self.saveSettings()
  self.menuRecentDB.clear()
  for rItem in self.recentPGN:
   if rItem is not None and os.path.isfile(rItem):
    self.menuRecentDB.addAction(rItem)
  if pgnFile != self.recoverFile:
   os.chdir(os.path.dirname(pgnFile))
   self.pgnFile = pgnFile
  self.setChessWindowTitle()
 
 @QtCore.pyqtSlot(QAction)
 def on_menuEncoding_triggered(self, action):
  for actAction in self.menuEncoding.actions():
   actAction.setChecked(False)
  action.setChecked(True)
  self.settings['Menu/Game']['encoding'] = action.text()
  self.saveSettings()
 @QtCore.pyqtSlot()
 def on_actionOpenDB_triggered(self):
  pgnFile, _ = QtWidgets.QFileDialog.getOpenFileName(self,"Load Game Database ...", "",
        "Pickle Portable Game Notation Files (*.ppgn);;Portable Game Notation Files (*.pgn);;All Files (*)", options = self.fileDialogOptions)
  if pgnFile is not None and len(pgnFile) > 0:
   self.openPGN(pgnFile, self.encodingDict[self.settings['Menu/Game']['encoding']])
   
 @QtCore.pyqtSlot()
 def on_actionRecoverDB_triggered(self):
  if not os.path.exists(self.recoverFile):
   self.notifyError('Recovery database not existing')
   return
  self.openPGN(self.recoverFile)
   
 @QtCore.pyqtSlot()
 def on_actionGameUp_triggered(self):
  if self.gameListTableView.on_menuMoveGame_triggered(self.actionGameUp):
   self.setInfoLabel(True)
   self.setChessWindowTitle()
 @QtCore.pyqtSlot()
 def on_actionCloseDB_triggered(self):
  if not self._allowNewGameList():
   return
  self.notify('Closing database ...')
  self.setChessWindowTitle()
[docs] @QtCore.pyqtSlot(QtGui.QCloseEvent)
 def closeEvent(self, ev):
  if not self._allowNewGameList():
   ev.ignore()
  else:
   if self.hintEngine is not None:
    self.hintEngine.kill(True)
   ev.accept() 
 @QtCore.pyqtSlot()
 def on_actionExit_triggered(self):
  if self._allowNewGameList():
   self.close()
 def saveDB(self, forceAppend : bool = False, saveCurrent : bool = False):
  if len(self.gameList) == 0:
    self.notifyError('No Game Database available')
    return
  if forceAppend:
   mode = 'a'
  else:
   mode = 'w'
  if saveCurrent and self.pgnFile is not None:
   pgnFile = self.pgnFile
  else:
   pgnFile, _ = QtWidgets.QFileDialog.getSaveFileName(self,
     "Save Game Database ...", self.gameListFile,
     "Portable Game Notation Files (*.ppgn *.pgn)", options = self.fileDialogOptions)
   if pgnFile is None or len(pgnFile) == 0:
    return
  _,  ext = os.path.splitext(pgnFile)
  if len(ext) == 0:
   ext = '.pgn'
   self.notify('Saving database to PGN file {}.pgn ...'.format(pgnFile))
  else:
   self.notify('Saving database to file {} ...'.format(pgnFile))
  if ext == '.ppgn':
   if mode == 'a':
    self.notifyError('Cannot append to PPGN file {}'.format(pgnFile))
    return
   try:
    with open(pgnFile, mode = 'wb') as f:
     pickle.dump(self.gameList, f)
   except:
    self.notifyError('Cannot open PPGN file {}'.format(pgnFile))
    return
   encoding = None
  elif ext == '.pgn':
   try:
    encoding = self.settings['Menu/Game']['encoding']
    pgnString = str()
    for game in self.gameList:
     exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True)
     pgnString += game.accept(exporter)
     if game != self.gameList[-1]:
      pgnString += '\n\n'
    with open(pgnFile, mode = mode,  encoding = self.encodingDict[encoding]) as f:
     f.write(pgnString)
   except:
    self.notifyError('Cannot save PGN file {}'.format(pgnFile))
    return
  else:
    self.notifyError('PGN file {} has improper extension (.pgn or .pgn expected)'.format(pgnFile))
    return
  self.updateSettingsList('Recent', self.recentPGN.items(), firstValue = (pgnFile, encoding))
  self.saveSettings()
  with open(self.recoverFile, mode = 'wb') as f:
   pickle.dump(self.gameList, f)
  self.gameListFile = os.path.split(pgnFile)[1]
  os.chdir(os.path.dirname(pgnFile))
  self.pgnFile = pgnFile
  self.undoListList = list()
  self.redoListList = list()
  self.setChessWindowTitle()
 @QtCore.pyqtSlot()
 def on_actionAppend2DB_triggered(self):
  self.notify('Appending database ...')
  self.saveDB(forceAppend = True, saveCurrent = False)
  
 @QtCore.pyqtSlot()
 def on_actionSaveDB_triggered(self):
  self.saveDB(forceAppend = False, saveCurrent = True)
   
 @QtCore.pyqtSlot()
 def on_actionSaveDBAs_triggered(self):
  self.saveDB(forceAppend = False, saveCurrent = False)
 @QtCore.pyqtSlot()
 def on_actionAddGame_triggered(self):
  self.notify('Adding game #{} ...'.format(self.gameID))
  self._addGame()
   
 @QtCore.pyqtSlot()
 def on_actionRemoveGames_triggered(self):
  if self.gameListTableView.on_actionRemoveGames_triggered():
   if self.game in self.gameList:
    self.setInfoLabel(True)
   else:
    self.gameSelected(0)
   self.setChessWindowTitle()
 @QtCore.pyqtSlot()
 def on_actionGameDown_triggered(self):
  if self.gameListTableView.on_menuMoveGame_triggered(self.actionGameDown):
   self.setInfoLabel(True)
   self.setChessWindowTitle()
 
 @QtCore.pyqtSlot(QAction)
 def on_menuRecentDB_triggered(self, action):
  fileName = action.text()
  self.openPGN(fileName, self.recentPGN[fileName])
 
 @QtCore.pyqtSlot(QAction)
 def on_menuEndGame_triggered(self, action):
  endGame = self.game.end()
  if endGame.board().is_game_over():
   self.notifyError('You cannot change the game result any more')
   return
  self.game.headers['Result'] = action.text()
  self.boardGraphicsView.setGameNode(self.gameNode)
  self.gameChanged(self.game)
  
 @QtCore.pyqtSlot()
 def on_actionSaveGame_triggered(self):
  pgnFile, _ = QtWidgets.QFileDialog.getSaveFileName(self,
     "Save Game ...", self.gameFile,"Portable Game Notation Files (*.pgn);;All Files (*)", options = self.fileDialogOptions)
  if pgnFile is not None and len(pgnFile) > 0:
   try:
    encoding = self.settings['Menu/Game']['encoding']
    f = open(pgnFile, mode = 'w',  encoding = self.encodingDict[encoding])
   except:
    self.notifyError('Cannot open PGN file {}'.format(pgnFile))
    return
   self.notify('Saving game #{} ...'.format(self.gameID))
   exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True)
   pgnString = self.game.accept(exporter)
   f.write(pgnString)
   f.close()
   self.updateSettingsList('Recent', self.recentPGN.items(), firstValue = (pgnFile, encoding))
   self.saveSettings()
   self.gameFile = os.path.split(pgnFile)[1]
   os.chdir(os.path.dirname(pgnFile))
 # ---------------------------------------------------------------------------
[docs] @QtCore.pyqtSlot(QtGui.QWheelEvent)
 def wheelEvent(self, ev):
  numDegrees = ev.angleDelta()
  if numDegrees.y() < 0:
   self.on_actionNextMove_triggered()
  elif numDegrees.y() > 0:
   self.on_actionPreviousMove_triggered()
  ev.accept() 
 @QtCore.pyqtSlot()
 def on_actionNextMove_triggered(self):
  self.gameNode = self.boardGraphicsView.nextMove()
  self.gameTreeViewWidget.selectNodeItem(self.gameNode)
  self.scorePlotGraphicsView.selectNodeItem(self.gameNode)
  self.setMoveLabel(self.gameNode.board())
 @QtCore.pyqtSlot()
 def on_actionPreviousMove_triggered(self):
  self.gameNode = self.boardGraphicsView.previousMove()
  self.gameTreeViewWidget.selectNodeItem(self.gameNode)
  self.scorePlotGraphicsView.selectNodeItem(self.gameNode)
  self.setMoveLabel(self.gameNode.board())
 @QtCore.pyqtSlot()
 def on_actionNextVariant_triggered(self):
  self.gameTreeViewWidget.selectSubnodeItem(self.gameNode, next = True)
 @QtCore.pyqtSlot()
 def on_actionPreviousVariant_triggered(self):
  self.gameTreeViewWidget.selectSubnodeItem(self.gameNode, next = False)
 @staticmethod
 def variantHead(gameNode):
  if gameNode is None:
   return (None, None)
  parentNode = gameNode.parent
  while parentNode is not None and len(parentNode.variations) < 2:
   gameNode = parentNode
   parentNode = gameNode.parent
  return (parentNode, gameNode)
 
 def promoteVariant(self, toMain):
  headParentNode, headNode = self.variantHead(self.gameNode)
  oldVariations = copy.copy(headParentNode.variations)
  oldID = oldVariations.index(headNode)
  if headParentNode is None or oldID == 0:
    self.notifyError('You cannot promote main line nodes')
    return
  if toMain or oldID < 2:
   self.gameTreeViewWidget.moveVariant2Main(headParentNode, oldID)
   headParentNode.promote_to_main(headNode)
  else:
   self.gameTreeViewWidget.moveVariant(headParentNode, oldID, True)
   headParentNode.promote(headNode)
  self.undoListList[self.gameID].append(('variations', [(headParentNode, oldVariations)]))
  self.gameNodeSelected(self.gameNode)
  if self.gameNode.is_mainline():
   self.scorePlotGraphicsView.setGame(self.game)
   self.scorePlotGraphicsView.selectNodeItem(self.gameNode)
  self.setChessWindowTitle()
  
 @QtCore.pyqtSlot()
 def on_actionPromoteVariant_triggered(self):
  self.promoteVariant(False)
 @QtCore.pyqtSlot()
 def on_actionPromoteVariant2Main_triggered(self):
  self.promoteVariant(True)
 @QtCore.pyqtSlot()
 def on_actionDemoteVariant_triggered(self):
  headParentNode, headNode = self.variantHead(self.gameNode)
  oldVariations = copy.copy(headParentNode.variations)
  oldID = oldVariations.index(headNode)
  isMainline = headNode.is_mainline()
  if headParentNode is None \
   
or oldID == len(headParentNode.variations) - 1:
    self.notifyError('You cannot demote last line nodes')
    return
  if oldID > 0:
   self.gameTreeViewWidget.moveVariant(headParentNode, oldID, False)
  else:
   self.gameTreeViewWidget.moveVariant2Main(headParentNode, 1)
  headParentNode.demote(headNode)
  self.undoListList[self.gameID].append(('variations', [(headParentNode, oldVariations)]))
  if False:
   self.gameTreeViewWidget.setGame(self.game)
  self.gameNodeSelected(self.gameNode)
  if isMainline:
   self.scorePlotGraphicsView.setGame(self.game)
   self.scorePlotGraphicsView.selectNodeItem(self.gameNode)
  self.setChessWindowTitle()
  
 @QtCore.pyqtSlot()
 def on_actionDeleteVariant_triggered(self):
  headParentNode, headNode = self.variantHead(self.gameNode)
  if headParentNode is None:
   self.notifyError('Use "Database/Remove Games" to remove a complete game')
   return
  oldVariations = copy.copy(headParentNode.variations)
  oldID = oldVariations.index(headNode)
  isMainline = headNode.is_mainline()
  if oldID > 0:
   self.gameTreeViewWidget.moveVariant(headParentNode, oldID, None)
  else:
   self.gameTreeViewWidget.moveVariant2Main(headParentNode, None)
  headParentNode.remove_variation(headNode)
  self.undoListList[self.gameID].append(('variations', [(headParentNode, oldVariations)]))
  if False:
   self.gameTreeViewWidget.setGame(self.game)
  if isMainline:
   self.scorePlotGraphicsView.setGame(self.game)
  self.gameNodeSelected(headParentNode)
  self.setChessWindowTitle()
 @QtCore.pyqtSlot()
 def on_actionDeleteAllVariants_triggered(self):
  while not self.gameNode.is_mainline():
   self.gameNode = self.gameNode.parent
  gameNodeValueDict = dict()
  for gameNode in self.game.mainline():
   if len(gameNode.variations) > 1:
    gameNodeValueDict[gameNode] = copy.copy(gameNode.variations)
    del gameNode.variations[:1]
  if len(gameNodeValueDict) > 0:
   gameNodeValueList = [gameNodeValueDict.pop(self.gameNode, str())]
   gameNodeValueList += gameNodeValueDict.items()
   self.undoListList[self.gameID].append(('nags', gameNodeValueList))
   self.gameNodeSelected(self.gameNode)
   self.setChessWindowTitle()
 
 @staticmethod
 def _deleteGameNodesLenGtZero(gameNode, attr, clearedAttr, gameNodeValueDict = dict()):
  attrValue = getattr(gameNode, attr)
  if len(attrValue) > 0:
   gameNodeValueDict[gameNode] = attrValue
   setattr(gameNode, attr, clearedAttr)
  for subNode in gameNode.variations:
   ChessMainWindow._deleteGameNodesLenGtZero(subNode, attr, clearedAttr, gameNodeValueDict)
  return gameNodeValueDict
   
 @QtCore.pyqtSlot()
 def on_actionDeleteAllNAGs_triggered(self):
  gameNodeValueDict = self._deleteGameNodesLenGtZero(self.game, 'nags', set())
  if len(gameNodeValueDict) != 0:
   gameNodeValueList = [(self.game, gameNodeValueDict.pop(self.game, set()))]
   gameNodeValueList += gameNodeValueDict.items()
   self.undoListList[self.gameID].append(('nags', gameNodeValueList))
   self.gameTreeViewWidget.setGame(self.game)
   self.setChessWindowTitle()
   
 @QtCore.pyqtSlot()
 def on_actionDeleteAllComments_triggered(self):
  gameNodeValueDict = self._deleteGameNodesLenGtZero(self.game, 'comment', str())
  if len(gameNodeValueDict) != 0:
   gameNodeValueList = [(self.game, gameNodeValueDict.pop(self.game, str()))]
   gameNodeValueList += gameNodeValueDict.items()
   self.undoListList[self.gameID].append(('comment', gameNodeValueList))
   self.gameTreeViewWidget.setGame(self.game)
   self.setChessWindowTitle()
   
 @QtCore.pyqtSlot()
 def on_actionUndoCurrentMove_triggered(self):
  if self.gameNode is None or self.gameNode.parent is None:
   return
  if not self.gameNode.is_end():
   self.notifyError('Select the end of the mainline or a variant')
   return
  gameNode = self.gameNode
  if len(gameNode.parent.variations) > 1:
   self.notifyError('Cannot undo move, node owns variants')
   return
  self.gameNode = self.gameNode.parent
  self.boardGraphicsView.setGameNode(self.gameNode)
  self.gameTreeViewWidget.removeGameNode(gameNode)
  if gameNode.is_mainline():
   self.scorePlotGraphicsView.removeLastNode()
  self.gameNode.remove_variation(gameNode)
  self.setChessWindowTitle()
 @QtCore.pyqtSlot()
 def on_actionUndo_triggered(self):
  if self.gameID is None or len(self.undoListList[self.gameID]) == 0:
   return
  item = self.undoListList[self.gameID].pop(-1)
  if isinstance(item, chess.pgn.GameNode):
   self.redoListList[self.gameID].append(item)
   gameNode = item
   parent = gameNode.parent
   self.gameTreeViewWidget.removeGameNode(gameNode)
   if gameNode.is_mainline():
    self.scorePlotGraphicsView.removeLastNode()
   parent.remove_variation(gameNode)
   self.gameNodeSelected(parent)
  elif isinstance(item, tuple):
   attr, gameNodeValueList = item
   gameNode = gameNodeValueList[0][0]
   if attr == 'headers':
    self.redoListList[self.gameID].append((attr, (gameNode, getattr(self.game, attr))))
    setattr(self.game, attr, gameNodeValueList[0][1])
   else:
    redoGameNodeValueList = list()
    if attr == 'variations':
     for headParentNode, savedVariations in gameNodeValueList:
      redoGameNodeValueList.append((headParentNode, getattr(headParentNode, attr)))
      variantDeleted = len(headParentNode.variations) < len(savedVariations)
      mainlineChanged = headParentNode.variations[0] != savedVariations[0]
      promotedToMain = mainlineChanged and not variantDeleted
      if not (promotedToMain or variantDeleted):
       for firstId, gameNode in enumerate(savedVariations):
        if headParentNode.variations[firstId] != savedVariations[firstId]:
         self.gameTreeViewWidget.moveVariant(headParentNode, firstId, False)
         headParentNode.variations = savedVariations
         break
      else:
       gameNode.variations = savedVariations
       self.gameSelected(self.gameID)
    elif attr == 'game':
     self.redoListList[self.gameID].append((attr, [(self.gameNode, pickle.dumps(self.game))]))
     self.game = pickle.dumps(gameNodeValueList[0][1])
    else:
     for node, oldAttrValue in gameNodeValueList:
      redoGameNodeValueList.append((node, getattr(node, attr)))
      setattr(node, attr, oldAttrValue)
     self.gameSelected(self.gameID)
    self.redoListList[self.gameID].append((attr, redoGameNodeValueList))
    self.gameNodeSelected(gameNode)
  else:
   raise ValueError('UIE: Undo item type {} not expected'.format(type(item)))
  if self.gameNode == self.game:
   self.scorePlotGraphicsView.setGame(self.game)
  self.gameTreeViewWidget.setGame(self.game)
  self.setChessWindowTitle()
 @QtCore.pyqtSlot()
 def on_actionRedo_triggered(self):
  if self.gameID is None or len(self.redoListList[self.gameID]) == 0:
   return
  item = self.redoListList[self.gameID].pop(-1)
  if isinstance(item, chess.pgn.GameNode):
   item.parent.variations.append(item)
   self.newGameNode(item)
   self.gameNodeSelected(item)
  elif isinstance(item, tuple):
   attr, gameNodeValueList = item
   gameNode = gameNodeValueList[0][0]
   if attr == 'headers':
    self.undoListList[self.gameID].append((attr, (gameNode, getattr(self.game, attr))))
    setattr(self.game, attr, gameNodeValueList[0][1])
   else:
    redoGameNodeValueList = list()
    if attr == 'variations':
     for headParentNode, savedVariations in gameNodeValueList:
      redoGameNodeValueList.append((headParentNode, getattr(headParentNode, attr)))
      variantDeleted = len(headParentNode.variations) < len(savedVariations)
      mainlineChanged = headParentNode.variations[0] != savedVariations[0]
      promotedToMain = mainlineChanged and not variantDeleted
      if not (promotedToMain or variantDeleted):
       for firstId, gameNode in enumerate(savedVariations):
        if headParentNode.variations[firstId] != savedVariations[firstId]:
         self.gameTreeViewWidget.moveVariant(headParentNode, firstId, False)
         headParentNode.variations = savedVariations
         break
      else:
       headParentNode.variations = savedVariations
       self.gameTreeViewWidget.setGame(self.game)
    else:
     for node, oldAttrValue in gameNodeValueList:
      redoGameNodeValueList.append((node, getattr(node, attr)))
      setattr(node, attr, oldAttrValue)
    self.undoListList[self.gameID].append((attr, redoGameNodeValueList))
    self.gameNodeSelected(gameNode)
  else:
   raise ValueError('UIE: Undo item type {} not expected'.format(type(item)))
  if self.gameNode == self.game:
   self.scorePlotGraphicsView.setGame(self.game)
  self.gameTreeViewWidget.setGame(self.game)
  self.setChessWindowTitle()
 def setChessWindowTitle(self):
  self.gameListChanged = False 
  for undoList in self.undoListList:
   self.gameListChanged = self.gameListChanged or len(undoList) > 0
   if self.gameListChanged:
    break
  title = 'MzChess - '
  if self.pgnFile is not None:
   title += os.path.basename(self.pgnFile)
  else:
   title += '<UNKNOWN>'
  if self.gameListChanged:
   title += ' *'
  self.setWindowTitle(title)
 # ---------------------------------------------------------------------------
 @QtCore.pyqtSlot(QAction)
 def on_menuSelectEngine_triggered(self, action):
  oldValue = self.settings['Menu/Engine']['selectedEngine']
  for actAction in self.menuSelectEngine.actions():
   actAction.setChecked(False)
  action.setChecked(True)
  self.settings['Menu/Engine']['selectedEngine'] = action.text()
  if oldValue is None:
   self.show_HintsScores()
  self.saveSettings()
  
 @QtCore.pyqtSlot(QAction)
 def on_menuSearchDepth_triggered(self, action):
  for actAction in self.menuSearchDepth.actions():
   actAction.setChecked(False)
  action.setChecked(True)
  tList = action.text().split(' ')
  self.settings['Menu/Engine']['searchDepth'] = str(int(tList[0]))
  self.saveSettings()
  
 @QtCore.pyqtSlot(QAction)
 def on_menuNumberOfAnnotations_triggered(self, action):
  for actAction in self.menuBlunderLimit.actions():
   actAction.setChecked(False)
  action.setChecked(True)
  naValue = action.text()
  self.settings['Menu/Engine']['numberOfAnnotations'] = naValue
  self.saveSettings()
 @QtCore.pyqtSlot(QAction)
 def on_menuBlunderLimit_triggered(self, action):
  for actAction in self.menuBlunderLimit.actions():
   actAction.setChecked(False)
  action.setChecked(True)
  blValue = action.text()
  if blValue == 'None':
   blunderLimit = -float('inf')
  else:
   blunderLimit = -float(blValue.split(' ')[0])
  self.settings['Menu/Engine']['blunderLimit'] = str(blunderLimit)
  self.saveSettings()
   
 @QtCore.pyqtSlot(QAction)
 def on_menuAnnotateVariants_triggered(self, action):
  for actAction in self.menuAnnotateVariants.actions():
   actAction.setChecked(False)
  action.setChecked(True)
  avValue = action.text().split(' ')[0]
  if avValue == 'None':
   self.settings['Menu/Engine']['annotateVariants'] = str(None)
  elif avValue == 'All':
   self.settings['Menu/Engine']['annotateVariants'] = str(2^32 - 1)
  else:
   self.settings['Menu/Engine']['annotateVariants'] = str(int(avValue))
  self.saveSettings()
 @QtCore.pyqtSlot()
 def on_actionAnnotateCurrentMove_triggered(self):
  if self.settings['Menu/Engine']['selectedEngine'] is None:
   self.notifyError('No engine selected')
   return
  if self.debugEngine:
   logFunction = self.logSignal.emit
  else:
   logFunction = None
  engine = MzChess.ChessEngine(
      self.engineDict[self.settings['Menu/Engine']['selectedEngine']], 
      limit = chess.engine.Limit(depth = self.settings['Menu/Engine']['searchDepth']), 
      log = logFunction)
  aEngine = MzChess.AnnotateEngine(notifyFunction = self.notifySignal.emit)
  annotateVariants = self.settings['Menu/Engine']['annotateVariants']
  if annotateVariants is not None and annotateVariants.isdigit():
   annotateVariants = int(annotateVariants)
  else:
   annotateVariants = 0
  self.notify('Scoring move {} of game #{} ...'.format(self.gameNode.move.uci(), self.gameID))
  aEngine.setup(engine, hintPLYs = annotateVariants, multiPV = int(self.settings['Menu/Engine']['numberOfAnnotations']))
  if aEngine.run(self.gameNode, numberOfPlys = 1):
   annotator = MzChess.Annotator(self.settings['Menu/Engine']['selectedEngine'], notifyFunction = self.notifySignal.emit)
   annotator.setBlunder(-float('inf'), addVariant = False)
   oldAttrValue = self.gameNode.comment
   annotator.apply(game = self.gameNode, scoreListList = aEngine.scoreListList, pvListList = None)
   self.undoListList[self.gameID].append(('comment', [(self.gameNode, oldAttrValue)]))
   if self.gameNode == self.game:
    self.scorePlotGraphicsView.setGame(self.game)
   self.gameTreeViewWidget.setGame(self.game)
   # self.gameHeaderTableView.setGame(self.game)
   self.gameNodeSelected(self.gameNode)
   self.setChessWindowTitle()
  else:
   self.notifyError('Annotation failed')
   return
 @QtCore.pyqtSlot()
 def on_actionAnnotateAll_triggered(self):
  if self.settings['Menu/Engine']['selectedEngine'] is None:
   self.notifyError('No engine selected')
   return
  
  if self.debugEngine:
   logFunction = self.logSignal.emit
  else:
   logFunction = None
  engine = MzChess.ChessEngine(
      self.engineDict[self.settings['Menu/Engine']['selectedEngine']], 
      limit = chess.engine.Limit(depth = self.settings['Menu/Engine']['searchDepth']), 
      log = logFunction)
  aEngine = MzChess.AnnotateEngine(notifyFunction = self.notifySignal.emit)
  annotateVariants = self.settings['Menu/Engine']['annotateVariants']
  if annotateVariants is not None and annotateVariants.isdigit():
   annotateVariants = int(annotateVariants.split(' ')[0])
  else:
   annotateVariants = 0
  gameNode = self.gameNode
  while gameNode.parent:
   if gameNode.parent.variations[0] != gameNode:
    break
   gameNode = gameNode.parent
  if gameNode == self.game:
   self.notify('Annotating game #{} ...'.format(self.gameID))
  else:
   self.notify('Annotating variant {} of #{} ...'.format(gameNode, self.gameID))
  aEngine.setup(engine, hintPLYs = annotateVariants, multiPV = int(self.settings['Menu/Engine']['numberOfAnnotations']))
  if aEngine.run(gameNode, numberOfPlys = None):
   annotator = MzChess.Annotator(self.settings['Menu/Engine']['selectedEngine'])
   addVariant = self.settings['Menu/Engine']['blunderLimit'] != '-inf'
   annotator.setBlunder(float(self.settings['Menu/Engine']['blunderLimit']), addVariant = addVariant)
   undoGameNodeValueList = list()
   for actGameNode in gameNode.mainline():
    if actGameNode.comment != '':
     undoGameNodeValueList.append((actGameNode, gameNode.comment))
   hintsAdded = annotator.apply(game = gameNode, scoreListList = aEngine.scoreListList, pvListList = aEngine.pvListList)
   if hintsAdded:
    self.undoListList[self.gameID].append(('game', [(self.gameNode, pickle.dumps(self.game))]))
   else:
    self.undoListList[self.gameID].append(('comment', [(self.gameNode, undoGameNodeValueList)]))
   if gameNode == self.game:
    self.scorePlotGraphicsView.setGame(self.game)
   self.gameTreeViewWidget.setGame(self.game)
   # self.gameHeaderTableView.setGame(self.game)
   self.gameNodeSelected(self.gameNode)
   self.setChessWindowTitle()
  else:
   self.notifyError('Annotation failed')
   return
 @QtCore.pyqtSlot(bool)
 def on_actionShowOptions_toggled(self, checked):
  self.boardGraphicsView.setDrawOptions(checked)
  self.settings['Menu/Game']['showOptions'] = str(checked)
  self.saveSettings()
 @QtCore.pyqtSlot(bool)
 def on_actionWarnOfDanger_toggled(self, checked):
  self.boardGraphicsView.setWarnOfDanger(checked)
  self.settings['Menu/Game']['warnOfDanger'] = str(checked)
  self.saveSettings()
 @QtCore.pyqtSlot(bool)
 def on_actionFlipBoard_toggled(self, checked):
  self.boardGraphicsView.setFlipped(checked)
  
 @QtCore.pyqtSlot()
 def on_actionSelectHeaderElements_triggered(self):
  headerElements = self.gameHeaderTableView.headerElements(withoutSevenTagRoster = True)
  self.itemSelector.setContent(headerElements, self.optionalHeaderItems)
  if not self.itemSelector.exec():
   return None
  self.game.headers = self._setGameHeader(self.itemSelector.selectedItems() )
  self.gameHeaderTableView.setGame(self.game)
  self.updateSettingsList('OptionalHeaderItems', self.optionalHeaderItems)
  self.saveSettings()
   
 def show_HintsScores(self):
  if self.settings['Menu/Engine']['selectedEngine'] is None:
   self.notifyError('No engine selected')
   return
  
  scoresChecked = self.actionShowScores.isChecked()
  hintsChecked = int(self.settings['Menu/Engine'].get('showHints', 0))
  self.settings['Menu/Engine']['showScores'] = str(scoresChecked)
  self.saveSettings()
  if hintsChecked > 0 or scoresChecked:
   if self.settings['Menu/Engine']['searchDepth'] is None:
    self.notifyError('"Engine/Search Depth" undefined.')
    return
   if self.debugEngine:
    logFunction = self.logSignal.emit
   else:
    logFunction = None
   self.hintEngine = MzChess.ChessEngine(
      self.engineDict[self.settings['Menu/Engine']['selectedEngine']], 
      limit = chess.engine.Limit(depth = self.settings['Menu/Engine']['searchDepth']), 
      log = logFunction)
   self.boardGraphicsView.setHint(enableHint = hintsChecked, enableScore = scoresChecked, engine = self.hintEngine)
   self.engineLabel.setText(self.settings['Menu/Engine']['selectedEngine'])
  else:
   self.hintEngine = None
   self.boardGraphicsView.setHint(enableHint = 0, enableScore = False, engine = None)
   self.engineLabel.setText('---')
 @QtCore.pyqtSlot(QAction)
 def on_menuShowHints_triggered(self, action):
  for actAction in self.menuShowHints.actions():
   actAction.setChecked(False)
  action.setChecked(True)
  naValue = self.hintDict[action.text()]
  self.settings['Menu/Engine']['showHints'] = naValue
  self.show_HintsScores()
    
 @QtCore.pyqtSlot(bool)
 def on_actionShowScores_toggled(self, checked):
  self.show_HintsScores()
  
 @QtCore.pyqtSlot()
 def on_actionConfigureEngine_triggered(self):
  configForm = MzChess.ConfigureEngine()
  newEngineDict = configForm.run(engineDict = self.engineDict, log = self.debugEngine)
  if newEngineDict is not None:
   self.engineDict = newEngineDict
   self.resetSelectEngine()
   MzChess.saveEngineSettings(self.settings, self.engineDict)
   self.saveSettings()
   
 @QtCore.pyqtSlot(bool)
 def on_actionDebugEngine_toggled(self, checked):
  self.debugEngine = checked
  if self.hintEngine is not None:
   if self.debugEngine:
    logFunction = self.logSignal.emit
   else:
    logFunction = None
   self.hintEngine.setLog(logFunction)
  
 @QtCore.pyqtSlot()
 def on_actionAbout_triggered(self):
  self.aboutDialog.exec()
 
 @QtCore.pyqtSlot()
 def on_actionHelp_triggered(self):
  QtGui.QDesktopServices.openUrl(self.helpIndex)
 # ---------------------------------------------------------------------------
 @QtCore.pyqtSlot() 
 def on_actionCopyFEN_triggered(self):
  self.notify('Copying current position of game #{} to clipboard ...'.format(self.gameID))
  fen = self.gameNode.board().fen()
  QtWidgets.QApplication.clipboard().setText(fen)
 @QtCore.pyqtSlot() 
 def on_actionPasteFEN_triggered(self):
  fen = QtWidgets.QApplication.clipboard().text()
  try:
   MzChess.checkFEN(fen)
  except ValueError as err:
   self.notifyError('Improper FEN {}:\n{}'.format(fen, str(err)))
   return
  self.notify('Pasting position from clipboard to game #{} ...'.format(len(self.gameList)))
  board = chess.Board(fen)
  game = chess.pgn.Game()
  game.headers = self.game.headers
  game.headers["Result"] = "*" 
  game.setup(board)
  game.headers.pop("SetUp", None) # remove non-standard header element used by chess package
  self._addGame(game) 
 
 @QtCore.pyqtSlot() 
 def on_actionFENBuilder_triggered(self):
  self.fenBuilder = QtCore.QProcess()
  self.fenBuilder.start(sys.executable, [os.path.join(self.fileDirectory,'qbuildfen.py')], QtCore.QIODevice.OpenModeFlag.NotOpen)
  
 @QtCore.pyqtSlot() 
 def on_actionCopyGame_triggered(self):
  self.notify('Copying game #{} to clipboard ...'.format(self.gameID))
  exporter = chess.pgn.StringExporter(headers=True, variations=True, comments=True)
  pgnString = self.game.accept(exporter)
  QtWidgets.QApplication.clipboard().setText(pgnString)
 @QtCore.pyqtSlot() 
 def on_actionPasteGame_triggered(self):
  pgnString = QtWidgets.QApplication.clipboard().text()
  pgn = io.StringIO(pgnString)
  game = read_game(pgn)
  if len(game.errors) != 0:
   self.notifyError('Improper PGN')
   return
  self.notify('Pasting game from clipboard to game #{} ...'.format(self.gameID + 1))
  self._addGame(game)
  
 # ---------------------------------------------------------------------------
 @QtCore.pyqtSlot(int)
 def gameSelected(self, gameID):
  if gameID != self.gameID or True:
   self.gameID = gameID
   self.game = self.gameList[gameID]
   self.gameNode = self.game
   self.notify('Loading game #{} ...'.format(self.gameID))
  try:
   self._showEcoCode(self.game, fromBeginning = True)
   self.boardGraphicsView.setGameNode(self.game)
   self.gameTreeViewWidget.setGame(self.game)
   self.gameHeaderTableView.setGame(self.game)
   self.scorePlotGraphicsView.setGame(self.game)
   self.setInfoLabel()
   self.setMoveLabel(self.gameNode.board())
  except:
   self.notifyError('UIE: Improper game ="{}"'.format(self.game))
   return
 @QtCore.pyqtSlot(list)
 def gameListHeaderChanged(self, gameListHeader):
  self.gameListHeader = gameListHeader
  self.updateSettingsList('GameListHeaders', self.gameListHeader)
  self.saveSettings()
 @QtCore.pyqtSlot()
 def gameListChanged(self):
  self.setInfoLabel(True)
  self.setChessWindowTitle()
 @QtCore.pyqtSlot(chess.pgn.Headers)
 def gameHeadersChanged(self,  headers):
  self.undoListList[self.gameID].append(('headers', [(self.gameNode, self.game.headers)]))
  if 'Comment' in headers:
   self.game.comment = headers['Comment']
   del headers['Comment']
  else:
   self.game.comment = ''
  if '?' not in headers["White"] and headers["White"] not in self.playerList:
   self.updateSettingsList('Players', self.playerList, firstValue = headers["White"])
  if '?' not in headers["Black"] and headers["Black"] not in self.playerList:
   self.updateSettingsList('Players', self.playerList, firstValue = headers["Black"])
  if '?' not in headers["Event"] and headers["Event"] not in self.eventList:
   self.updateSettingsList('Events', self.eventList, firstValue = headers["Event"])
  if '?' not in headers["Site"] and headers["Site"] not in self.siteList:
   self.updateSettingsList('Sites', self.siteList, firstValue = headers["Site"])
  self.saveSettings()
  self.game.headers = headers
  self.setChessWindowTitle()
 @QtCore.pyqtSlot(chess.pgn.GameNode)
 def newGameNode(self, gameNode):
  self.gameNode = gameNode
  self.undoListList[self.gameID].append(self.gameNode)
  self._showEcoCode(self.game, fromBeginning = False)
  if gameNode.is_mainline():
   self.scorePlotGraphicsView.addGameNodes(gameNode)
   self.scorePlotGraphicsView.selectNodeItem(gameNode)
   if 'PlyCount' in self.game.headers:
    self.game.headers['PlyCount'] = str(gameNode.board().ply())
    self.gameHeaderTableView.setPlyCount(self.game.headers['PlyCount'])
   board = self.gameNode.board()
   if self.gameNode.is_end() and board.is_game_over():
    if board.is_checkmate():
     if board.turn == chess.WHITE:
      self.game.headers['Result'] = '0-1'
     else:
      self.game.headers['Result'] = '1-0'
    else:
     self.game.headers['Result'] = '1/2-1/2'
  if gameNode.is_main_variation():
   self.gameTreeViewWidget.addGameNodes(gameNode)
  else:
   self.gameTreeViewWidget.addVariant(gameNode)
  self.gameChanged(self.game)
  self.setMoveLabel(self.gameNode.board())
   
 @QtCore.pyqtSlot(chess.pgn.GameNode)
 def gameNodeSelected(self, gameNode):
  self.gameNode = gameNode
  self.boardGraphicsView.setGameNode(self.gameNode)
  self.gameTreeViewWidget.selectNodeItem(self.gameNode)
  if self.gameNode.is_mainline():
   self.scorePlotGraphicsView.selectNodeItem(self.gameNode)
  self.setMoveLabel(self.gameNode.board())
 @QtCore.pyqtSlot(chess.pgn.Game)
 def gameChanged(self, game):
  self.gameHeaderTableView.setGameResult(game.headers['Result'])
  self.gameTreeViewWidget.setGameResult(game.headers['Result'])
  self.game = game
  self.setChessWindowTitle()
  
 @QtCore.pyqtSlot(chess.pgn.GameNode, tuple)
 def gameNodeChanged(self, gameNode, attrTuple):
  attr, oldAttrValue = attrTuple
  self.undoListList[self.gameID].append((attr, [(gameNode, oldAttrValue)]))
  self.setChessWindowTitle() 
[docs]class MzClassApplication(QtWidgets.QApplication):
 def __init__(self, argv : List[str], notifyFct : Callable[[str], None] = print) -> None: 
  super(MzClassApplication, self).__init__(argv)
  self.notifyFct = notifyFct
[docs] def notify(self, rec, ev):
  rc = super(MzClassApplication, self).notify(rec, ev)
  self.notifyFct('{} -> Type(Event)= {}, handled = {}'.format(rec, ev.type(),  rc))
  return rc  
 # ---------------------------------------------------------------------------
import os,  os.path
def runMzChess(notifyFct : Optional[Callable[[str], None]] = None):
 global qApp
 os.chdir(os.path.expanduser('~'))
 if notifyFct is not None:
  qApp = MzClassApplication(sys.argv)
 chessMainWindow = ChessMainWindow()
 chessMainWindow.show()
 chessMainWindow.setup()
 qApp.exec()
def _runMzChess():
 print('Hello, world')
if __name__ == "__main__":
 runMzChess(None)