Source code for schrodinger.ui.qt.table

"""
Contains classes that can be used to display a QTable that can show both
structures and text data in arbitrary cells.

Copyright Schrodinger, LLC. All rights reserved.
"""

import csv
import itertools
import warnings
from past.utils import old_div

import schrodinger.structure as struct
import schrodinger.ui.qt.structure2d as structure2d
from schrodinger.infra import canvas2d
from schrodinger.Qt import QtCore
from schrodinger.Qt import QtGui
from schrodinger.Qt import QtWidgets
from schrodinger.Qt.QtCore import Qt
from schrodinger.structutils import smiles as smiles_mod
from schrodinger.ui import sketcher
from schrodinger.ui.qt import filedialog
from schrodinger.ui.qt import swidgets
from schrodinger.ui.qt import utils as qt_utils
from schrodinger.utils import csv_unicode
from schrodinger.utils.withnone import NO_ITEM

TIMEOUT = 200


[docs]class EmittingScrollBar(QtWidgets.QScrollBar): """ Works the same as a normal scrollbar, but emits signals when it is shown or hidden. """ shown = QtCore.pyqtSignal(int) hidden = QtCore.pyqtSignal(int)
[docs] def showEvent(self, event): """ Emit a signal to indicate that the scrollbar has been shown :type event: QShowEvent object :parameter event: the event generated when shown """ self.shown.emit(self.orientation())
[docs] def hideEvent(self, event): """ Emit a signal to indicate that the scrollbar has been hidden :type event: QHideEvent object :parameter event: the event generated when hidden """ self.hidden.emit(self.orientation())
[docs]class SHeaderView(QtWidgets.QHeaderView): """ Class for horizontal and vertical headers of a table. This class is robust to a resizing issue that can occur when the whole table begins to resize when the user resizes a row or column. It uses globalPos() for the mouse move event during resizing rather than pos(), as pos() is relative to the widget corner (which itself is moving when the problem occurs). """
[docs] def __init__(self, orientation, parent): """ Instantiate an SHeaderView object :type orientation: int (QtCore.Qt.Horizontal or QtCore.Qt.Vertical) :param orientation: whether this is horizontal or vertical :type parent: QTableView object :parameter parent: The table that should be resized by this header """ QtWidgets.QHeaderView.__init__(self, orientation, parent) self.parent = parent self._resizing = -1 self._last_pos = None
[docs] def mouseReleaseEvent(self, event): """ Stop any resizing that we are doing :type event: QMouseEvent :param event: the event object """ if self._resizing > -1 and event.button() == QtCore.Qt.LeftButton: self._resizing = -1 QtWidgets.QHeaderView.mouseReleaseEvent(self, event)
[docs] def mouseMoveEvent(self, event): """ Makes resizing of rows/columns robust to motion of the widget itself by using global mouse coordinates rather than local-to-the-widget coordinates. Note that, if the mouse is pressed in the header, the header still gets mouse move events until the mouse button is released even if the cursor leaves the header area. :type event: QMouseEvent :param event: the event object """ if self._resizing > -1: if self.orientation() == QtCore.Qt.Horizontal: current = event.globalX() else: current = event.globalY() # Change the size of the row/column by the amount the mouse has # moved since the last event. delta = current - self._last_pos self._last_pos = current if self.orientation() == QtCore.Qt.Horizontal: self.parent.setColumnWidth( self._resizing, self.parent.columnWidth(self._resizing) + delta) else: self.parent.setRowHeight( self._resizing, self.parent.rowHeight(self._resizing) + delta) else: QtWidgets.QHeaderView.mouseMoveEvent(self, event)
[docs]class DataViewerTable(QtWidgets.QTableView):
[docs] def __init__(self, model=None, parent=None, aspect_ratio=True, fill='rows', gang='both', resizable='none', fit_view='none', vscrollbar='default', hscrollbar='default', cell_height=100, cell_width=100, enable_copy=True, universal_row_gang_setting=False, universal_column_gang_setting=False): """ :type model: QAbstractTableModel :param model: The model for this table. The typical class to use with this table is the StructureDataViewerModel class :type parent: QWidget :param parent: The widget that owns this table widget :type aspect_ratio: bool :param aspect_ratio: if True (default) keeps the cell height & width aspect ratio locked at the ratio indicated by cell_height and cell width. Note that True implies gang="both". If aspect_ratio is true, fill can be "rows" OR "columns", but NOT "both". :type fill: "rows", "columns", "both" or "none" :param fill: The specified table feature will expand or contract to exactly fit within the table's viewport. - "none": means neither rows or columns will expand - "rows": means rows will expand (default) - "columns": means columns will expand - "both": means both rows and columns will expand fill cannot be "both" if aspect_ratio is True. Setting a feature to fill will turn off the corresponding scrollbar. :type gang: "rows", "columns", "both" or "none" :param gang: Controls whether rows and/or columns can be independently resized. - "none" - rows and columns can be independently resized - "rows" - rows are ganged so that changing the size of one row will change all the rows - "columns" - columns are ganged so that changing the size of one column will change all the columns - "both" - both rows and columns are ganged (default) :type universal_row_gang_setting: bool :param universal_row_gang_setting: If true, then the gang setting applies to ALL rows and a subset of rows may not be ganged. If False (the default), either a subset or all rows may be ganged. It is never necessary to set this option, however setting it does allow for a slight improvement in memory usage for very large tables. Note that this setting applies whether the rows are all ganged or all not ganged. Note that this setting in combination with ganging rows will prevent actions such as hiding the row that change the row size. :type universal_column_gang_setting: bool :param universal_column_gang_setting: If true, then the gang setting applies to ALL columns and a subset of columns may not be ganged. If False (the default), either a subset or all columns may be ganged. It is never necessary to set this option, however setting it does allow for a slight improvement in memory usage for very large tables. Note that this setting applies whether the columns are all ganged or all not ganged. Note that this setting in combination with ganging columns will prevent actions such as hiding the column that change the column size. :type resizable: "rows", "columns", "both" or "none" :param resizable: the table features that can be resized by the user. - "none" - neither rows nor columns can be resized by the user - "rows" - rows can be resized by the user - "columns" - columns can be resized by the user - "both" - both rows and columns can be resized by the user (default) Note - a feature that has fill turned on cannot be resized by the user, and if fill is turned on and aspect_ratio is True, neither rows nor columns can be resized by the user. :type fit_view: "rows", "columns", "both" or "none" :param fit_view: the table features that determine the size of the table's viewport (the part visible to the user). When this is turned on for a feature, the visible part of the table resizes dynamically to exactly fit that feature. For instance, if fit_view='rows', the visible part of the table will resize to exactly fit all the rows any time the size of a row changes. - "none" - neither rows nor columns determine the viewport size - "rows" - rows determine the viewport height - "columns" - columns determine the viewport width - "both" - both rows and columns determine the viewport size Note - a feature that has fill turned on cannot have viewport_fit turned on. fill uses the size of the viewport to determine the size of the cells, while fit_view uses the sie of the cells to determine the size of the viewport. Note 2 - Setting a feature to fit_view will turn off the corresponding scrollbar. Note 3 - User resizing of rows with both gang and fit_view='rows' or 'both' tends to be a bit wonky because the amount the row resizes depends on the placement of the mouse relative to the table, and the table moves as it resizes to the rows, so you tend to get very large changes rapidly. :type vscrollbar: "on", "off" or "default" :param vscrollbar: Vertical scrollbar setting - "on" - always on - "off" - always off - "default" - (default) on as needed If either fit_view or fill = "rows" or "both", the vertical scrollbar is turned off :type hscrollbar: "on", "off" or "default" :param hscrollbar: Horizontal scrollbar setting - "on" - always on - "off" - always off - "default" - (default) on as needed If either fit_view for fill ="columns" or "both", the horizontal scrollbar is turned off :type cell_width: int :param cell_width: default width of cells in pixels :type cell_height: int :param cell_height: default height of cells in pixels :type enable_copy: bool :param enable_copy: True if data in the table can be copied to the clipboard. Cells with structures copy the title of the structure to the clipboard, else the string conversion of the structure to the clipboard. """ # Set up the table/model/delegate triumverant QtWidgets.QTableView.__init__(self, parent) self.setModel(model) self.delegate = None self.setHorizontalHeader(SHeaderView(QtCore.Qt.Horizontal, self)) self.setVerticalHeader(SHeaderView(QtCore.Qt.Vertical, self)) #: Whether the table should automatically resize in response to changes self.auto_size = True # gang info self.column_is_ganged = [] self.row_is_ganged = [] self._universal_row_gang = universal_row_gang_setting self._universal_column_gang = universal_column_gang_setting self.setGangPolicy(gang, resize=False, policy_check=False) # Set up the sizing/resizing parameters self.setAspectRatioPolicy(aspect_ratio, resize=False, policy_check=False) # fill info self.setFillPolicy(fill, resize=False, policy_check=False, enforce_scroll_bars=False) # user resize info self.setResizablePolicy(resizable, resize=False, policy_check=False) # fit_viewport info self.setFitViewPolicy(fit_view, resize=False, policy_check=False, enforce_scroll_bars=False) self.checkSizingPolicies() # default sizes self.default_cell_size = QtCore.QSize(cell_width, cell_height) try: self._default_ratio = old_div(cell_width, float(cell_height)) except ArithmeticError: self._default_ratio = 1.0 self.tool_tip_size = (200, 200) # Scrollbar setup self.setHorizontalScrollBar(EmittingScrollBar(QtCore.Qt.Horizontal)) self.setVerticalScrollBar(EmittingScrollBar(QtCore.Qt.Vertical)) self.verticalScrollBar().shown.connect(self.scrollBarChanged) self.horizontalScrollBar().shown.connect(self.scrollBarChanged) self.verticalScrollBar().hidden.connect(self.scrollBarChanged) self.horizontalScrollBar().hidden.connect(self.scrollBarChanged) 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.draw_timer = QtCore.QTimer() sb_policies = { 'default': QtCore.Qt.ScrollBarAsNeeded, 'on': QtCore.Qt.ScrollBarAlwaysOn, 'off': QtCore.Qt.ScrollBarAlwaysOff } if self.fill_rows or self.fit_view_rows: self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) else: self.setVerticalScrollBarPolicy( sb_policies.get(vscrollbar.lower(), QtCore.Qt.ScrollBarAsNeeded)) if self.fill_columns or self.fit_view_columns: self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) else: self.setHorizontalScrollBarPolicy( sb_policies.get(hscrollbar.lower(), QtCore.Qt.ScrollBarAsNeeded)) # Monitor when the column width and row height is adjusted self.horizontalHeader().sectionResized.connect(self.columnResized) self.verticalHeader().sectionResized.connect(self.rowResized) self.copy_enabled = enable_copy # When self.lock_aspect_ratio is True, either row or column is set to # fill, and the scrollbars are set to default (as needed) we can run # into infinite loops when the viewport is nearly identical to the # table size. This happens (if fill=row) because the row heights # adjust to the viewport size, then the columns adjust to keep the # aspect ratio correct, which causes a change in state of the scrollbar # (either hidden or not), which changes the viewport size, which causes # the rows to adjust in size, which causes the columns to adjust to keep # the aspect ratio correct, which causes a change in state of the # scrollbar (either hidden or not) ... and so on. To avoid this, we use # the following: # self.avoid_loops: True if we need to watch out for this. We don't # turn this on for the first sizing of the table, as that seems to # cause some scrollbar toggling itself. # self.stop_loop: True if we've detected a loop and need to stop it. # We don't stop a loop immediately, because resizing once more # gets the table to fit properly in the current viewport # self.loop_sign: The pattern of scrollbar visibility that signals a # loop # self.toggle_length: The number of turns we let the scrollbar toggle # before we decide that we're in a loop # self.vbar_states = history of the vertical scrollbar visibility # self.hbar_states = history of the horizontal scrollbar visibility self.doing_row_resize = self.doing_col_resize = False self.doing_fix_resize = False self.row_remainder = self.col_remainder = 0 self.stop_loop = False self.avoid_loops = False self.loop_sign = [False, True, False] self.toggle_length = len(self.loop_sign) self.initiateCellSizes() self.fixSize() self.avoid_loops = self.lock_aspect_ratio and (self.fill_rows or self.fill_columns) self.vbar_states = [] self.hbar_states = [] self.model().layoutChanged.connect(self.fixSize) self.setMouseTracking(True) self.mouse_row = -1 self.mouse_column = -1 self.model().columnsInserted.connect(self.columnsInserted) self.model().columnsMoved.connect(self.columnsMoved) self.model().columnsRemoved.connect(self.columnsRemoved) self.event_filter = ToolTipFilter(self) self.viewport().installEventFilter(self.event_filter)
[docs] def copyImageToClipboard(self): """ Copy an image of the table to the clipboard """ size = self.size() # Don't copy the scrollbars vsb = self.verticalScrollBar() if vsb.isVisible(): size.setWidth(size.width() - vsb.size().width()) hsb = self.horizontalScrollBar() if hsb.isVisible(): size.setHeight(size.height() - hsb.size().height()) my_image = QtGui.QImage(size, QtGui.QImage.Format_RGB32) origin = QtCore.QPoint(0, 0) region = QtGui.QRegion(0, 0, size.width(), size.height()) self.render(my_image, origin, region) QtWidgets.QApplication.clipboard().setImage(my_image)
[docs] def headerClicked(self, col): section = self.horizontalHeader().sortIndicatorSection() if col == section: order = self.horizontalHeader().sortIndicatorOrder() else: order = QtCore.Qt.AscendingOrder self.model().sort(col, order)
[docs] def enforceScrollBarPolicies(self): """ Sets the scrollbar policies as required by the sizing policies """ if self.fill_rows or self.fit_view_rows: self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) else: self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) if self.fill_columns or self.fit_view_columns: self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) else: self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
[docs] def setFitViewPolicy(self, fit_view, resize=True, policy_check=True, enforce_scroll_bars=True): """ Sets whether the viewport should be resized to the rows/columns or not :type fit_view: "rows", "columns", "both" or "none" :param fit_view: the table features that determine the size of the table's viewport (the part visible to the user). When this is turned on for a feature, the visible part of the table resizes dynamically to exactly fit that feature. For instance, if fit_view='rows', the visible part of the table will resize to exactly fit all the rows any time the size of a row changes. - "none" - neither rows nor columns determine the viewport size - "rows" - rows determine the viewport height - "columns" - columns determine the viewport width - "both" - both rows and columns determine the viewport size Note - a feature that has fill turned on cannot have viewport_fit turned on. fill uses the size of the viewport to determine the size of the cells, while fit_view uses the sie of the cells to determine the size of the viewport. Note 2 - Setting a feature to fit_view will turn off the corresponding scrollbar. Note 3 - User resizing of rows with both gang and fit_view='rows' or 'both' tends to be a bit wonky because the amount the row resizes depends on the placement of the mouse relative to the table, and the table moves as it resizes to the rows, so you tend to get very large changes rapidly. :type resize: bool :param resize: Whether to resize the table :type policy_check: bool :param policy_check: Whether to check for conflicting sizing policies. :type enforce_scroll_bars: bool :param enforce_scroll_bars: Whether to enforce the default scrollbar policy for fit view settings """ fit_view = fit_view.lower() self.fit_view_rows = fit_view in ['rows', 'both'] self.fit_view_columns = fit_view in ['columns', 'both'] if enforce_scroll_bars: self.enforceScrollBarPolicies() if policy_check: self.checkSizingPolicies() if resize: self.fixSize()
[docs] def setFillPolicy(self, fill, resize=True, policy_check=True, enforce_scroll_bars=True): """ Sets whether columns or rows should fill the viewport or not :type fill: "rows", "columns", "both" or "none" :param fill: The specified table feature will expand or contract to exactly fit within the table's viewport. "both" means both rows and columns will expand, and "none" means neither will. fill cannot be "both" if aspect_ratio is True. Setting a feature to fill will turn off the corresponding scrollbar. :type resize: bool :param resize: Whether to resize the table :type policy_check: bool :param policy_check: Whether to check for conflicting sizing policies. :type enforce_scroll_bars: bool :param enforce_scroll_bars: Whether to enforce the default scrollbar policy for fill settings """ fill = fill.lower() self.fill_rows = fill in ['rows', 'both'] self.fill_columns = fill in ['columns', 'both'] self.avoid_loops = self.lock_aspect_ratio and (self.fill_rows or self.fill_columns) if enforce_scroll_bars: self.enforceScrollBarPolicies() if policy_check: self.checkSizingPolicies() if resize: self.fixSize()
[docs] def setGangPolicy(self, gang, resize=True, policy_check=True): """ Sets whether the rows or columns are ganged or not :type gang: "rows", "columns", "both" or "none" :param gang: Controls whether rows and/or columns can be independently resized. - "none": rows and columns can be independently resized - "rows": rows are ganged so that changing the size of one row will change all the rows - "columns": columns are ganged so that changing the size of one column will change all the columns - "both" - both rows and columns are ganged (default) :type resize: bool :param resize: Whether to resize the table :type policy_check: bool :param policy_check: Whether to check for conflicting sizing policies. Not used for this routine, but kept for consistency with other sizing policy routines """ gang = gang.lower() self.gang_rows = gang in ['rows', 'both'] self.gang_columns = gang in ['columns', 'both'] if not self._universal_row_gang: self.row_is_ganged = [self.gang_rows] * self.model().rowCount() if not self._universal_column_gang: self.column_is_ganged = [self.gang_columns ] * self.model().columnCount() if resize: self.fixSize()
[docs] def isRowGanged(self, row): """ Returns True if row is ganged, False if not :type row: int :param row: the row to check for ganging """ if self.gang_rows: if self._universal_row_gang: return True else: try: return self.row_is_ganged[row] except IndexError: return False else: return False
[docs] def isColumnGanged(self, column): """ Returns True if column is ganged, False if not :type column: int :param column: the column to check for ganging """ if self.gang_columns: if self._universal_column_gang: return True else: try: return self.column_is_ganged[column] except IndexError: return False else: return False
[docs] def setRowIsGanged(self, row, is_ganged): """ Sets a flag on row to indicate whether its size is ganged with other rows or not. Note that this has no effect if rows are not already ganged. This function should not be called if universal_row_gang_settings was set to True. :type row: int :param row: the row number this applies to :type is_ganged: bool :param is_ganged: True if the row should be ganged, False if not """ if self._universal_row_gang: raise AttributeError('Table was created with universal settings' +\ ', individual rows may not be set.') self.row_is_ganged[row] = is_ganged
[docs] def setColumnIsGanged(self, column, is_ganged): """ Sets a flag on column to indicate whether its size is ganged with other columns or not. Note that this has no effect if columns are not already ganged. This function should not be called if universal_column_gang_setting was set to True. :type column: int :param column: the column number this applies to :type is_ganged: bool :param is_ganged: True if the column should be ganged, False if not """ if self._universal_column_gang: raise AttributeError('Table was created with universal settings' +\ ', individual columns may not be set.') self.column_is_ganged[column] = is_ganged
[docs] def setAspectRatioPolicy(self, fixed, resize=True, policy_check=True): """ Sets whether the cell aspect ratio is locked or not :type fixed: bool :param fixed: If True, cell aspect ratio is locked. If False, it is not :type resize: bool :param resize: Whether to resize the table :type policy_check: bool :param policy_check: Whether to check for conflicting sizing policies. """ self.lock_aspect_ratio = fixed if fixed: self.setGangPolicy('both', resize=False, policy_check=False) try: self.avoid_loops = self.fill_rows or self.fill_columns except AttributeError: pass if policy_check: self.checkSizingPolicies() if resize: self.fixSize()
[docs] def setResizablePolicy(self, resizable, resize=True, policy_check=True): """ Sets whether the rows/columns can be resized by the user :type resizable: "rows", "columns", "both" or "none" :param resizable: the table features that can be resized by the user. - "none" - neither rows nor columns can be resized by the user - "rows" - rows can be resized by the user - "columns" - columns can be resized by the user - "both" - both rows and columns can be resized by the user (default) Note - a feature that has fill turned on cannot be resized by the user, and if fill is turned on and aspect_ratio is True, neither rows nor columns can be resized by the user. :type resize: bool :param resize: Whether to resize the table :type policy_check: bool :param policy_check: Whether to check for conflicting sizing policies. """ # user resize info resizable = resizable.lower() self.resizable_rows = resizable in ['rows', 'both'] self.resizable_columns = resizable in ['columns', 'both'] # Give the headers the proper user resize settings - the default is # user-resizable, so we only set them to fixed if need be. if not self.resizable_columns: self.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.Fixed) else: self.horizontalHeader().setSectionResizeMode( QtWidgets.QHeaderView.Interactive) if not self.resizable_rows: self.verticalHeader().setSectionResizeMode( QtWidgets.QHeaderView.Fixed) else: self.verticalHeader().setSectionResizeMode( QtWidgets.QHeaderView.Interactive) if policy_check: self.checkSizingPolicies() if resize: self.fixSize()
[docs] def setSortingEnabled(self, sorting): """ Sets sorting enabled :type sorting: bool :param sorting: If True, sorting is enabled """ if sorting: self.horizontalHeader().sectionClicked.connect(self.headerClicked) self.horizontalHeader().setSectionsClickable(True) self.horizontalHeader().setSortIndicatorShown(True) self.horizontalHeader().setSortIndicator(-1, 0) else: self.horizontalHeader().sectionClicked.disconnect( self.headerClicked) self.horizontalHeader().setSectionsClickable(False) self.horizontalHeader().setSortIndicatorShown(False) self.horizontalHeader().setSortIndicator(-1, 0)
[docs] def checkSizingPolicies(self): """ Raises a ValueError if we have conflicting sizing policies """ if all([self.lock_aspect_ratio, self.fill_rows, self.fill_columns]): raise ValueError("fill cannot be 'both' if aspect_ratio is True") # Look for fill/resizable policy conflicts if (self.lock_aspect_ratio and (self.fill_columns or self.fill_rows)) \ and (self.resizable_rows or self.resizable_columns): raise ValueError("rows or columns can not be resized if " \ "aspect_ratio is True and rows or columns " \ "are set to fill") elif self.fill_columns and self.resizable_columns: raise ValueError("columns can not be resized if they are set to " \ "fill") elif self.fill_rows and self.resizable_rows: raise ValueError("rows can not be resized if they are set to fill") # Make sure we don't have conflicting fill/fit_view settings if self.fit_view_rows and self.fill_rows: raise ValueError("Both fill and fit_view cannot be set for rows") elif self.fit_view_columns and self.fill_columns: raise ValueError("Both fill and fit_view cannot be set for columns")
[docs] def initiateCellSizes(self, rows=True, columns=True): """ Sets all the cells to their intial default size :type rows: bool :param rows: whether to set the rows to their default height :type columns: bool :param columns: whether to set the columns to their default width """ self.doing_fix_resize = True if rows: for row in range(self.model().rowCount()): self.setRowHeight(row, self.default_cell_size.height()) if columns: for column in range(self.model().columnCount()): self.setColumnWidth(column, self.default_cell_size.width()) self.doing_fix_resize = False
[docs] def rowsInserted(self, index, start, end): """ Slot that receives a signal when rows are inserted in the model. Make sure our row_is_ganged list stays current. :type index: QModelIndex :param index: unused :type start: int :param start: The starting index of the rows inserted :type end: int :param end: The ending index of the rows inserted (inclusive) """ QtWidgets.QTableView.rowsInserted(self, index, start, end) if not self._universal_row_gang: try: for ind in range(start, end + 1): self.row_is_ganged.insert(start, True) except IndexError: pass self.fixSize()
[docs] def rowsMoved(self, index1, start, end, index2, place): """ Slot that receives a signal when rows are moved in the model. Make sure our row_is_ganged list stays current. :type index1: QModelIndex :param index1: unused :type start: int :param start: The starting index of the rows moved :type end: int :param end: The ending index of the rows moved (inclusive) :type index2: QModelIndex :param index2: unused :type place: int :param place: The new starting index of the moved rows """ if not self._universal_row_gang: try: myvals = [] for ind in range(end, start - 1, -1): myvals.append(self.row_is_ganged.pop(ind)) for val in myvals: self.row_is_ganged.insert(place, val) except: pass
[docs] def rowsRemoved(self, index, start, end): """ Slot that receives a signal when rows are removed in the model. Make sure our row_is_ganged list stays current. :type index: QModelIndex :param index: unused :type start: int :param start: The starting index of the rows removed :type end: int :param end: The ending index of the rows removed (inclusive) """ # This slot gets called TWICE by model.endRemoveRows(), once by # endRemoveRows and once by its call to layoutChanged, so only # remove the list items for the first call. if not self._universal_row_gang: if self.row_is_ganged and \ len(self.row_is_ganged) != self.model().rowCount(): try: for ind in range(start, end + 1): del self.row_is_ganged[start] except IndexError: pass self.fixSize()
[docs] def columnsInserted(self, index, start, end): """ Slot that receives a signal when columns are inserted in the model. Make sure our column_is_ganged list stays current. :type index: QModelIndex :param index: unused :type start: int :param start: The starting index of the columns inserted :type end: int :param end: The ending index of the columns inserted (inclusive) """ if not self._universal_column_gang: try: for ind in range(start, end + 1): self.column_is_ganged.insert(start, True) except IndexError: pass self.fixSize()
[docs] def columnsMoved(self, index1, start, end, index2, place): """ Slot that receives a signal when columns are moved in the model. Make sure our column_is_ganged list stays current. :type index1: QModelIndex :param index1: unused :type start: int :param start: The starting index of the columns moved :type end: int :param end: The ending index of the columns moved (inclusive) :type index2: QModelIndex :param index2: unused :type place: int :param place: The new starting index of the moved columns """ if not self._universal_column_gang: try: myvals = [] for ind in range(end, start - 1, -1): myvals.append(self.column_is_ganged.pop(ind)) for val in myvals: self.column_is_ganged.insert(place, val) except: pass
[docs] def columnsRemoved(self, index, start, end): """ Slot that receives a signal when columns are removed in the model. Make sure our column_is_ganged list stays current. :type index: QModelIndex :param index: unused :type start: int :param start: The starting index of the columns removed :type end: int :param end: The ending index of the columns removed (inclusive) """ # This slot gets called TWICE by model.endRemoveColumns(), once by # endRemoveColumns and once by its call to layoutChanged, so only # remove the list items for the first call. if not self._universal_column_gang: if self.column_is_ganged and \ len(self.column_is_ganged) != self.model().columnCount(): try: for ind in range(start, end + 1): del self.column_is_ganged[start] except IndexError: pass self.fixSize()
[docs] def columnCountChanged(self, old_count, new_count): """ Called when a column is added or deleted, and sets the columns to their initial size. :type old_count: int :param old_count: the old number of columns :type new_count: int :param new_count: the num number of columns """ QtWidgets.QTableView.columnCountChanged(self, old_count, new_count) self.initiateCellSizes(rows=False) self.fixSize()
[docs] def rowCountChanged(self, old_count, new_count): """ Called when a row is added or deleted, and sets the rows to their initial size. :type old_count: int :param old_count: the old number of rows :type new_count: int :param new_count: the num number of rows """ QtWidgets.QTableView.rowCountChanged(self, old_count, new_count) self.initiateCellSizes(columns=False) self.fixSize()
[docs] def keyPressEvent(self, event): """ :type event: QKeyEvent :param event: The keypress event Currently only does anything for Ctrl-C (copy to clipboard) """ if self.copy_enabled and event.matches(QtGui.QKeySequence.Copy): # Iterate over all the selected cells, place a tab between cells in # a row and a return between rows - then put this in the clipboard. # This way the data will paste into Excel and other programs just # like it is in the table. def index_key(index): return index.row(), index.column() # Sort the data so it goes left to right across each row in # descending rows. indexes = self.selectedIndexes() indexes.sort(key=index_key) mydata = "" current_row = None for ind in indexes: # For new rows, strip any trailing tab and add a return if ind.row() != current_row: if current_row is not None: # Remove any trailing tab and add a return. Note: rstrip # will remove multiple tabs if the cells are empty. We # don't want that. if mydata.endswith('\t'): mydata = mydata[:-1] mydata = mydata + '\n' current_row = ind.row() value = self.model().data(ind) if value is None: value = "" elif self.delegate.isStructure(value): if value.title: value = value.title try: mydata = mydata + str(value) + '\t' except ValueError: mydata = mydata + '\t' # Remove the last tab so the cursor stays in the last cell. Can't # use rstrip because we only want to remove at most 1. if mydata.endswith('\t'): mydata = mydata[:-1] # Put the data in to the clipboard QtWidgets.QApplication.clipboard().setText(mydata) else: # This is the proper way to handle all other non-matching events QtWidgets.QTableView.keyPressEvent(self, event)
[docs] def setItemDelegate(self, delegate): """ Set the delegate for this table :type delegate: StructureDataViewerDelegate :param delegate: delegate that draws the data for this table """ self.delegate = delegate QtWidgets.QTableView.setItemDelegate(self, delegate)
[docs] def setDelegate(self, delegate): """ Set the delegate for this table :type delegate: StructureDataViewerDelegate :param delegate: delegate that draws the data for this table """ self.delegate = delegate
[docs] def rowResized(self, row, old_size, new_size, manual=False): """ Resizes any cells that need to be resized when a row size is changed :type row: int :param row: the row number that changed :type old_size: int :param old_size: the old size of the row :type new_size: int :param new_size: the new size of the row :type manual: bool :param manual: True if this a manual resize being called by a script. If True, this forces the method to execute even if AutoResizing has been set to false. """ if not self.auto_size and not manual: return if (not self.doing_row_resize and not self.doing_fix_resize): # Need to set the doing_resize flag to avoid recursive calls to this # routine, as it gets called any time a row size is changed by # the user or by this routine. self.doing_row_resize = True if self.isRowGanged(row): # We want to keep all rows the same size last_row = self.model().rowCount() - 1 # Find the last visible row, as we'll adjust the size of that # one slightly to fit things properly last_visible_row = 0 if new_size != 0: for myrow in range(last_row, 0, -1): if not self.isRowHidden(myrow): last_visible_row = myrow break for arow in range(last_row + 1): if arow != row and self.isRowGanged(row): if arow != last_visible_row: self.setRowHeight(arow, new_size) else: self.setRowHeight(arow, new_size + self.row_remainder) if self.lock_aspect_ratio: # Change the columns to have the same width as the row height self.resizeColumnsToContents() self.doing_row_resize = False if self.fit_view_rows: # Fix the viewport size to the total table height self.fitToRows()
[docs] def columnResized(self, column, old_size, new_size, manual=False): """ Resizes any cells that need to be resized when a column size is changed :type column: int :param column: the column number that changed :type old_size: int :param old_size: the old size of the column :type new_size: int :param new_size: the new size of the column :type manual: bool :param manual: True if this a manual resize being called by a script. If True, this forces the method to execute even if AutoResizing has been set to false. """ if not self.auto_size and not manual: return if not self.doing_col_resize and not self.doing_fix_resize: # Need to set the doing_resize flag to avoid recursive calls to this # routine, as it gets called any time a column size is changed by # the user or by this routine. self.doing_col_resize = True if self.isColumnGanged(column): # We want to keep all columns the same size last_column = self.model().columnCount() - 1 # Find the last visible column, as we'll adjust the size of that # one slightly to fit things properly last_visible_column = 0 # Make sure we aren't ganging all columns to be zero sized # isColumnHidden is not usually set here if new_size is zero if new_size != 0: for col in range(last_column, 0, -1): if not self.isColumnHidden(col): last_visible_column = col break for col in range(last_column + 1): if col != column and self.isColumnGanged(col): if col != last_visible_column: self.setColumnWidth(col, new_size) else: self.setColumnWidth( col, new_size + self.col_remainder) if self.lock_aspect_ratio: # Change the columns to have the same width as the column width self.resizeRowsToContents() if self.fit_view_columns: # Fix the viewport size to the total table width self.fitToColumns() # Delay reset of `self.doing_col_resize` to avoid recursive loops QtCore.QTimer.singleShot(0, self.reEnableColumnResize)
[docs] def reEnableColumnResize(self): self.doing_col_resize = False
[docs] def fitToRows(self): """ Fits the viewport to exactly show all the rows """ height = self.totalRowHeight() height = height + self.horizontalHeader().height() if self.horizontalScrollBar().isVisible(): height = height + self.horizontalScrollBar().height() self.setFixedHeight(height)
[docs] def fitToColumns(self): """ Fits the viewport to exactly show all the columns """ width = self.totalColumnWidth() width = width + self.verticalHeader().width() if self.verticalScrollBar().isVisible(): width = width + self.verticalScrollBar().width() self.setFixedWidth(width)
[docs] def totalRowHeight(self): """ The sum of the heights of all the rows :rtype: int :return: the sum of all the row heights """ height = 0 for row in range(self.model().rowCount()): height = height + self.rowHeight(row) return height
[docs] def averageGangedRowHeight(self): """ This routine has been changed to return the MOST COMMON non-zero height of all the ganged rows :rtype: int :return: the most common integer average row height, or the default height if there are no non-zero ganged rows """ heights = {} for row in range(self.model().rowCount()): if self.isRowGanged(row): height = self.rowHeight(row) if height > 0: heights[height] = heights.get(height, 0) + 1 most_common = self.default_cell_size.height() max_num = 0 for aheight, number in heights.items(): if number > max_num: max_num = number most_common = aheight return most_common
[docs] def totalColumnWidth(self): """ The sum of the widths of all the columns :rtype: int :return: the sum of all the column widths """ width = 0 for col in range(self.model().columnCount()): width = width + self.columnWidth(col) return width
[docs] def averageGangedColumnWidth(self): """ This routine has been changed to return the MOST COMMON non-zero width of all the ganged columns :rtype: int :return: the most common integer average column width, or the default width if there are no non-zero ganged columns """ widths = {} for column in range(self.model().columnCount()): if self.isColumnGanged(column): width = self.columnWidth(column) if width > 0: widths[width] = widths.get(width, 0) + 1 most_common = self.default_cell_size.width() max_num = 0 for awidth, number in widths.items(): if number > max_num: max_num = number most_common = awidth return most_common
[docs] def scrollBarChanged(self, orientation): """ A scrollbar has changed appearance, make sure we have the proper size """ self.fixSize()
[docs] def setAutoSizing(self, state): """ Turn the auto resizing of rows and column on or off. Normally this should be on (default), however, if multiple changes will be made and no resizing needs to be done until all changes are complete, then setting AutoSizing to False will save considerable time for large tables. Be sure to setAutoSizing(True) when finished so the table can adjust to the changes made. :type state: bool :param state: True if rows and columns should be resized automatically in response to table changes, False if not """ self.auto_size = state if state: self.fixSize()
[docs] def fixSize(self, manual=False): """ Resize everything as the user and program desires :type manual: bool :param manual: True if this a manual resize being called by a script. If True, this forces the method to execute even if AutoResizing has been set to false. """ if not self.auto_size and not manual: return self.doing_fix_resize = True # Make sure to do a resize the very first time the table shows self.resizeRowsToContents() self.resizeColumnsToContents() self.doing_fix_resize = False
[docs] def fitCellsToData(self, include_column_header=False): """ Resizes rows and columns to fit the data they contain using the sizeHint from the delegate :type include_column_header: bool :param include_column_header: True if the column header should be included when figuring the column width, False if not """ self.fitColumnsToData(include_header=include_column_header) self.fitRowsToData()
[docs] def fitRowsToData(self): """ Resizes all the rows to fit the data they contain using the sizeHint from the delegate. """ for row in range(self.model().rowCount()): self.fitRowToData(row)
[docs] def fitRowToData(self, row): """ Resizes row to the largest sizeHint of any of its cells. Uses the sizeHint of the delegate. :type row: int :param row: the row of interest """ hint = 0 if self.verticalHeader().isVisible(): buffer = 4 else: buffer = 0 options = qt_utils.get_view_item_options(self) for column in range(self.model().columnCount()): index = self.model().index(row, column) hint = max( hint, self.itemDelegate().sizeHint(options, index).height() + buffer) self.setRowHeight(row, hint)
[docs] def fitColumnsToData(self, include_header=False): """ Resizes all the columns to fit the data they contain using the sizeHint from the delegate. :type include_header: bool :param include_header: True if the header should be included in the calculation of column width, False if not """ for column in range(self.model().columnCount()): self.fitColumnToData(column, include_header=include_header)
[docs] def fitColumnToData(self, column, include_header=False): """ Resizes column to the largest sizeHint of any of its cells. Uses the sizeHint of the delegate. :type column: int :param column: the column of interest :type include_header: bool :param include_header: True if the header should be included in the calculation of column width, False if not """ hint = 0 options = qt_utils.get_view_item_options(self) for row in range(self.model().rowCount()): index = self.model().index(row, column) shint = self.itemDelegate().sizeHint(options, index, actual_column_width=True) hint = max(hint, shint.width()) if include_header: shint = self.horizontalHeader().sectionSizeHint(column) hint = max(hint, shint) self.setColumnWidth(column, hint)
[docs] def resizeRowsToContents(self): """ Resizes all the rows according to all the resize properties """ for row in range(self.model().rowCount()): self.setRowHeight(row, self.sizeHintForRow(row)) if self.fit_view_rows: # Fix the viewport size to the total row height self.fitToRows()
[docs] def resizeColumnsToContents(self): """ Resizes all the columns according to all the resize properties """ for col in range(self.model().columnCount()): self.setColumnWidth(col, self.sizeHintForColumn(col)) if self.fit_view_columns: # Fix the viewport size to the total row height self.fitToColumns()
[docs] def resizeEvent(self, event): """ Called when the viewport changes size. Changes the size of any cells that need to be changed in response to this - typically because fill has been set on rows or columns. :type event: QEvent :param event: the event that occured """ # Infinite loop detection and elimination. See comments in __init__ as # to what is going on here. if self.stop_loop: # A loop was detected last time through, stop it by not resizing self.stop_loop = False return if self.avoid_loops: # This is where we check to see if we are in a loop. Keep a history # of the scrollbar visibilities. If they are toggling on and off, # then we are in a loop self.hbar_states.insert(0, self.horizontalScrollBar().isVisible()) self.vbar_states.insert(0, self.verticalScrollBar().isVisible()) try: # Only save the important states so the lists don't grow large self.hbar_states = self.hbar_states[:self.toggle_length] self.vbar_states = self.vbar_states[:self.toggle_length] except IndexError: # Happens if the lists are small enough already pass if (self.hbar_states == self.loop_sign or self.vbar_states == self.loop_sign): # Found a loop, reset the history and set the stop loop flag. self.hbar_states = [] self.vbar_states = [] self.stop_loop = True if event.type() == 14 and event.oldSize() == QtCore.QSize(-1, -1): # Make sure to do a resize the very first time the table shows self.fixSize() QtWidgets.QTableView.resizeEvent(self, event) num_cols = self.model().columnCount() num_rows = self.model().rowCount() if num_cols <= 0: return # Fit the table into the viewport if self.fill_columns: # Calculate the column height needed to perfectly fill the viewport width = self.sizeHintForColumn(0) if self.gang_columns: # Only set the first column, since that resize will trigger all # the other columns. self.setColumnWidth(0, width) # Also have to set the last column, in case the sizeHint hasn't # changed but self.col_remainder has. self.setColumnWidth(num_cols - 1, width + self.col_remainder) else: # Set all the columns to this width for col in range(num_cols - 1): self.setColumnWidth(col, width) self.setColumnWidth(num_cols - 1, width + self.col_remainder) # Change the row heights if need be if self.lock_aspect_ratio: self.resizeRowsToContents() if self.fill_rows: # Calculate the row height needed to perfectly fill the viewport height = self.sizeHintForRow(0) # Set all the rows to this height if self.gang_rows: # Only set the first row, since that resize will trigger all # the other rows to resize. self.setRowHeight(0, height) # Also have to set the last row, in case the sizeHint hasn't # changed but self.row_remainder has. self.setRowHeight(num_rows - 1, height + self.row_remainder) else: for row in range(num_rows - 1): self.setRowHeight(row, height) self.setRowHeight(num_rows - 1, height + self.row_remainder) # Change the column widths if need be if self.lock_aspect_ratio: self.resizeColumnsToContents()
[docs] def sizeHintForColumn(self, column): """ This method gives the size hints for columns - its existance means that the resizeColumnsToContents() method uses this hint rather than hints from the delegate. :type column: int :param column: the column we want a size hint for :rtype: int :return: the size hint for column """ if self.lock_aspect_ratio and not self.fill_columns and \ self.isColumnGanged(column): hint = self.averageGangedRowHeight() * self._default_ratio elif self.fill_columns: table_width = self.viewport().width() hint, self.col_remainder = divmod(table_width, self.model().columnCount()) if column == self.model().columnCount() - 1: hint = hint + self.col_remainder else: # To use the delegate size hint for the cell, call delegate.sizeHint # here (and implement the sizeHint method for the delegate). hint = self.columnWidth(column) return hint
[docs] def sizeHintForRow(self, row): """ This method gives the size hints for rows - its existance means that the resizeRowsToContents() method uses this hint rather than hints from the delegate. :type row: int :param row: the row we want a size hint for :rtype: int :return: the size hint for row """ if self.lock_aspect_ratio and not self.fill_rows and \ self.isRowGanged(row): hint = old_div(self.averageGangedColumnWidth(), self._default_ratio) elif self.fill_rows: table_height = self.viewport().height() hint, self.row_remainder = divmod(table_height, self.model().rowCount()) if row == self.model().rowCount() - 1: hint = hint + self.row_remainder else: # To use the delegate size hint for the cell, call delegate.sizeHint # here (and implement the sizeHint method for the delegate). hint = self.rowHeight(row) return hint
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] def setToolTipSize(self, width, height): """ :param width: width in pixels of tooltip :type width: int :param height: height in pixels of tooltip :type height: int """ self.tool_tip_size = (width, height)
[docs]class ViewerModel(QtCore.QAbstractTableModel): sizeChanged = QtCore.pyqtSignal()
[docs] def __init__(self, rowcount=0, columncount=4, unique=False): QtCore.QAbstractTableModel.__init__(self) self._row_count = rowcount self._column_count = columncount self._structs = [] self._unique = unique if self._unique: self._structSmiles = set() self._smiles_gen = \ smiles_mod.SmilesGenerator( \ stereo=smiles_mod.STEREO_FROM_ANNOTATION_AND_GEOM)
[docs] def structCount(self): return len(self._structs)
[docs] def appendStruct(self, struct): if self._unique: if not self._addSmiles(struct): return False self._structs.append(struct) self.sizeChanged.emit() return True
def _addSmiles(self, struct): """ If structure already exists, then the smiles value is not added. :return: Return whether the structure was successfully added or not added if the structure already exists in model. :rtype: bool """ smiles = self._smiles_gen.getSmiles(struct) if smiles in self._structSmiles: return False self._structSmiles.add(smiles) return True
[docs] def clearStructs(self): self._structs = [] if self._unique: self._structSmiles = set() self.sizeChanged.emit()
[docs] def getStruct(self, index): location = index.row() * self.columnCount() + index.column() if location >= self.structCount(): return None return self._structs[location]
[docs] def setStruct(self, index, struct): """ Given the index, replace the structure with the new value. If the _unique flag is true and the new value already exists in table, it is not added. :return: Return whether the new structure was successfully set at the specified index. :rtype: bool """ location = index.row() * self.columnCount() + index.column() if location >= self.structCount(): return False if self._unique: if not self._addSmiles(struct): return False self._removeSmiles(location) self._structs[location] = struct return True
[docs] def removeStruct(self, row, column): location = row * self.columnCount() + column if location >= self.structCount(): return None if self._unique: self._removeSmiles(location) del self._structs[location] self.sizeChanged.emit()
def _removeSmiles(self, location): """ Remove smiles value given the location. :param location: the index in self._structs, which corresponds to a structure, to be removed. :type location: int """ smiles = self._smiles_gen.getSmiles(self._structs[location]) self._structSmiles.remove(smiles)
[docs] def rowCount(self, parent=QtCore.QModelIndex()): # noqa: M511 return self._row_count
[docs] def columnCount(self, parent=QtCore.QModelIndex()): # noqa: M511 return self._column_count
[docs] def data(self, index, role=QtCore.Qt.DisplayRole): return
[docs] def insertRows(self, row, count, parent=QtCore.QModelIndex()): # noqa: M511 self.beginInsertRows(parent, row, row + count - 1) self._row_count += count self.endInsertRows() return True
[docs] def insertColumns(self, row, count, parent=QtCore.QModelIndex()): # noqa: M511 self.beginInsertColumns(parent, row, row + count - 1) self._column_count += count self.endInsertColumns() return True
[docs] def removeRows(self, row, count, parent=QtCore.QModelIndex()): # noqa: M511 self.beginRemoveRows(parent, row, row + count - 1) self._row_count -= count self.endRemoveRows() return True
[docs] def removeColumns(self, row, count, parent=QtCore.QModelIndex()): # noqa: M511 self.beginRemoveColumns(parent, row, row + count - 1) self._column_count -= count self.endRemoveColumns() return True
[docs] def resize(self, row, column): self.beginResetModel() if self.rowCount() > row: self.removeRows(0, self.rowCount() - row) if self.columnCount() > column: self.removeColumns(0, self.columnCount() - column) if self.rowCount() < row: self.insertRows(0, row - self.rowCount()) if self.columnCount() < column: self.insertColumns(0, column - self.columnCount()) self.endResetModel()
[docs] def resizeRows(self, row): self.resize(row, self.columnCount())
[docs] def resizeColumns(self, cols): rows = int(old_div(self.structCount(), cols)) if rows * cols < self.structCount(): rows += 1 self.resize(rows, cols)
[docs] def setTable(self, view): pass
[docs] def reset(self): """ PANEL-8852: Method added to prevent QtUpgradeError. This preserves backwards compatibility with QtCore.QAbstractTableModel.reset() but does not reset anything. """ self.beginResetModel() self.endResetModel()
[docs]class StructureDataViewerModel(ViewerModel): """ A table model that handles 2-D structures and data. This inherits ViewerModel, but is designed to be less 2-D structure-centric than that model, which assumes all the cells contain structures. """ # setHeaderData emits headerDataChanged without any arguments headerDataChanged = QtCore.pyqtSignal([Qt.Orientation, int, int], [])
[docs] def __init__(self, rows=4, columns=8, vlabels=None, hlabels=None): """ :type rows: int :param rows: number of rows in the table :type columns: int :param columns: number of columns in the table :type vlabels: list of strings :param vlabels: labels for the row headers :type hlabels: list of strings :param hlabels: labels for the column headers """ QtCore.QAbstractTableModel.__init__(self) self._row_count = rows self._column_count = columns self.columnSortOrderIndicator = [] self._v_headers = [] self._h_headers = [] self.setSize(rows, columns) if vlabels: # self._v_headers is initialize to numbers by the setSize call self._v_headers = vlabels if hlabels: self._h_headers = hlabels else: # self._v_headers is not initialized to numbers by the setSize call self._h_headers = [x + 1 for x in range(self._column_count)] for col in range(self._column_count): self.columnSortOrderIndicator.append(None) self._unique = False self.cur_id = 0 self.sort_v_headers = False
[docs] def setTable(self, table): """ :type table: DataViewerTable :param table: the table this model contains data for :deprecated table: This function should not be used, because a model can refer to multiple views. """ self.table = table
[docs] def setDelegate(self, delegate): """ Sets the Delegate that draws the data from this model. :type delegate: StructureDataViewerDelegate :param delegate: delegate that draws the data from this model :deprecated delegate: This function should not be used, because a model can have many delegates """ self.delegate = delegate
[docs] def myDataChanged(self, row1, column1, row2, column2): """ Emits the proper signal to indicate that data in the table has changed. Rows and columns are 0-indexed :type row1: non-negative int :param row1: row index of the top, left-most cell that changed :type row2: non-negative int :param row2: row index of the bottom, right-most cell that changed :type column1: non-negative int :param column1: column index of the top, left-most cell that changed :type column2: non-negative int :param column2: column index of the bottom, right-most cell changed """
[docs] def initializeValueMatrix(self, reset_row_labels=True, reset_column_labels=False): """ Resets all the cells to values of None and resets the row labels (by default) :type reset_row_labels: bool :param reset_row_labels: if True, all row labels are reset to simple numbers, if False, they are not. This is True by default. :type reset_column_labels: bool :param reset_column_labels: if True, all column labels are reset to simple numbers, if False, they are not. This is False by default. """ self.beginResetModel() self.values = [] for row in range(self._row_count): self.values.append([]) for col in range(self._column_count): self.values[-1].append(None) if reset_row_labels: self._v_headers = [x + 1 for x in range(self._row_count)] if reset_column_labels: self._h_headers = [x + 1 for x in range(self._column_count)] self.endResetModel()
[docs] def setToolTip(self, row, column, value): """ Sets the tooltip for the cell located at row, column :type row: int :param row: row the cell is in (0-indexed) :type column: int :param column: column the cell is in (0-indexed) :type value: type that can be set in a tooltip :param value: value for the tooltip in the cell at (row, column) """
[docs] def insertColumn(self, column, index=None, renumber_headers=False): """ Insert a column into the model and table :type column: int :param column: the column index to insert a new column at :type index: QModelIndex() :param index: unused :type renumber_headers: bool :param renumber_headers: True if all the column headers should be renumbered, false if not (leave as default if custom column headers have been used) """ if index is None: index = QtCore.QModelIndex() self.insertColumns(column, 1, index=index, renumber_headers=renumber_headers)
[docs] def insertColumns(self, place, columns, index=None, renumber_headers=False): """ Insert columns into the model and table at index place :type place: int :param place: the index to insert the columns at :type columns: int :param columns: the number of columns to insert :type index: QModelIndex() :param index: unused :type renumber_headers: bool :param renumber_headers: True if all the column headers should be renumbered, false if not (leave as default if custom column headers have been used) """ if index is None: index = QtCore.QModelIndex() self.beginInsertColumns(index.parent(), place, place + columns - 1) self._column_count += columns for row in self.values: for ind in range(columns): row.insert(place, None) for col in range(columns - 1, -1, -1): self._h_headers.insert(place, None) self.columnSortOrderIndicator.insert(place, None) if renumber_headers: self._h_headers = [x for x in range(1, self.columnCount() + 1)] self.endInsertColumns()
[docs] def removeColumn(self, column, index=None, renumber_headers=False): """ Remove a column from the model and table :type column: int :param column: the column index to remove :type index: QModelIndex() :param index: unused """ if index is None: index = QtCore.QModelIndex() self.removeColumns(column, 1, index=index, renumber_headers=renumber_headers)
[docs] def removeColumns(self, place, columns, index=None, renumber_headers=False): """ Remove columns from the model and table starting at index place :type place: int :param place: the index to begin removing columns at :type columns: int :param columns: the number of columns to remove :type index: QModelIndex() :param index: unused """ if index is None: index = QtCore.QModelIndex() self.beginRemoveColumns(index.parent(), place, place + columns - 1) self._column_count -= columns for row in self.values: for ind in range(columns): del row[place] if renumber_headers: self._h_headers = [x for x in range(1, self.columnCount() + 1)] self.endRemoveColumns()
[docs] def insertRow(self, row, index=None, renumber_headers=False): """ Insert a row into the model and table :type row: int :param row: the row index to insert a new row at :type index: QModelIndex() :param index: unused :type renumber_headers: bool :param renumber_headers: True if all the row headers should be renumbered, false if not (leave as default if custom row headers have been used) """ if index is None: index = QtCore.QModelIndex() self.insertRows(row, 1, index=index, renumber_headers=renumber_headers)
[docs] def insertRows(self, place, rows, index=None, renumber_headers=False): """ Insert rows into the model and table at index place :type place: int :param place: the index to insert the rows at :type rows: int :param rows: the number of rows to insert :type index: QModelIndex() :param index: unused :type renumber_headers: bool :param renumber_headers: True if all the row headers should be renumbered, false if not (leave as default if custom row headers have been used) """ if index is None: index = QtCore.QModelIndex() self.beginInsertRows(index.parent(), place, place + rows - 1) self._row_count += rows try: blank_row = [None] * self.columnCount() except IndexError: blank_row = [] for row in range(rows - 1, -1, -1): self.values.insert(place, blank_row[:]) if renumber_headers: self._v_headers = [x for x in range(1, self.rowCount() + 1)] self.endInsertRows()
[docs] def removeRow(self, row, index=None, renumber_headers=False): """ Remove a row from the model and table :type row: int :param row: the row index to remove :type index: QModelIndex() :param index: unused """ if index is None: index = QtCore.QModelIndex() self.removeRows(row, 1, index=index, renumber_headers=renumber_headers)
[docs] def removeRows(self, place, rows, index=None, renumber_headers=False): """ Remove rows from the model and table starting at index place :type place: int :param place: the index to begin removing rows at :type rows: int :param rows: the number of rows to remove :type index: QModelIndex() :param index: unused """ if index is None: index = QtCore.QModelIndex() # The third argument to beginRemoveRows should be the last row being # removed. Using place+rows-1 accomplishes this. self.beginRemoveRows(index.parent(), place, place + rows - 1) self._row_count -= rows for row in range(rows): del self.values[place] if renumber_headers: self._v_headers = [x for x in range(1, self.rowCount() + 1)] self.endRemoveRows()
[docs] def sortKey(self, myitem): """ Returns the key to sort item by :type myitem: model item :param myitem: base class expects an iterable with the sort data of interest as the second item :return: The second item in myitem, or `NO_ITEM` if myitem does not have at least 2 items. """ try: if self.sort_v_headers: return myitem[0][self.sort_column][1] else: return myitem[self.sort_column][1] except (TypeError, IndexError): return NO_ITEM
[docs] def sortKeyUnsorted(self, myitem): """ Returns the key to sort item by. In this case, it returns the cur_id of the data, allowing the sort to acheive the original data order. :type myitem: model item :param myitem: base class expects an iterable with the sort data of interest as the second item :return: The first item in myitem, or `NO_ITEM` if myitem does not have at least 2 items. """ try: if self.sort_v_headers: return myitem[0][self.sort_column][0] else: return myitem[self.sort_column][0] except (TypeError, IndexError): return NO_ITEM
[docs] def setVerticalHeadersSortable(self, state): """ Sets vertical header labels to be sortable :type state: bool :param state: True if vertical headers should move with a column sort, or False if they should not (False for simple row numbers, for example) """ self.sort_v_headers = state
[docs] def sort(self, column, order=QtCore.Qt.AscendingOrder): """ This functions exists so that we can transition away from sorting directly in the model. If you are writing a new table, this function should in called a QSortFilterProxyModel, not in the source model. """ self.sortByColumn(column, order)
[docs] def sortByColumn(self, column, order=QtCore.Qt.AscendingOrder): """ Sort by data by column :type column: int :param column: the index of the column to sort by :type order: Qt.SortOrder :param order: Qt.AscendingOrder (0), Qt.DescendingOrder (1) or -1 for returning to original data order """ self.sort_column = column self.layoutAboutToBeChanged.emit() if self.sort_v_headers: self.values = [ (x, y) for x, y in itertools.zip_longest(self.values, self._v_headers) ] if order == QtCore.Qt.AscendingOrder: self.values.sort(key=self.sortKey) elif order == QtCore.Qt.DescendingOrder: self.values.sort(key=self.sortKey, reverse=True) else: self.values.sort(key=self.sortKeyUnsorted) if self.sort_v_headers: self._v_headers = [x[1] for x in self.values] self.values = [x[0] for x in self.values] self.layoutChanged.emit()
[docs] def sortHeaderKey(self, myitem): """ Returns the key to sort item by, which in this case is the Header :type myitem: model item :param myitem: base class expects an iterable with the sort data of interest as the second item :return: The second item in myitem, or `NO_ITEM` if myitem does not have at least 2 items. """ try: return myitem[1] except (TypeError, IndexError): return NO_ITEM
[docs] def sortHeaderKeyUnsorted(self, myitem): """ Returns the key to sort item by. In this case, it returns the cur_id of the data, allowing the sort to acheive the original data order. :type myitem: model item :param myitem: base class expects an iterable with the sort data of interest as the second item :return: The second item in myitem, or `NO_ITEM` if myitem does not have at least 2 items. """ try: return myitem[0][0] except (TypeError, IndexError): return NO_ITEM
[docs] def sortByVerticalHeader(self, order=QtCore.Qt.AscendingOrder): """ Sort by data by the Vertical Header values :type order: Qt.SortOrder :param order: Qt.AscendingOrder (0) or Qt.DescendingOrder (1) """ self.layoutAboutToBeChanged.emit() self.values = [(x, y) for x, y in zip(self.values, self._v_headers)] if order == QtCore.Qt.AscendingOrder: self.values.sort(key=self.sortHeaderKey) elif order == QtCore.Qt.DescendingOrder: self.values.sort(key=self.sortHeaderKey, reverse=True) else: self.values.sort(key=self.sortHeaderKeyUnsorted) self._v_headers = [x[1] for x in self.values] self.values = [x[0] for x in self.values] self.layoutChanged.emit()
[docs] def setCellValue(self, row, column, value, timer=None): """ Sets the data for the cell located at row, column :type row: int :param row: row the cell is in (0-indexed) :type column: int :param column: column the cell is in (0-indexed) :param value: value to store in the cell at (row, column) value can be either a structure or numerical/string data :type timer: bool :deprecated timer: This variable is no longer necessary. Formerly, we would perform a full scale update with 300ms timer. This caused an unnecessary race condition when updating cells. Now we implement the dataChanged signal for individual modelindexes, which should not have a performance penalty. """ if timer is not None: warnings.warn("timer option is deprecated and no longer needed", DeprecationWarning) self.cur_id = self.cur_id + 1 self.values[row][column] = (self.cur_id, value) index = self.index(row, column) self.dataChanged.emit(index, index)
[docs] def setHeaderData(self, section, orientation, label, role=QtCore.Qt.DisplayRole): """ Changes the label of the row or column for the table to draw :type section: int :param section: the index of the row or column header of interest :type orientation: int :param orientation: QtCore.Qt.Horizontal (columns) or QtCore.Qt.Vertical (rows) :type label: string :param label: text for the label :type role: int :param role: should be 0 for the actual label text, unused - this method only sets the label text """ def set_header(section, label, header_list): try: header_list[section] = label except IndexError: header_list.extend([None] * (section - len(header_list) + 1)) header_list[section] = label if role == QtCore.Qt.DisplayRole: if orientation == QtCore.Qt.Horizontal: set_header(section, label, self._h_headers) elif orientation == QtCore.Qt.Vertical: set_header(section, label, self._v_headers) else: ViewerModel.setHeaderData(self, section, orientation, label, role) self.headerDataChanged[tuple()].emit() return True
[docs] def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): """ Returns the label of the row or column for the table to draw :type section: int :param section: the index of the row or column header of interest :type orientation: int :param orientation: 0 = horizontal (columns), 1 = vertical (rows) :type role: int :param role: should be 0 for the actual label text """ try: if self._h_headers and orientation == QtCore.Qt.Horizontal \ and role == QtCore.Qt.DisplayRole: return self._h_headers[section] elif self._v_headers and orientation == QtCore.Qt.Vertical \ and role == QtCore.Qt.DisplayRole: return self._v_headers[section] except IndexError: return str(section + 1)
[docs] def getCellValue(self, row, column): """ Returns the value of the cell in the cell at row, column :type row: int :param row: the row of the cell of interest (0-indexed) :type column: int :param row: the column of the cell of interest (0-indexed) :rtype: Any :return: data or object stored in the model at row, column """ try: return self.values[row][column][1] except (IndexError, TypeError): return None
[docs] def setSize(self, numrows, numcolumns, reset_row_labels=True, reset_column_labels=False): """ Sets the size of the table :type numrows: int :param numrows: the number of rows the table should have :type numcolumns: int :param numcolumns: the number of columns the table should have :type reset_row_labels: bool :param reset_row_labels: if True, all row labels are reset to simple numbers, if False, they are not :type reset_column_labels: bool :param reset_column_labels: if True, all column labels are reset to simple numbers, if False, they are not """ self.beginResetModel() self.resize(numrows, numcolumns) self.initializeValueMatrix(reset_row_labels=reset_row_labels, reset_column_labels=reset_column_labels) self.endResetModel()
[docs] def data(self, index, role=QtCore.Qt.DisplayRole): """ Returns the data (either Display or ToolTip) if requested. The tooltips for structures are a different class, and handled manually by the StructureToolTip class. :type index: QModelIndex :param index: index of the cell for which data is requested :type role: Qt.ItemDataRole :param role: role of the data requested. Currently only responds to Qt.ToolTipRole """ # This routine gets called for many types of data, we only react to # tooltip requests row = index.row() column = index.column() value = self.getCellValue(row, column) if role == QtCore.Qt.DisplayRole: return value elif role == QtCore.Qt.ToolTipRole: return value
[docs] def clearTable(self): """ Resets all the data in the table to None """ self.beginResetModel() self.initializeValueMatrix() self.endResetModel()
[docs]class GenericViewerDelegate(QtWidgets.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. """
[docs] def __init__(self, tableview, tablemodel=None): """ :type tableview: ViewerTable :param tableview: Table this delegate paints to :type tablemodel: StructureDataViewerModel :param tablemodel: DEPRECATED Model containing the data this delegate paints This parameter should not be used, because a delegate should only get information from a QModelIndex, not a model directly """ QtWidgets.QItemDelegate.__init__(self) self.table = tableview if tablemodel: self.model = tablemodel self._paint_wait = False self.qpolygon = QtGui.QPolygon(4) #Used to draw hourglass self.table.setDelegate(self) self.table.setItemDelegate(self)
[docs] def paint(self, painter, option, index): """ This handles the logic behind painting/not-painting when the scrollbar is being dragged. """ painter.save() if option.state & QtWidgets.QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) painter.setBrush(QtGui.QColor(0, 0, 0)) if self.table.isScrolling() and self.paintWait(): self._paint_passive(painter, option, index) else: self._paint(painter, option, index) painter.restore()
[docs] 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
[docs] def paintWait(self): """ Returns if we are drawing during while the scrollbar is dragged.""" return self._paint_wait
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, QtCore.QPoint(centerx - 5, centery - 10)) self.qpolygon.setPoint(1, QtCore.QPoint(centerx + 5, centery - 10)) self.qpolygon.setPoint(2, QtCore.QPoint(centerx - 5, centery + 10)) self.qpolygon.setPoint(3, QtCore.QPoint(centerx + 5, centery + 10)) painter.drawPolygon(self.qpolygon) if not self.table.draw_timer.isActive(): self.table.draw_timer.start(TIMEOUT)
[docs]class StructureDataViewerDelegate(GenericViewerDelegate): """ A table that can display both 2-D structures and data. This Delegate should be used with the StructureDataViewerModel Much of this class is built off of the StructureViewerDelegate class. Structures are cached for quicker drawing of the table while keeping memory use low. Data is not cached. """
[docs] def __init__(self, tableview, tablemodel=None, structure_class=None, max_scale=0.5, elide=QtCore.Qt.ElideRight, alignment=None): """ Intialize the StructreDataViewerDelegate :type tableview: ViewerTable :param tableview: Table this delegate paints to :type tablemodel: StructureDataViewerModel :param tablemodel: DEPRECATED Model containing the data this delegate paints This parameter should not be used, because a delegate should only get information from a QModelIndex, not a model directly :type structure_class: class :keyword structure_class: class (or superclass) of the objects that should be displayed in the table as 2D pictures - usually schrodinger.structure.Structure (the default) :type max_scale: float :param max_scale: restricts the maximum scale-up of 2D structure images so that very small molecules don't look so large. :type elide: int :param elide: Determines where '...' should occur in strings that are too large to fit in the table cell. Common values are Qt.ElideLeft, Qt.ElideRight, Qt.ElideMiddle, Qt.ElideNone :type alignment: Qt.Alignment object :param alignment: The alignment of text in the cells. To put a structure in a cell, store a structure_class object in that cell of the model. To put any other type of data in a cell, store that data in that cell of the model. """ GenericViewerDelegate.__init__(self, tableview, tablemodel=tablemodel) self.picture_cache = _CacheClass() self.generate_one_structure = False self.max_scale = max_scale if not structure_class: self.structure_class = struct.Structure else: self.structure_class = structure_class if tablemodel: tablemodel.setDelegate(self) self.elide = elide if not alignment: # Align text to the left of the cell and center vertically self.alignment = QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter else: self.alignment = alignment self.adaptor = canvas2d.ChmMmctAdaptor() self.model2d = canvas2d.ChmRender2DModel() self.renderer = canvas2d.Chm2DRenderer(self.model2d)
def _paint(self, painter, option, index, passive=False): """ Generates the object that should be painted on the cell. If the cell contains a structure, it attempts to retrieve this structure from the structure cache. If unsuccessful, it either generates a new structure or paints an hourglass, depending on the value of passive. Once a structure is obtained, it is passed on to the paintCell function. If the cell contains data, it just passes the data on to the paintCell function. :type painter: QtGui.QPainter object :param option: Appears to be the part of the gui that contains the cell :param index: Index of the cell :param passive: whether to generate a new structure or not if one is not found in the cache. """ struct = index.data(QtCore.Qt.DisplayRole) if struct is None: #Skip painting if there's no structure for this cell return elif self.isStructure(struct): # This cell contains a 2D chemical structure, try and grab it from # the cache. pic = self.picture_cache.get(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: # We need to plot a structure, and no structure is # available, generate one and store it in the cache pic = self.generatePicture(struct) self.picture_cache.store(struct, pic) self.paintStructure(painter, option, pic, index) else: self.paintStructure(painter, option, pic, index) else: self.paintCell(painter, option, struct)
[docs] def sizeHint(self, option, index, actual_column_width=False, actual_size=False, actual_row_height=False): """ Returns the sizeHint for the data in the cell at index. If the data is a structure, returns the default cell size for the table. :type option: QStyleOptionViewItem :param option: the style options for this item :type index: QModelIndex :param index: The index of the cell being examimed :type actual_size: bool :param actual_column_width: True if the column widths and row heights should be calculated from the width of the data as a string. Normally, sizeHint seems to return 0 for the width of text and an acceptable constant for the height, so actual_column_width is the better option. :type actual_row_height: bool :param actual_column_width: True if the row heights should be calculated from the height of the data as a string. Normally, sizeHint returns an acceptable constant for the row height, so this is not usually needed. :type actual_column_width: bool :param actual_column_width: True if the column widths should be calculated from the width of the data as a string. Normally, sizeHint seems to return 0 for the width of text. """ data = index.data(QtCore.Qt.DisplayRole) if self.isStructure(data): return self.table.default_cell_size else: hint = GenericViewerDelegate.sizeHint(self, option, index) if actual_column_width or actual_row_height or actual_size: qdata = str(data) metrics = self.table.fontMetrics() if actual_column_width or actual_size: # The generic form of this routine always returns 0 for the # width of text width = metrics.horizontalAdvance(qdata) hint.setWidth(width + 20) if actual_row_height or actual_size: # The generic form of this routine generally returns a correct # answer, so this is typically not needed. height = metrics.height() hint.setHeight(height) return hint
def _paint_passive(self, painter, option, index): self._paint(painter, option, index, passive=True)
[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 isStructure(self, object): """ Returns true if object is the type given as a structure """ return self.structure_class and \ isinstance(object, self.structure_class)
[docs] def generatePicture(self, struct): """ Generates a 2D chemical structure of the object If object is a chemical structure, returns the picture is a 2D rendering of that structure; None otherwise. :type struct: Object of type StructureDataViewerDelegate.structure_class :param struct: The structure object :rtype: QPicture or None :return: QPicture of struct or None """ if struct: settings = sketcher.RendererSettings() pic = sketcher.Renderer.pictureFromStructure( struct.handle, settings) # Will return an empty QPicture on failure return pic else: return None
[docs] def paintStructure(self, painter, option, pic, index=None): """ Draws the given QPicture (typically a 2D structure image) into the specified painter. Called from the paint() method of the delegate. Adds a bit of padding all around the cell, then passes the data on to the proper drawing routine. :type painter: QtGui.QPainter object :param option: Appears to be the part of the gui that contains the cell :type pic: QPicture :param pic: the picture to be painted :param index: Model index for the cell which is being painted. Not used in default implementation, but may be used in subclass implementations. :type index: QModelIndex """ 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()) swidgets.draw_picture_into_rect(painter, pic, r, max_scale=self.max_scale)
[docs] def paintCell(self, painter, option, data): """ Adds a bit of padding all around the cell, then passes the data on to the proper drawing routine. :type painter: QtGui.QPainter object :param option: Appears to be the part of the gui that contains the cell :type data: type convertable to string :param data: the data to be painted into the cell as a string """ 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()) if option.state & QtWidgets.QStyle.State_Selected: selected_brush = option.palette.highlightedText() else: selected_brush = False self.drawDataIntoRect(painter, data, r, selected_brush)
[docs] def drawDataIntoRect(self, painter, data, rect, selected_brush=False): """ Draws string data onto a cell :type painter: QtGui.QPainter :param painter: The painter that is drawing the table :type data: string, int, or float :param data: the information to be drawn on the table :type rect: QtCore.Qrect :param rect: The rectangle that defines the current cell of the table """ fm = self.table.fontMetrics() qdata = fm.elidedText(str(data), self.elide, rect.width()) # Get the bounding box of this text if it was drawn in rect text_rect = painter.boundingRect(rect, self.alignment, qdata) # We don't want to draw the text past the cell boundaries. # rect = the cell rectangle, text_rect = the data rectangle # Start at the left side of the cell, unless the data is smaller text_rect.setLeft(max(rect.left(), text_rect.left())) # Start at the top of the cell, unless the data is shorter text_rect.setTop(max(rect.top(), text_rect.top())) # If the data fits lengthwise in the cell, paint the whole thing, # otherwise paint only as much as fits in the cell. text_rect.setWidth(min(rect.width(), text_rect.width())) # If the data fits heightwise in the cell, paint the whole thing, # otherwise paint only as much as fits in the cell. text_rect.setHeight(min(rect.height(), text_rect.height())) if selected_brush: painter.setBrush(selected_brush) else: painter.setBrush(QtGui.QBrush(QtCore.Qt.black, QtCore.Qt.NoBrush)) painter.drawText(text_rect, self.alignment, qdata)
[docs]class ToolTipFilter(QtCore.QObject):
[docs] def __init__(self, view): self.view = view self.structure_tip = structure2d.StructureToolTip() self.last_tooltip = (-1, -1) super(ToolTipFilter, self).__init__()
[docs] def eventFilter(self, obj, event): if event.type() == QtCore.QEvent.ToolTip: index = self.view.indexAt(event.pos()) delegate = self.view.itemDelegate(index) value = index.data(QtCore.Qt.ToolTipRole) if hasattr(delegate, "isStructure") and delegate.isStructure(value): # Data in this cell is a structure, make our manual tooltip. # But only if the tooltip isn't already shown for this cell QtWidgets.QToolTip.hideText() row = index.row() column = index.column() if self.last_tooltip != (row, column): # We create a new StructureToolTip each time to avoid some # issues that can crop up with re-using them. self.structure_tip = structure2d.StructureToolTip() pic = delegate.generatePicture(value) self.structure_tip.show(pic=pic) self.last_tooltip = (row, column) return True else: self.structure_tip.hide() self.last_tooltip = (-1, -1) return QtCore.QObject.eventFilter(self, obj, event) else: if event.type() == QtCore.QEvent.Leave: self.structure_tip.hide() self.last_tooltip = (-1, -1) return QtCore.QObject.eventFilter(self, obj, event)
class _CacheNode: def __init__(self, reference): self.reference = reference self.next = None class _CacheClass: """ This class caches the last few QPictures given to it. It is designed to cache visible cells in the table. """ def __init__(self): self.first = None self.last = None self.count = 0 self.max_sts = 500 self.lookup_dict = {} def clear(self): self.first = None self.last = None self.count = 0 self.max_sts = 500 self.lookup_dict = {} def store(self, reference, picture): if self.count == self.max_sts: # Discard the first node: node = self.first self.first = node.next self.count -= 1 del self.lookup_dict[node.reference] # Create a node: node = _CacheNode(reference) if self.first: # if contains nodes already self.last.next = node else: # this is first node self.first = node # Point self.last to the new node self.last = node self.count += 1 self.lookup_dict[reference] = picture def get(self, reference): try: return self.lookup_dict[reference] except KeyError: return None def delete(self, reference): current = self.first if current is None: return if current.reference == reference: # Discard the first node: self.first = current.next self.count -= 1 del self.lookup_dict[current.reference] return if current.next is None: return while current.next.reference != reference: current = current.next if current.next is None: return delnode = current.next if self.last == delnode: self.last == current else: current.next = delnode.next self.count -= 1 del self.lookup_dict[delnode.reference]
[docs]class SEasySDTable(DataViewerTable): """ A simple class that automatically sets up the table/model/delegate trio for a table object """
[docs] def __init__(self, **table_args): """ Creates an SEasySDTable class object. __init__ Keywords for the model, delegate and table objects can be passed in and will be passed on as appropriate. By default, this will set up a sortable table using a proxymodel. The sourcemodel should be used when need to add/remove data. The proxymodel should be used when you need to reference data from the view. """ def extract_kws(kw_list, kw_dict): """ Pull any keywords in kw_list out of the table_args dictionary and put them into the given kw_dict """ for kw in kw_list: if kw in table_args: kw_dict[kw] = table_args[kw] del table_args[kw] # Extract the keyword arguments for the model model_args = {} model_keywords = ['rows', 'columns', 'vlabels', 'hlabels'] extract_kws(model_keywords, model_args) # Extract the keyword arguments for the delegate delegate_args = {} delegate_keywords = [ 'structure_class', 'max_scale', 'elide', 'alignment' ] extract_kws(delegate_keywords, delegate_args) # Create the model/table/delegate self.sourcemodel = StructureDataViewerModel(**model_args) self.proxymodel = QtCore.QSortFilterProxyModel() # maintain Qt4 dynamicSortFilter default self.proxymodel.setDynamicSortFilter(False) self.proxymodel.setSourceModel(self.sourcemodel) #Initially set the sourcemodel with the DataViewerTable object, #because the signals for columnsInserted(), rowsInserted() need #to be connected to the sourcemodel DataViewerTable.__init__(self, self.sourcemodel, **table_args) self.setSortingEnabled(True) #After initialization, all model information should go through the #proxymodel self.setModel(self.proxymodel) self.delegate = StructureDataViewerDelegate(self, **delegate_args) self.setItemDelegate(self.delegate)
[docs]class VStackedDataViewerTable(DataViewerTable):
[docs] def __init__(self, model, model2, scrollbar_at_bottom=True, bottom_table=None, **kwarg): """ Two DataViewerTables stacked on top of each other. They share a single horizontal header and a single scrollbar. The object returned when this class is created is the upper table. The lower table is accessed via VStackedDataViewerTable.table2. The VStackedDataViewerTable has a layout property .mylayout which is what should be added to the GUI to properly place this widget. :type model: QAbstractTableModel :param model: The model for the upper table. The typical class to use with this table is the StructureDataViewerModel class. :type model2: QAbstractTableModel :param model: The model for the lower table. The typical class to use with this table is the StructureDataViewerModel class. :type bottom_table: VSubDataViewerTable :param model: The class for the lower table. If not supplied, a VSubDataViewerTable object will be used. Keyword arguments are documented in the DataViewerTable class """ # Set up the table/model/delegate triumverant if bottom_table is None: self.table2 = VSubDataViewerTable(model2, self, **kwarg) else: self.table2 = bottom_table(model2, self, **kwarg) DataViewerTable.__init__(self, model, **kwarg) if scrollbar_at_bottom: self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.table2.setHorizontalScrollBar(self.horizontalScrollBar()) else: self.table2.setHorizontalScrollBarPolicy( QtCore.Qt.ScrollBarAlwaysOff) self.setHorizontalScrollBar(self.table2.horizontalScrollBar()) self.mylayout = swidgets.SVBoxLayout() self.mylayout.addWidget(self) self.mylayout.addWidget(self.table2)
[docs] def headerClicked(self, col): DataViewerTable.headerClicked(self, col) self.table2.model().sortByColumn( col, self.model().columnSortOrderIndicator[col])
[docs] def enforceScrollBarPolicies(self): """ Sets the scrollbar policies as required by the sizing policies """ DataViewerTable.enforceScrollBarPolicies(self) self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
[docs] def setColumnWidth(self, column, width): """ Sets column to width in both tables :type column: int :param column: the index of the column to change :type width: int :param width: the new width of the column """ DataViewerTable.setColumnWidth(self, column, width) self.table2.setColumnWidth(column, width)
[docs] def resizeRowsToContents(self): """ Resizes all the rows according to all the resize properties """ DataViewerTable.resizeRowsToContents(self) self.table2.resizeRowsToContents()
[docs] def setFixedWidth(self, width): """ Sets the viewport size to width :type width: int :param width: new viewport size in pixels """ DataViewerTable.setFixedWidth(self, width) self.table2.setFixedWidth(self, width)
[docs] def fitColumnToData(self, column, include_header=False): """ Resizes column to the largest sizeHint of any of its cells. Uses the sizeHint of the delegate. :type column: int :param column: the column of interest """ hint = 0 options = qt_utils.get_view_item_options(self) for row in range(self.model().rowCount()): index = self.model().index(row, column) shint = self.itemDelegate().sizeHint(options, index, actual_column_width=True) hint = max(hint, shint.width()) options2 = qt_utils.get_view_item_options(self.table2) for row in range(self.table2.model().rowCount()): index = self.table2.model().index(row, column) shint = self.table2.itemDelegate().sizeHint( options2, index, actual_column_width=True) hint = max(hint, shint.width()) if include_header: shint = self.horizontalHeader().sectionSizeHint(column) hint = max(hint, shint) self.setColumnWidth(column, hint)
[docs] def resizeEvent(self, event): """ Called when the viewport changes size. Changes the size of any cells that need to be changed in response to this - typically because fill has been set on rows or columns. :type event: QEvent :param event: the event that occured """ # Infinite loop detection and elimination. See comments in __init__ as # to what is going on here. if self.stop_loop: # A loop was detected last time through, stop it by not resizing self.stop_loop = False return if self.avoid_loops: # This is where we check to see if we are in a loop. Keep a history # of the scrollbar visibilities. If they are toggling on and off, # then we are in a loop self.hbar_states.insert(0, self.horizontalScrollBar().isVisible()) self.vbar_states.insert(0, self.verticalScrollBar().isVisible()) try: # Only save the important states so the lists don't grow large self.hbar_states = self.hbar_states[:self.toggle_length] self.vbar_states = self.vbar_states[:self.toggle_length] except IndexError: # Happens if the lists are small enough already pass if (self.hbar_states == self.loop_sign or self.vbar_states == self.loop_sign): # Found a loop, reset the history and set the stop loop flag. self.hbar_states = [] self.vbar_states = [] self.stop_loop = True if event.type() == 14 and event.oldSize() == QtCore.QSize(-1, -1): # Make sure to do a resize the very first time the table shows self.fixSize() QtWidgets.QTableView.resizeEvent(self, event) num_cols = self.model().columnCount() num_rows = self.model().rowCount() if num_cols <= 0: return # Fit the table into the viewport if self.fill_columns: # Calculate the column height needed to perfectly fill the viewport width = self.sizeHintForColumn(0) if self.gang_columns: # Only set the first column, since that resize will trigger all # the other columns. self.setColumnWidth(0, width) # Also have to set the last column, in case the sizeHint hasn't # changed but self.col_remainder has. self.setColumnWidth(num_cols - 1, width + self.col_remainder) else: # Set all the columns to this width for col in range(num_cols - 1): self.setColumnWidth(col, width) self.setColumnWidth(num_cols - 1, width + self.col_remainder) # Change the row heights if need be if self.lock_aspect_ratio: self.resizeRowsToContents() if self.fill_rows: # Calculate the row height needed to perfectly fill the viewport height = self.sizeHintForRow(0) # Set all the rows to this height if self.gang_rows: # Only set the first row, since that resize will trigger all # the other rows to resize. self.setRowHeight(0, height) self.table2.setRowHeight(0, height) # Also have to set the last row, in case the sizeHint hasn't # changed but self.row_remainder has. self.table2.setRowHeight(num_rows - 1, height + self.row_remainder) else: for row in range(num_rows - 1): self.setRowHeight(row, height) self.setRowHeight(num_rows - 1, height + self.row_remainder) # Change the column widths if need be if self.lock_aspect_ratio: self.resizeColumnsToContents()
[docs] def sizeHintForRow(self, row): """ This method gives the size hints for rows - its existance means that the resizeRowsToContents() method uses this hint rather than hints from the delegate. :type row: int :param row: the row we want a size hint for :rtype: int :return: the size hint for row """ if self.lock_aspect_ratio and not self.fill_rows and \ self.isRowGanged(row): hint = old_div(self.averageGangedColumnWidth(), self._default_ratio) elif self.fill_rows: table_height = self.viewport().height() hint, self.row_remainder = divmod(table_height, self.model().rowCount()) else: # To use the delegate size hint for the cell, call delegate.sizeHint # here (and implement the sizeHint method for the delegate). hint = self.rowHeight(row) return hint
[docs]class VSubDataViewerTable(DataViewerTable):
[docs] def __init__(self, model, master_table, **kwarg): """ Sub-table for a vertically stacked table. :type master_table: DataViewerTable :param master_table: the table that controls this one (the upper table in a vertically stacked pair of tables). All other parameters are documented in DataViewerTable class. """ # Set up the table/model/delegate triumverant DataViewerTable.__init__(self, model, **kwarg) self.horizontalHeader().setVisible(False) self.master_table = master_table
[docs] def initiateCellSizes(self, rows=True, columns=True): """ Sets all the cells to their intial default size :type rows: bool :param rows: whether to set the rows to their default height :type columns: bool :param columns: whether to set the columns to their default width """ self.doing_fix_resize = True if rows: for row in range(self.model().rowCount()): self.setRowHeight(row, self.default_cell_size.height()) self.doing_fix_resize = False
[docs] def columnsInserted(self, index, start, end): """ Slot that receives a signal when columns are inserted in the model. Make sure our column_is_ganged list stays current. :type index: QModelIndex :param index: unused :type start: int :param start: The starting index of the columns inserted :type end: int :param end: The ending index of the columns inserted (inclusive) """ if not self._universal_column_gang: try: for ind in range(start, end + 1): self.column_is_ganged.insert(start, True) except IndexError: pass
[docs] def columnsRemoved(self, index, start, end): """ Slot that receives a signal when columns are removed in the model. Make sure our column_is_ganged list stays current. :type index: QModelIndex :param index: unused :type start: int :param start: The starting index of the columns removed :type end: int :param end: The ending index of the columns removed (inclusive) """ if not self._universal_column_gang: try: for ind in range(start, end + 1): del self.column_is_ganged[start] except IndexError: pass
[docs] def columnCountChanged(self, old_count, new_count): """ Called when a column is added or deleted, and sets the columns to their initial size. :type old_count: int :param old_count: the old number of columns :type new_count: int :param new_count: the num number of columns """ QtWidgets.QTableView.columnCountChanged(self, old_count, new_count)
[docs] def rowResized(self, row, old_size, new_size): """ Resizes any cells that need to be resized when a row size is changed :type row: int :param row: the row number that changed :type old_size: int :param old_size: the old size of the row :type new_size: int :param new_size: the new size of the row """ if (not self.doing_row_resize and not self.doing_fix_resize): # Need to set the doing_resize flag to avoid recursive calls to this # routine, as it gets called any time a row size is changed by # the user or by this routine. self.doing_row_resize = True if self.rowIsGanged(row): # We want to keep all rows the same size last_row = self.model().rowCount() - 1 # Find the last visible row, as we'll adjust the size of that # one slightly to fit things properly last_visible_row = 0 if new_size != 0: for myrow in range(last_row, 0, -1): if not self.isRowHidden(myrow): last_visible_row = myrow break for arow in range(last_row + 1): if arow != row and self.rowIsGanged(row): if arow != last_visible_row: self.setRowHeight(arow, new_size) else: self.setRowHeight(arow, new_size + self.row_remainder) if self.lock_aspect_ratio: # Change the columns to have the same width as the row height self.resizeColumnsToContents() self.doing_row_resize = False if self.fit_view_rows: # Fix the viewport size to the total table height self.fitToRows()
[docs] def columnResized(self, column, old_size, new_size): """ Doesn't do anything - let the master table take care of this :type column: int :param column: the column number that changed :type old_size: int :param old_size: the old size of the column :type new_size: int :param new_size: the new size of the column """
[docs] def fitToRows(self): """ Fits the viewport to exactly show all the rows - master table does this """
[docs] def fitToColumns(self): """ Fits the viewport to exactly show all the columns - master table does this """
[docs] def scrollBarChanged(self, orientation): """ A scrollbar has changed appearance, make sure we have the proper size """ self.master_table.fixSize()
[docs] def fixSize(self): """ Resize everything as the user and program desires """ self.doing_fix_resize = True # Make sure to do a resize the very first time the table shows self.resizeRowsToContents() self.doing_fix_resize = False
[docs] def fitCellsToData(self): """ Resizes rows and columns to fit the data they contain using the sizeHint from the delegate """ self.fitRowsToData()
[docs] def fitRowsToData(self): """ Resizes all the rows to fit the data they contain using the sizeHint from the delegate. """ for row in range(self.model().rowCount()): self.fitRowToData(row)
[docs] def fitRowToData(self, row): """ Resizes row to the largest sizeHint of any of its cells. Uses the sizeHint of the delegate. :type row: int :param row: the row of interest """ hint = 0 if self.verticalHeader().isVisible(): buffer = 4 else: buffer = 0 options = qt_utils.get_view_item_options(self) for column in range(self.model().columnCount()): index = self.model().index(row, column) hint = max( hint, self.itemDelegate().sizeHint(options, index).height() + buffer) self.setRowHeight(row, hint)
[docs] def fitColumnsToData(self): """ Resizes all the columns to fit the data they contain using the sizeHint from the delegate. Master table does this. """
[docs] def fitColumnToData(self, column): """ Resizes column to the largest sizeHint of any of its cells. Uses the sizeHint of the delegate. Master table does this. :type column: int :param column: the column of interest """
[docs] def resizeRowsToContents(self): """ Resizes all the rows according to all the resize properties """ for row in range(self.model().rowCount()): self.setRowHeight(row, self.sizeHintForRow(row)) if self.fit_view_rows: # Fix the viewport size to the total row height self.fitToRows()
[docs] def resizeColumnsToContents(self): """ Resizes all the columns according to all the resize properties. Master table does this. """
[docs] def resizeEvent(self, event): """ Called when the viewport changes size. Changes the size of any cells that need to be changed in response to this - typically because fill has been set on rows or columns. :type event: QEvent :param event: the event that occured """ QtWidgets.QTableView.resizeEvent(self, event)
[docs] def sizeHintForColumn(self, column): """ This method gives the size hints for columns - its existance means that the resizeColumnsToContents() method uses this hint rather than hints from the delegate. :type column: int :param column: the column we want a size hint for :rtype: int :return: the size hint for column """ if self.lock_aspect_ratio and not self.fill_columns and \ self.isColumnGanged(column): hint = self.averageGangedRowHeight() * self._default_ratio elif self.fill_columns: table_width = self.viewport().width() hint, self.col_remainder = divmod(table_width, self.model().columnCount()) if column == self.model().columnCount() - 1: hint = hint + self.col_remainder else: # To use the delegate size hint for the cell, call delegate.sizeHint # here (and implement the sizeHint method for the delegate). hint = self.columnWidth(column) return hint
[docs] def sizeHintForRow(self, row): """ This method gives the size hints for rows - its existance means that the resizeRowsToContents() method uses this hint rather than hints from the delegate. :type row: int :param row: the row we want a size hint for :rtype: int :return: the size hint for row """ if self.lock_aspect_ratio and not self.fill_rows and \ self.rowIsGanged(row): hint = old_div(self.averageGangedColumnWidth(), self._default_ratio) elif self.fill_rows: table_height = self.viewport().height() hint, self.row_remainder = divmod( table_height, self.model().rowCount() + self.master_table.rowCount()) if row == self.model().rowCount() - 1: hint = hint + self.row_remainder else: # To use the delegate size hint for the cell, call delegate.sizeHint # here (and implement the sizeHint method for the delegate). hint = self.rowHeight(row) return hint
[docs]class ExportableProxyModel(QtCore.QSortFilterProxyModel): """ A proxy model that allows data to be exported to a CSV file """
[docs] def exportToCsv(self, filename, role=Qt.DisplayRole, header_role=Qt.DisplayRole, unicode_output=False): """ Export the contents of the model to a CSV file. :param filename: The filename to write the CSV file to :type filename: str :param role: The data role to export for the table contents :type role: int :param header_role: The data role to export for the table headers :param header_role: int :param unicode_output: If `True`, Unicode characters are allowed. The CSV file will contain a tag (UTF-8 BOM) that allows Excel and LibreOffice to recognize the file as Unicode. (This tag will be added regardless of whether the output contains any Unicode characters.) If `False`, the CSV file will not contain the tag and Unicode characters will lead to an exception. :type unicode_output: bool """ rows = self.getRowData(role=role, header_role=header_role) write_row_data(filename, rows, unicode_output=unicode_output)
[docs] def getRowData(self, role=Qt.DisplayRole, header_role=Qt.DisplayRole): """ Extract row data from table model. :param role: The data role to export for the table contents :type role: int :param header_role: The data role to export for the table headers :param header_role: int :return: a list of row data, where the first item in the list contains the headers :rtype: list[list[object]] """ headers = [ self.headerData(idx, Qt.Horizontal, header_role) for idx in range(self.columnCount()) ] rows = [headers] for row_idx in range(self.rowCount()): row = [] for col_idx in range(self.columnCount()): index = self.index(row_idx, col_idx) data = self.data(index, role) if data is None: # print Nones as empty string instead of "None" to match # PyQt behavior data = "" row.append(data) rows.append(row) return rows
[docs]def get_row_data(model, role=Qt.DisplayRole, header_role=Qt.DisplayRole): """ Extract row data from table model. :param model: a table model or proxy :type model: QtCore.QAbstractItemModel :param role: The data role to export for the table contents :type role: int :param header_role: The data role to export for the table headers :param header_role: int :return: a list of lists, where each list represents the row of a table :rtype: list[list[object]] """ proxy_model = ExportableProxyModel() proxy_model.setSourceModel(model) return proxy_model.getRowData(role, header_role)
[docs]def write_row_data(file_path, rows, unicode_output=False): """ Write row data to a CSV file. :param file_path: the path for the .csv file :type file_path: str :param rows: a list of row data lists, where the first element of the list should contain the headers :type rows: list[list[object]] :param unicode_output: If `True`, Unicode characters are allowed. The CSV file will contain a tag (UTF-8 BOM) that allows Excel and LibreOffice to recognize the file as Unicode. (This tag will be added regardless of whether the output contains any Unicode characters.) If `False`, the CSV file will not contain the tag and Unicode characters will lead to an exception. :type unicode_output: bool """ if unicode_output: csv_writer = csv_unicode.writer else: csv_writer = csv.writer with csv_unicode.writer_open(file_path) as handle: writer = csv_writer(handle) writer.writerows(rows)
[docs]def export_to_csv(model, filename, role=Qt.DisplayRole, header_role=Qt.DisplayRole, unicode_output=False): """ Export the contents of the specified model to a CSV file :param model: The model (or proxy model) to export :type model: `PyQt5.QtCore.QAbstractItemModel` :param filename: The filename to write the CSV file to :type filename: str :param role: The data role to export for the table contents :type role: int :param header_role: The data role to export for the table headers :param header_role: int :param unicode_output: If `True`, Unicode characters are allowed. The CSV file will contain a tag (UTF-8 BOM) that allows Excel and LibreOffice to recognize the file as Unicode. (This tag will be added regardless of whether the output contains any Unicode characters.) If `False`, the CSV file will not contain the tag and Unicode characters will lead to an exception. :type unicode_output: bool """ proxy_model = ExportableProxyModel() proxy_model.setSourceModel(model) proxy_model.exportToCsv(filename, role, header_role, unicode_output)
[docs]def export_to_csv_with_dialog(model, parent, caption="Export", dialog_id=None, role=Qt.DisplayRole, header_role=Qt.DisplayRole, unicode_output=False): """ Open a dialog prompting the user for a CSV export file and then export the contents of the specified model to the selected file. :param model: The model (or proxy model) to export :type model: `PyQt5.QtCore.QAbstractItemModel` :param parent: The parent widget of the export dialog :type parent: `PyQt5.QtWidgets.QWidget` :param caption: The title of the export dialog :type caption: str :param dialog_id: The id for the filedialog. Dialogs with the same identifier will remember the last directory chosen by the user with any dialog of the same id and open in that directory. :type dialog_id: str, int, or float :param role: The data role to export for the table contents :type role: int :param header_role: The data role to export for the table headers :param header_role: int :param unicode_output: If `True`, Unicode characters are allowed. The CSV file will contain a tag (UTF-8 BOM) that allows Excel and LibreOffice to recognize the file as Unicode. (This tag will be added regardless of whether the output contains any Unicode characters.) If `False`, the CSV file will not contain the tag and Unicode characters will lead to an exception. :type unicode_output: bool """ file_filter = "CSV file (*.csv)" filename = filedialog.get_save_file_name(parent=parent, caption=caption, filter=file_filter, id=dialog_id) if filename: export_to_csv(model, filename, role, header_role, unicode_output)
[docs]class PerColumnSortableTableView(QtWidgets.QTableView): """ A table view that prevents sorting on certain columns. Subclasses should override `UNSORTABLE_COLS`. :cvar UNSORTABLE_COLS: A set of columns to be made unsortable. This variable should be overridden in subclasses. :vartype UNSORTABLE_COLS: set """ UNSORTABLE_COLS = set()
[docs] def __init__(self, parent=None): super(PerColumnSortableTableView, self).__init__(parent) self.setSortingEnabled(True) self._hheader = PerColumnSortableHeaderView(self.UNSORTABLE_COLS, self) self.setHorizontalHeader(self._hheader)
[docs]class PerColumnSortableHeaderView(QtWidgets.QHeaderView): """ A horizontal table header that ignores clicks on certain columns, which prevents sorting on these columns. """
[docs] def __init__(self, unclickable_cols, parent=None): super(PerColumnSortableHeaderView, self).__init__(Qt.Horizontal, parent) self.setSectionsClickable(True) self._unclickable_cols = unclickable_cols
[docs] def mousePressEvent(self, event): """ If the user has pressed on an unsortable column, then make the header non-clickable so the table won't be sorted and won't have a new sort indicator drawn. If the user pressed on any other column, then make the header clickable so things behave normally. :param event: The mouse press event :type event: `QtGui.QMouseEvent` """ self._disableClickableIfUnclickableCol(event) super(PerColumnSortableHeaderView, self).mousePressEvent(event)
[docs] def mouseReleaseEvent(self, event): """ Make the header clickable after processing the mouse release. This ensures that column headers will always respond properly to hovering, even if the user just clicked on an unclickable column. :param event: The mouse release event :type event: `QtGui.QMouseEvent` """ if self.sectionsClickable(): self._disableClickableIfUnclickableCol(event) super(PerColumnSortableHeaderView, self).mouseReleaseEvent(event) self.setSectionsClickable(True)
def _disableClickableIfUnclickableCol(self, event): """ If the specified event has happened over an unclickable column, disable clicking for the header. :param event: The mouse event to check :type event: `QtGui.QMouseEvent` """ # This idea is based on https://wiki.qt.io/Qt_project_org_faq# # setClickable.28.29_and_setSortIndicatorShown.28.29_affect_the_entire_ # QHeaderView.2C_is_there_a_way_to_have_it_affect_only_certain_ # sections.3F col = self.visualIndexAt(event.pos().x()) clickable = col not in self._unclickable_cols self.setSectionsClickable(clickable)
if __name__ == '__main__': print(__doc__)