'''Database Editor
================================
The *database* editor is based on Qt's QTableView. 
|DatabaseEditor|
It allows for 3 types of actions:
 * select a game by a double-click into the corresponding row
 * add/remove the displayed header items (limited to the 7-tag roster) by a right-clicking the column header
 * changing the sequence of games in the database by drag/drop or the menu items *Move Games>Up/Down*
.. |DatabaseEditor| image:: gameListTableView.png
  :width: 800
  :alt: Game Editor
'''
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
 from PyQt5.QtWidgets import QAction
else:
 from PyQt6 import QtWidgets, QtCore
 from PyQt6.QtGui import QAction
[docs]class GameListTableModel(QtCore.QAbstractTableModel):
 
 def __init__(self, gameList, gameHeaderKeys, parent = None):
  super(GameListTableModel, self).__init__(parent)
  self.gameHeaderKeys = gameHeaderKeys
  self.gameList = gameList
[docs] def rowCount(self, parent = None):
  return len(self.gameList) 
[docs] def columnCount(self, parent = None):
  return len(self.gameHeaderKeys) 
[docs] def flags(self, index):
  return QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsDragEnabled | QtCore.Qt.ItemFlag.ItemIsDropEnabled 
[docs] def supportedDropActions(self):
  return QtCore.Qt.DropAction.MoveAction 
[docs] def data(self, index, role = QtCore.Qt.ItemDataRole.DisplayRole):
  if index.isValid():
   if role == QtCore.Qt.ItemDataRole.DisplayRole:
    actGameHeaders = self.gameList[index.row()].headers
    columnKey = self.gameHeaderKeys[index.column()]
    return actGameHeaders[columnKey]
   elif role == QtCore.Qt.ItemDataRole.TextAlignmentRole:
    return int(QtCore.Qt.AlignmentFlag.AlignCenter | QtCore.Qt.AlignmentFlag.AlignVCenter)
  return None 
[docs] def moveRows(self, srcRowRange, tgtRow):
  if tgtRow < 0 or tgtRow > self.rowCount():
   return False
  assert isinstance(srcRowRange, range)
  parent = QtCore.QModelIndex()
  self.beginMoveRows(parent, srcRowRange.start, srcRowRange.stop - 1, parent, tgtRow)
  newGameList = list()
  if tgtRow < srcRowRange.start:
   newGameList += self.gameList[:tgtRow]
   newGameList += self.gameList[srcRowRange.start:srcRowRange.stop]
   newGameList += self.gameList[tgtRow:srcRowRange.start]
   newGameList += self.gameList[srcRowRange.stop:]
  else:
   newGameList += self.gameList[:srcRowRange.start]
   newGameList += self.gameList[srcRowRange.stop:tgtRow]
   newGameList += self.gameList[srcRowRange.start:srcRowRange.stop]
   newGameList += self.gameList[tgtRow:]
  for n, game in enumerate(newGameList):
   self.gameList[n] = newGameList[n]
  self.endMoveRows()
  return True 
[docs] def removeRows(self, srcRowList):
  if len(srcRowList) == 0:
   return False
  newGameList = list()
  for row, game in enumerate(self.gameList):
   if row not in srcRowList:
    newGameList.append(game)
  self.beginResetModel()
  for n, game in enumerate(newGameList):
   self.gameList[n] = newGameList[n]
  del self.gameList[len(newGameList):]
  self.endResetModel()
  return True  
  
