Source code for gameheaderview
'''
Game Header Editor
=====================
The game header editor is a 2-column table which assigns to values to header elements.
The `PGN`_ standard lists the supported header elements. 
The header elements of 7-tag roster, 
i.e. *Event*, *Site*, *Date*, *Round*, *White*, *Black* and *Result*, are mandatory.
The *Game/Select Header Elements ...* menu entry opens a dialog to add/remove header elements.
|HeaderEditor|
Depending on the type, the values are edited using a *text*, *date* and *time* editors.
To avoid inconsistent header data, the following header elements are readonly:
* *Result*, *Annotator*, *PlyCount*
* any kind of opening information, i.e. *Opening*, *Variation*, *SubVariation*, *ECO*, *NIC*
.. |HeaderEditor| image:: headerEditor.png
  :width: 800
  :alt: Header Editor
.. _PGN: https://github.com/fsmosca/PGN-Standard
'''
from typing import Optional, List, Tuple, Any
import copy
import datetime
from enum import IntEnum, unique
import sys,  os.path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import MzChess
if MzChess.useQt5():
 from PyQt5 import QtWidgets, QtCore
else:
 from PyQt6 import QtWidgets, QtCore
import chess, chess.pgn
[docs]@unique
class KeyType(IntEnum):
 STR = 0
 INT = 1
 LABEL = 2
 DATE = 3
 TIME = 4
 EVENT = 5
 SITE = 6
 PLAYER = 7
 TITLE = 8
[docs]class GameHeaderView(QtWidgets.QTableWidget):
 '''Game Header Editor object
 '''
 stdKey2ValueTypeDict = {
   # seven tag roster
   "Event" : ("?", KeyType.EVENT),
   "Site" : ("?", KeyType.SITE),
   "Date" : ("????.??.??", KeyType.DATE),
   "Round" : ("?", KeyType.STR),
   "White" : ("?", KeyType.PLAYER),
   "Black" : ("?", KeyType.PLAYER),
   "Result" : ("*", KeyType.LABEL), 
   # player related information 
   "WhiteTitle" : ("", KeyType.TITLE), 
   "BlackTitle" : ("", KeyType.TITLE), 
   "WhiteElo" : ("", KeyType.INT), 
   "BlackElo" : ("", KeyType.INT), 
   "WhiteUSCF" : ("", KeyType.INT), 
   "BlackUSCF" : ("", KeyType.INT), 
   "WhiteNA" : ("", KeyType.STR), 
   "BlackNA" : ("", KeyType.STR), 
   # event related information 
   "EventDate" : ("", KeyType.DATE),
   "EventSponsor" : ("", KeyType.STR),
   "Section" : ("", KeyType.STR),
   "Stage" : ("", KeyType.STR),
   "Board" : ("", KeyType.INT),
   # opening information 
   "Opening" : ("", KeyType.LABEL), 
   "Variation" : ("", KeyType.LABEL), 
   "SubVariation" : ("", KeyType.LABEL), 
   "ECO" : ("", KeyType.LABEL), 
   "NIC" : ("", KeyType.LABEL), 
   # time
   "Time" : ("??:??:??", KeyType.TIME),
   "UTCTime" : ("??:??:??", KeyType.TIME),
   "UTCDate" : ("????.??.??", KeyType.DATE),
   # time control
   "TimeContol" : ("?", KeyType.STR), 
   # alternative starting positions
   "FEN" : (chess.STARTING_FEN, KeyType.LABEL), 
   # game termination
   "Termination" : ("", KeyType.STR), 
   # miscellaneous
   "Annotator" : ("", KeyType.LABEL), 
   "PlyCount" : ("0", KeyType.LABEL)}
 gameResult = ["1-0", "0-1", "1/2-1/2", "*"]
 def __init__(self, parent = None) -> None:
  super(GameHeaderView, self).__init__(parent)
  self.notifyGameHeadersChangedSignal = None
  self.gameResultLabel = None
  self.eventList = ['?']
  self.siteList = ['?']
  self.playerList = ['?']
  
 def setup(self, notifyGameHeadersChangedSignal  : Optional[QtCore.pyqtSignal] = None, 
                 eventList : List[str] = list(), siteList : List[str] = list(), playerList : List[str] = list()) -> None:
  self.eventList = ['?'] + eventList
  self.siteList = ['?'] + siteList
  self.playerList = ['?'] + playerList
  self.notifyGameHeadersChangedSignal = notifyGameHeadersChangedSignal
  self.resetGame()
 
