class ABDataFrameView(QTableView): """ Base class for showing pandas dataframe objects as tables. """ ALL_FILTER = "All Files (*.*)" CSV_FILTER = "CSV (*.csv);; All Files (*.*)" TSV_FILTER = "TSV (*.tsv);; All Files (*.*)" EXCEL_FILTER = "Excel (*.xlsx);; All Files (*.*)" def __init__(self, parent=None): super().__init__(parent) self.setVerticalScrollMode(QTableView.ScrollPerPixel) self.setHorizontalScrollMode(QTableView.ScrollPerPixel) self.setWordWrap(True) self.setAlternatingRowColors(True) self.setSortingEnabled(True) self.verticalHeader().setDefaultSectionSize(22) # row height self.verticalHeader().setVisible(True) # Use a custom ViewOnly delegate by default. # Can be overridden table-wide or per column in child classes. self.setItemDelegate(ViewOnlyDelegate(self)) self.table_name = 'LCA results' # Initialize attributes which are set during the `sync` step. # Creating (and typing) them here allows PyCharm to see them as # valid attributes. self.model: Optional[PandasModel] = None self.proxy_model: Optional[QSortFilterProxyModel] = None def get_max_height(self) -> int: return (self.verticalHeader().count())*self.verticalHeader().defaultSectionSize() + \ self.horizontalHeader().height() + self.horizontalScrollBar().height() + 5 def sizeHint(self) -> QSize: return QSize(self.width(), self.get_max_height()) def rowCount(self) -> int: return 0 if self.model is None else self.model.rowCount() @Slot(name="updateProxyModel") def update_proxy_model(self) -> None: self.proxy_model = QSortFilterProxyModel(self) self.proxy_model.setSourceModel(self.model) self.proxy_model.setSortCaseSensitivity(Qt.CaseInsensitive) self.setModel(self.proxy_model) @Slot(name="resizeView") def custom_view_sizing(self) -> None: """ Custom table resizing to perform after setting new (proxy) model. """ self.setMaximumHeight(self.get_max_height()) @Slot(name="exportToClipboard") def to_clipboard(self): """ Copy dataframe to clipboard """ rows = list(range(self.model.rowCount())) cols = list(range(self.model.columnCount())) self.model.to_clipboard(rows, cols, include_header=True) def savefilepath(self, default_file_name: str, caption: str = None, file_filter: str = None): """ Construct and return default path where data is stored Uses the application directory for AB """ safe_name = safe_filename(default_file_name, add_hash=False) caption = caption or "Choose location to save lca results" filepath, _ = QFileDialog.getSaveFileName( parent=self, caption=caption, dir=os.path.join(ab_settings.data_dir, safe_name), filter=file_filter or self.ALL_FILTER, ) # getSaveFileName can now weirdly return Path objects. return str(filepath) if filepath else filepath @Slot(name="exportToCsv") def to_csv(self): """ Save the dataframe data to a CSV file. """ filepath = self.savefilepath(self.table_name, file_filter=self.CSV_FILTER) if filepath: if not filepath.endswith('.csv'): filepath += '.csv' self.model.to_csv(filepath) @Slot(name="exportToExcel") def to_excel(self, caption: str = None): """ Save the dataframe data to an excel file. """ filepath = self.savefilepath(self.table_name, caption, file_filter=self.EXCEL_FILTER) if filepath: if not filepath.endswith('.xlsx'): filepath += '.xlsx' self.model.to_excel(filepath) @Slot(QKeyEvent, name="copyEvent") def keyPressEvent(self, e): """ Allow user to copy selected data from the table NOTE: by default, the table headers (column names) are also copied. """ if e.modifiers() & Qt.ControlModifier: # Should we include headers? headers = e.modifiers() & Qt.ShiftModifier if e.key() == Qt.Key_C: # copy selection = [ self.model.proxy_to_source(p) for p in self.selectedIndexes() ] rows = [index.row() for index in selection] columns = [index.column() for index in selection] rows = sorted(set(rows), key=rows.index) columns = sorted(set(columns), key=columns.index) self.model.to_clipboard(rows, columns, headers)
class ObjListWindow(PBDialog): """Create a window managing a list (of bibtexs or of experiments)""" def __init__(self, parent=None, gridLayout=False): """Init using parent class and create common definitions Parameters: parent: the parent object gridLayout (boolean, default False): if True, use a QGridLayout, otherwise a QVBoxLayout """ super(ObjListWindow, self).__init__(parent) self.tableWidth = None self.proxyModel = None self.gridLayout = gridLayout self.filterInput = None self.proxyModel = None self.tableview = None if gridLayout: self.currLayout = QGridLayout() else: self.currLayout = QVBoxLayout() self.setLayout(self.currLayout) def triggeredContextMenuEvent(self, row, col, event): """Not implemented: requires a subclass""" raise NotImplementedError() def handleItemEntered(self, index): """Not implemented: requires a subclass""" raise NotImplementedError() def cellClick(self, index): """Not implemented: requires a subclass""" raise NotImplementedError() def cellDoubleClick(self, index): """Not implemented: requires a subclass""" raise NotImplementedError() def createTable(self, *args, **kwargs): """Not implemented: requires a subclass""" raise NotImplementedError() def changeFilter(self, string): """Change the filter of the current view. Parameter: string: the filter string to be matched """ self.proxyModel.setFilterRegExp(str(string)) def addFilterInput(self, placeholderText, gridPos=(1, 0)): """Add a `QLineEdit` to change the filter of the list. Parameter: placeholderText: the text to be shown when no filter is present gridPos (tuple): if gridLayout is active, the position of the `QLineEdit` in the `QGridLayout` """ self.filterInput = QLineEdit("", self) self.filterInput.setPlaceholderText(placeholderText) self.filterInput.textChanged.connect(self.changeFilter) if self.gridLayout: self.currLayout.addWidget(self.filterInput, *gridPos) else: self.currLayout.addWidget(self.filterInput) self.filterInput.setFocus() def setProxyStuff(self, sortColumn, sortOrder): """Prepare the proxy model to filter and sort the view. Parameter: sortColumn: the index of the column to use for sorting at the beginning sortOrder: the order for sorting (`Qt.AscendingOrder` or `Qt.DescendingOrder`) """ self.proxyModel = QSortFilterProxyModel(self) self.proxyModel.setSourceModel(self.tableModel) self.proxyModel.setFilterCaseSensitivity(Qt.CaseInsensitive) self.proxyModel.setSortCaseSensitivity(Qt.CaseInsensitive) self.proxyModel.setFilterKeyColumn(-1) self.tableview = PBTableView(self) self.tableview.setModel(self.proxyModel) self.tableview.setSortingEnabled(True) self.tableview.setMouseTracking(True) self.tableview.setSelectionBehavior(QAbstractItemView.SelectRows) try: self.tableview.sortByColumn(self.tableModel.header.index("bibkey"), Qt.AscendingOrder) except (IndexError, ValueError): pass self.tableview.sortByColumn(sortColumn, sortOrder) try: self.proxyModel.sort(self.tableModel.header.index("bibkey"), Qt.AscendingOrder) except (IndexError, ValueError): pass self.proxyModel.sort(sortColumn, sortOrder) self.currLayout.addWidget(self.tableview) def finalizeTable(self, gridPos=(1, 0)): """Resize the table to fit the contents, connect functions, add to layout Parameter: gridPos (tuple): if gridLayout is active, the position of the `QLineEdit` in the `QGridLayout` """ self.tableview.resizeColumnsToContents() maxh = QDesktopWidget().availableGeometry().height() maxw = QDesktopWidget().availableGeometry().width() self.setMaximumHeight(maxh) self.setMaximumWidth(maxw) hwidth = self.tableview.horizontalHeader().length() swidth = self.tableview.style().pixelMetric(QStyle.PM_ScrollBarExtent) fwidth = self.tableview.frameWidth() * 2 if self.tableWidth is None: if hwidth > maxw - (swidth + fwidth): self.tableWidth = maxw - (swidth + fwidth) else: self.tableWidth = hwidth + swidth + fwidth self.tableview.setFixedWidth(self.tableWidth) self.setMinimumHeight(600) self.tableview.resizeColumnsToContents() self.tableview.resizeRowsToContents() self.tableview.entered.connect(self.handleItemEntered) self.tableview.clicked.connect(self.cellClick) self.tableview.doubleClicked.connect(self.cellDoubleClick) if self.gridLayout: self.currLayout.addWidget(self.tableview, *gridPos) else: self.currLayout.addWidget(self.tableview) def recreateTable(self): """Delete the previous table widget and other layout items, then create new ones """ self.cleanLayout() self.createTable()
class Window(QWidget): def __init__(self): super(Window, self).__init__() self.proxyModel = QSortFilterProxyModel() self.proxyModel.setDynamicSortFilter(True) self.sourceGroupBox = QGroupBox("Original Model") self.proxyGroupBox = QGroupBox("Sorted/Filtered Model") self.sourceView = QTreeView() self.sourceView.setRootIsDecorated(False) self.sourceView.setAlternatingRowColors(True) self.proxyView = QTreeView() self.proxyView.setRootIsDecorated(False) self.proxyView.setAlternatingRowColors(True) self.proxyView.setModel(self.proxyModel) self.proxyView.setSortingEnabled(True) self.sortCaseSensitivityCheckBox = QCheckBox("Case sensitive sorting") self.filterCaseSensitivityCheckBox = QCheckBox("Case sensitive filter") self.filterPatternLineEdit = QLineEdit() self.filterPatternLineEdit.setClearButtonEnabled(True) self.filterPatternLabel = QLabel("&Filter pattern:") self.filterPatternLabel.setBuddy(self.filterPatternLineEdit) self.filterSyntaxComboBox = QComboBox() self.filterSyntaxComboBox.addItem("Regular expression", REGULAR_EXPRESSION) self.filterSyntaxComboBox.addItem("Wildcard", WILDCARD) self.filterSyntaxComboBox.addItem("Fixed string", FIXED_STRING) self.filterSyntaxLabel = QLabel("Filter &syntax:") self.filterSyntaxLabel.setBuddy(self.filterSyntaxComboBox) self.filterColumnComboBox = QComboBox() self.filterColumnComboBox.addItem("Subject") self.filterColumnComboBox.addItem("Sender") self.filterColumnComboBox.addItem("Date") self.filterColumnLabel = QLabel("Filter &column:") self.filterColumnLabel.setBuddy(self.filterColumnComboBox) self.filterPatternLineEdit.textChanged.connect(self.filterRegExpChanged) self.filterSyntaxComboBox.currentIndexChanged.connect(self.filterRegExpChanged) self.filterColumnComboBox.currentIndexChanged.connect(self.filterColumnChanged) self.filterCaseSensitivityCheckBox.toggled.connect(self.filterRegExpChanged) self.sortCaseSensitivityCheckBox.toggled.connect(self.sortChanged) sourceLayout = QHBoxLayout() sourceLayout.addWidget(self.sourceView) self.sourceGroupBox.setLayout(sourceLayout) proxyLayout = QGridLayout() proxyLayout.addWidget(self.proxyView, 0, 0, 1, 3) proxyLayout.addWidget(self.filterPatternLabel, 1, 0) proxyLayout.addWidget(self.filterPatternLineEdit, 1, 1, 1, 2) proxyLayout.addWidget(self.filterSyntaxLabel, 2, 0) proxyLayout.addWidget(self.filterSyntaxComboBox, 2, 1, 1, 2) proxyLayout.addWidget(self.filterColumnLabel, 3, 0) proxyLayout.addWidget(self.filterColumnComboBox, 3, 1, 1, 2) proxyLayout.addWidget(self.filterCaseSensitivityCheckBox, 4, 0, 1, 2) proxyLayout.addWidget(self.sortCaseSensitivityCheckBox, 4, 2) self.proxyGroupBox.setLayout(proxyLayout) mainLayout = QVBoxLayout() mainLayout.addWidget(self.sourceGroupBox) mainLayout.addWidget(self.proxyGroupBox) self.setLayout(mainLayout) self.setWindowTitle("Basic Sort/Filter Model") self.resize(500, 450) self.proxyView.sortByColumn(1, Qt.AscendingOrder) self.filterColumnComboBox.setCurrentIndex(1) self.filterPatternLineEdit.setText("Andy|Grace") self.filterCaseSensitivityCheckBox.setChecked(True) self.sortCaseSensitivityCheckBox.setChecked(True) def setSourceModel(self, model): self.proxyModel.setSourceModel(model) self.sourceView.setModel(model) def filterRegExpChanged(self): syntax_nr = self.filterSyntaxComboBox.currentData() pattern = self.filterPatternLineEdit.text() if syntax_nr == WILDCARD: pattern = QRegularExpression.wildcardToRegularExpression(pattern) elif syntax_nr == FIXED_STRING: pattern = QRegularExpression.escape(pattern) regExp = QRegularExpression(pattern) if not self.filterCaseSensitivityCheckBox.isChecked(): options = regExp.patternOptions() options |= QRegularExpression.CaseInsensitiveOption regExp.setPatternOptions(options) self.proxyModel.setFilterRegularExpression(regExp) def filterColumnChanged(self): self.proxyModel.setFilterKeyColumn(self.filterColumnComboBox.currentIndex()) def sortChanged(self): if self.sortCaseSensitivityCheckBox.isChecked(): caseSensitivity = Qt.CaseSensitive else: caseSensitivity = Qt.CaseInsensitive self.proxyModel.setSortCaseSensitivity(caseSensitivity)