Source code for schrodinger.ui.qt.structtable

"""
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] def isScrolling(self): return self._is_scrolling
[docs] def isActivelyScrolling(self): return self._is_actively_scrolling
[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()