wordless/wl_widgets/wl_tables.py
# ----------------------------------------------------------------------
# Wordless: Widgets - Tables
# Copyright (C) 2018-2023 Ye Lei (叶磊)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# ----------------------------------------------------------------------
import csv
import os
import random
import re
import traceback
import bs4
import docx
import openpyxl
from PyQt5.QtCore import pyqtSignal, QCoreApplication, QItemSelection, Qt
from PyQt5.QtGui import QFont, QStandardItem, QStandardItemModel
from PyQt5.QtWidgets import (
QAbstractItemView, QApplication, QDialog, QFileDialog, QHeaderView,
QLabel, QPushButton, QTableView
)
from wordless.wl_checks import wl_checks_misc, wl_checks_work_area
from wordless.wl_dialogs import wl_dialogs_misc
from wordless.wl_nlp import wl_nlp_utils
from wordless.wl_utils import wl_misc, wl_paths, wl_threading
from wordless.wl_widgets import wl_buttons
_tr = QCoreApplication.translate
# pylint: disable=unnecessary-lambda
class Wl_Table(QTableView):
def __init__(
self, parent,
headers, header_orientation = 'hor',
editable = False,
drag_drop = False
):
super().__init__(parent)
self.main = wl_misc.find_wl_main(parent)
self.headers = headers
self.header_orientation = header_orientation
self.settings = self.main.settings_custom
self.table_settings = {}
model = QStandardItemModel()
model.table = self
self.setModel(model)
if self.header_orientation == 'hor':
self.model().setHorizontalHeaderLabels(self.headers)
elif self.header_orientation == 'vert':
self.model().setVerticalHeaderLabels(self.headers)
self.verticalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.horizontalHeader().setHighlightSections(False)
self.verticalHeader().setHighlightSections(False)
self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
if editable:
self.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.SelectedClicked)
else:
self.setEditTriggers(QAbstractItemView.NoEditTriggers)
if drag_drop:
self.setDragEnabled(True)
self.setAcceptDrops(True)
self.viewport().setAcceptDrops(True)
self.setDragDropMode(QAbstractItemView.InternalMove)
self.setDragDropOverwriteMode(False)
self.setSelectionBehavior(QAbstractItemView.SelectRows)
# Remove dotted gray border around selected cells
self.setFocusPolicy(Qt.NoFocus)
if self.header_orientation == 'hor':
self.setStyleSheet('''
QTableView::item:hover {
background-color: #C7C7C7;
color: #000;
}
QTableView::item:selected {
background-color: #C7C7C7;
color: #000;
}
QHeaderView::section {
color: #FFF;
font-weight: bold;
}
QHeaderView::section:horizontal {
background-color: #5C88C5;
}
QHeaderView::section:horizontal:hover {
background-color: #3265B2;
}
QHeaderView::section:horizontal:pressed {
background-color: #3265B2;
}
QHeaderView::section:vertical {
background-color: #737373;
}
QHeaderView::section:vertical:hover {
background-color: #606060;
}
QHeaderView::section:vertical:pressed {
background-color: #606060;
}
''')
elif self.header_orientation == 'vert':
self.setStyleSheet('''
QTableView::item {
color: #000;
}
QTableView::item:hover {
background-color: #EEE;
}
QTableView::item:selected {
background-color: #EEE;
color: #000;
}
QHeaderView::section {
color: #FFF;
font-weight: bold;
}
QHeaderView::section:horizontal {
background-color: #888;
}
QHeaderView::section:horizontal:hover {
background-color: #777;
}
QHeaderView::section:horizontal:pressed {
background-color: #666;
}
QHeaderView::section:vertical {
background-color: #5C88C5;
}
QHeaderView::section:vertical:hover {
background-color: #3265B2;
}
QHeaderView::section:vertical:pressed {
background-color: #264E8C;
}
''')
self.model().itemChanged.connect(self.item_changed)
self.selectionModel().selectionChanged.connect(self.selection_changed)
# There seems to be a bug with QAbstractItemView.InternalMove
# See: https://bugreports.qt.io/browse/QTBUG-87057
def dropEvent(self, event):
if self.indexAt(event.pos()).row() == -1:
row_dropped_on = self.model().rowCount()
else:
row_dropped_on = self.indexAt(event.pos()).row()
data = []
data_selected = []
rows_selected = self.get_selected_rows()
for row in range(self.model().rowCount()):
data.append([])
for col in range(self.model().columnCount()):
item = self.model().takeItem(row, col)
widget = self.indexWidget(self.model().index(row, col))
if widget:
data[-1].append(type(widget)(widget.text(), self))
else:
data[-1].append(item)
for row in rows_selected:
data_selected.append(data[row].copy())
data[row] = []
for row in reversed(data_selected):
data.insert(row_dropped_on, row)
data = [row for row in data if row]
self.disable_updates()
self.clr_table()
self.model().setRowCount(len(data))
self.model().setColumnCount(len(data[0]))
for i, row in enumerate(data):
for j, item in enumerate(row):
if isinstance(item, QStandardItem):
self.model().setItem(i, j, item)
else:
self.setIndexWidget(self.model().index(i, j), item)
self.enable_updates()
event.accept()
def item_changed(self):
if self.is_empty():
self.setEnabled(False)
else:
self.setEnabled(True)
self.resizeColumnsToContents()
self.resizeRowsToContents()
for i in range(self.model().columnCount()):
self.setColumnWidth(i, self.columnWidth(i) + 22)
self.selection_changed()
def selection_changed(self):
pass
def disable_updates(self):
self.num_rows_old = self.model().rowCount()
self.num_cols_old = self.model().columnCount()
self.setUpdatesEnabled(False)
self.blockSignals(True)
self.horizontalHeader().blockSignals(True)
self.verticalHeader().blockSignals(True)
self.model().blockSignals(True)
self.selectionModel().blockSignals(True)
self.hide()
def enable_updates(self, emit_signals = True):
self.setUpdatesEnabled(True)
self.blockSignals(False)
self.horizontalHeader().blockSignals(False)
self.verticalHeader().blockSignals(False)
self.model().blockSignals(False)
self.selectionModel().blockSignals(False)
self.show()
self.horizontalHeader().sectionCountChanged.emit(self.num_cols_old, self.model().columnCount())
self.verticalHeader().sectionCountChanged.emit(self.num_rows_old, self.model().rowCount())
if emit_signals:
self.model().itemChanged.emit(QStandardItem())
self.selectionModel().selectionChanged.emit(QItemSelection(), QItemSelection())
def is_empty(self):
if self.header_orientation == 'hor':
return not any((
self.model().item(0, i) or self.indexWidget(self.model().index(0, i))
for i in range(self.model().columnCount())
))
else:
return not any((
self.model().item(i, 0) or self.indexWidget(self.model().index(i, 0))
for i in range(self.model().rowCount())
))
def is_visible(self):
return any((
not self.isRowHidden(i)
for i in range(self.model().rowCount())
))
def is_selected(self):
return bool(self.selectionModel().selectedIndexes())
def get_header_labels_hor(self):
return (
self.model().headerData(row, Qt.Horizontal)
for row in range(self.model().columnCount())
)
def get_header_labels_vert(self):
return (
self.model().headerData(col, Qt.Vertical)
for col in range(self.model().rowCount())
)
def find_header_hor(self, text):
return list(self.get_header_labels_hor()).index(text)
def find_header_vert(self, text):
return list(self.get_header_labels_vert()).index(text)
def find_headers_hor(self, text):
return [
i
for i, header in enumerate(self.get_header_labels_hor())
if text in header
]
def find_headers_vert(self, text):
return [
i
for i, header in enumerate(self.get_header_labels_vert())
if text in header
]
def find_header(self, text):
if self.header_orientation == 'hor':
return self.find_header_hor(text = text)
else:
return self.find_header_vert(text = text)
def find_headers(self, text):
if self.header_orientation == 'hor':
return self.find_headers_hor(text = text)
else:
return self.find_headers_vert(text = text)
def add_header_hor(self, label):
self.add_headers_hor(labels = [label])
def add_header_vert(self, label):
self.add_headers_vert(labels = [label])
def add_headers_hor(self, labels):
self.ins_headers_hor(i = self.model().columnCount(), labels = labels)
def add_headers_vert(self, labels):
self.ins_headers_vert(i = self.model().rowCount(), labels = labels)
def ins_header_hor(self, i, label):
self.ins_headers_hor(i = i, labels = [label])
def ins_header_vert(self, i, label):
self.ins_headers_vert(i = i, labels = [label])
def ins_headers_hor(self, i, labels):
headers = list(self.get_header_labels_hor())
headers[i:i] = labels
self.model().setHorizontalHeaderLabels(headers)
def ins_headers_vert(self, i, labels):
headers = list(self.get_header_labels_vert())
headers[i:i] = labels
self.model().setVerticalHeaderLabels(headers)
def get_selected_rows(self, visible_only = False):
selected_rows = sorted({index.row() for index in self.selectionModel().selectedIndexes()})
if visible_only:
return [row for row in selected_rows if not self.isRowHidden(row)]
else:
return selected_rows
def get_selected_cols(self, visible_only = False):
selected_col = sorted({index.column() for index in self.selectionModel().selectedIndexes()})
if visible_only:
return [col for col in selected_col if not self.isColumnHidden(col)]
else:
return selected_col
def _add_row(self, row = None, texts = None):
if texts is None:
texts = self.defaults_row
if self.is_empty():
self.clr_table(0)
if row is None:
self.model().appendRow([QStandardItem(text) for text in texts])
else:
self.model().insertRow(row, [QStandardItem(text) for text in texts])
self.model().itemChanged.emit(QStandardItem())
def add_row(self, texts = None):
self._add_row(texts = texts)
self.setCurrentIndex(self.model().index(self.model().rowCount() - 1, 0))
def ins_row(self, texts = None):
row = self.get_selected_rows()[0]
self._add_row(row = row, texts = texts)
self.setCurrentIndex(self.model().index(row, 0))
def del_row(self):
for i in reversed(self.get_selected_rows()):
self.model().removeRow(i)
self.model().itemChanged.emit(QStandardItem())
def clr_table(self, num_headers = 1):
self.model().clear()
if self.header_orientation == 'hor':
self.model().setHorizontalHeaderLabels(self.headers)
self.model().setRowCount(num_headers)
else:
self.model().setVerticalHeaderLabels(self.headers)
self.model().setColumnCount(num_headers)
self.model().itemChanged.emit(QStandardItem())
# Export visible rows only
@wl_misc.log_time
def exp_selected_cells(self):
return self.exp_all_cells(rows_to_exp = self.get_selected_rows(visible_only = True))
@wl_misc.log_time
def exp_all_cells(self, rows_to_exp = None):
caption = _tr('wl_tables', 'Export Table')
default_dir = self.main.settings_custom['general']['exp']['tables']['default_path']
default_type = self.main.settings_custom['general']['exp']['tables']['default_type']
default_ext = re.search(r'(?<=\(\*\.)[a-zA-Z0-9]+(?=[;\)])', default_type).group()
# Errors (Search terms, stop word lists, file checking, etc.)
if self.tab == 'err':
file_path, file_type = QFileDialog.getSaveFileName(
parent = self,
caption = caption,
directory = os.path.join(wl_checks_misc.check_dir(default_dir), f'wordless_error.{default_ext}'),
filter = ';;'.join(self.main.settings_global['file_types']['exp_tables']),
initialFilter = default_type
)
# Concordancer (with zapping)
elif self.tab == 'concordancer' and self.main.settings_custom['concordancer']['zapping_settings']['zapping']:
file_path, file_type = QFileDialog.getSaveFileName(
parent = self,
caption = caption,
directory = os.path.join(wl_checks_misc.check_dir(default_dir), f'wordless_results_{self.tab}.docx'),
filter = ';;'.join(self.main.settings_global['file_types']['exp_tables_concordancer_zapping']),
)
# Concordancer (without zapping) & Parallel Concordancer
elif self.tab in ['concordancer', 'concordancer_parallel']:
file_path, file_type = QFileDialog.getSaveFileName(
parent = self,
caption = caption,
directory = os.path.join(wl_checks_misc.check_dir(default_dir), f'wordless_results_{self.tab}.{default_ext}'),
filter = ';;'.join(self.main.settings_global['file_types']['exp_tables_concordancer']),
initialFilter = default_type
)
# Other modules
else:
file_path, file_type = QFileDialog.getSaveFileName(
parent = self,
caption = caption,
directory = os.path.join(wl_checks_misc.check_dir(default_dir), f'wordless_results_{self.tab}.{default_ext}'),
filter = ';;'.join(self.main.settings_global['file_types']['exp_tables']),
initialFilter = default_type
)
if file_path:
dialog_progress = wl_dialogs_misc.Wl_Dialog_Progress(self.main, text = _tr('wl_tables', 'Exporting table...'))
worker_exp_table = Wl_Worker_Exp_Table(
self.main,
dialog_progress = dialog_progress,
update_gui = self.update_gui_exp,
table = self,
file_path = file_path,
file_type = file_type,
rows_to_exp = (
rows_to_exp or
# Export visible rows only
[row for row in range(self.model().rowCount()) if not self.isRowHidden(row)]
)
)
thread_exp_table = wl_threading.Wl_Thread(worker_exp_table)
thread_exp_table.start_worker()
return ''
# Do not log time if the export dialog is closed
else:
return 'skip_logging_time'
def update_gui_exp(self, err_msg, file_path):
if not err_msg:
self.results_saved = True
wl_checks_work_area.check_err_exp_table(self.main, err_msg, file_path)
class Wl_Worker_Exp_Table(wl_threading.Wl_Worker):
worker_done = pyqtSignal(str, str)
def run(self):
try:
if 'headers_int' not in self.table.__dict__:
self.table.headers_int = []
if 'headers_float' not in self.table.__dict__:
self.table.headers_float = []
if 'headers_pct' not in self.table.__dict__:
self.table.headers_pct = []
settings_concordancer = self.main.settings_custom['concordancer']['zapping_settings']
len_rows = len(self.rows_to_exp)
# Export visible columns only
cols = [col for col in range(self.table.model().columnCount()) if not self.table.isColumnHidden(col)]
# CSV files
if '*.csv' in self.file_type:
encoding = self.main.settings_custom['general']['exp']['tables']['default_encoding']
with open(self.file_path, 'w', encoding = encoding, newline = '') as f:
csv_writer = csv.writer(f)
if self.table.header_orientation == 'hor':
# Horizontal headers
headers_hor = [
self.table.model().horizontalHeaderItem(col).text()
for col in cols
]
csv_writer.writerow(self.clean_text_csv(headers_hor))
# Cells
for i, row in enumerate(self.rows_to_exp):
row_to_exp = []
for col in cols:
if self.table.model().item(row, col):
cell_text = self.table.model().item(row, col).text()
else:
cell_text = self.table.indexWidget(self.table.model().index(row, col)).text()
cell_text = wl_nlp_utils.html_to_text(cell_text)
row_to_exp.append(cell_text)
csv_writer.writerow(self.clean_text_csv(row_to_exp))
self.progress_updated.emit(self.tr('Exporting table... ({} / {})').format(i + 1, len_rows))
# Profiler
else:
# Horizontal headers
headers_hor = [
self.table.model().horizontalHeaderItem(col).text()
for col in cols
]
csv_writer.writerow([''] + self.clean_text_csv(headers_hor))
# Vertical headers and cells
for i, row in enumerate(self.rows_to_exp):
row_to_exp = [self.table.model().verticalHeaderItem(row).text()]
for col in cols:
row_to_exp.append(self.table.model().item(row, col).text())
csv_writer.writerow(self.clean_text_csv(row_to_exp))
self.progress_updated.emit(self.tr('Exporting table... ({} / {})').format(i + 1, len_rows))
# Excel workbooks
elif '*.xlsx' in self.file_type:
workbook = openpyxl.Workbook()
worksheet = workbook.active
dpi_horizontal = QApplication.primaryScreen().logicalDotsPerInchX()
dpi_vertical = QApplication.primaryScreen().logicalDotsPerInchY()
match self.table.tab:
case 'concordancer':
freeze_panes = 'A2'
# Left, Node, Right
cols_labels = [0, 1, 2]
cols_table_items = []
case 'concordancer_parallel':
freeze_panes = 'A2'
cols_labels = []
# Parallel Unit No. (%)
cols_table_items = [0, 1]
case 'dependency_parser':
freeze_panes = 'A2'
# Sentence
cols_labels = [5]
cols_table_items = []
case _:
freeze_panes = 'B2'
cols_labels = []
cols_table_items = []
worksheet.freeze_panes = freeze_panes
if self.table.header_orientation == 'hor':
# Horizontal headers
for col_cell, col_item in enumerate(cols):
cell = worksheet.cell(1, 1 + col_cell)
cell.value = self.table.model().horizontalHeaderItem(col_item).text()
self.style_header_hor(cell)
worksheet.column_dimensions[openpyxl.utils.get_column_letter(1 + col_cell)].width = self.table.horizontalHeader().sectionSize(col_item) / dpi_horizontal * 13 + 3
# Cells
for row_cell, row_item in enumerate(self.rows_to_exp):
for col_cell, col_item in enumerate(cols):
cell = worksheet.cell(2 + row_cell, 1 + col_cell)
if (
(
cols_labels
and not cols_table_items
and col_item in cols_labels
) or (
not cols_labels
and cols_table_items
and col_item not in cols_table_items
)
):
cell_val = self.table.indexWidget(self.table.model().index(row_item, col_item)).text()
cell_val = self.remove_invalid_xml_chars(cell_val)
cell.value = cell_val
self.style_cell_rich_text(cell, self.table.indexWidget(self.table.model().index(row_item, col_item)))
else:
cell_val = self.table.model().item(row_item, col_item).text()
cell_val = self.remove_invalid_xml_chars(cell_val)
cell.value = cell_val
self.style_cell(cell, self.table.model().item(row_item, col_item))
self.progress_updated.emit(self.tr('Exporting table... ({} / {})').format(row_cell + 1, len_rows))
# Profiler
else:
# Horizontal headers
for col_cell, col_item in enumerate(cols):
cell = worksheet.cell(1, 2 + col_cell)
cell.value = self.table.model().horizontalHeaderItem(col_item).text()
self.style_header_hor(cell)
worksheet.column_dimensions[openpyxl.utils.get_column_letter(2 + col_cell)].width = self.table.horizontalHeader().sectionSize(col_item) / dpi_horizontal * 13 + 3
worksheet.column_dimensions[openpyxl.utils.get_column_letter(1)].width = self.table.verticalHeader().width() / dpi_horizontal * 13 + 3
# Vertical headers
for row_cell, row_item in enumerate(self.rows_to_exp):
cell = worksheet.cell(2 + row_cell, 1)
cell.value = self.table.model().verticalHeaderItem(row_item).text()
self.style_header_vert(cell)
# Cells
for row_cell, row_item in enumerate(self.rows_to_exp):
for col_cell, col_item in enumerate(cols):
cell = worksheet.cell(2 + row_cell, 2 + col_cell)
cell_val = self.table.model().item(row_item, col_item).text()
cell_val = self.remove_invalid_xml_chars(cell_val)
cell.value = cell_val
self.style_cell(cell, self.table.model().item(row_item, col_item))
self.progress_updated.emit(self.tr('Exporting table... ({} / {})').format(row_cell + 1, len_rows))
# Row height
worksheet.row_dimensions[1].height = self.table.horizontalHeader().height() / dpi_vertical * 72
for i, _ in enumerate(worksheet.rows):
worksheet.row_dimensions[2 + i].height = self.table.verticalHeader().sectionSize(0) / dpi_vertical * 72
self.progress_updated.emit(self.tr('Saving file...'))
workbook.save(self.file_path)
elif '*.docx' in self.file_type:
doc = docx.Document()
# Concordancer
if self.table.tab == 'concordancer':
outputs = []
for i, row in enumerate(self.rows_to_exp):
para_text = []
for col in range(3):
para_text.append(self.table.indexWidget(self.table.model().index(row, col)).text().strip())
# Zapping
if settings_concordancer['zapping']:
# Node
para_text[1] = settings_concordancer['placeholder'] * settings_concordancer['replace_keywords_with']
outputs.append([' '.join(para_text), self.table.indexWidget(self.table.model().index(row, col))])
if settings_concordancer['zapping']:
# Randomize outputs
if settings_concordancer['randomize_outputs']:
random.shuffle(outputs)
# Assign line numbers
if settings_concordancer['add_line_nums']:
for i, _ in enumerate(outputs):
outputs[i][0] = f'{i + 1}. ' + outputs[i][0]
for i, (para_text, item) in enumerate(outputs):
para = self.add_para(doc)
self.style_para_rich_text(para, para_text, item)
self.progress_updated.emit(self.tr('Exporting table... ({} / {})').format(i + 1, len_rows))
self.progress_updated.emit(self.tr('Saving file...'))
# Parallel Concordancer
elif self.table.tab == 'concordancer_parallel':
for i, row in enumerate(self.rows_to_exp):
if i > 0:
self.add_para(doc)
for col in range(2, self.table.model().columnCount()):
para_text = self.table.indexWidget(self.table.model().index(row, col)).text().strip()
para = self.add_para(doc)
self.style_para_rich_text(para, para_text, self.table.indexWidget(self.table.model().index(row, col)))
self.progress_updated.emit(self.tr('Exporting table... ({} / {})').format(i + 1, len_rows))
# Add the last empty paragraph
self.add_para(doc)
doc.save(self.file_path)
self.main.settings_custom['general']['exp']['tables']['default_path'] = wl_paths.get_normalized_dir(self.file_path)
self.main.settings_custom['general']['exp']['tables']['default_type'] = self.file_type
err_msg = ''
except PermissionError:
err_msg = 'permission_err'
except Exception: # pylint: disable=broad-exception-caught
err_msg = traceback.format_exc()
self.worker_done.emit(err_msg, self.file_path)
# Clean text before writing to CSV files
def clean_text_csv(self, items):
for i, item in enumerate(items):
items[i] = item.replace('\n', ' ')
items[i] = re.sub(r'\s+', ' ', items[i])
items[i] = items[i].strip()
return items
# Remove invalid XML characters
def remove_invalid_xml_chars(self, text):
# openpyxl.cell.cell.ILLEGAL_CHARACTERS_RE is not complete
# Reference: https://www.w3.org/TR/xml/#charsets
return re.sub(r'[^\u0009\u000A\u000D\u0020-\uD7FF\uE000-\uFFFD\U00010000-\U0010FFFF]+', '', text)
def style_header(self, cell):
cell.font = openpyxl.styles.Font(
name = self.main.settings_custom['general']['ui_settings']['font_family'],
size = self.main.settings_custom['general']['ui_settings']['font_size'],
bold = True,
color = 'FFFFFF'
)
def style_header_hor(self, cell):
self.style_header(cell)
# Headers
if self.table.header_orientation == 'hor':
cell.fill = openpyxl.styles.PatternFill(
fill_type = 'solid',
fgColor = '5C88C5'
)
# File names
elif self.table.header_orientation == 'vert':
cell.fill = openpyxl.styles.PatternFill(
fill_type = 'solid',
fgColor = '888888'
)
cell.alignment = openpyxl.styles.Alignment(
horizontal = 'center',
vertical = 'center',
wrap_text = True
)
def style_header_vert(self, cell):
self.style_header(cell)
# Line numbers
if self.table.header_orientation == 'hor':
cell.fill = openpyxl.styles.PatternFill(
fill_type = 'solid',
fgColor = '888888'
)
cell.alignment = openpyxl.styles.Alignment(
horizontal = 'right',
vertical = 'center',
wrap_text = True
)
# Headers
elif self.table.header_orientation == 'vert':
cell.fill = openpyxl.styles.PatternFill(
fill_type = 'solid',
fgColor = '5C88C5'
)
cell.alignment = openpyxl.styles.Alignment(
horizontal = 'left',
vertical = 'center',
wrap_text = True
)
def style_cell_alignment(self, cell, item):
if isinstance(item, QStandardItem):
alignment = item.textAlignment()
else:
alignment = item.alignment()
if alignment & Qt.AlignLeft == Qt.AlignLeft:
alignment_hor = 'left'
elif alignment & Qt.AlignRight == Qt.AlignRight:
alignment_hor = 'right'
elif alignment & Qt.AlignHCenter == Qt.AlignHCenter:
alignment_hor = 'center'
elif alignment & Qt.AlignJustify == Qt.AlignJustify:
alignment_hor = 'justify'
# Default
else:
alignment_hor = 'left'
if alignment & Qt.AlignTop == Qt.AlignTop:
alignment_vert = 'top'
elif alignment & Qt.AlignBottom == Qt.AlignBottom:
alignment_vert = 'bottom'
elif alignment & Qt.AlignVCenter == Qt.AlignVCenter:
alignment_vert = 'center'
# Not sure
elif alignment & Qt.AlignBaseline == Qt.AlignBaseline:
alignment_vert = 'justify'
# Default
else:
alignment_vert = 'center'
cell.alignment = openpyxl.styles.Alignment(
horizontal = alignment_hor,
vertical = alignment_vert,
wrap_text = True
)
def style_cell(self, cell, item):
# Modify number format
val = cell.value
try:
if val[-1] == '%':
cell.value = float(val[:-1]) / 100
else:
cell.value = float(val)
if val[-1] == '%':
precision_pcts = self.main.settings_custom['tables']['precision_settings']['precision_pcts']
if precision_pcts:
cell.number_format = '0.' + '0' * precision_pcts + '%'
else:
cell.number_format = '0%'
else:
i_decimal_point = val.find('.')
if i_decimal_point > -1:
cell.number_format = '0.' + '0' * (len(val) - i_decimal_point - 1)
else:
cell.number_format = '0'
# Skip text
except ValueError:
pass
font_family = item.font().family()
if font_family != 'Consolas':
font_family = self.main.settings_custom['general']['ui_settings']['font_family']
cell.font = openpyxl.styles.Font(
name = font_family,
size = self.main.settings_custom['general']['ui_settings']['font_size'],
bold = item.font().bold(),
italic = item.font().italic()
)
self.style_cell_alignment(cell, item)
def style_cell_rich_text(self, cell, item):
rich_texts = []
font_family = item.font().family()
if font_family != 'Consolas':
font_family = self.main.settings_custom['general']['ui_settings']['font_family']
# Trick the parser to force it to always wrap HTML with <html><body><p></p></body></html>
soup = bs4.BeautifulSoup(' ' + cell.value, features = 'lxml')
for html in soup.body.p.contents:
if isinstance(html, bs4.element.Tag):
text = html.text.strip()
else:
text = html.strip()
if text:
if isinstance(html, bs4.element.Tag) and html.has_attr('style'):
style = html['style']
re_color = re.search(r'(?<=color: #)([0-9a-fA-F]{3}|[0-9a-fA-F]{6})(?=;)', style)
if re_color:
color = re_color.group()
# 3-digit color shorthand
if len(color) == 3:
color = color[0] * 2 + color[1] * 2 + color[2] * 2
else:
color = '000000'
bold = 'font-weight: bold;' in style
italic = 'font-style: italic;' in style
rich_texts.append(openpyxl.cell.rich_text.TextBlock(
openpyxl.cell.text.InlineFont(
rFont = font_family,
sz = self.main.settings_custom['general']['ui_settings']['font_size'],
b = bold,
i = italic,
# 6/8-digit aRGB hex values without "#"
color = color
),
text + ' '
))
else:
rich_texts.append(openpyxl.cell.rich_text.TextBlock(
openpyxl.cell.text.InlineFont(
rFont = font_family,
sz = self.main.settings_custom['general']['ui_settings']['font_size'],
color = '000000'
),
text + ' '
))
if rich_texts:
# Remove trailing space after the last part of the text
rich_texts[-1].text = rich_texts[-1].text.strip()
cell.value = openpyxl.cell.rich_text.CellRichText(rich_texts)
self.style_cell_alignment(cell, item)
def style_cell_concordancer_node(self, cell, item):
cell.font = openpyxl.styles.Font(
name = item.font().family(),
size = self.main.settings_custom['general']['ui_settings']['font_size'],
bold = True,
color = 'FF0000'
)
self.style_cell_alignment(cell, item)
def add_para(self, doc):
para = doc.add_paragraph()
para.alignment = docx.enum.text.WD_ALIGN_PARAGRAPH.JUSTIFY
self.style_para_spacing(para)
return para
def style_para_rich_text(self, para, para_text, item):
font_family = item.font().family()
if font_family != 'Consolas':
font_family = self.main.settings_custom['general']['ui_settings']['font_family']
# Trick the parser to force it always wrap HTML with <html><body><p></p></body></html>
soup = bs4.BeautifulSoup(' ' + para_text, features = 'lxml')
for html in soup.body.p.contents:
if isinstance(html, bs4.element.Tag):
text = html.text.strip()
else:
text = html.strip()
if text:
if para.text:
para.add_run(' ')
run = para.add_run(text)
run.font.name = self.main.settings_custom['general']['ui_settings']['font_family']
run.font.size = docx.shared.Pt(self.main.settings_custom['general']['ui_settings']['font_size'])
if isinstance(html, bs4.element.Tag) and html.has_attr('style'):
style = html['style']
re_color = re.search(r'(?<=color: #)([0-9a-fA-F]{3}|[0-9a-fA-F]{6})(?=;)', style)
if re_color:
color = re_color.group()
# 3-digit color shorthand
if len(color) == 3:
color = color[0] * 2 + color[1] * 2 + color[2] * 2
else:
color = '000000'
bold = 'font-weight: bold;' in style
italic = 'font-style: italic;' in style
run.bold = bold
run.italic = italic
run.font.color.rgb = docx.shared.RGBColor.from_string(color)
else:
run.font.color.rgb = docx.shared.RGBColor.from_string('000000')
self.style_para_spacing(para)
def style_para_spacing(self, para):
para.paragraph_format.space_before = docx.shared.Pt(0)
para.paragraph_format.space_after = docx.shared.Pt(0)
para.paragraph_format.line_spacing = 1.5
class Wl_Table_Add_Ins_Del_Clr(Wl_Table):
def __init__(self, parent, headers, col_edit = None):
super().__init__(
parent = parent,
headers = headers,
editable = True,
drag_drop = True
)
self.col_edit = col_edit
self.button_add = QPushButton(_tr('wl_tables', 'Add'), self)
self.button_ins = QPushButton(_tr('wl_tables', 'Insert'), self)
self.button_del = QPushButton(_tr('wl_tables', 'Remove'), self)
self.button_clr = QPushButton(_tr('wl_tables', 'Clear'), self)
self.button_add.clicked.connect(lambda: self.add_row())
self.button_ins.clicked.connect(lambda: self.ins_row())
self.button_del.clicked.connect(lambda: self.del_row())
self.button_clr.clicked.connect(lambda: self.clr_table(0))
def item_changed(self):
if not self.is_empty():
self.button_clr.setEnabled(True)
else:
self.button_clr.setEnabled(False)
super().item_changed()
def selection_changed(self):
if self.selectionModel().selectedIndexes():
self.button_ins.setEnabled(True)
self.button_del.setEnabled(True)
else:
self.button_ins.setEnabled(False)
self.button_del.setEnabled(False)
def add_row(self, texts = None):
super().add_row(texts = texts)
if self.col_edit is not None:
self.edit(self.model().index(self.model().rowCount() - 1, self.col_edit))
def ins_row(self, texts = None):
super().ins_row(texts = texts)
if self.col_edit is not None:
self.edit(self.model().index(self.get_selected_rows()[0], self.col_edit))
class Wl_Table_Item(QStandardItem):
def read_data(self):
if (
self.column() in self.model().table.headers_int
or self.column() in self.model().table.headers_float
or self.column() in self.model().table.headers_pct
):
return self.val
else:
return self.text()
def __lt__(self, other):
return self.read_data() < other.read_data()
class Wl_Table_Item_Err(QStandardItem):
def read_data(self):
return self.text()
def __lt__(self, other):
return self.read_data() < other.read_data()
class Wl_Table_Data(Wl_Table):
def __init__(
self, main, tab,
headers, header_orientation = 'hor',
headers_int = None, headers_float = None,
headers_pct = None, headers_cum = None,
cols_breakdown_file = None, cols_breakdown_span_position = None,
enable_sorting = False, generate_fig = True
):
super().__init__(
main, headers, header_orientation,
editable = False,
drag_drop = False
)
self.tab = tab
self.headers_int_old = headers_int or {}
self.headers_float_old = headers_float or {}
self.headers_pct_old = headers_pct or {}
self.headers_cum_old = headers_cum or {}
self.cols_breakdown_file_old = cols_breakdown_file or {}
self.cols_breakdown_span_position_old = cols_breakdown_span_position or {}
self.enable_sorting = enable_sorting
if enable_sorting:
self.setSortingEnabled(True)
if header_orientation == 'hor':
self.horizontalHeader().sortIndicatorChanged.connect(self.sorting_changed)
else:
self.verticalHeader().sortIndicatorChanged.connect(self.sorting_changed)
self.model().itemChanged.connect(self.item_changed)
self.selectionModel().selectionChanged.connect(self.selection_changed)
self.button_generate_table = QPushButton(_tr('Wl_Table_Data', 'Generate table'), self)
self.button_generate_fig = QPushButton(_tr('Wl_Table_Data', 'Generate figure'), self)
self.button_exp_selected_cells = QPushButton(_tr('Wl_Table_Data', 'Export selected cells...'), self)
self.button_exp_all_cells = QPushButton(_tr('Wl_Table_Data', 'Export all cells...'), self)
self.button_clr_table = QPushButton(_tr('Wl_Table_Data', 'Clear table'), self)
if not generate_fig:
self.button_generate_fig.hide()
self.button_generate_table.clicked.connect(lambda: self.generate_table())
self.button_generate_fig.clicked.connect(lambda: self.generate_fig())
self.button_exp_selected_cells.clicked.connect(lambda: self.exp_selected_cells())
self.button_exp_all_cells.clicked.connect(lambda: self.exp_all_cells())
self.button_clr_table.clicked.connect(lambda: self.clr_table(confirm = True))
self.main.wl_file_area.table_files.model().itemChanged.connect(self.file_changed)
self.clr_table()
self.file_changed()
def item_changed(self):
if not self.is_empty() and self.is_visible():
self.button_exp_all_cells.setEnabled(True)
else:
self.button_exp_all_cells.setEnabled(False)
if not self.is_empty():
self.button_clr_table.setEnabled(True)
else:
self.button_clr_table.setEnabled(False)
super().item_changed()
self.selectionModel().selectionChanged.emit(QItemSelection(), QItemSelection())
def selection_changed(self):
# Enable "Export selected cells" only if any visible rows are selected
if not self.is_empty() and self.get_selected_rows(visible_only = True):
self.button_exp_selected_cells.setEnabled(True)
else:
self.button_exp_selected_cells.setEnabled(False)
def sorting_changed(self):
if not self.is_empty():
if self.tr('Rank') in self.get_header_labels_hor():
self.update_ranks()
if self.table_settings['show_cum_data']:
self.toggle_cum_data()
def file_changed(self):
if list(self.main.wl_file_area.get_selected_files()):
self.button_generate_table.setEnabled(True)
self.button_generate_fig.setEnabled(True)
else:
self.button_generate_table.setEnabled(False)
self.button_generate_fig.setEnabled(False)
def add_header_hor(
self, label,
is_int = False, is_float = False,
is_pct = False, is_cum = False,
is_breakdown_file = False, is_breakdown_span_position = False
):
self.add_headers_hor(
labels = [label],
is_int = is_int, is_float = is_float,
is_pct = is_pct, is_cum = is_cum,
is_breakdown_file = is_breakdown_file, is_breakdown_span_position = is_breakdown_span_position
)
def add_header_vert(
self, label,
is_int = False, is_float = False,
is_pct = False, is_cum = False
):
self.add_headers_vert(
labels = [label],
is_int = is_int, is_float = is_float,
is_pct = is_pct, is_cum = is_cum
)
def add_headers_hor(
self, labels,
is_int = False, is_float = False,
is_pct = False, is_cum = False,
is_breakdown_file = False, is_breakdown_span_position = False
):
self.ins_headers_hor(
i = self.model().columnCount(), labels = labels,
is_int = is_int, is_float = is_float,
is_pct = is_pct, is_cum = is_cum,
is_breakdown_file = is_breakdown_file, is_breakdown_span_position = is_breakdown_span_position
)
def add_headers_vert(
self, labels,
is_int = False, is_float = False,
is_pct = False, is_cum = False
):
self.ins_headers_vert(
i = self.model().rowCount(), labels = labels,
is_int = is_int, is_float = is_float,
is_pct = is_pct, is_cum = is_cum,
)
def ins_header_hor(
self, i, label,
is_int = False, is_float = False,
is_pct = False, is_cum = False,
is_breakdown_file = False, is_breakdown_span_position = False
):
self.ins_headers_hor(
i = i, labels = [label],
is_int = is_int, is_float = is_float,
is_pct = is_pct, is_cum = is_cum,
is_breakdown_file = is_breakdown_file, is_breakdown_span_position = is_breakdown_span_position
)
def ins_header_vert(
self, i, label,
is_int = False, is_float = False,
is_pct = False, is_cum = False
):
self.ins_headers_vert(
i = i, labels = [label],
is_int = is_int, is_float = is_float,
is_pct = is_pct, is_cum = is_cum
)
def ins_headers_hor(
self, i, labels,
is_int = False, is_float = False,
is_pct = False, is_cum = False,
is_breakdown_file = False, is_breakdown_span_position = False
):
# Re-calculate column indexes
if self.header_orientation == 'hor':
headers_int = [
self.model().horizontalHeaderItem(col).text()
for col in self.headers_int
]
headers_float = [
self.model().horizontalHeaderItem(col).text()
for col in self.headers_float
]
headers_pct = [
self.model().horizontalHeaderItem(col).text()
for col in self.headers_pct
]
headers_cum = [
self.model().horizontalHeaderItem(col).text()
for col in self.headers_cum
]
cols_breakdown_file = [
self.model().horizontalHeaderItem(col).text()
for col in self.cols_breakdown_file
]
cols_breakdown_span_position = [
self.model().horizontalHeaderItem(col).text()
for col in self.cols_breakdown_span_position
]
super().ins_headers_hor(i, labels)
if self.header_orientation == 'hor':
if is_int:
headers_int.extend(labels)
if is_float:
headers_float.extend(labels)
if is_pct:
headers_pct.extend(labels)
if is_cum:
headers_cum.extend(labels)
self.headers_int = {self.find_header_hor(header) for header in headers_int}
self.headers_float = {self.find_header_hor(header) for header in headers_float}
self.headers_pct = {self.find_header_hor(header) for header in headers_pct}
self.headers_cum = {self.find_header_hor(header) for header in headers_cum}
if is_breakdown_file:
cols_breakdown_file.extend(labels)
if is_breakdown_span_position:
cols_breakdown_span_position.extend(labels)
self.cols_breakdown_file = {
self.find_header_hor(header)
for header in cols_breakdown_file
}
self.cols_breakdown_span_position = {
self.find_header_hor(header)
for header in cols_breakdown_span_position
}
def ins_headers_vert(
self, i, labels,
is_int = False, is_float = False,
is_pct = False, is_cum = False
):
# Re-calculate row indexes
headers_int = [self.model().verticalHeaderItem(row).text() for row in self.headers_int]
headers_float = [self.model().verticalHeaderItem(row).text() for row in self.headers_float]
headers_pct = [self.model().verticalHeaderItem(row).text() for row in self.headers_pct]
headers_cum = [self.model().verticalHeaderItem(row).text() for row in self.headers_cum]
super().ins_headers_vert(i, labels)
if is_int:
headers_int.extend(labels)
if is_float:
headers_float.extend(labels)
if is_pct:
headers_pct.extend(labels)
if is_cum:
headers_cum.extend(labels)
self.headers_int = {self.find_header_vert(header) for header in headers_int}
self.headers_float = {self.find_header_vert(header) for header in headers_float}
self.headers_pct = {self.find_header_vert(header) for header in headers_pct}
self.headers_cum = {self.find_header_vert(header) for header in headers_cum}
def set_item_num(self, row, col, val, total = -1):
if self.header_orientation == 'hor':
header = col
else:
header = row
# Integers
if header in self.headers_int:
val = int(val)
item = Wl_Table_Item(str(val))
# Floats
elif header in self.headers_float:
val = float(val)
precision = self.main.settings_custom['tables']['precision_settings']['precision_decimals']
item = Wl_Table_Item(f'{val:.{precision}f}')
# Percentages
elif header in self.headers_pct:
if total > 0:
val = val / total
# Handle zero division error
elif total == 0:
val = 0
# Set values directly
elif total == -1:
val = float(val)
precision = self.main.settings_custom['tables']['precision_settings']['precision_pcts']
item = Wl_Table_Item(f'{val:.{precision}%}')
item.val = val
item.setFont(QFont('Consolas'))
item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.model().setItem(row, col, item)
def set_item_num_val(self, row, col, val):
if self.header_orientation == 'hor':
header = col
else:
header = row
item = self.model().item(row, col)
# Integers
if header in self.headers_int:
item.setText(str(val))
# Floats
elif header in self.headers_float:
val = float(val)
precision = self.main.settings_custom['tables']['precision_settings']['precision_decimals']
item.setText(f'{val:.{precision}f}')
# Percentages
elif header in self.headers_pct:
precision = self.main.settings_custom['tables']['precision_settings']['precision_pcts']
item.setText(f'{val:.{precision}%}')
item.val = val
def set_item_p_val(self, row, col, val):
precision = self.main.settings_custom['tables']['precision_settings']['precision_p_vals']
item = Wl_Table_Item(f'{val:.{precision}f}')
item.val = val
item.setFont(QFont('Consolas'))
item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.model().setItem(row, col, item)
def set_item_err(self, row, col, text, alignment_hor = 'center'):
item = Wl_Table_Item_Err(text)
if alignment_hor == 'center':
alignment_hor = Qt.AlignHCenter
elif alignment_hor == 'left':
alignment_hor = Qt.AlignLeft
elif alignment_hor == 'right':
alignment_hor = Qt.AlignRight
item_font = QFont(self.main.settings_custom['general']['ui_settings']['font_family'])
item_font.setItalic(True)
item.setFont(item_font)
item.setTextAlignment(alignment_hor | Qt.AlignVCenter)
self.model().setItem(row, col, item)
def update_ranks(self):
data_prev = ''
rank_prev = 1
rank_next = 1
sort_section = self.horizontalHeader().sortIndicatorSection()
sort_order = self.horizontalHeader().sortIndicatorOrder()
col_rank = self.find_header_hor(self.tr('Rank'))
self.sortByColumn(sort_section, sort_order)
if sort_section != col_rank:
self.disable_updates()
for row in range(self.model().rowCount()):
if not self.isRowHidden(row):
data_cur = self.model().item(row, sort_section).read_data()
if self.main.settings_custom['tables']['rank_settings']['continue_numbering_after_ties']:
if data_cur == data_prev:
self.model().item(row, col_rank).val = rank_prev
self.model().item(row, col_rank).setText(str(rank_prev))
else:
self.model().item(row, col_rank).val = rank_next
self.model().item(row, col_rank).setText(str(rank_next))
rank_prev = rank_next
rank_next += 1
data_prev = data_cur
else:
if data_cur == data_prev:
self.model().item(row, col_rank).val = rank_prev
self.model().item(row, col_rank).setText(str(rank_prev))
else:
self.model().item(row, col_rank).val = rank_next
self.model().item(row, col_rank).setText(str(rank_next))
rank_prev = rank_next
rank_next += 1
data_prev = data_cur
self.enable_updates()
def toggle_pct_data(self):
self.disable_updates()
if self.header_orientation == 'hor':
if self.table_settings['show_pct_data']:
for col in self.headers_pct:
if (
col not in self.cols_breakdown_file
or self.table_settings['show_breakdown_file']
):
self.showColumn(col)
else:
for col in self.headers_pct:
self.hideColumn(col)
elif self.header_orientation == 'vert':
if self.table_settings['show_pct_data']:
for row in self.headers_pct:
self.showRow(row)
else:
for row in self.headers_pct:
self.hideRow(row)
self.enable_updates()
def toggle_pct_data_span_position(self):
self.disable_updates()
if self.header_orientation == 'hor':
if self.table_settings['show_pct_data']:
for col in self.headers_pct:
if (
(
col not in self.cols_breakdown_file
or self.table_settings['show_breakdown_file']
) and (
col not in self.cols_breakdown_span_position
or self.table_settings['show_breakdown_span_position']
)
):
self.showColumn(col)
else:
for col in self.headers_pct:
self.hideColumn(col)
elif self.header_orientation == 'vert':
if self.table_settings['show_pct_data']:
for row in self.headers_pct:
self.showRow(row)
else:
for row in self.headers_pct:
self.hideRow(row)
self.enable_updates()
def toggle_cum_data(self):
precision_decimals = self.main.settings_custom['tables']['precision_settings']['precision_decimals']
precision_pcts = self.main.settings_custom['tables']['precision_settings']['precision_pcts']
# Boost performance
if self.enable_sorting:
self.sortByColumn(self.horizontalHeader().sortIndicatorSection(), self.horizontalHeader().sortIndicatorOrder())
self.disable_updates()
self.setSortingEnabled(False)
if self.header_orientation == 'hor':
if self.table_settings['show_cum_data']:
for col in self.headers_cum:
val_cum = 0
# Integers
if col in self.headers_int:
for row in range(self.model().rowCount()):
if not self.isRowHidden(row):
item = self.model().item(row, col)
val_cum += item.val
item.setText(str(val_cum))
# Floats
elif col in self.headers_float:
for row in range(self.model().rowCount()):
if not self.isRowHidden(row):
item = self.model().item(row, col)
val_cum += item.val
item.setText(f'{val_cum:.{precision_decimals}}')
# Percentages
elif col in self.headers_pct:
for row in range(self.model().rowCount()):
if not self.isRowHidden(row):
item = self.model().item(row, col)
val_cum += item.val
item.setText(f'{val_cum:.{precision_pcts}%}')
else:
for col in self.headers_cum:
# Integers
if col in self.headers_int:
for row in range(self.model().rowCount()):
if not self.isRowHidden(row):
item = self.model().item(row, col)
item.setText(str(item.val))
# Floats
elif col in self.headers_float:
for row in range(self.model().rowCount()):
if not self.isRowHidden(row):
item = self.model().item(row, col)
item.setText(f'{item.val:.{precision_decimals}}')
# Percentages
elif col in self.headers_pct:
for row in range(self.model().rowCount()):
if not self.isRowHidden(row):
item = self.model().item(row, col)
item.setText(f'{item.val:.{precision_pcts}%}')
elif self.header_orientation == 'vert':
if self.table_settings['show_cum_data']:
for row in self.headers_cum:
val_cum = 0
for col in range(self.model().columnCount() - 1):
item = self.model().item(row, col)
if not self.isColumnHidden(col) and not isinstance(item, Wl_Table_Item_Err):
val_cum += item.val
# Integers
if row in self.headers_int:
item.setText(str(val_cum))
# Floats
elif row in self.headers_float:
item.setText(f'{val_cum:.{precision_decimals}}')
# Percentages
elif row in self.headers_pct:
item.setText(f'{val_cum:.{precision_pcts}%}')
else:
for row in self.headers_cum:
for col in range(self.model().columnCount() - 1):
item = self.model().item(row, col)
if not self.isColumnHidden(col) and not isinstance(item, Wl_Table_Item_Err):
# Integers
if row in self.headers_int:
item.setText(str(item.val))
# Floats
elif row in self.headers_float:
item.setText(f'{item.val:.{precision_decimals}}')
# Percentages
elif row in self.headers_pct:
item.setText(f'{item.val:.{precision_pcts}%}')
self.enable_updates()
if self.enable_sorting:
self.setSortingEnabled(True)
def toggle_breakdown_file(self):
self.disable_updates()
if self.table_settings['show_breakdown_file']:
for col in self.cols_breakdown_file:
if (
self.header_orientation == 'vert'
or col not in self.headers_pct
or self.table_settings['show_pct_data']
):
self.showColumn(col)
else:
for col in self.cols_breakdown_file:
self.hideColumn(col)
self.enable_updates()
def toggle_breakdown_file_span_position(self):
self.disable_updates()
if self.table_settings['show_breakdown_file']:
for col in self.cols_breakdown_file:
if (
(
self.header_orientation == 'vert'
or col not in self.headers_pct
or self.table_settings['show_pct_data']
) and (
col not in self.cols_breakdown_span_position
or self.table_settings['show_breakdown_span_position']
)
):
self.showColumn(col)
else:
for col in self.cols_breakdown_file:
self.hideColumn(col)
self.enable_updates()
def toggle_breakdown_span_position(self):
self.disable_updates()
if self.table_settings['show_breakdown_span_position']:
for col in self.cols_breakdown_span_position:
if (
(
self.header_orientation == 'vert'
or col not in self.headers_pct
or self.table_settings['show_pct_data']
) and (
col not in self.cols_breakdown_file
or self.table_settings['show_breakdown_file']
)
):
self.showColumn(col)
else:
for col in self.cols_breakdown_span_position:
self.hideColumn(col)
self.enable_updates()
def filter_table(self):
self.disable_updates()
for i, row_filter in enumerate(self.row_filters):
if row_filter:
self.showRow(i)
else:
self.hideRow(i)
self.enable_updates()
if self.tr('Rank') in self.get_header_labels_hor():
self.update_ranks()
if self.table_settings['show_cum_data']:
self.toggle_cum_data()
def generate_table(self):
pass
def generate_fig(self):
pass
def clr_table(self, num_headers = 1, confirm = False):
confirmed = True
# Ask for confirmation if results have not been exported
if confirm:
if not self.is_empty() and not self.results_saved:
dialog_clr_table = wl_dialogs_misc.Wl_Dialog_Clr_Table(self.main)
result = dialog_clr_table.exec_()
if result == QDialog.Rejected:
confirmed = False
if confirmed:
self.model().clear()
if self.header_orientation == 'hor':
self.horizontalHeader().blockSignals(True)
self.model().setColumnCount(len(self.headers))
self.model().setRowCount(num_headers)
self.model().setHorizontalHeaderLabels(self.headers)
self.horizontalHeader().blockSignals(False)
self.horizontalHeader().sectionCountChanged.emit(0, num_headers)
else:
self.verticalHeader().blockSignals(True)
self.model().setRowCount(len(self.headers))
self.model().setColumnCount(num_headers)
self.model().setVerticalHeaderLabels(self.headers)
self.verticalHeader().blockSignals(False)
self.verticalHeader().sectionCountChanged.emit(0, num_headers)
for i in range(self.model().rowCount()):
self.showRow(i)
for i in range(self.model().columnCount()):
self.showColumn(i)
self.headers_int = {self.find_header(header) for header in self.headers_int_old}
self.headers_float = {self.find_header(header) for header in self.headers_float_old}
self.headers_pct = {self.find_header(header) for header in self.headers_pct_old}
self.headers_cum = {self.find_header(header) for header in self.headers_cum_old}
self.cols_breakdown_file = {
self.find_header_hor(col)
for col in self.cols_breakdown_file_old
}
self.cols_breakdown_span_position = {
self.find_header_hor(col)
for col in self.cols_breakdown_span_position_old
}
self.results_saved = False
self.model().itemChanged.emit(QStandardItem())
return confirmed
# Avoid circular imports
from wordless.wl_results import wl_results_filter, wl_results_search, wl_results_sort # pylint: disable=wrong-import-position
class Wl_Table_Data_Search(Wl_Table_Data):
def __init__(
self, main, tab,
headers, header_orientation = 'hor',
headers_int = None, headers_float = None,
headers_pct = None, headers_cum = None,
cols_breakdown_file = None, cols_breakdown_span_position = None,
enable_sorting = False, generate_fig = True
):
super().__init__(
main, tab,
headers, header_orientation,
headers_int, headers_float,
headers_pct, headers_cum,
cols_breakdown_file, cols_breakdown_span_position,
enable_sorting, generate_fig
)
self.model().itemChanged.connect(self.results_changed)
self.label_num_results = QLabel()
self.button_results_search = wl_buttons.Wl_Button(_tr('wl_tables', 'Search in results'), self)
self.dialog_results_search = wl_results_search.Wl_Dialog_Results_Search(self.main, table = self)
self.button_results_search.setMinimumWidth(140)
self.button_generate_table.clicked.connect(self.dialog_results_search.clr_history)
self.button_results_search.clicked.connect(self.dialog_results_search.show)
self.results_changed()
def results_changed(self):
rows_visible = len([i for i in range(self.model().rowCount()) if not self.isRowHidden(i)])
if not self.is_empty() and rows_visible:
self.label_num_results.setText(_tr('wl_tables', 'Number of results: ') + str(rows_visible))
self.button_results_search.setEnabled(True)
else:
self.label_num_results.setText(_tr('wl_tables', 'Number of results: 0'))
self.button_results_search.setEnabled(False)
class Wl_Table_Data_Sort_Search(Wl_Table_Data):
def __init__(
self, main, tab,
headers, header_orientation = 'hor',
headers_int = None, headers_float = None,
headers_pct = None, headers_cum = None,
cols_breakdown_file = None, cols_breakdown_span_position = None,
enable_sorting = False, generate_fig = True
):
super().__init__(
main, tab,
headers, header_orientation,
headers_int, headers_float,
headers_pct, headers_cum,
cols_breakdown_file, cols_breakdown_span_position,
enable_sorting, generate_fig
)
self.model().itemChanged.connect(self.results_changed)
self.label_num_results = QLabel()
self.button_results_sort = wl_buttons.Wl_Button(_tr('wl_tables', 'Sort results'), self)
self.button_results_search = wl_buttons.Wl_Button(_tr('wl_tables', 'Search in results'), self)
self.dialog_results_sort = wl_results_sort.Wl_Dialog_Results_Sort_Concordancer(self.main, table = self)
self.dialog_results_search = wl_results_search.Wl_Dialog_Results_Search(self.main, table = self)
self.button_results_sort.setMinimumWidth(140)
self.button_results_search.setMinimumWidth(140)
self.button_generate_table.clicked.connect(self.dialog_results_search.clr_history)
self.button_results_sort.clicked.connect(self.dialog_results_sort.show)
self.button_results_search.clicked.connect(self.dialog_results_search.show)
self.results_changed()
def results_changed(self):
rows_visible = len([i for i in range(self.model().rowCount()) if not self.isRowHidden(i)])
if not self.is_empty() and rows_visible:
self.label_num_results.setText(_tr('wl_tables', 'Number of results: ') + str(rows_visible))
self.button_results_sort.setEnabled(True)
self.button_results_search.setEnabled(True)
else:
self.label_num_results.setText(_tr('wl_tables', 'Number of results: 0'))
self.button_results_sort.setEnabled(False)
self.button_results_search.setEnabled(False)
class Wl_Table_Data_Filter_Search(Wl_Table_Data):
def __init__(
self, main, tab,
headers, header_orientation = 'hor',
headers_int = None, headers_float = None,
headers_pct = None, headers_cum = None,
cols_breakdown_file = None, cols_breakdown_span_position = None,
enable_sorting = False, generate_fig = True
):
super().__init__(
main, tab,
headers, header_orientation,
headers_int, headers_float,
headers_pct, headers_cum,
cols_breakdown_file, cols_breakdown_span_position,
enable_sorting, generate_fig
)
self.model().itemChanged.connect(self.results_changed)
self.label_num_results = QLabel()
self.button_results_filter = wl_buttons.Wl_Button(_tr('wl_tables', 'Filter results'), self)
self.button_results_search = wl_buttons.Wl_Button(_tr('wl_tables', 'Search in results'), self)
self.dialog_results_search = wl_results_search.Wl_Dialog_Results_Search(self.main, table = self)
self.button_results_filter.setMinimumWidth(140)
self.button_results_search.setMinimumWidth(140)
self.button_generate_table.clicked.connect(self.dialog_results_search.clr_history)
self.button_results_filter.clicked.connect(self.results_filter_clicked)
self.button_results_search.clicked.connect(self.dialog_results_search.show)
self.results_changed()
def results_changed(self):
rows_visible = len([i for i in range(self.model().rowCount()) if not self.isRowHidden(i)])
if not self.is_empty():
self.label_num_results.setText(_tr('wl_tables', 'Number of results: ') + str(rows_visible))
self.button_results_filter.setEnabled(True)
else:
self.label_num_results.setText(_tr('wl_tables', 'Number of results: 0'))
self.button_results_filter.setEnabled(False)
if not self.is_empty() and rows_visible:
self.button_results_search.setEnabled(True)
else:
self.button_results_search.setEnabled(False)
def results_filter_clicked(self):
match self.tab:
case 'dependency_parser':
wl_dialog_results_filter = wl_results_filter.Wl_Dialog_Results_Filter_Dependency_Parser(
self.main,
table = self
)
case 'wordlist_generator' | 'ngram_generator':
wl_dialog_results_filter = wl_results_filter.Wl_Dialog_Results_Filter_Wordlist_Generator(
self.main,
table = self
)
case 'collocation_extractor' | 'colligation_extractor' | 'keyword_extractor':
wl_dialog_results_filter = wl_results_filter.Wl_Dialog_Results_Filter_Collocation_Extractor(
self.main,
table = self
)
wl_dialog_results_filter.show()