[docs] @staticmethod
 def headerElements(withoutSevenTagRoster : bool = False) -> List[str]:
  '''Get game header elements
:param withoutSevenTagRoster: returns only the optional header elements
:returns: list of header elements
  '''
  if not withoutSevenTagRoster:
   hElements = GameHeaderView.stdKey2ValueTypeDict.keys()
  else:
   hElements = list()
   for el in GameHeaderView.stdKey2ValueTypeDict.keys():
    if el not in chess.pgn.Headers():
     hElements.append(el)
  return hElements
 def _createCompleteEdit(self, selList : List[str], value : Any) -> QtWidgets.QLineEdit:
  completer = QtWidgets.QCompleter(selList)
  completer.setCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseInsensitive)
  completer.setFilterMode(QtCore.Qt.MatchFlag.MatchContains)
  completer.setCompletionMode(QtWidgets.QCompleter.CompletionMode.InlineCompletion)
  item = QtWidgets.QLineEdit(str(value))
  item.setCompleter(completer)
  item.editingFinished.connect(self.on_editingFinished)
  return item
 def _showTable(self, keyValueTypeList : List[Tuple[str, str, KeyType]]) -> None:
  self.horizontalHeader().hide()
  self.verticalHeader().hide()
  self.setRowCount(len(keyValueTypeList))
  self.setColumnCount(2)
  for row, self.defaultKeyValueType in enumerate(keyValueTypeList):
   key, value, type = self.defaultKeyValueType
   item = QtWidgets.QLabel(str(key))
   self.setCellWidget(row, 0, item)
  test = QtWidgets.QLabel()
  zeroWidth = test.fontMetrics().size(QtCore.Qt.TextFlag.TextSingleLine, '0').width()
  self.setColumnWidth(0, 20*zeroWidth)
  
  self.itemList = list()
  for row, self.defaultKeyValueType in enumerate(keyValueTypeList):
   key, value, type = self.defaultKeyValueType
   if type == KeyType.STR:
    item = QtWidgets.QLineEdit(str(value))
    item.editingFinished.connect(self.on_editingFinished)
   elif type == KeyType.LABEL:
    item = QtWidgets.QLabel(str(value))
    item.setStyleSheet("background-color : white; color : gray;")
    if key == 'PlyCount':
     self.plyCountLabel = item
    elif key == 'Result':
     self.gameResultLabel = item
   elif type == KeyType.DATE:
    try:
     datetime.date.fromisoformat(value.replace('.','-'))
     date = QtCore.QDate.fromString(value, "yyyy.MM.dd")
    except:
     date = QtCore.QDate.currentDate()
     self.gameHeaders[key] = date.toString('yyyy.MM.dd')
    item = QtWidgets.QDateEdit(date)
    item.dateChanged.connect(self.on_dateChanged)
   elif type == KeyType.TIME:
    try:
     _time = QtCore.QTime.fromString(value, "hh:mm:ss")
    except:
     _time = QtCore.QTime.currentTime()
    item = QtWidgets.QTimeEdit(_time)
    item.timeChanged.connect(self.on_timeChanged)
   elif type == KeyType.INT:
    item = QtWidgets.QSpinBox()
    item.setMaximum(4000)
    item.setSpecialValueText('-')
    item.valueChanged.connect(self.on_spinBox_changed)
   elif type == KeyType.EVENT:
    item = self._createCompleteEdit(self.eventList, value)
   elif type == KeyType.SITE:
    item = self._createCompleteEdit(self.siteList, value)
   elif type == KeyType.PLAYER:
    item = self._createCompleteEdit(self.playerList, value)
   else:
    continue
   
   self.itemList.append(item)
   self.setCellWidget(row, 1, item)
  self.update()
  
 def _getHeaders(self) -> chess.pgn.Headers:
  return self.gameHeaders
[docs] def resetGame(self) -> None:
  '''Resets editor to standard header
  '''
  self.setGame(chess.pgn.Game())
[docs] def setPlyCount(self, newCount : int) -> None:
  '''Sets the 'PlyCount' header element, if selected 
:param newCount: actual number of halfmoves
  '''
  if self.plyCountLabel is not None:
   self.gameHeaders['PlyCount'] = newCount
   self.plyCountLabel.setText(str(newCount))