[docs]class GameListTableView(QtWidgets.QTableView):
 sevenTagRoster = ["Event", "Site", "Round", "Date", "White", "Black", "Result"]
 def __init__(self, parent = None):
  super(GameListTableView, self).__init__(parent)
  self.doubleClicked.connect(self.on_doubleClicked)
  self.horizontalHeader().setStretchLastSection(True)
  self.horizontalScrollBar().setDisabled(True)
  self.notifyDoubleClickSignal = None
  self.notifyHeaderChangedSignal = None
  self.notifyListChangedSignal = None
  self.setSelectionBehavior(QtWidgets.QTableView.SelectionBehavior.SelectRows)
  self.sizeHints = None
  self.gameHeaderKeys = ["Date", "White", "Black", "Result"]
  headerWidget = self.horizontalHeader()
  headerWidget.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
  headerWidget.customContextMenuRequested.connect(self.on_menuHorizontalHeader_requested)
  self.context = QtWidgets.QMenu(self)
  self.context.triggered.connect(self.on_menuContext_triggered)
  for tag in self.sevenTagRoster:
   self.context.addAction('Show {}'.format(tag))
  self.context.addSeparator()
  self.context.addAction('Hide')
  
  self.setAcceptDrops(True)
  self.setDragEnabled(True)
  self.setDragDropMode(QtWidgets.QAbstractItemView.DragDropMode.InternalMove)
  
 @QtCore.pyqtSlot(QAction)
 def on_menuContext_triggered(self, action):
  actionText = action.text()
  newGameHeaderKeys = self.gameHeaderKeys.copy()
  if actionText == 'Hide':
   if len(newGameHeaderKeys) <= 1:
    self.notifyError('At least 1 column required')
    return
   newGameHeaderKeys.pop(self.contextColumn)
  else:
   item = actionText.split(' ')[1]
   newGameHeaderKeys.insert(self.contextColumn, item)
  self.gameHeaderKeys = newGameHeaderKeys
  if self.notifyHeaderChangedSignal is not None:
   self.notifyHeaderChangedSignal.emit(newGameHeaderKeys)
   
 def _selection2rowRange(self):
  selection = self.selectionModel().selectedRows()   
  if len(selection) == 0:
   self.notifyError('No game selected.')
   return None
  srcRowList = list()
  for rowSelection in selection:
   srcRowList.append(rowSelection.row())
  srcRowList = sorted(srcRowList)
  for n, srcRow in enumerate(srcRowList):
   if n > 0:
    if srcRowList[n] - srcRowList[n-1] != 1:
     self.notifyError('The selected rows must be simply connected.')
     return None
  return range(srcRowList[0], srcRowList[-1] + 1)
 def on_actionRemoveGames_triggered(self):
  selection = self.selectionModel().selectedRows()   
  srcRowList = list()
  for rowSelection in selection:
   srcRowList.append(rowSelection.row())
  return self.model().removeRows(srcRowList)
 def on_menuMoveGame_triggered(self, action):
  srcRowRange = self._selection2rowRange()
  if srcRowRange is None:
   return False
  if action.text() == 'Down':
   return self.model().moveRows(srcRowRange, srcRowRange.stop + 1)
  else:
   return self.model().moveRows(srcRowRange, srcRowRange.start - 1)
