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