[docs] def setGameResult(self, result : str) -> None:
  '''Sets the 'Result' header element
:param result: one out of *1-0*, *0-1*, *1/2-1/2*, *\**
  '''
  if result not in self.gameResult:
   result = self.gameResult[3]
  self.gameHeaders['Result'] = result
  self.gameResultLabel.setText(result)
[docs] def setGame(self, game : chess.pgn.Game) -> None:
  '''Edit the header of a game. It corrects the following header elements
  
  * *Result*
  * *FEN*, if available
  * *PlyCount*, if available
:param game: game to be edited
  '''
  self.clear()
  self.gameHeaders = copy.deepcopy(game.headers)
  for key, defValue in chess.pgn.Headers().items():
   if key not in self.gameHeaders:
    self.gameHeaders[key] = defValue
  if 'FEN' in self.gameHeaders:
   self.gameHeaders['FEN'] = game.board().fen()
  if self.gameHeaders['Result'] not in self.gameResult:
   value = self.gameResult[3]
  plyCount = 0
  gameNode = game
  while gameNode is not None:
   plyCount += 1
   gameNode = gameNode.next()
  if 'PlyCount' in self.gameHeaders:
   self.gameHeaders['PlyCount'] = str(plyCount)
  keyValueTypeList = list()
  for key, value in self.gameHeaders.items():
   if key in self.stdKey2ValueTypeDict:
    type = self.stdKey2ValueTypeDict[key][1]
   else:
    key = '??? {} ???'.format(key)
    type = KeyType.STR
   keyValueTypeList.append((key, value, type))
  self._showTable(keyValueTypeList)
 
 def _getKV(self) -> Tuple[int, str, QtWidgets.QLineEdit]:
  item = self.sender()
  row = self.itemList.index(item)
  key = self.cellWidget(row, 0).text()
  return key, self.cellWidget(row, 1)
  
 @QtCore.pyqtSlot()
 def on_editingFinished(self):
  key, valueItem = self._getKV()
  text = valueItem.text().strip(' \t\n')
  self.gameHeaders[key] = text
  self._emitHeader()
  
 @QtCore.pyqtSlot(QtCore.QDate)
 def on_dateChanged(self, date):
  key, _ = self._getKV()
  self.gameHeaders[key] = date.toString('yyyy.MM.dd')
  self._emitHeader()
 @QtCore.pyqtSlot(QtCore.QTime)
 def on_timeChanged(self, _time):
  key, _ = self._getKV()
  self.gameHeaders[key] = _time.toString('hh:mm:ss')
  self._emitHeader()
 @QtCore.pyqtSlot(int)
 def on_spinBox_changed(self, value):
  key, _ = self._getKV()
  if value != 0:
   self.gameHeaders[key] = str(value)
  else:
   self.gameHeaders[key] = ''
  self._emitHeader()
   
 def _emitHeader(self) -> None:
  for key, value in self.gameHeaders.items():
   if len(value) == 0:
    del self.gameHeaders[key]
  if self.notifyGameHeadersChangedSignal is not None:
   self.notifyGameHeadersChangedSignal.emit(self.gameHeaders)
  
if __name__ == "__main__":
 import io, sys
 from pgnParse import read_game
 if True:
  newData = """[Event "matein2"]
[Site "problem solved"]
[Date "????.??.??"]
[Round "?"]
[White "?"]
[Black "?"]
[Result "*"]
[Time "??:??:??"]
[FEN "1k6/Rp1K4/1P5P/8/P7/3pP3/1p1P4/8 w - - 0 1"]
  
1.h7 b1=Q! $20 {[% -4.80]} 2.h8=R# *"""
 else:
  ps = "C:/Users/Reinh/OneDrive/Dokumente/Schach/ps.pgn"
  with open(ps, mode = 'r',  encoding = 'utf-8') as f:
   newdata = f.read()
 
 pgn = io.StringIO(newData)
 game = read_game(pgn)
 app = QtWidgets.QApplication([])
 tbl = GameHeaderView()
 tbl.setup(
   eventList = ["Wöchentliche Schachmeisterschaften"], 
   siteList = ["München GER", "Internet"], 
   playerList = ["März, Reinhard", "Schlüter, Paul"])
 tbl.setGame(game)
 tbl.resize(360,240)
 tbl.show()
 sys.exit(app.exec_())