GarageCalc1/src/clsTableWidget.py
2021-07-12 20:56:14 +02:00

374 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") + ":")