diff --git a/banking_breakdown/ui/custom_ui_items.py b/banking_breakdown/ui/custom_ui_items.py new file mode 100644 index 0000000..a1dceaf --- /dev/null +++ b/banking_breakdown/ui/custom_ui_items.py @@ -0,0 +1,178 @@ +import pandas as pd +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QPixmap, QAction +from PyQt6.QtWidgets import QHBoxLayout, QLabel, QMenu, QInputDialog, \ + QMessageBox + +from banking_breakdown.ui.pandas_model import PandasModel + + +class WarningItem(QHBoxLayout): + """Item appearing at top of Window with warning icon.""" + + 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): + """Context menu appearing when right-clicking the header of the QTableView. + """ + + 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() diff --git a/banking_breakdown/ui/main_window.py b/banking_breakdown/ui/main_window.py index 4aaa502..d3a9b35 100644 --- a/banking_breakdown/ui/main_window.py +++ b/banking_breakdown/ui/main_window.py @@ -1,181 +1,13 @@ import typing -from functools import partial - import pandas as pd from PyQt6 import uic from PyQt6.QtCore import Qt, QSortFilterProxyModel -from PyQt6.QtGui import QPixmap, QAction -from PyQt6.QtWidgets import QMainWindow, QPushButton, QHBoxLayout, QLabel, \ - QVBoxLayout, QMenu, QTableView, QInputDialog, QMessageBox, QFileDialog, \ - QListWidget +from PyQt6.QtGui import QAction +from PyQt6.QtWidgets import QMainWindow, QPushButton, QVBoxLayout, \ + QTableView, QInputDialog, QMessageBox, QFileDialog, QListWidget 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() +from banking_breakdown.ui.custom_ui_items import WarningItem, HeaderContextMenu class MainWindow(QMainWindow): @@ -226,7 +58,8 @@ class MainWindow(QMainWindow): header = self._table_view.horizontalHeader() header.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - header.customContextMenuRequested.connect(self._header_right_clicked) + header.customContextMenuRequested.connect( + self._handle_header_right_click) self._list_widget.itemSelectionChanged.connect( self._handle_list_selection_changed) @@ -234,18 +67,22 @@ class MainWindow(QMainWindow): self._table_view.selectionModel().selectionChanged.connect( self._handle_table_selection_changed) + # Table data updates + def set_statement_data(self, df: pd.DataFrame): self._pandas_model.set_dataframe(df) self._dataframe_update_callback() + self._resize_table_columns_to_content() 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) + self._update_categories_from_dataframe() + + def _resize_table_columns_to_content(self): + df = self._pandas_model.get_dataframe() # Resize columns @@ -259,11 +96,15 @@ class MainWindow(QMainWindow): len(col)) self._table_view.setColumnWidth(i, max_char * 10) + # List data updates + 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): + def _update_categories_from_dataframe(self): + df = self._pandas_model.get_dataframe() + df_categories = df['category'].unique() current_categories = [self._list_widget.item(x).text() for x in range(self._list_widget.count())] @@ -272,6 +113,8 @@ class MainWindow(QMainWindow): self._add_categories([category for category in missing if category != ' ']) + # Warnings + def _add_warning_item(self, text: str): warning_item = WarningItem(text=text, parent=self) @@ -300,7 +143,9 @@ class MainWindow(QMainWindow): "The column 'balance' does not exist. Please rename the column" " containing the balance after each transaction to 'balance'") - def _header_right_clicked(self, pos): + # Event handlers + + def _handle_header_right_click(self, pos): column = self._table_view.horizontalHeader().logicalIndexAt(pos) context = HeaderContextMenu(parent=self, column=column, @@ -345,9 +190,9 @@ class MainWindow(QMainWindow): row_indices = [self._table_view.model().mapToSource(index).row() for index in indexes] - df = self.get_statement_data() + df = self._pandas_model.get_dataframe() df.loc[row_indices, 'category'] = category - self.set_statement_data(df) + self._pandas_model.set_dataframe(df) def _handle_apply_click(self): category = self._list_widget.selectedItems()[0].text() @@ -365,6 +210,8 @@ class MainWindow(QMainWindow): df = self.get_statement_data() df.to_csv(filename, index=False) + # Enable / Disable buttons + def _check_enable_delete_button(self): if len(self._list_widget.selectedItems()) > 0: self._delete_button.setEnabled(True)