"""
This module provides classes that can be used to create Qt tables that show
2D structures in the cells.
This module was designed to be used with 2D Viewer and CombiGlide panels.
Please notify the module developers if you plan to use it with your code,
because the module behavior may change without notice.
Copyright Schrodinger, LLC. All rights reserved.
"""
# Contributors: Pat Lorton, Matvey Adzhigirey
import sys
import warnings
from past.utils import old_div
import schrodinger.ui.qt.swidgets as swidgets
import schrodinger.ui.qt.table as table
from schrodinger import structure
from schrodinger.infra import canvas2d
from schrodinger.Qt.QtCore import QPoint
from schrodinger.Qt.QtCore import QSize
from schrodinger.Qt.QtCore import Qt
from schrodinger.Qt.QtCore import QTimer
from schrodinger.Qt.QtGui import QColor
from schrodinger.Qt.QtGui import QPolygon
from schrodinger.Qt.QtWidgets import QApplication
from schrodinger.Qt.QtWidgets import QItemDelegate
from schrodinger.Qt.QtWidgets import QTableView
#This script subclasses several functions from qt that break our typical
#"style guide rules".
TIMEOUT = table.TIMEOUT # Milliseconds
[docs]def draw_picture_into_rect(*args, **kwargs):
"""
Draw a QPicture into a given rectangle.
This function has been moved to swidgets.draw_picture_into_rect.
:type painter: QtGui.QPainter object
:param painter: QPainter object that will do the drawing into the cell.
:type pic: QPicture
:param pic: the picture to be drawn into the rectangle
:type rect: QRect
:param rect: the rectangle that defines the drawing region
:type max_scale: int
:param max_scale: The maximum amount to scale the picture while attempting
to make it fit into rect. This value is multipled by the scale factor
required to fit a 150(wide) x 100 (high) picture to fit that picture into
rect. The resulting product (max_scale * reference picture scale) is the
true maximum scale factor allowed.
"""
swidgets.draw_picture_into_rect(*args, **kwargs)
class _CacheNode(table._CacheNode):
"""
This class has been moved into the new table module
"""
class _CacheClass(table._CacheClass):
"""
This class caches the last few QPictures given to it. It is designed to
cache visible cells in the table.
It has been moved into the new table module
"""
class _GenericViewerDelegate(QItemDelegate):
"""
This is the class that handles the actual drawing of data. This doesn't
handle any actual data, so you'll have to subclass it to get it to work.
For your own widget table, you should subclass this class, add any data
input you need into the __init__, and reimplement the _paint class for
your needs, using your data.
An example of this is the StructureReaderDelegate in this file.
"""
def __init__(self, tableview, tablemodel):
QItemDelegate.__init__(self)
self.tableview = tableview
self.tablemodel = tablemodel
self._paint_wait = False
self.qpolygon = QPolygon(4) #Used to draw hourglass
self._cell_size = QSize(350, 200)
self.tableview.setDelegate(self)
tablemodel.layoutChanged.connect(self.autoResizeCells)
def paint(self, painter, option, index):
"""
This handles the logic behind painting/not-painting when the scrollbar
is being dragged.
"""
QItemDelegate.paint(self, painter, option, index)
painter.setBrush(QColor(0, 0, 0))
if self.tableview.isScrolling() and self.paintWait():
self._paint_passive(painter, option, index)
else:
self._paint(painter, option, index)
def setPaintWait(self, val):
"""
Use this function to turn on/off delaying drawing of cells until the
scrollbar is not being used. This may be useful in CPU intensive
cell-drawing delegates.
"""
self._paint_wait = val
def paintWait(self):
""" Returns if we are drawing during while the scrollbar is dragged."""
return self._paint_wait
def sizeHint(self, option, index):
""" Tells the default width,height of Cells """
return self._cell_size
def autoResizeCells(self):
"""
Auto-resizes the cells based on the number of columns in the table
model and the width of the table.
"""
num_cols = self.tablemodel.columnCount()
# Need to subtract 5 pixels to keep horizontal scroll bar from appearing:
table_width = self.tableview.viewport().width() - 5
width = old_div(table_width, num_cols)
# Make sure the cells are never too wide:
while width * num_cols > table_width:
width -= 1
# Retain the same height/width ratio:
height = self._cell_size.height() * width / self._cell_size.width()
self.setCellSize(width, height)
def setCellSize(self, width, height):
""" Sets the default cell size, and resizes appropriately."""
self._cell_size = QSize(width, height)
# Will adjust the size of the actual cells to _cell_size:
self.tableview.resizeRowsToContents()
self.tableview.resizeColumnsToContents()
def _paint(self, painter, option, index):
""" Reimplement this to draw what you want in the cell. """
def _paint_passive(self, painter, option, index):
"""
This function paints a temporary cell if we're moving too
fast for real content to be drawn. By default this is just a
primitive hourglass, though you can implement to draw real info
if you desire.
"""
centerx = option.rect.left() + old_div(option.rect.width(), 2)
centery = option.rect.top() + old_div(option.rect.height(), 2)
self.qpolygon.setPoint(0, QPoint(centerx - 5, centery - 10))
self.qpolygon.setPoint(1, QPoint(centerx + 5, centery - 10))
self.qpolygon.setPoint(2, QPoint(centerx - 5, centery + 10))
self.qpolygon.setPoint(3, QPoint(centerx + 5, centery + 10))
painter.drawPolygon(self.qpolygon)
if not self.tableview.draw_timer.isActive():
self.tableview.draw_timer.start(TIMEOUT)
[docs]class ViewerTable(QTableView):
[docs] def __init__(self, tablemodel, parent=None):
msg = 'The ViewerTable class has been superseded by the ' + \
'DataViewerTable class in the schrodinger.ui.qt.table module'
warnings.warn(msg, DeprecationWarning, stacklevel=2)
QTableView.__init__(self, parent)
self._is_scrolling = False
self._is_actively_scrolling = False
self.verticalScrollBar().sliderPressed.connect(self._sliderPressed)
self.verticalScrollBar().sliderReleased.connect(self._sliderReleased)
self.verticalScrollBar().sliderMoved.connect(self._sliderMoved)
self.setModel(tablemodel)
self.tablemodel = tablemodel
self.draw_timer = QTimer()
self._delegate = None
self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
self.setAutoResize(True)
# part of EV:97430 - fix ZeroDivisionError
self._previous_ratio = 1.0
[docs] def cellAspectRatio(self):
size = self.delegate().sizeHint(None, None)
try:
ratio = old_div(size.width(), float(size.height()))
self._previous_ratio = ratio
return ratio
except ArithmeticError:
return self._previous_ratio
[docs] def setDelegate(self, d):
self._delegate = d
[docs] def delegate(self):
return self._delegate
[docs] def setAutoResize(self, val):
self._auto_resize = val
[docs] def autoResize(self):
return self._auto_resize
[docs] def resizeEvent(self, event):
QTableView.resizeEvent(self, event)
if self.tablemodel.columnCount() <= 0:
return
if not self.delegate() or not self.autoResize():
return
num_cols = self.tablemodel.columnCount()
# Need to subtract 5 pixels to keep horizontal scroll bar from appearing:
table_width = self.viewport().width() - 5
width = old_div(table_width, num_cols)
# Make sure the cells are never too wide:
while width * num_cols > table_width:
width -= 1
self.delegate().setCellSize(width, old_div(width,
self.cellAspectRatio()))
def _sliderReleased(self):
self._redrawTable()
self.draw_timer.timeout.disconnect(self._timerFired)
self.draw_timer.stop()
self._is_scrolling = False
self._is_actively_scrolling = False
def _sliderPressed(self):
self._is_scrolling = True
self._is_actively_scrolling = True
# Fire one second since scroll bar is no longer moving:
self.draw_timer.setSingleShot(True)
self.draw_timer.start(TIMEOUT)
self.draw_timer.timeout.connect(self._timerFired)
def _sliderMoved(self, int):
# Keep re-setting the timer while the scroll bar is moved:
self._is_actively_scrolling = True
self.draw_timer.start(TIMEOUT)
def _timerFired(self):
self.itemDelegate().generate_one_structure = True
self._is_actively_scrolling = False
self._redrawTable()
def _redrawTable(self):
# Redraw the visible cells:
self.viewport().update()
[docs]class ViewerModel(table.ViewerModel):
"""
This class has been moved to the new table module
"""
[docs]class StructureViewerDelegate(_GenericViewerDelegate):
[docs] def __init__(self, tableview, tablemodel, filename=None):
_GenericViewerDelegate.__init__(self, tableview, tablemodel)
self.model = tablemodel
self.picture_cache = _CacheClass()
self.generate_one_structure = False
def _paint(self, painter, option, index, passive=False):
struct = self.tablemodel.getStruct(index)
if struct is None:
#Skip painting if there's no structure for this cell
return
pic = self.picture_cache.get(int(struct))
if not pic:
if passive and self.generate_one_structure:
passive = False
self.generate_one_structure = False
if passive:
_GenericViewerDelegate._paint_passive(self, painter, option,
index)
else:
pic = self.generatePicture(index)
self.picture_cache.store(int(struct), pic)
self.paintCell(painter, option, index, pic)
else:
self.paintCell(painter, option, index, pic)
def _paint_passive(self, painter, option, index):
self._paint(painter, option, index, passive=True)
[docs] def generatePicture(self, index):
"""
Overwrite this method. It should return a QPicture object
for item index.
"""
[docs] def clearCache(self):
"""
Will clear all QPictures that are cached.
"""
# Will cause previous QPictures to be garbage-collected:
self.picture_cache = _CacheClass()
[docs] def paintCell(self, painter, option, index, pic):
"""
Overwrite this method for custom cell drawing
"""
r = option.rect
padding_factor = 0.04
r.setLeft(option.rect.left() + padding_factor * option.rect.width())
r.setRight(option.rect.right() - padding_factor * option.rect.width())
r.setTop(option.rect.top() + padding_factor * option.rect.height())
r.setBottom(option.rect.bottom() -
padding_factor * option.rect.height())
# TODO use padding_factor option of swidgets.draw_picture_into_rect()
draw_picture_into_rect(painter, pic, r)
[docs]class StructureReaderDelegate(StructureViewerDelegate):
[docs] def __init__(self, tableview, tablemodel, filename=None):
StructureViewerDelegate.__init__(self, tableview, tablemodel)
self.model = tablemodel
self.adaptor = canvas2d.ChmMmctAdaptor()
self.model = canvas2d.ChmRender2DModel()
self.renderer = canvas2d.Chm2DRenderer(self.model)
self.setFile(filename)
[docs] def generatePicture(self, index):
struct = self.tablemodel.getStruct(index)
if struct:
chmmol = self.adaptor.create(int(struct))
pic = self.renderer.getQPicture(chmmol)
return pic
else:
return None
[docs] def setFile(self, filename=None):
self.picture_cache.clear()
if filename is None:
self.tablemodel.clearStructs()
self.tablemodel.resize(0, self.tablemodel.columnCount())
else:
sr = structure.StructureReader(filename)
self.tablemodel.clearStructs()
duplicates = False
for struct in sr: #A somewhat inefficient, small example
duplicates |= not self.tablemodel.appendStruct(struct)
self.recalculateTableSize()
return duplicates
[docs] def recalculateTableSize(self):
rows = int(old_div(self.tablemodel.structCount(), \
self.tablemodel.columnCount()))
if rows * self.tablemodel.columnCount() < self.tablemodel.structCount():
rows += 1
self.tablemodel.resize(rows, self.tablemodel.columnCount())
[docs] def appendFile(self, filename):
self.picture_cache.clear()
sr = structure.StructureReader(filename)
duplicates = False
for struct in sr: #A somewhat inefficient, small example
duplicates |= not self.tablemodel.appendStruct(struct)
self.recalculateTableSize()
return duplicates
######################################################
#
#
# This is just a sample of how to use this table.
#
#
######################################################
if __name__ == '__main__':
app = QApplication([])
vm = ViewerModel(500, 5)
view = ViewerTable(vm)
vd = StructureReaderDelegate(view, vm, sys.argv[1])
vd.setPaintWait(True)
view.setItemDelegate(vd)
view.resizeRowsToContents()
view.resizeColumnsToContents()
view.resize(1300, 700)
view.show()
app.exec_()