Separate WarningItem and HeaderContextMenu from MainWindow

This commit is contained in:
Andreas Tsouchlos 2024-01-04 19:34:01 +01:00
parent 393f654a57
commit 050f5f0ae4

View File

@ -12,12 +12,183 @@ from PyQt6.QtWidgets import QMainWindow, QPushButton, QHBoxLayout, QLabel, \
from banking_breakdown.ui.pandas_model import PandasModel
class WarningItem(QHBoxLayout):
def __init__(self, text: str, parent=None):
super(WarningItem, self).__init__()
self._warningIcon = QLabel()
pixmap = QPixmap("res/warning.png")
self._warningIcon.setPixmap(pixmap)
self._label = QLabel(text)
self._label.setWordWrap(True)
self.addWidget(self._warningIcon)
self.addWidget(self._label)
self.setStretch(0, 0)
self.setStretch(1, 1)
def hide(self):
self._label.hide()
self._warningIcon.hide()
class HeaderContextMenu(QMenu):
def __init__(self, column, pandas_model: PandasModel, callback=None,
parent=None):
super(HeaderContextMenu, self).__init__()
self._column = column
self._pandas_model = pandas_model
self._callback = callback
self._column_text \
= self._pandas_model.headerData(self._column,
Qt.Orientation.Horizontal)
# Define assign action
assign_menu = QMenu("Assign type", self)
assign_date_action = QAction("date", self)
assign_float_action = QAction("float", self)
assign_menu.addAction(assign_date_action)
assign_menu.addAction(assign_float_action)
assign_date_action.triggered.connect(self._assign_date_handler)
assign_float_action.triggered.connect(self._assign_float_handler)
# Define other actions
rename_action = QAction("Rename", self)
delete_action = QAction("Delete", self)
switch_action = QAction("Switch position", self)
rename_action.triggered.connect(self._rename_handler)
delete_action.triggered.connect(self._delete_handler)
switch_action.triggered.connect(self._switch_handler)
# Add actions to menu
self.addAction(rename_action)
self.addAction(delete_action)
self.addAction(switch_action)
self.addAction(assign_menu.menuAction())
def _rename_handler(self):
new_name, flag = QInputDialog.getText(self, "Rename column",
"New name:",
text=self._column_text)
if not flag:
return
if (new_name != self._column_text) and (new_name != ''):
df = self._pandas_model.get_dataframe()
df = df.rename(columns={self._column_text: new_name})
self._pandas_model.set_dataframe(df)
if self._callback:
self._callback()
def _delete_handler(self):
button = QMessageBox.question(self, "Delete column",
f"Are you sure you want to delete"
f" column '{self._column_text}'?")
if button == QMessageBox.StandardButton.Yes:
df = self._pandas_model.get_dataframe()
df = df.iloc[:, [j for j, c
in enumerate(df.columns) if j != self._column]]
self._pandas_model.set_dataframe(df)
if self._callback:
self._callback()
def _switch_handler(self):
df = self._pandas_model.get_dataframe()
columns = [column for column in df.columns
if column != self._column_text]
other_name, flag = QInputDialog.getItem(self, "Switch column position",
f"Switch position of colum"
f" '{self._column_text}' with:",
columns, editable=False)
if not flag:
return
column_titles = list(df.columns)
index1, index2 = column_titles.index(
self._column_text), column_titles.index(other_name)
column_titles[index1], column_titles[index2] \
= column_titles[index2], column_titles[index1]
df = df.reindex(columns=column_titles)
self._pandas_model.set_dataframe(df)
if self._callback:
self._callback()
def _assign_date_handler(self):
date_format, flag = QInputDialog.getText(self, "Format",
"Format:",
text="%d.%m.%Y")
if not flag:
return
df = self._pandas_model.get_dataframe()
try:
df[self._column_text] \
= pd.to_datetime(df[self._column_text], format=date_format)
except:
QMessageBox.warning(self, "No action performed",
"An error occurred.")
self._pandas_model.set_dataframe(df)
if self._callback:
self._callback()
def _assign_float_handler(self):
chars = ['.', ',']
decimal_sep, flag = QInputDialog.getItem(self, "Decimal separator",
"Decimal separator:",
chars, editable=False)
if not flag:
return
df = self._pandas_model.get_dataframe()
try:
if decimal_sep == ',':
df[self._column_text] \
= df[self._column_text].str.replace(',', '.').astype(float)
else:
df[self._column_text] = df[self._column_text].astype(float)
except:
QMessageBox.warning(self, "No action performed",
"An error occurred.")
self._pandas_model.set_dataframe(df)
if self._callback:
self._callback()
class MainWindow(QMainWindow):
def __init__(self):
super(MainWindow, self).__init__()
uic.loadUi("res/main_window.ui", self)
self.setContentsMargins(9, 9, 9, 9)
self._warnings = []
# Extract elements
self._warning_layout \
= self.findChild(QVBoxLayout, "warningLayout")
self._create_button \
@ -35,7 +206,15 @@ class MainWindow(QMainWindow):
self._action_save \
= self.findChild(QAction, "actionSave")
self._warnings = []
# Set up QTableView model
self._pandas_model = PandasModel(self)
self._proxyModel = QSortFilterProxyModel(self)
self._proxyModel.setSourceModel(self._pandas_model)
self._table_view.setModel(self._proxyModel)
self._proxyModel.setDynamicSortFilter(False)
# Set event handlers
self._create_button.clicked.connect(self._handle_create_click)
self._delete_button.clicked.connect(self._handle_delete_click)
@ -52,17 +231,23 @@ class MainWindow(QMainWindow):
self._list_widget.itemSelectionChanged.connect(
self._handle_list_selection_changed)
self._pandas_model = PandasModel(self)
self._proxyModel = QSortFilterProxyModel(self)
self._proxyModel.setSourceModel(self._pandas_model)
self._table_view.setModel(self._proxyModel)
self._proxyModel.setDynamicSortFilter(False)
self._table_view.selectionModel().selectionChanged.connect(
self._handle_table_selection_changed)
def set_statement_data(self, df: pd.DataFrame):
if 'category' not in df.columns:
df['category'] = [' '] * len(df.index)
self._pandas_model.set_dataframe(df)
self._dataframe_update_callback()
def get_statement_data(self) -> pd.DataFrame:
return self._pandas_model.get_dataframe()
def _dataframe_update_callback(self):
df = self._pandas_model.get_dataframe()
self._show_warnings()
self._update_categories_from_dataframe(df)
# Resize columns
if len(df.columns) < 10: # Experimentally determined threshold
# Properly resize columns (takes longer)
@ -74,18 +259,9 @@ class MainWindow(QMainWindow):
len(col))
self._table_view.setColumnWidth(i, max_char * 10)
self._show_warnings(df)
self._update_categories_from_dataframe(df)
# Upon reloading the data, the selection is cleared but no signal
# is generated. Manually call the signal handler
self._handle_table_selection_changed()
self._table_view.selectionModel().selectionChanged.connect(
self._handle_table_selection_changed)
def get_statement_data(self) -> pd.DataFrame:
return self._table_view.model().sourceModel().get_dataframe()
def _add_categories(self, categories: typing.Sequence[str]):
for category in categories:
self._list_widget.addItem(category)
def _update_categories_from_dataframe(self, df: pd.DataFrame):
df_categories = df['category'].unique()
@ -96,186 +272,42 @@ class MainWindow(QMainWindow):
self._add_categories([category for category
in missing if category != ' '])
def _add_categories(self, categories: typing.Sequence[str]):
for category in categories:
self._list_widget.addItem(category)
def _add_warning_item(self, text: str):
warning_item = WarningItem(text=text, parent=self)
def _add_warning_text(self, text: str):
layout = QHBoxLayout()
self._warning_layout.addLayout(warning_item)
self._warnings.append(warning_item)
warningIcon = QLabel()
pixmap = QPixmap("res/warning.png")
warningIcon.setPixmap(pixmap)
def _show_warnings(self):
for warning_item in self._warnings:
warning_item.hide()
self._warning_layout.removeItem(warning_item)
label = QLabel(text)
label.setWordWrap(True)
layout.addWidget(warningIcon)
layout.addWidget(label)
layout.setStretch(0, 0)
layout.setStretch(1, 1)
self._warning_layout.addLayout(layout)
self._warnings.append((layout, warningIcon, label))
def _show_warnings(self, df: pd.DataFrame):
for (layout, icon, text) in self._warnings:
icon.hide()
text.hide()
self._warning_layout.removeItem(layout)
df = self._pandas_model.get_dataframe()
if 't' not in df.columns:
self._add_warning_text(
"The column 't' does not exist. Please rename the"
" column containing the dates of the transactions"
" to 't'.")
self._add_warning_item(
"The column 't' does not exist. Please rename the column"
" containing the dates of the transactions to 't'.")
if 'value' not in df.columns:
self._add_warning_text(
"The column 'value' does not exist. Please rename"
" the column containing the values of the"
" transactions to 'value'.")
self._add_warning_item(
"The column 'value' does not exist. Please rename the column"
" containing the values of the transactions to 'value'.")
if 'balance' not in df.columns:
self._add_warning_text(
"The column 'balance' does not exist. Please"
" rename the column containing the balance"
" after each transaction to 'balance'")
self._add_warning_item(
"The column 'balance' does not exist. Please rename the column"
" containing the balance after each transaction to 'balance'")
def _header_right_clicked(self, pos):
column = self._table_view.horizontalHeader().logicalIndexAt(pos)
context = QMenu(self)
rename_action = QAction("Rename", self)
delete_action = QAction("Delete", self)
switch_action = QAction("Switch position", self)
assign_menu = QMenu("Assign type", self)
date_action = QAction("date", self)
float_action = QAction("float", self)
assign_menu.addAction(date_action)
assign_menu.addAction(float_action)
date_action.triggered.connect(
partial(self._header_assign_date_handler, column))
float_action.triggered.connect(
partial(self._header_assign_float_handler, column))
rename_action.triggered.connect(
partial(self._header_rename_handler, column))
delete_action.triggered.connect(
partial(self._header_delete_handler, column))
switch_action.triggered.connect(
partial(self._header_switch_handler, column))
context.addAction(rename_action)
context.addAction(delete_action)
context.addAction(switch_action)
context.addAction(assign_menu.menuAction())
context = HeaderContextMenu(parent=self, column=column,
pandas_model=self._pandas_model,
callback=self._dataframe_update_callback)
context.exec(self.sender().mapToGlobal(pos))
def _header_rename_handler(self, column):
model = self._table_view.horizontalHeader().model()
column_text = model.headerData(column, Qt.Orientation.Horizontal)
new_name, flag = QInputDialog.getText(self, "Rename column",
"New name:",
text=column_text)
if not flag:
return
if (new_name != column_text) and (new_name != ''):
df = self.get_statement_data()
df = df.rename(columns={column_text: new_name})
self.set_statement_data(df)
def _header_delete_handler(self, column):
model = self._table_view.horizontalHeader().model()
column_text = model.headerData(column, Qt.Orientation.Horizontal)
button = QMessageBox.question(self, "Delete column",
f"Are you sure you want to delete"
f" column '{column_text}'?")
if button == QMessageBox.StandardButton.Yes:
df = self.get_statement_data()
df = df.iloc[:, [j for j, c
in enumerate(df.columns) if j != column]]
self.set_statement_data(df)
def _header_switch_handler(self, column):
model = self._table_view.horizontalHeader().model()
column_text = model.headerData(column, Qt.Orientation.Horizontal)
df = self.get_statement_data()
columns = [column for column in df.columns if column != column_text]
other_name, flag = QInputDialog.getItem(self, "Switch column position",
f"Switch position of colum"
f" '{column_text}' with:",
columns, editable=False)
if not flag:
return
column_titles = list(df.columns)
index1, index2 = column_titles.index(column_text), column_titles.index(
other_name)
column_titles[index1], column_titles[index2] \
= column_titles[index2], column_titles[index1]
df = df.reindex(columns=column_titles)
self.set_statement_data(df)
def _header_assign_date_handler(self, column):
model = self._table_view.horizontalHeader().model()
column_text = model.headerData(column, Qt.Orientation.Horizontal)
date_format, flag = QInputDialog.getText(self, "Format",
"Format:",
text="%d.%m.%Y")
if not flag:
return
df = self.get_statement_data()
try:
df[column_text] \
= pd.to_datetime(df[column_text], format=date_format)
except:
QMessageBox.warning(self, "No action performed",
"An error occurred.")
self.set_statement_data(df)
def _header_assign_float_handler(self, column):
model = self._table_view.horizontalHeader().model()
column_text = model.headerData(column, Qt.Orientation.Horizontal)
chars = ['.', ',']
decimal_sep, flag = QInputDialog.getItem(self, "Decimal separator",
"Decimal separator:",
chars, editable=False)
if not flag:
return
df = self.get_statement_data()
try:
if decimal_sep == ',':
df[column_text] \
= df[column_text].str.replace(',', '.').astype(float)
else:
df[column_text] = df[column_text].astype(float)
except:
QMessageBox.warning(self, "No action performed",
"An error occurred.")
self.set_statement_data(df)
def _handle_create_click(self):
new_name, flag = QInputDialog.getText(self, "Create category",
"New category:",