373 lines
14 KiB
Python
373 lines
14 KiB
Python
#!/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.clipboard_data: list = None
|
|
|
|
# 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())
|
|
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):
|
|
clip_text = qApp.clipboard().text().rstrip('\n').split("\t")
|
|
|
|
cur_row = self.currentRow()
|
|
cur_col = self.currentColumn()
|
|
|
|
col = 0
|
|
|
|
for data in clip_text:
|
|
item = QTableWidgetItem(data)
|
|
self.setItem(cur_row, cur_col+col, item)
|
|
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()
|
|
|
|
clipboard_data = []
|
|
for idx in sel_idx:
|
|
clipboard_data.append(idx.data(Qt.DisplayRole))
|
|
|
|
clip_text = "\t".join(str(text or "") for text in clipboard_data)
|
|
qApp.clipboard().setText(clip_text)
|
|
|
|
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") + ":")
|