516 lines
18 KiB
Python
516 lines
18 KiB
Python
import os
|
|
from operator import itemgetter
|
|
from datetime import timedelta, datetime
|
|
# from PyQt5.Qt import QApplication, QClipboard
|
|
# from PyQt5.QtCore import (
|
|
# Qt, QVariant, QAbstractTableModel, pyqtSignal, QModelIndex
|
|
# )
|
|
# from PyQt5.QtWidgets import (
|
|
# QTableView, QVBoxLayout, QAbstractItemView, QLineEdit, QDialog, QLabel,
|
|
# QHBoxLayout, QItemDelegate, QMenu, QAction)
|
|
# from PyQt5.QtGui import QPixmap, QIcon, QCursor
|
|
from PySide6.QtWidgets import QApplication
|
|
from PySide6.QtCore import (
|
|
Qt, QAbstractTableModel, Signal, QModelIndex
|
|
)
|
|
from PySide6.QtWidgets import (
|
|
QTableView, QVBoxLayout, QAbstractItemView, QLineEdit, QDialog, QLabel,
|
|
QHBoxLayout, QItemDelegate, QMenu
|
|
)
|
|
from PySide6.QtGui import QPixmap, QIcon, QCursor, QAction
|
|
from .buttons import ActionButton
|
|
from ..tools import get_screen
|
|
_horizontal = Qt.Horizontal
|
|
|
|
__all__ = ['Item', 'SearchWindow', 'TableModel']
|
|
|
|
DELTA_LOCALE = -5 # deltatime col
|
|
DIR = os.path.abspath(os.path.normpath(os.path.join(__file__, '..', '..')))
|
|
|
|
ICONS = {
|
|
'image': os.path.join(DIR, 'share/icon-camera.svg'),
|
|
'stock': os.path.join(DIR, 'share/icon-stock.svg'),
|
|
'copy': os.path.join(DIR, 'share/icon-copy.svg'),
|
|
}
|
|
|
|
|
|
class Item(QItemDelegate):
|
|
|
|
def __init__(self, values, fields):
|
|
super(Item, self).__init__()
|
|
_ = [setattr(self, n, str(v)) for n, v in zip(fields, values)]
|
|
|
|
|
|
class SearchWindow(QDialog):
|
|
|
|
def __init__(self, parent, headers, records, methods, filter_column=[],
|
|
cols_width=[], title=None, fill=False, widgets=[]):
|
|
"""
|
|
parent: parent window
|
|
headers: is a ordered dict of data with name field-column as keys.
|
|
records: is a tuple with two values: a key called 'objects' or 'values',
|
|
and a list of instances values or plain values for build data model:
|
|
[('a' 'b', 'c'), ('d', 'e', 'f')...]
|
|
on_selected_method: method to call when triggered the selection
|
|
filter_column: list of column to search values, eg: [0,2]
|
|
title: title of window
|
|
cols_width: list of width of columns, eg. [120, 60, 280]
|
|
fill: Boolean that define if the table must be fill with all data and
|
|
values and these are visibles
|
|
"""
|
|
super(SearchWindow, self).__init__(parent)
|
|
|
|
self.parent = parent
|
|
self.headers = headers
|
|
self.fields_names = list(headers.keys())
|
|
self.records = records
|
|
self.fill = fill
|
|
self.methods = methods
|
|
self.on_selected_method = methods.get('on_selected_method')
|
|
self.on_return_method = methods.get('on_return_method')
|
|
self.filter_column = filter_column
|
|
self.widgets = widgets
|
|
self.cols_width = cols_width
|
|
self.rows = []
|
|
self.current_row = None
|
|
if not title:
|
|
title = 'BUSCAR...'
|
|
self.setWindowTitle(title)
|
|
width, height = get_screen()
|
|
self.setFixedSize(int(width * 0.9), int(height * 0.9))
|
|
|
|
self.create_table()
|
|
self.create_widgets()
|
|
self.create_layout()
|
|
self.create_connections()
|
|
if records:
|
|
if records[0] == 'objects':
|
|
self.set_from_objects(records[1])
|
|
elif records[0] == 'values':
|
|
self.set_from_values(records[1])
|
|
elif records[0] == 'data':
|
|
self.set_from_data(records[1])
|
|
|
|
def get_id(self):
|
|
if self.current_row:
|
|
return self.current_row['id']
|
|
|
|
def clear_rows(self):
|
|
if self.fill:
|
|
self.table_model.items = []
|
|
self.table_model.currentItems = []
|
|
self.table_model.layoutChanged.emit()
|
|
|
|
def clear_filter(self):
|
|
self.filter_field.setText('')
|
|
self.filter_field.setFocus()
|
|
if self.fill:
|
|
self.table_model.items = []
|
|
self.table_view.selectRow(-1)
|
|
self.table_model.currentItems = []
|
|
self.table_model.layoutChanged.emit()
|
|
|
|
def set_from_data(self, values):
|
|
if self.fill:
|
|
self.clear_filter()
|
|
self.table_model.set_rows(values, typedata='list')
|
|
|
|
def set_from_values(self, values):
|
|
if self.fill:
|
|
self.clear_rows()
|
|
self.table_model.set_rows(values)
|
|
self.update_count_field()
|
|
|
|
def update_count_field(self):
|
|
values = self.table_model.currentItems
|
|
self.label_count.setText(str(len(values)))
|
|
|
|
def activate_counter(self):
|
|
self.label_control = QLabel('0')
|
|
self.label_control.setObjectName('label_count')
|
|
self.label_control.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
|
|
self.filter_layout.addWidget(self.label_control)
|
|
|
|
def set_counter_control(self, val):
|
|
self.label_control.setText(str(val))
|
|
|
|
def set_from_objects(self, objects):
|
|
self.rows = []
|
|
rows_append = self.rows.append
|
|
for object_ in objects:
|
|
row = []
|
|
row_append = row.append
|
|
for field, data in self.headers.items():
|
|
val = getattr(object_, field)
|
|
if hasattr(val, 'name'):
|
|
val = getattr(val, 'name')
|
|
elif data['type'] == 'number':
|
|
val = val
|
|
elif data['type'] == 'int':
|
|
val = str(val)
|
|
elif data['type'] == 'date':
|
|
val = val.strftime('%d/%m/%Y')
|
|
row_append(val)
|
|
rows_append(row)
|
|
self.table_model.set_rows(self.rows)
|
|
|
|
def create_table(self):
|
|
# set the table model
|
|
self.table_model = TableModel(self, self.rows, self.headers,
|
|
self.filter_column, fill=self.fill)
|
|
self.table_model.sort(2, Qt.AscendingOrder)
|
|
self.table_view = QTableView()
|
|
self.table_view.setModel(self.table_model)
|
|
self.table_view.setMinimumSize(450, 350)
|
|
self.table_view.setColumnHidden(0, True)
|
|
|
|
self.table_view.setAlternatingRowColors(True)
|
|
self.table_view.setSelectionBehavior(QAbstractItemView.SelectRows)
|
|
self.table_view.setGridStyle(Qt.DotLine)
|
|
for i in range(len(self.cols_width)):
|
|
self.table_view.setColumnWidth(i, self.cols_width[i])
|
|
|
|
vh = self.table_view.verticalHeader()
|
|
vh.setVisible(False)
|
|
hh = self.table_view.horizontalHeader()
|
|
hh.setStretchLastSection(True)
|
|
|
|
# enable sorting
|
|
self.table_view.setSortingEnabled(True)
|
|
|
|
def create_widgets(self):
|
|
self.filter_label = QLabel("FILTRO:")
|
|
self.filter_field = QLineEdit()
|
|
self.label_count = QLabel('0')
|
|
self.label_count.setObjectName('label_count')
|
|
self.label_count.setAlignment(Qt.AlignCenter | Qt.AlignVCenter)
|
|
self.pushButtonOk = ActionButton('ok', self.action_selection_changed)
|
|
self.pushButtonCancel = ActionButton('cancel', self.action_close)
|
|
|
|
def create_layout(self):
|
|
layout = QVBoxLayout()
|
|
if self.widgets:
|
|
for widget in self.widgets:
|
|
layout.addLayout(widget)
|
|
self.filter_layout = QHBoxLayout()
|
|
self.filter_layout.addWidget(self.filter_label)
|
|
self.filter_layout.addWidget(self.filter_field)
|
|
self.filter_layout.addWidget(self.label_count)
|
|
layout.addLayout(self.filter_layout)
|
|
layout.addWidget(self.table_view)
|
|
|
|
buttons_layout = QHBoxLayout()
|
|
buttons_layout.addWidget(self.pushButtonCancel)
|
|
buttons_layout.addWidget(self.pushButtonOk)
|
|
layout.addLayout(buttons_layout)
|
|
self.filter_field.setFocus()
|
|
self.setLayout(layout)
|
|
|
|
def create_connections(self):
|
|
self.filter_field.textChanged.connect(self.action_text_changed)
|
|
self.filter_field.returnPressed.connect(self.action_filter_return_pressed)
|
|
self.table_view.clicked.connect(self.action_selection_changed)
|
|
self.table_view.activated.connect(self.action_table_activated)
|
|
|
|
def action_table_activated(self):
|
|
pass
|
|
|
|
def execute(self):
|
|
self.current_row = None
|
|
self.filter_field.setFocus()
|
|
return self.exec()
|
|
|
|
def show(self):
|
|
self.clear_filter()
|
|
self.filter_field.setFocus()
|
|
super(SearchWindow, self).show()
|
|
|
|
def hide(self):
|
|
self.clear_filter()
|
|
super(SearchWindow, self).hide()
|
|
self.parent.label_input.setFocus()
|
|
|
|
def action_close(self):
|
|
self.parent.label_input.setFocus()
|
|
self.close()
|
|
|
|
def action_selection_changed(self):
|
|
selected = self.table_view.currentIndex()
|
|
# current_row is a dict with values used on mainwindow
|
|
self.current_row = self.table_model.getCurrentRow(selected)
|
|
if selected.row() < 0:
|
|
self.filter_field.setFocus()
|
|
else:
|
|
column = selected.column()
|
|
name_field = self.table_model.header_fields[column]
|
|
if self.methods.get(name_field):
|
|
|
|
parent_method = self.methods[name_field]
|
|
parent_method()
|
|
return
|
|
self.hide()
|
|
if self.parent:
|
|
getattr(self.parent, self.on_selected_method)()
|
|
self.filter_field.setText('')
|
|
|
|
def action_text_changed(self):
|
|
text = self.filter_field.text()
|
|
self.table_model.setFilter(searchText=text)
|
|
self.update_count_field()
|
|
self.table_model.layoutChanged.emit()
|
|
scroll = self.table_view.verticalScrollBar()
|
|
scroll.setValue(0)
|
|
|
|
def action_filter_return_pressed(self):
|
|
if hasattr(self.parent, self.on_return_method):
|
|
method = getattr(self.parent, self.on_return_method)
|
|
method()
|
|
|
|
def keyPressEvent(self, event):
|
|
key = event.key()
|
|
selected = self.table_view.currentIndex()
|
|
if key == Qt.Key_Down:
|
|
if not self.table_view.hasFocus():
|
|
self.table_view.setFocus()
|
|
self.table_view.selectRow(selected.row() + 1)
|
|
elif key == Qt.Key_Up:
|
|
if selected.row() == 0:
|
|
self.filter_field.setFocus()
|
|
else:
|
|
self.table_view.selectRow(selected.row() - 1)
|
|
elif key == Qt.Key_Return:
|
|
if selected.row() < 0:
|
|
self.filter_field.setFocus()
|
|
else:
|
|
self.action_selection_changed()
|
|
elif key == Qt.Key_Escape:
|
|
self.hide()
|
|
else:
|
|
pass
|
|
super(SearchWindow, self).keyPressEvent(event)
|
|
|
|
def contextMenuEvent(self, event):
|
|
self.menu = QMenu(self)
|
|
|
|
pix_copy = QPixmap()
|
|
pix_copy.load(ICONS['copy'])
|
|
icon_copy = QIcon()
|
|
icon_copy.addPixmap(pix_copy)
|
|
|
|
copyAction = QAction(icon_copy, 'copy', self)
|
|
copyAction.triggered.connect(lambda: self.copySlot(event))
|
|
self.menu.addAction(copyAction)
|
|
# add other required actions
|
|
self.menu.popup(QCursor.pos())
|
|
|
|
def copySlot(self, event):
|
|
# get the selected cell
|
|
selected = self.table_view.currentIndex()
|
|
clipboard = QApplication.clipboard()
|
|
clipboard.setText(selected.data())
|
|
# get the selected row and column
|
|
# row = selected.row()
|
|
# col = selected.column()
|
|
|
|
|
|
class TableModel(QAbstractTableModel):
|
|
sigItem_selected = Signal(str)
|
|
|
|
def __init__(self, parent, rows, headers, filter_column=[], fill=False, *args):
|
|
"""
|
|
rows: a list of dicts with values
|
|
headers: a list of strings
|
|
filter_column: list of index of columns for use as filter
|
|
fill: If is True ever the rows will be visible
|
|
"""
|
|
QAbstractTableModel.__init__(self, parent, *args)
|
|
self.rows = rows
|
|
self.fill = fill
|
|
self.headers = headers
|
|
self.header_fields = [h for h in headers.keys()]
|
|
self.header_name = [h['desc'] for h in headers.values()]
|
|
self.rows = []
|
|
self.currentItems = []
|
|
self.items = []
|
|
self.searchField = None
|
|
self.mainColumn = 2
|
|
self.filter_column = filter_column
|
|
self.create_icons()
|
|
|
|
if rows and fill:
|
|
self.set_rows(rows)
|
|
|
|
def create_icons(self):
|
|
pix_camera = QPixmap()
|
|
pix_camera.load(ICONS['image'])
|
|
icon_camera = QIcon()
|
|
icon_camera.addPixmap(pix_camera)
|
|
|
|
pix_stock = QPixmap()
|
|
pix_stock.load(ICONS['stock'])
|
|
icon_stock = QIcon()
|
|
icon_stock.addPixmap(pix_stock)
|
|
self.icons = {
|
|
'icon_image': icon_camera,
|
|
'icon_stock': icon_stock,
|
|
}
|
|
|
|
def _get_item(self, values):
|
|
res = {}
|
|
for name, data in self.headers.items():
|
|
if '.' in name:
|
|
attrs = name.split('.')
|
|
val = values
|
|
for attr in attrs:
|
|
try:
|
|
val = val[attr + '.']
|
|
except Exception:
|
|
if val:
|
|
val = val[attr]
|
|
# val = values.get(attrs[0] + '.')
|
|
# if val:
|
|
# val = val.get(attrs[1])
|
|
# else:
|
|
# val = values.get(name)
|
|
else:
|
|
val = values[name]
|
|
if val:
|
|
if data['type'] == 'date':
|
|
val = val.strftime('%d-%m-%Y')
|
|
elif data['type'] == 'number':
|
|
val = '{0:,}'.format(val)
|
|
elif data['type'] == 'datetime':
|
|
value_obj = datetime.strptime(val, '%Y-%m-%dT%H:%M:%S.%f')
|
|
mod_hours = value_obj + timedelta(hours=DELTA_LOCALE)
|
|
val = mod_hours.strftime('%d/%m/%Y %I:%M %p')
|
|
elif data['type'] == 'time':
|
|
try:
|
|
value_obj = datetime.strptime(val, '%Y-%m-%dT%H:%M:%S.%f')
|
|
except Exception:
|
|
value_obj = datetime.strptime(val, '%Y-%m-%dT%H:%M:%S')
|
|
mod_hours = value_obj + timedelta(hours=DELTA_LOCALE)
|
|
val = mod_hours.strftime('%I:%M %p')
|
|
if data['type'] == 'icon':
|
|
val = self.icons['icon_' + data['icon']]
|
|
|
|
res[name] = val
|
|
return res
|
|
|
|
def set_rows(self, rows):
|
|
self.beginResetModel()
|
|
self.endResetModel()
|
|
self.rows = rows
|
|
for values in rows:
|
|
self.insertRows(self._get_item(values))
|
|
|
|
if self.fill is True:
|
|
self.currentItems = self.items
|
|
|
|
def set_rows_list(self, rows):
|
|
self.beginResetModel()
|
|
self.endResetModel()
|
|
for values in rows:
|
|
self.insertRows(Item(values, self.header_fields))
|
|
|
|
if self.fill is True:
|
|
self.currentItems = self.items
|
|
|
|
def rowCount(self, parent):
|
|
return len(self.rows)
|
|
|
|
def columnCount(self, parent):
|
|
return len(self.header_fields)
|
|
|
|
def getCurrentRow(self, index):
|
|
row = index.row()
|
|
if self.currentItems and row >= 0 and len(self.currentItems) > row:
|
|
return self.currentItems[row]
|
|
|
|
def data(self, index, role, col=None):
|
|
if not index.isValid():
|
|
return
|
|
|
|
row = index.row()
|
|
if col is None:
|
|
column = index.column()
|
|
else:
|
|
column = col
|
|
item = None
|
|
if self.currentItems and len(self.currentItems) > row:
|
|
item = self.currentItems[row]
|
|
|
|
name_field = self.header_fields[column]
|
|
|
|
data = self.headers[name_field]
|
|
if role == Qt.DisplayRole and item:
|
|
if column is not None:
|
|
return item.get(name_field)
|
|
elif role == Qt.DecorationRole:
|
|
if item:
|
|
return item[name_field]
|
|
elif role == Qt.TextAlignmentRole:
|
|
if item:
|
|
align = Qt.AlignmentFlag(Qt.AlignLeft)
|
|
if data['type'] == 'icon':
|
|
align = Qt.AlignmentFlag(Qt.AlignHCenter)
|
|
elif data['type'] == 'number':
|
|
align = Qt.AlignmentFlag(Qt.AlignRight)
|
|
return align
|
|
elif role == Qt.UserRole:
|
|
return item
|
|
|
|
def headerData(self, col, orientation, role):
|
|
if role != Qt.DisplayRole:
|
|
return None
|
|
try:
|
|
return self.header_name[col]
|
|
except:
|
|
return None
|
|
|
|
def insertRows(self, item, row=0, column=1, index=QModelIndex()):
|
|
self.beginInsertRows(index, row, row + 1)
|
|
self.items.append(item)
|
|
self.endInsertRows()
|
|
|
|
def sort(self, column, order):
|
|
name_field = self.header_fields[column]
|
|
if 'icon-' in name_field or not self.items:
|
|
return
|
|
|
|
if order == Qt.AscendingOrder:
|
|
reverse = False
|
|
else:
|
|
reverse = True
|
|
self.items.sort(key=lambda x: (x[name_field] is not None, x[name_field]), reverse=reverse)
|
|
self.layoutChanged.emit()
|
|
|
|
def setFilter(self, searchText=None, mainColumn=None, order=None):
|
|
if not searchText:
|
|
return
|
|
|
|
if mainColumn is not None:
|
|
self.mainColumn = mainColumn
|
|
self.order = order
|
|
|
|
self.currentItems = self.items
|
|
|
|
if searchText and self.filter_column:
|
|
matchers = tuple(t.lower() for t in searchText.split(' '))
|
|
self.filteredItems = []
|
|
for item in self.currentItems:
|
|
values = list(item.values())
|
|
values.pop(0)
|
|
values_clear = list(filter(None, values))
|
|
exists = all(mt in ''.join(values_clear).lower() for mt in matchers)
|
|
if exists:
|
|
self.filteredItems.append(item)
|
|
self.currentItems = self.filteredItems
|
|
|
|
self.layoutChanged.emit()
|
|
|
|
def clear_filter(self):
|
|
if self.fill:
|
|
self.items = []
|
|
self.currentItems = []
|
|
self.layoutChanged.emit()
|