#!/usr/bin/env python3 """ project: clsTableWidget file: clsTableWidget.py summary: Implements my own TableWidget class author: Paul Salajean (p.salajean[at]gmx.de) license: GPL version: 0.1 """ # Standard library imports import sys import os # Third party imports from PySide2.QtWidgets import QApplication, QTableWidget, QAbstractItemView, QTableWidgetItem, QMenu, \ QMainWindow, QMessageBox, QComboBox, QDialog from PySide2.QtCore import Qt, QFile, QItemSelectionModel, QCoreApplication, Slot from PySide2.QtGui import QIcon, QColor, QPalette from PySide2.QtUiTools import QUiLoader # local imports import icons_rc import clsTableWidget_icons_rc from utils import resource_path, convert_uom_to_length, convert_uom_to_mass # local globals SCRIPT_PATH = os.path.abspath(os.path.dirname(__file__)) DEFAULT_UOM_LENGTH = None DEFAULT_UOM_MASS = None UI_DLG_UOM = "./ui/dlg_uom.ui" # ICON_CLEAR = SCRIPT_PATH + "./img/icons8-clear-symbol-16.png" # ICON_COPY = SCRIPT_PATH + "./img/icons8-copy-16.png" # ICON_ERASER = SCRIPT_PATH + "/img/icons8-eraser-16.png" # ICON_INSERT = SCRIPT_PATH + "/img/icons8-insert-clip-16.png" # ICON_APPEND = SCRIPT_PATH + "/img/icons8-append-clip-16.png" # ICON_PASTE = SCRIPT_PATH + "/img/icons8-paste-16.png" # ICON_CUT = SCRIPT_PATH + "/img/icons8-scissors-16.png" # ICON_ADD_ROW = SCRIPT_PATH + "/img/icons8-add-row-16.png" # ICON_DEL = SCRIPT_PATH + "/img/icons8-delete-16.png" # def resource_path(relative_path): # """ Get absolute path to resource, works for dev and for PyInstaller """ # try: # # PyInstaller creates a temp folder and stores path in _MEIPASS # base_path = sys._MEIPASS # print("base_path", base_path) # except Exception: # base_path = os.path.abspath(".") # return os.path.join(base_path, relative_path) class TableWidget(QTableWidget): def __init__(self, parent=None, uom_length=DEFAULT_UOM_LENGTH, uom_mass=DEFAULT_UOM_MASS): super().__init__() self.parent = parent self.default_uom_length = uom_length self.default_uom_mass = uom_mass # self.setSelectionMode(QAbstractItemView.ContiguousSelection) self.setSelectionMode(QAbstractItemView.SingleSelection) self.setSelectionBehavior(QTableWidget.SelectItems) #self.setSelectionBehavior(QTableWidget.SelectRows) self.setAlternatingRowColors(True) self.setSortingEnabled(True) self.horizontalHeader().setSectionsMovable(True) self.horizontalHeader().setStretchLastSection(True) # Context-menu self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.on_context_menu) # self.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed | QAbstractItemView.AnyKeyPressed) self.vertHeader = self.verticalHeader() self.vertHeader.setContextMenuPolicy(Qt.CustomContextMenu) self.vertHeader.customContextMenuRequested.connect(self.on_rowheadercontext_menu) self.vertHeader.setSelectionMode(QAbstractItemView.SingleSelection) self.vertHeader.sectionClicked.connect(self.select_row) self.vertHeader.setSectionsMovable(True) self.row_selected = False def select_row(self, row): self.setSelectionBehavior(QTableWidget.SelectRows) self.selectRow(row) self.setSelectionBehavior(QTableWidget.SelectItems) self.row_selected = True def on_context_menu(self, position): menu = QMenu() item_cut = menu.addAction(QIcon(u":ICONS/ICON_CUT"), QCoreApplication.translate("TableWidget", "Cut") + "\tCtrl+X") item_copy = menu.addAction(QIcon(u":ICONS/ICON_COPY"), QCoreApplication.translate("TableWidget", "Copy") + "\tCtrl+C") item_paste = menu.addAction(QIcon(u":ICONS/ICON_PASTE"), QCoreApplication.translate("TableWidget", "Paste") + "\tCtrl+V") menu.addSeparator() item_delete = menu.addAction(QIcon(u":ICONS/ICON_ERASER"), QCoreApplication.translate("TableWidget", "Delete") + "\tDel") ac = menu.exec_(self.mapToGlobal(position)) if ac == item_cut: self.item_cut() elif ac == item_copy: self.item_copy() elif ac == item_paste: self.item_paste() elif ac == item_delete: self.item_del() def on_rowheadercontext_menu(self, position): menu = QMenu() row_cut = menu.addAction(QIcon(u":ICONS/ICON_CUT"), QCoreApplication.translate("TableWidget", "Cut row")) row_copy = menu.addAction(QIcon(u":ICONS/ICON_COPY"), QCoreApplication.translate("TableWidget", "Copy row")) row_paste = menu.addAction(QIcon(u":ICONS/ICON_PASTE"), QCoreApplication.translate("TableWidget", "Paste row")) menu.addSeparator() row_insert_before = menu.addAction(QIcon(u":ICONS/ICON_INSERT"), QCoreApplication.translate("TableWidget", "Insert row before")) row_insert_after = menu.addAction(QIcon(u":ICONS/ICON_APPEND"), QCoreApplication.translate("TableWidget", "Insert row after")) menu.addSeparator() row_remove = menu.addAction(QIcon(u":ICONS/ICON_DEL"), QCoreApplication.translate("TableWidget", "Remove row")) row_delete_items = menu.addAction(QIcon(u":ICONS/ICON_ERASER"), QCoreApplication.translate("TableWidget", "Delete items")) ac = menu.exec_(self.mapToGlobal(position)) row = self.vertHeader.logicalIndexAt(position) if ac == row_cut: self.item_cut() elif ac == row_copy: self.item_copy() elif ac == row_paste: self.item_paste() elif ac == row_insert_before: self.row_insert(row) elif ac == row_insert_after: self.row_insert(row+1) elif ac == row_remove: self.row_remove(row) elif ac == row_delete_items: self.row_delete_items() def row_insert(self, row): self.insertRow(row) def has_data(self, row): """ Check if target already contains data. """ # sel_idx = self.selectionModel().selectedIndexes() for col in range(self.columnCount()): item = self.item(row, col) if item: if len(item.text()) > 0: return True return False def row_remove(self, row): if self.has_data(row): msg = QCoreApplication.translate("TableWidget", "Row Nr.") + " " + str(row+1) + " " + QCoreApplication.translate("TableWidget", "to be removed?") reply = QMessageBox.question(self, QCoreApplication.translate("TableWidget", "Remove"), msg, \ QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.No: return False self.removeRow(row) return True def row_delete_items(self): self.item_del() def keyPressEvent(self, event): super().keyPressEvent(event) modifiers = QApplication.keyboardModifiers() key = event.key() if modifiers == Qt.ControlModifier: if key == Qt.Key_C: self.item_copy() elif key == Qt.Key_V: self.item_paste() elif key == Qt.Key_X: self.item_cut() elif key == Qt.Key_A: self.setSelectionMode(QAbstractItemView.ContiguousSelection) self.selectAll() self.setSelectionMode(QAbstractItemView.SingleSelection) if key == Qt.Key_Delete: self.item_del() elif key == Qt.Key_F3: item = self.item(self.currentRow(), self.currentColumn()) try: self.parent.cols_with_uom_length self.parent.cols_with_uom_mass except: return False else: if self.currentColumn() in self.parent.cols_with_uom_length: self.f3_pressed(item, "UOM_TYPE_LENGTH") elif self.currentColumn() in self.parent.cols_with_uom_mass: self.f3_pressed(item, "UOM_TYPE_MASS") elif key == Qt.Key_Escape: self.clearSelection() def f3_pressed(self, item, uom_type) -> bool: if item: try: float(item.text().replace(",", ".")) except ValueError: return False # we've got an existing text here loader = QUiLoader() path = os.path.join(os.path.dirname(__file__), resource_path(UI_DLG_UOM)) ui_file = QFile(path) ui_file.open(QFile.ReadOnly) self.dlg = loader.load(ui_file, self) ui_file.close() self.retranslateUi_dlg(self.dlg) self.dlg.uom_type = uom_type if uom_type == "UOM_TYPE_MASS": self.dlg.cmbUOM.addItems(["kg", "g"]) else: # anticipate uom type length self.dlg.cmbUOM.addItems(["m", "cm", "mm"]) if item: self.dlg.efValue.setText(item.text()) print("UOM of the cell:", item.data(Qt.UserRole)) self.dlg.cmbUOM.setCurrentText(item.data(Qt.UserRole)) if item.data(Qt.UserRole): self.old_uom = item.data(Qt.UserRole) else: if uom_type == "UOM_TYPE_MASS": self.old_uom = self.default_uom_mass else: self.old_uom = self.default_uom_length self.dlg.cmbUOM.currentIndexChanged.connect(self.on_cmbUOM_itemChanged) if self.dlg.exec_() == QDialog.Accepted: if not item: item = QTableWidgetItem("Dummy") self.setItem(self.currentRow(), self.currentColumn(), item) item.setText(self.dlg.efValue.text()) #item.setStyleSheet("border: 1px solid yellow;") item.setData(Qt.UserRole, self.dlg.cmbUOM.currentText()) ## store UOM-Text at UserRole item.setData(Qt.UserRole+1, self.dlg.cmbUOM.currentText()) ## store UOM-Type at UserRole+1 (1==UOM_LENGTH, 2==UOM_MASS) if self.dlg.cmbUOM.currentText() not in (self.default_uom_length, self.default_uom_mass): item.setData(Qt.BackgroundRole, QColor(Qt.yellow)) item.setToolTip("[" + item.data(Qt.UserRole) + "]") else: item.setData(Qt.BackgroundRole, None) item.setToolTip(None) return True @Slot(int) def on_cmbUOM_itemChanged(self, index): try: old_val = float(self.dlg.efValue.text().replace(",", ".")) except ValueError: # special case: edit field is emppty because user changes UOM first. self.old_uom = self.dlg.cmbUOM.currentText() pass else: new_uom = self.dlg.cmbUOM.currentText() if self.dlg.uom_type == "UOM_TYPE_LENGTH": new_val = convert_uom_to_length(old_val, self.old_uom, new_uom) elif self.dlg.uom_type == "UOM_TYPE_MASS": new_val = convert_uom_to_mass(old_val, self.old_uom, new_uom) self.dlg.efValue.setText(str(new_val).replace(".", ",")) self.old_uom = new_uom # def setUOM(self, item, uom): # if uom not in (DEFAULT_UOM_LENGTH, DEFAULT_UOM_MASS): # item.setData(Qt.UserRole, uom) # item.setData(Qt.BackgroundRole, QColor(Qt.yellow)) # item.setToolTip(f"[{uom}]") # else: # item.setData(Qt.BackgroundRole, None) # item.setToolTip(None) def setUOM(self, item, uom): item.setData(Qt.UserRole, uom) if uom not in (self.default_uom_length, self.default_uom_mass): item.setData(Qt.BackgroundRole, QColor(Qt.yellow)) item.setToolTip(f"[{uom}]") else: item.setData(Qt.BackgroundRole, None) item.setToolTip(None) def item_paste(self): if self.clipboard_data: cur_row = self.currentRow() cur_col = self.currentColumn() # ask_confirmation = True if self.row_selected: cur_col = 0 col = 0 if len(self.clipboard_data) == 1: data = self.clipboard_data[0] item = QTableWidgetItem(data) self.setItem(cur_row, cur_col, item) item.setSelected(True) else: for data in self.clipboard_data: item = QTableWidgetItem(data) # if item: # if len(item.text()) >0: # if ask_confirmation: # msg = QCoreApplication.translate("TableWidget", "Zelle enthält bereits Daten. Überschreiben?") # reply = QMessageBox.question(self, QCoreApplication.translate("TableWidget", "Überschreiben"), msg, \ # QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) # if reply == QMessageBox.No: # return False # ask_confirmation = False self.setItem(cur_row, col, item) item.setSelected(True) col += 1 def item_cut(self): self.item_copy() sel_idx = self.selectedIndexes() for idx in sel_idx: item = self.itemFromIndex(idx) try: item.setData(Qt.DisplayRole, None) except AttributeError: pass def item_copy(self): sel_idx = self.selectedIndexes() sel_rows = self.selectionModel().selectedRows() if len(sel_idx) == 1: self.row_selected = False self.sel_ranges = self.selectedRanges() self.clipboard_data = [] for idx in sel_idx: self.clipboard_data.append(idx.data()) def item_del(self, ask_cofirmation=True): sel_idx = self.selectionModel().selectedIndexes() for idx in sel_idx: row = idx.row() col = idx.column() item = self.item(row, col) if item: if self.has_data(row) and ask_cofirmation: msg = QCoreApplication.translate("TableWidget", "Delete cell content?") reply = QMessageBox.question(self, QCoreApplication.translate("TableWidget", "Delete"), msg, \ QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes) if reply == QMessageBox.No: return False ask_cofirmation = False item.setData(Qt.DisplayRole, None) # remove text item.setData(Qt.BackgroundRole, None) # remove cell background color item.setData(Qt.UserRole, None) # remove UOM item.setToolTip(None) # remove tooltip if len(sel_idx) == self.columnCount() * self.rowCount(): try: self.parent.is_modified = False # set parents modification flag (if present) except AttributeError: pass def retranslateUi_dlg(self, dlg): dlg.setWindowTitle(QCoreApplication.translate("TableWidget","UOM")) dlg.lblValue.setText(QCoreApplication.translate("TableWidget","Value") + ":") dlg.lblUOM.setText(QCoreApplication.translate("TableWidget","UOM") + ":")