[docs] def dropEvent(self, event):
  if (event.source() is self \
   
and event.dropAction() == QtCore.Qt.DropAction.MoveAction and self.dragDropMode() == QtWidgets.QAbstractItemView.DragDropMode.InternalMove):
   if MzChess.useQt5():
    tgtRow = self.indexAt(event.pos()).row()
   else:
    tgtRow = self.indexAt(event.position().toPoint()).row()
   srcRowRange = self._selection2rowRange()
   if srcRowRange is not None and tgtRow != srcRowRange.start + 1:
    if tgtRow >= srcRowRange.start and tgtRow <= srcRowRange.stop:
     self.notifyError('The target row must be outside the range of source rows.')
     return None
    self.model().moveRows(srcRowRange, tgtRow)
   if self.notifyListChangedSignal is not None:
    self.notifyListChangedSignal.emit()
   event.accept()
  else:
   super().dropEvent(event) 
   
 def on_menuHorizontalHeader_requested(self, pos):
  self.contextColumn = self.columnAt(pos.x())
  self.context.exec(self.mapToGlobal(pos))
  return
  self.popupMenu = QtWidgets.QMenu()
  self.popupMenu.exec()
  return
 
 def notifyError(self, str : str) -> None:
  msgBox = QtWidgets.QMessageBox()
  msgBox.setIcon(QtWidgets.QMessageBox.Icon.Critical)
  msgBox.setText(str)
  msgBox.setWindowTitle("Error ...")
  msgBox.exec()
 def moveRow(self, up=True):
  selection = self.selectedIndexes()
  if selection:
   header = self.verticalHeader()
   row = header.visualIndex(selection[0].row())
   if up and row > 0:
    header.moveSection(row, row - 1)
   elif not up and row < header.count() - 1:
    header.moveSection(row, row + 1)
 @property
 def gameHeaderKeys(self):
  return self._gameHeaderKeys
 @gameHeaderKeys.setter
 def gameHeaderKeys(self, newGameHeaderKeys):
  if len(newGameHeaderKeys) == 0:
   self.notifyError('len(newGameHeaderKeys) > 0')
   return
  for tag in newGameHeaderKeys:
   if tag not in self.sevenTagRoster:
    self.notifyError('{} not in {}'.format(tag, self.sevenTagRoster))
    return     
  self._gameHeaderKeys = newGameHeaderKeys
  model = self.model()
  if model is not None:
   model.beginResetModel()
   model.gameHeaderKeys = self._gameHeaderKeys 
   model.endResetModel()
  self.contextColumn = -1
  return 
 def setGameList(self, gameList):
  model = GameListTableModel(gameList, self.gameHeaderKeys)
  self.setModel(model)
  self.sizeHints = 4*[0]
  totSize = 0
  for column, key in enumerate(model.gameHeaderKeys):
   self.sizeHints[column] = self.fontMetrics().size(QtCore.Qt.TextFlag.TextSingleLine, key).width()
   totSize += self.columnWidth(column)
   for n, game in enumerate(gameList):
    if key not in game.headers:
     raise ValueError('GameListTableModel: key {} not in game #{}'.format(key,n))
    actSize = self.fontMetrics().size(QtCore.Qt.TextFlag.TextSingleLine, game.headers[key]).width()
    self.sizeHints[column] = max(self.sizeHints[column], actSize)
  self.setColumnWidths()
  
 def setup(self, notifyDoubleClickSignal = None, notifyHeaderChangedSignal = None, notifyListChangedSignal = None):
  self.notifyDoubleClickSignal = notifyDoubleClickSignal
  self.notifyHeaderChangedSignal = notifyHeaderChangedSignal
  self.notifyListChangedSignal  = notifyListChangedSignal 
  
 def resetDB(self):
  self.reset()
 
 def setColumnWidths(self):
  if self.sizeHints is None:
   return
  self.setColumnWidth(0, int(self.sizeHints[0]*1.2))
  #self.setColumnWidth(1, middleWidth)
  #self.setColumnWidth(2, middleWidth)
  self.setColumnWidth(3, self.sizeHints[3])
[docs] def resizeEvent(self, ev):
  QtWidgets.QAbstractItemView.resizeEvent(self, ev)
  self.setColumnWidths() 
  
 @QtCore.pyqtSlot(QtCore.QModelIndex)
 def on_doubleClicked(self, index):
  if self.notifyDoubleClickSignal is not None:
   self.notifyDoubleClickSignal.emit(index.row())
  else:
   print('game #{} selected'.format(index.row())) 
if __name__ == "__main__":
 from pgnParse import read_game
 fileDirectory = os.path.dirname(os.path.abspath(__file__))
 pgnName = os.path.join(fileDirectory, 'training', 'matein', 'matein1.pgn')
 pgn = open(pgnName, mode = 'r',  encoding = 'utf-8')
 gameList = list()
 for n in range(10):
  game = read_game(pgn)
  if game is None:
   break
  gameList.append(game)
 app = QtWidgets.QApplication([])
 gameView = GameListTableView()
 gameView.show()
 gameView.setGameList(gameList)
 gameView.gameHeaderKeys = ['Event', 'Date']
 
 sys.exit(app.exec())