diff --git a/SIPPCompare.db b/SIPPCompare.db new file mode 100644 index 0000000..0e2d14c Binary files /dev/null and b/SIPPCompare.db differ diff --git a/SIPPCompare.spec b/SIPPCompare.spec index 3f784b7..4322e84 100644 --- a/SIPPCompare.spec +++ b/SIPPCompare.spec @@ -5,7 +5,7 @@ a = Analysis( ['src\\main.py'], pathex=[], binaries=[], - datas=[('gui/*.ui', 'gui'), ('icon2.ico', '.')], + datas=[('gui/*.ui', 'gui'), ('gui/dialogs/*.ui', 'gui/dialogs'), ('icon2.ico', '.')], hiddenimports=[], hookspath=[], hooksconfig={}, diff --git a/gui/dialogs/platform_rename.ui b/gui/dialogs/platform_rename.ui new file mode 100644 index 0000000..341d53c --- /dev/null +++ b/gui/dialogs/platform_rename.ui @@ -0,0 +1,99 @@ + + + PlatformRename + + + + 0 + 0 + 300 + 100 + + + + + 300 + 100 + + + + + 300 + 100 + + + + Name Platform + + + + + 7 + 41 + 287 + 22 + + + + + 11 + + + + + + + 35 + 9 + 241 + 20 + + + + + 11 + + + + Enter a new name for the platform + + + + + + 220 + 70 + 75 + 24 + + + + + 10 + + + + OK + + + + + + + rename_plat_ok_but + clicked() + PlatformRename + accept() + + + 257 + 71 + + + 149 + 44 + + + + + diff --git a/gui/main_gui.ui b/gui/main_gui.ui index 8b5f2de..f618c7f 100644 --- a/gui/main_gui.ui +++ b/gui/main_gui.ui @@ -220,13 +220,13 @@ File - + - + - Edit Platforms + Platform List diff --git a/gui/output_window.ui b/gui/output_window.ui index 3ba39c9..1425a60 100644 --- a/gui/output_window.ui +++ b/gui/output_window.ui @@ -1,37 +1,108 @@ - ResultsWindow - + OutputWindow + 0 0 - 400 - 355 + 1330 + 685 - 400 - 355 + 1330 + 685 - 400 - 355 + 1330 + 685 Results - + + + + -10 + -10 + 1350 + 650 + + + + + + + 1231 + 642 + 91 + 31 + + + + + 10 + + + + Save graph + + + + + + 370 + 635 + 581 + 41 + + + + 1 + + + 20 + + + Qt::Orientation::Horizontal + + + QSlider::TickPosition::TicksAbove + + + 1 + + + + + + 1134 + 642 + 91 + 31 + + + + + 10 + + + + Save CSV + + + 10 - 10 - 381 - 301 + 640 + 341 + 31 @@ -39,66 +110,19 @@ 11 - - - - - 318 - 323 - 75 - 24 - - - - - 10 - - - OK - - - - - - 238 - 323 - 75 - 24 - - - - - 10 - - - - Save + Fees over 1 year(s) (assuming no change in value) - - output - res_save_but - res_ok_but - + + + MplWidget + QWidget +
widgets/mpl_widget
+ 1 +
+
- - - res_ok_but - clicked(bool) - ResultsWindow - close() - - - 357 - 281 - - - 199 - 149 - - - - + diff --git a/gui/platform_edit.ui b/gui/platform_edit.ui index fe3a57d..f7c638f 100644 --- a/gui/platform_edit.ui +++ b/gui/platform_edit.ui @@ -2,6 +2,9 @@ PlatformEdit + + Qt::WindowModality::ApplicationModal + 0 @@ -10,6 +13,18 @@ 567 + + + 630 + 567 + + + + + 630 + 567 + + Platform Editor @@ -215,7 +230,7 @@ - Fund dealing fee* + Fund dealing fee @@ -339,7 +354,7 @@ 8 - 262 + 540 191 21 @@ -505,6 +520,24 @@ on the value above £ there is no charge + + + + 10 + 284 + 151 + 16 + + + + + 11 + + + + Fund platform fee* + + diff --git a/gui/platform_list.ui b/gui/platform_list.ui new file mode 100644 index 0000000..128cde3 --- /dev/null +++ b/gui/platform_list.ui @@ -0,0 +1,153 @@ + + + PlatformList + + + + 0 + 0 + 263 + 473 + + + + + 263 + 473 + + + + + 263 + 473 + + + + Platform List + + + + + 10 + 34 + 243 + 371 + + + + + 11 + + + + + + + 11 + 413 + 121 + 24 + + + + + 10 + + + + Add platform + + + + + + 135 + 413 + 121 + 24 + + + + + 10 + + + + Remove platform + + + + + + 136 + 442 + 121 + 24 + + + + + 10 + + + + Save + + + + + + 11 + 442 + 121 + 24 + + + + + 10 + + + + Edit platform + + + + + + 12 + 8 + 231 + 20 + + + + + 11 + + + + Platform enabled? + + + + + + + plist_save_but + clicked() + PlatformList + close() + + + 196 + 453 + + + 131 + 236 + + + + + diff --git a/src/data_struct.py b/src/data_struct.py new file mode 100644 index 0000000..6e61055 --- /dev/null +++ b/src/data_struct.py @@ -0,0 +1,16 @@ +class Platform: + def __init__(self, plat_id, fund_plat_fee, plat_name, enabled, fund_deal_fee, + share_plat_fee, share_plat_max_fee, share_deal_fee, + share_deal_reduce_trades, share_deal_reduce_amount, + ): + + self.plat_id = plat_id + self.fund_plat_fee = fund_plat_fee + self.plat_name = plat_name + self.enabled = enabled + self.fund_deal_fee = fund_deal_fee + self.share_plat_fee = share_plat_fee + self.share_plat_max_fee = share_plat_max_fee + self.share_deal_fee = share_deal_fee + self.share_deal_reduce_trades = share_deal_reduce_trades + self.share_deal_reduce_amount = share_deal_reduce_amount diff --git a/src/db_handler.py b/src/db_handler.py new file mode 100644 index 0000000..47c7cdf --- /dev/null +++ b/src/db_handler.py @@ -0,0 +1,264 @@ +import os +import sqlite3 + +import resource_finder +from data_struct import Platform + + +class DBHandler: + def __init__(self): + # Function to create all necessary database tables if the DB file doesn't exist + def create_tables(): + self.cur.execute(""" + CREATE TABLE "tblPlatforms" ( + "PlatformID" INTEGER NOT NULL UNIQUE, + "PlatformName" TEXT, + "IsEnabled" INTEGER NOT NULL, + PRIMARY KEY("PlatformID") + ) + """) + + self.cur.execute(""" + CREATE TABLE "tblFlatPlatFees" ( + "PlatformID" INTEGER NOT NULL UNIQUE, + "SharePlatFee" REAL NOT NULL, + "SharePlatMaxFee" REAL, + PRIMARY KEY("PlatformID"), + FOREIGN KEY("PlatformID") REFERENCES "tblPlatforms"("PlatformID") + ) + """) + + self.cur.execute(""" + CREATE TABLE "tblFlatDealFees" ( + "PlatformID" INTEGER NOT NULL UNIQUE, + "FundDealFee" REAL, + "ShareDealFee" REAL NOT NULL, + "ShareDealReduceTrades" REAL, + "ShareDealReduceAmount" REAL, + PRIMARY KEY("PlatformID"), + FOREIGN KEY("PlatformID") REFERENCES "tblPlatforms"("PlatformID") + ) + """) + + self.cur.execute(""" + CREATE TABLE "tblFundPlatFee" ( + "PlatformID" INTEGER NOT NULL, + "Band" REAL NOT NULL, + "Fee" REAL NOT NULL, + PRIMARY KEY("PlatformID","Band","Fee"), + FOREIGN KEY("PlatformID") REFERENCES "tblPlatforms"("PlatformID") + ) + """) + + self.cur.execute(""" + CREATE TABLE "tblUserDetails" ( + "UserID" INTEGER NOT NULL UNIQUE, + "PensionValue" REAL NOT NULL, + "SliderValue" INTEGER NOT NULL, + "ShareTrades" INTEGER NOT NULL, + "FundTrades" INTEGER NOT NULL, + PRIMARY KEY("UserID") + ) + """) + + if not os.path.exists(resource_finder.get_res_path("SIPPCompare.db")): + db_exists = False + else: + db_exists = True + + self.conn = sqlite3.connect(resource_finder.get_res_path("SIPPCompare.db")) + self.cur = self.conn.cursor() + if not db_exists: + create_tables() + + # Retrieve the list of platform names for list population + def retrieve_plat_list(self) -> list: + res = self.cur.execute("SELECT PlatformName FROM tblPlatforms").fetchall() + plat_name_list = [] + for platform in res: + plat_name_list.append(platform[0]) + + return plat_name_list + + def write_platforms(self, plat_list: list[Platform]): + for i in range(len(plat_list)): + platforms_data = [ + plat_list[i].plat_id, + plat_list[i].plat_name, + plat_list[i].enabled + ] + + flat_plat_fees_data = [ + plat_list[i].plat_id, + plat_list[i].share_plat_fee, + plat_list[i].share_plat_max_fee + ] + + flat_deal_fees_data = [ + plat_list[i].plat_id, + plat_list[i].fund_deal_fee, + plat_list[i].share_deal_fee, + plat_list[i].share_deal_reduce_trades, + plat_list[i].share_deal_reduce_amount + ] + + res = self.cur.execute(f""" + SELECT EXISTS( + SELECT PlatformID FROM tblPlatforms + WHERE PlatformID = {i} + )""").fetchall() + + if res[0][0] == 1: + self.cur.execute(f""" + UPDATE tblPlatforms SET + PlatformID = ?, + PlatformName = ?, + IsEnabled = ? + WHERE PlatformID = {i} + """, platforms_data) + + self.cur.execute(f""" + UPDATE tblFlatPlatFees SET + PlatformID = ?, + SharePlatFee = ?, + SharePlatMaxFee = ? + WHERE PlatformID = {i} + """, flat_plat_fees_data) + + self.cur.execute(f""" + UPDATE tblFlatDealFees SET + PlatformID = ?, + FundDealFee = ?, + ShareDealFee = ?, + ShareDealReduceTrades = ?, + ShareDealReduceAmount = ? + WHERE PlatformID = {i} + """, flat_deal_fees_data) + + self.cur.execute(f"DELETE FROM tblFundPlatFee WHERE PlatformID = {i}") + else: + self.cur.execute("INSERT INTO tblPlatforms VALUES (?, ?, ?)", platforms_data) + self.cur.execute("INSERT INTO tblFlatPlatFees VALUES (?, ?, ?)", flat_plat_fees_data) + self.cur.execute("INSERT INTO tblFlatDealFees VALUES (?, ?, ?, ?, ?)", flat_deal_fees_data) + + exec_str = f"INSERT INTO tblFundPlatFee VALUES\n" + for x in range(len(plat_list[i].fund_plat_fee[0])): + band = plat_list[i].fund_plat_fee[0][x] + fee = plat_list[i].fund_plat_fee[1][x] + exec_str += f"({i}, {band}, {fee}),\n" + exec_str = exec_str[:-2] + self.cur.execute(exec_str) + + self.conn.commit() + + # Retrieve all info about all platforms in DB and initialise Platform objects + def retrieve_platforms(self) -> list[Platform]: + # Retrieve all one-to-one relations + platforms_res = self.cur.execute(""" + SELECT + -- tblPlatforms + tblPlatforms.PlatformID, PlatformName, IsEnabled, + -- tblFlatPlatFees + SharePlatFee, SharePlatMaxFee, + -- tblFlatDealFees + FundDealFee, + ShareDealFee, + ShareDealReduceTrades, + ShareDealReduceAmount + FROM tblPlatforms + INNER JOIN tblFlatPlatFees ON + tblPlatforms.PlatformID = tblFlatPlatFees.PlatformID + INNER JOIN tblFlatDealFees ON + tblPlatforms.PlatformID = tblFlatDealFees.PlatformID + """).fetchall() + + platforms = [] + for platform in platforms_res: + # plat_id, plat_name, enabled, share_plat_fee, share_plat_max_fee, fund_deal_fee, + # share_deal_fee, share_deal_reduce_trades, share_deal_reduce_amount + this_platform = [platform[0], platform[1], platform[2], platform[3], platform[4], + platform[5], platform[6], platform[7], platform[8]] + platforms.append(this_platform) + + # Insert 2D array into each platform data in preparation for fund_plat_fee retrival + for platform in platforms: + platform.insert(1, [[], []]) + + # Get all records from tblFundPlatFee, add them to the platforms list based on ID + # WARNING: This code is dependent on PlatformID being sequential from 0 in DB records + fund_plat_fee_res = self.cur.execute("SELECT * FROM tblFundPlatFee ORDER BY PlatformID ASC").fetchall() + for i in range(len(fund_plat_fee_res)): + plat_id = fund_plat_fee_res[i][0] + platforms[plat_id][1][0].append(fund_plat_fee_res[i][1]) + platforms[plat_id][1][1].append(fund_plat_fee_res[i][2]) + + platform_obj_list: list[Platform] = [] + for platform in platforms: + platform_obj_list.append(Platform( + platform[0], platform[1], platform[2], platform[3], + platform[6], platform[4], platform[5], platform[7], + platform[8], platform[9] + )) + + return platform_obj_list + + # This function writes the details the user entered this session to the DB + def write_user_details(self, pension_val: float, slider_val: int, share_trades: int, fund_trades: int): + # Hardcode UserID as 0 + user_details_data = (0, pension_val, slider_val, share_trades, fund_trades) + + # Check if there is already a record in tblUserDetails + res = self.cur.execute("SELECT EXISTS(SELECT 1 FROM tblUserDetails)").fetchone() + if res[0] == 0: + # If there isn't then insert a new record + self.cur.execute("INSERT INTO tblUserDetails VALUES (?, ?, ?, ?, ?)", user_details_data) + else: + # If there is then update the existing record (only ever one record as of now) + self.cur.execute(""" + UPDATE tblUserDetails SET + UserID = ?, + PensionValue = ?, + SliderValue = ?, + ShareTrades = ?, + FundTrades = ? + """, user_details_data) + self.conn.commit() + + # Function to retrieve details entered by the user in prev session from DB + def retrieve_user_details(self) -> dict: + res = self.cur.execute("SELECT EXISTS(SELECT 1 FROM tblUserDetails)").fetchone() + if res[0] == 0: + return {"NO_RECORD": None} + + res = self.cur.execute("SELECT * FROM tblUserDetails") + res_tuple: tuple = res.fetchone() + user_details_dict: dict[str, float | int] = { + "user_id": res_tuple[0], + "pension_val": res_tuple[1], + "slider_val": res_tuple[2], + "share_trades": res_tuple[3], + "fund_trades": res_tuple[4] + } + + return user_details_dict + + def toggle_platform_state(self, index: int, state: bool): + self.cur.execute("UPDATE tblPlatforms SET IsEnabled = ? WHERE PlatformID = ?", [state, index]) + self.conn.commit() + + def remove_platform(self, index: int): + tbl_list = ["tblPlatforms", "tblFlatPlatFees", "tblFlatDealFees", "tblFundPlatFee"] + for tbl in tbl_list: + self.cur.execute(f"DELETE FROM {tbl} WHERE PlatformID = {index}") + + res = self.cur.execute("SELECT PlatformID from tblPlatforms").fetchall() + n = len(res) + for i in range(n): + for tbl in tbl_list: + self.cur.execute(f""" + UPDATE {tbl} + SET PlatformID = {index + i} + WHERE PlatformID = {index + 1 + i} + """) + + self.conn.commit() diff --git a/src/main.py b/src/main.py index 14c9b4d..2517bfe 100644 --- a/src/main.py +++ b/src/main.py @@ -1,20 +1,11 @@ -from PyQt6.QtWidgets import QApplication - import sys -import platform_edit +from PyQt6.QtWidgets import QApplication + +from main_window import SIPPCompare app = QApplication(sys.argv) -# Show platform edit window first, before main win -# When debugging, can be useful to autofill values to save time -if len(sys.argv) > 1: - if sys.argv[1] == "--DEBUG_AUTOFILL": - window = platform_edit.PlatformEdit(True) - else: - window = platform_edit.PlatformEdit(False) -else: - window = platform_edit.PlatformEdit(False) - +window = SIPPCompare() window.show() app.exec() diff --git a/src/main_window.py b/src/main_window.py index d56484a..d35fc3e 100644 --- a/src/main_window.py +++ b/src/main_window.py @@ -1,48 +1,41 @@ -from PyQt6.QtGui import QIntValidator, QIcon -from PyQt6.QtWidgets import QMainWindow, QWidget from PyQt6 import uic +from PyQt6.QtGui import QIntValidator, QIcon +from PyQt6.QtWidgets import QMainWindow, QApplication -import output_window import resource_finder +from db_handler import DBHandler +from platform_list import PlatformList +from output_window import OutputWindow class SIPPCompare(QMainWindow): - # Receive instance of PlatformEdit() as parameter - def __init__(self, plat_edit_win: QWidget): + def __init__(self): super().__init__() # Import Qt Designer UI XML file uic.loadUi(resource_finder.get_res_path("gui/main_gui.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) # Initialise class variables - # Inputs - self.optional_boxes = [] - self.fund_plat_fee = 0.0 - self.plat_name = "" - self.fund_deal_fee = 0.0 - self.share_plat_fee = 0.0 - self.share_plat_max_fee = 0.0 - self.share_deal_fee = 0.0 - self.share_deal_reduce_trades = 0.0 - self.share_deal_reduce_amount = 0.0 - # Results self.fund_plat_fees = 0.0 self.fund_deal_fees = 0.0 self.share_plat_fees = 0.0 self.share_deal_fees = 0.0 + self.results = [] # Create window objects - self.platform_win = plat_edit_win - self.output_win = output_window.OutputWindow() + self.db = DBHandler() + self.platform_list_win = PlatformList(self.db) + self.output_win = OutputWindow() # Handle events self.calc_but.clicked.connect(self.calculate_fees) - # Menu bar entry (File -> Edit Platforms) - self.actionEdit_Platforms.triggered.connect(self.show_platform_edit) + # Menu bar entry (File -> Platform List) + self.actionList_Platforms.triggered.connect(self.show_platform_list) # Update percentage mix label when slider moved self.mix_slider.valueChanged.connect(self.update_slider_lab) self.value_input.valueChanged.connect(self.check_valid) + # Validate input self.share_trades_combo.currentTextChanged.connect(self.check_valid) self.fund_trades_combo.currentTextChanged.connect(self.check_valid) @@ -50,12 +43,22 @@ class SIPPCompare(QMainWindow): self.share_trades_combo.setValidator(QIntValidator(0, 999)) self.fund_trades_combo.setValidator(QIntValidator(0, 99)) + # Restore last session + prev_session_data = self.db.retrieve_user_details() + if "NO_RECORD" not in prev_session_data: + self.value_input.setValue(prev_session_data["pension_val"]) + self.mix_slider.setValue(prev_session_data["slider_val"]) + self.share_trades_combo.setCurrentText(str(prev_session_data["share_trades"])) + self.fund_trades_combo.setCurrentText(str(prev_session_data["fund_trades"])) + self.calc_but.setFocus() + # Display slider position as mix between two nums (funds/shares) def update_slider_lab(self): slider_val = self.mix_slider.value() mix_lab_str = f"Investment mix (funds {slider_val}% / shares {100 - slider_val}%)" self.mix_lab.setText(mix_lab_str) + # Ensure that trade fields aren't blank and pension value > 0 def check_valid(self): if self.share_trades_combo.currentText() != "" \ and self.fund_trades_combo.currentText() != "" \ @@ -64,90 +67,71 @@ class SIPPCompare(QMainWindow): else: self.calc_but.setEnabled(False) - # Get variables from platform editor input fields - def init_variables(self): - self.optional_boxes = self.platform_win.get_optional_boxes() - self.fund_plat_fee = self.platform_win.get_fund_plat_fee() - self.share_plat_fee = self.platform_win.get_share_plat_fee() - self.share_deal_fee = self.platform_win.get_share_deal_fee() - - # TODO: This is HORRIBLE - find better way of doing it! (maybe enums?) - if self.optional_boxes[0]: - self.plat_name = self.platform_win.get_plat_name() - else: - self.plat_name = None - - if self.optional_boxes[1]: - self.fund_deal_fee = self.platform_win.get_fund_deal_fee() - else: - self.fund_deal_fee = None - - if self.optional_boxes[2]: - self.share_plat_max_fee = self.platform_win.get_share_plat_max_fee() - else: - self.share_plat_max_fee = None - - if self.optional_boxes[3]: - self.share_deal_reduce_trades = self.platform_win.get_share_deal_reduce_trades() - else: - self.share_deal_reduce_trades = None - - if self.optional_boxes[4]: - self.share_deal_reduce_amount = self.platform_win.get_share_deal_reduce_amount() - else: - self.share_deal_reduce_amount = None - - # Calculate fees + # Calculate fees for all active platforms def calculate_fees(self): - self.init_variables() - # Set to zero each time to avoid persistence - self.fund_plat_fees = 0 + # Set to empty list each time to avoid persistence + self.results = [] + + # Get user input value_num = float(self.value_input.value()) - # Funds/shares mix slider_val: int = self.mix_slider.value() - funds_value = (slider_val / 100) * value_num fund_trades_num = int(self.fund_trades_combo.currentText()) - if self.fund_deal_fee is not None: - self.fund_deal_fees = fund_trades_num * self.fund_deal_fee - - for i in range(1, len(self.fund_plat_fee[0])): - band = self.fund_plat_fee[0][i] - prev_band = self.fund_plat_fee[0][i - 1] - fee = self.fund_plat_fee[1][i] - gap = (band - prev_band) - - if funds_value > gap: - self.fund_plat_fees += gap * (fee / 100) - funds_value -= gap - else: - self.fund_plat_fees += funds_value * (fee / 100) - break - - shares_value = (1 - (slider_val / 100)) * value_num - if self.share_plat_max_fee is not None: - if (self.share_plat_fee * shares_value / 12) > self.share_plat_max_fee: - self.share_plat_fees = self.share_plat_max_fee * 12 - else: - self.share_plat_fees = self.share_plat_fee * shares_value - share_trades_num = int(self.share_trades_combo.currentText()) - if self.share_deal_reduce_trades is not None: - if (share_trades_num / 12) >= self.share_deal_reduce_trades: - self.share_deal_fees = self.share_deal_reduce_amount * share_trades_num - else: - self.share_deal_fees = self.share_deal_fee * share_trades_num + shares_value = (1 - (slider_val / 100)) * value_num + for platform in self.platform_list_win.plat_list: + if not platform.enabled: + continue + + fund_plat_fees = 0.0 + fund_deal_fees = 0.0 + share_plat_fees = 0.0 + share_deal_fees = 0.0 + plat_name = platform.plat_name + + if platform.fund_deal_fee is not None: + fund_deal_fees = fund_trades_num * platform.fund_deal_fee + + funds_value = (slider_val / 100) * value_num + for i in range(1, len(platform.fund_plat_fee[0])): + band = platform.fund_plat_fee[0][i] + prev_band = platform.fund_plat_fee[0][i - 1] + fee = platform.fund_plat_fee[1][i] + gap = (band - prev_band) + + if funds_value > gap: + fund_plat_fees += gap * (fee / 100) + funds_value -= gap + else: + fund_plat_fees += funds_value * (fee / 100) + break + + if platform.share_plat_max_fee is not None: + if (platform.share_plat_fee * shares_value / 12) > platform.share_plat_max_fee: + share_plat_fees = platform.share_plat_max_fee * 12 + else: + share_plat_fees = platform.share_plat_fee * shares_value + + if platform.share_deal_reduce_trades is not None: + if (share_trades_num / 12) >= platform.share_deal_reduce_trades: + share_deal_fees = platform.share_deal_reduce_amount * share_trades_num + else: + share_deal_fees = platform.share_deal_fee * share_trades_num + + self.results.append([fund_plat_fees, fund_deal_fees, share_plat_fees, share_deal_fees, plat_name]) + + # Save details entered by user for next session + self.db.write_user_details(value_num, slider_val, share_trades_num, fund_trades_num) self.show_output_win() # Show the output window - this func is called from calculate_fee() def show_output_win(self): # Refresh the results when new fees are calculated - self.output_win.display_output(self.fund_plat_fees, self.fund_deal_fees, - self.share_plat_fees, self.share_deal_fees, - self.plat_name - ) + self.output_win.display_output(self.results, 1) + self.output_win.activateWindow() + self.output_win.raise_() self.output_win.show() + QApplication.alert(self.output_win) - # Show the platform editor window (currently run-time only) - def show_platform_edit(self): - self.platform_win.show() + def show_platform_list(self): + self.platform_list_win.show() diff --git a/src/output_window.py b/src/output_window.py index 06933da..aad49d1 100644 --- a/src/output_window.py +++ b/src/output_window.py @@ -1,11 +1,32 @@ -from PyQt6.QtGui import QIcon -from PyQt6.QtWidgets import QWidget -from PyQt6 import uic - -import datetime import os +from datetime import datetime + +from PyQt6 import uic +from PyQt6.QtGui import QIcon, QFont +from PyQt6.QtWidgets import QWidget, QFileDialog, QMessageBox, QDialogButtonBox import resource_finder +from widgets.mpl_widget import MplWidget + + +class SaveFailure(QMessageBox): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) + self.setWindowTitle("Save failure") + + self.setIcon(QMessageBox.Icon.Critical) + font = QFont() + font.setPointSize(11) + self.setFont(font) + self.setText("Failed to save file") + self.setDetailedText( + "This could be due to a permissions issue, or the file being in use by another process" + ) + + self.setStandardButtons(QMessageBox.StandardButton.Ok) + font.setPointSize(10) + self.findChild(QDialogButtonBox).setFont(font) class OutputWindow(QWidget): @@ -15,50 +36,103 @@ class OutputWindow(QWidget): uic.loadUi(resource_finder.get_res_path("gui/output_window.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) - # Initialise class variables - self.results_str = "" - self.platform_name = "" + # Define class variables + self.save_err_dialog = SaveFailure() + self.canvas = self.graphWidget.canvas + self.ax = self.canvas.axes + self.fig = self.canvas.figure + self.results = [] # Handle events - self.res_save_but.clicked.connect(self.save_results) + self.save_graph_but.clicked.connect(self.save_graph) + self.save_csv_but.clicked.connect(self.save_csv) + self.time_slider.valueChanged.connect(self.change_time) - def save_results(self): - # Use datatime for txt filename - cur_time = datetime.datetime.now() - if not os.path.exists("output"): - os.makedirs("output") - filename_str = f"output/" - if self.platform_name is not None: - filename_str += f"{self.platform_name}" - else: - filename_str += "Unnamed" - filename_str += f"-{cur_time.year}.{cur_time.month}.{cur_time.day}.txt" - output_file = open(filename_str, "wt", encoding = "utf-8") - output_file.write(self.results_str) + def display_output(self, results: list, years: int): + self.results = results + self.ax.clear() + self.ax.cla() + self.canvas.draw_idle() - # Display fees in output window as plaintext readout - def display_output(self, fund_plat_fees: float, fund_deal_fees: float, - share_plat_fees: float, share_deal_fees: float, plat_name: str): - self.platform_name = plat_name - if self.platform_name is not None: - self.results_str = f"Fees breakdown (Platform \"{self.platform_name}\"):" - else: - self.results_str = f"Fees breakdown:" + names = [] + values = [] + for result in results: + names.append(result[4]) + values.append(sum(result[:4]) * years) - self.results_str += "\n\nPlatform fees:" - # :.2f is used in order to display 2 decimal places (currency form) - self.results_str += f"\n\tFund platform fees: £{round(fund_plat_fees, 2):.2f}" - self.results_str += f"\n\tShare platform fees: £{round(share_plat_fees, 2):.2f}" - total_plat_fees = fund_plat_fees + share_plat_fees - self.results_str += f"\n\tTotal platform fees: £{round(total_plat_fees, 2):.2f}" + names = sorted(names, key=lambda x: values[names.index(x)], reverse=True) + values = sorted(values, reverse=True) - self.results_str += "\n\nDealing fees:" - self.results_str += f"\n\tFund dealing fees: £{round(fund_deal_fees, 2):.2f}" - self.results_str += f"\n\tShare dealing fees: £{round(share_deal_fees, 2):.2f}" - total_deal_fees = fund_deal_fees + share_deal_fees - self.results_str += f"\n\tTotal dealing fees: £{round(total_deal_fees, 2):.2f}" + h_bars = self.ax.barh(names, values) + self.ax.bar_label(h_bars, label_type='center', labels=[f"£{x:,.2f}" for x in h_bars.datavalues]) - total_fees = total_plat_fees + total_deal_fees - self.results_str += f"\n\nTotal fees: £{round(total_fees, 2):.2f}" + def save_graph(self): + file_picker = QFileDialog(self) + file_picker.setFileMode(QFileDialog.FileMode.AnyFile) + file_picker.setDefaultSuffix("png") + file_picker.setWindowTitle("Save results as PNG") + file_picker.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) + file_picker.setNameFilter("*.png") + file_path = "" + cur_time = datetime.now() + filename_str = f"{file_path}/SIPPCompare-{cur_time.year}.{cur_time.month}.{cur_time.day}.png" + file_picker.selectFile(filename_str) + if file_picker.exec(): + file_path = file_picker.selectedFiles()[0] - self.output.setText(self.results_str) + try: + self.fig.savefig(file_path, dpi=150) + except OSError: + self.save_err_dialog.exec() + + def save_csv(self): + # TODO: Sort CSV output, either alphabetically or by total fees + file_picker = QFileDialog(self) + file_picker.setFileMode(QFileDialog.FileMode.AnyFile) + file_picker.setDefaultSuffix("csv") + file_picker.setWindowTitle("Save results as CSV") + file_picker.setAcceptMode(QFileDialog.AcceptMode.AcceptSave) + file_picker.setNameFilter("*.csv") + file_path = "" + cur_time = datetime.now() + filename_str = f"{file_path}/SIPPCompare-{cur_time.year}.{cur_time.month}.{cur_time.day}.csv" + file_picker.selectFile(filename_str) + if file_picker.exec(): + file_path = file_picker.selectedFiles()[0] + + try: + csvfile = open(file_path, "wt") + csv_string = ( + "Platform Name,Fund Platform Fee,Share Platform Fee,Fund Dealing Fee," + "Share Dealing Fee,Total Platform Fees,Total Dealing Fees,Total Fund Fees," + "Total Share Fees,Total Fees" + ) + + for result in self.results: + csv_string += '\n' + pn = result[4] + fpf = result[0] + spf = result[2] + fdf = result[1] + sdf = result[3] + + tpf = fpf + spf + tdf = sdf + fdf + + tff = fpf + fdf + tsf = spf + sdf + tf = tff + tsf + csv_string += ( + f"{pn},\"£{fpf:,.2f}\",\"£{spf:,.2f}\",\"£{fdf:,.2f}\",\"£{sdf:,.2f}\"," + f"\"£{tpf:,.2f}\",\"£{tdf:,.2f}\",\"£{tff:,.2f}\",\"£{tsf:,.2f}\",\"£{tf:,.2f}\"" + ) + + csvfile.write(csv_string) + csvfile.close() + except OSError: + self.save_err_dialog.exec() + + def change_time(self): + years: int = self.time_slider.value() + self.time_lab.setText(f"Fees over {years} year(s) (assuming no change in value)") + self.display_output(self.results, years) diff --git a/src/platform_edit.py b/src/platform_edit.py index 663d8f8..a3d14a6 100644 --- a/src/platform_edit.py +++ b/src/platform_edit.py @@ -1,39 +1,29 @@ -from PyQt6.QtCore import QRegularExpression +from PyQt6 import uic +from PyQt6.QtCore import QRegularExpression, QRect from PyQt6.QtGui import QRegularExpressionValidator, QFont, QIcon from PyQt6.QtWidgets import QWidget, QLabel -from PyQt6 import uic -from widgets.fastedit_spinbox import FastEditQDoubleSpinBox -import main_window import resource_finder +from db_handler import DBHandler +from data_struct import Platform +from widgets.fastedit_spinbox import FastEditQDoubleSpinBox class PlatformEdit(QWidget): - def __init__(self, autofill: bool): + def __init__(self, plat: Platform): super().__init__() # Import Qt Designer UI XML file uic.loadUi(resource_finder.get_res_path("gui/platform_edit.ui"), self) self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) # Initialise class variables - # Create main window object, passing this instance as param - self.main_win = main_window.SIPPCompare(self) - - self.fund_plat_fee = [] - self.plat_name = "" - self.fund_deal_fee = 0.0 - self.share_plat_fee = 0.0 - self.share_plat_max_fee = 0.0 - self.share_deal_fee = 0.0 - self.share_deal_reduce_trades = 0.0 - self.share_deal_reduce_amount = 0.0 + self.plat = plat + self.fund_plat_fee = self.plat.fund_plat_fee self.widgets_list_list = [] - - self.fund_fee_rows = 1 - # Debugging feature: set with "--DEBUG_AUTOFILL" cmd argument - self.autofill = autofill - if autofill: - self.save_but.setEnabled(True) + if len(self.plat.fund_plat_fee[0]) > 1: + self.fund_fee_rows = len(self.plat.fund_plat_fee[0]) - 1 + else: + self.fund_fee_rows = 1 self.required_fields = [ self.share_plat_fee_box, @@ -64,6 +54,57 @@ class PlatformEdit(QWidget): False ] + # Set optional checkboxes based on DB storage + if self.plat.plat_name is None: + self.check_boxes_ticked[0] = False + self.plat_name_check.setChecked(False) + else: + self.check_boxes_ticked[0] = True + self.plat_name_check.setChecked(True) + self.plat_name_box.setText(self.plat.plat_name) + + if self.plat.fund_deal_fee is None: + self.check_boxes_ticked[1] = False + self.fund_deal_fee_check.setChecked(False) + else: + self.check_boxes_ticked[1] = True + self.fund_deal_fee_check.setChecked(True) + self.fund_deal_fee_box.setValue(self.plat.fund_deal_fee) + + self.share_plat_fee_box.setValue(self.plat.share_plat_fee * 100) + + if self.plat.share_plat_max_fee is None: + self.check_boxes_ticked[2] = False + self.share_plat_max_fee_check.setChecked(False) + else: + self.check_boxes_ticked[2] = True + self.share_plat_max_fee_check.setChecked(True) + self.share_plat_max_fee_box.setValue(self.plat.share_plat_max_fee) + + self.share_deal_fee_box.setValue(self.plat.share_deal_fee) + + if self.plat.share_deal_reduce_trades is None: + self.check_boxes_ticked[3] = False + self.share_deal_reduce_trades_check.setChecked(False) + else: + self.check_boxes_ticked[3] = True + self.share_deal_reduce_trades_check.setChecked(True) + self.share_deal_reduce_trades_box.setValue(int(self.plat.share_deal_reduce_trades)) + + if self.plat.share_deal_reduce_trades is None: + self.check_boxes_ticked[4] = False + self.share_deal_reduce_amount_check.setChecked(False) + else: + self.check_boxes_ticked[4] = True + self.share_deal_reduce_amount_check.setChecked(True) + self.share_deal_reduce_amount_box.setValue(self.plat.share_deal_reduce_amount) + + # Populate fund platform fee rows from DB + if len(self.plat.fund_plat_fee[0]) > 1: + self.first_tier_box.setValue(self.plat.fund_plat_fee[0][1]) + self.first_tier_fee_box.setValue(self.plat.fund_plat_fee[1][1]) + self.add_row(loading=True) + # Handle events for field in self.required_fields: field.valueChanged.connect(self.check_valid) @@ -93,7 +134,7 @@ class PlatformEdit(QWidget): QRegularExpressionValidator(QRegularExpression("\\w*")) ) - def create_plat_fee_struct(self): + def create_plat_fee_struct(self) -> list: plat_fee_struct = [[0], [0]] plat_fee_struct[0].append(self.first_tier_box.value()) plat_fee_struct[1].append(self.first_tier_fee_box.value()) @@ -108,32 +149,34 @@ class PlatformEdit(QWidget): # Get fee structure variables from user input when "Save" clicked def init_variables(self): - # If debugging, save time by hardcoding - if self.autofill: - self.plat_name = "AJBell" - self.fund_plat_fee = [ - [0, 250000, 1000000, 2000000], - [0, 0.25, 0.1, 0.05] - ] - self.fund_deal_fee = 1.50 - self.share_plat_fee = 0.0025 - self.share_plat_max_fee = 3.50 - self.share_deal_fee = 5.00 - self.share_deal_reduce_trades = 10 - self.share_deal_reduce_amount = 3.50 - self.check_boxes_ticked = [True, True, True, True, True] - else: - self.plat_name = self.plat_name_box.text() - self.fund_plat_fee = self.create_plat_fee_struct() - self.fund_deal_fee = float(self.fund_deal_fee_box.value()) - self.share_plat_fee = float(self.share_plat_fee_box.value()) / 100 - self.share_plat_max_fee = float(self.share_plat_max_fee_box.value()) - self.share_deal_fee = float(self.share_deal_fee_box.value()) - self.share_deal_reduce_trades = float(self.share_deal_reduce_trades_box.value()) - self.share_deal_reduce_amount = float(self.share_deal_reduce_amount_box.value()) + self.plat.fund_plat_fee = self.create_plat_fee_struct() + self.plat.share_plat_fee = float(self.share_plat_fee_box.value()) / 100 + self.plat.share_deal_fee = float(self.share_deal_fee_box.value()) - # Once user input is received show main window - self.main_win.show() + if self.check_boxes_ticked[0]: + self.plat.plat_name = self.plat_name_box.text() + else: + self.plat.plat_name = None + + if self.check_boxes_ticked[1]: + self.plat.fund_deal_fee = float(self.fund_deal_fee_box.value()) + else: + self.plat.fund_deal_fee = None + + if self.check_boxes_ticked[2]: + self.plat.share_plat_max_fee = float(self.share_plat_max_fee_box.value()) + else: + self.plat.share_plat_max_fee = None + + if self.check_boxes_ticked[3]: + self.plat.share_deal_reduce_trades = int(self.share_deal_reduce_trades_box.value()) + else: + self.plat.share_deal_reduce_trades = None + + if self.check_boxes_ticked[4]: + self.plat.share_deal_reduce_amount = float(self.share_deal_reduce_amount_box.value()) + else: + self.plat.share_deal_reduce_amount = None # This method does multiple things in order to validate the user's inputs: # 1) Check all required fields have a non-zero value @@ -215,50 +258,67 @@ class PlatformEdit(QWidget): max_band = self.first_tier_box.value() self.val_above_lab.setText(f"on the value above £{int(max_band)} there is no charge") - def add_row(self): - widgets = [] - font = QFont() - font.setPointSize(11) + def add_row(self, loading: bool = False): + if loading: + rows_needed = self.fund_fee_rows - 1 + else: + rows_needed = 1 - widgets.append(QLabel(self.gridLayoutWidget_2)) - widgets[0].setFont(font) + for x in range(rows_needed): + widgets = [] + font = QFont() + font.setPointSize(11) - widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2)) - widgets[1].setPrefix("£") - widgets[1].setMaximum(9999999) - widgets[1].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons) - widgets[1].setFont(font) - widgets[1].valueChanged.connect(self.check_valid) - widgets[1].valueChanged.connect(self.update_tier_labels) + widgets.append(QLabel(self.gridLayoutWidget_2)) + widgets[0].setFont(font) - widgets.append(QLabel(self.gridLayoutWidget_2)) - widgets[2].setText(f"the fee is") - widgets[2].setFont(font) + widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2)) + widgets[1].setPrefix("£") + widgets[1].setMaximum(9999999) + widgets[1].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons) + widgets[1].setFont(font) + if loading: + widgets[1].setValue(self.plat.fund_plat_fee[0][x+2]) + widgets[1].valueChanged.connect(self.check_valid) + widgets[1].valueChanged.connect(self.update_tier_labels) - widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2)) - widgets[3].setSuffix("%") - widgets[3].setMaximum(100) - widgets[3].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons) - widgets[3].setFont(font) - widgets[3].valueChanged.connect(self.check_valid) + widgets.append(QLabel(self.gridLayoutWidget_2)) + widgets[2].setText(f"the fee is") + widgets[2].setFont(font) - # TODO: why 28.5? - self.gridLayoutWidget_2.setGeometry(11, 309, 611, int(round(28.5 * (self.fund_fee_rows + 1), 0))) - for i in range(len(widgets)): - self.gridLayout_2.addWidget(widgets[i], self.fund_fee_rows, i, 1, 1) + widgets.append(FastEditQDoubleSpinBox(self.gridLayoutWidget_2)) + widgets[3].setSuffix("%") + widgets[3].setMaximum(100) + widgets[3].setButtonSymbols(FastEditQDoubleSpinBox.ButtonSymbols.NoButtons) + widgets[3].setFont(font) + if loading: + widgets[3].setValue(self.plat.fund_plat_fee[1][x+2]) + widgets[3].valueChanged.connect(self.check_valid) - self.fund_fee_rows += 1 + if loading: + grid_height = int(round(28.5 * self.fund_fee_rows)) + else: + grid_height = int(round(28.5 * (self.fund_fee_rows + 1))) + self.gridLayoutWidget_2.setGeometry(QRect(11, 309, 611, grid_height)) + for i in range(len(widgets)): + if loading: + self.gridLayout_2.addWidget(widgets[i], x + 1, i, 1, 1) + else: + self.gridLayout_2.addWidget(widgets[i], self.fund_fee_rows, i, 1, 1) - self.widgets_list_list.append(widgets) - cur_label_idx = self.gridLayout_2.indexOf(widgets[0]) - cur_box_idx = self.gridLayout_2.indexOf(widgets[1]) - cur_label_pos = list(self.gridLayout_2.getItemPosition(cur_label_idx))[:2] - cur_box_pos = list(self.gridLayout_2.getItemPosition(cur_box_idx))[:2] + if not loading: + self.fund_fee_rows += 1 - prev_box_row = cur_box_pos[0] - 1 - prev_box_item = self.gridLayout_2.itemAtPosition(prev_box_row, cur_box_pos[1]).widget() - cur_label_item = self.gridLayout_2.itemAtPosition(cur_label_pos[0], cur_label_pos[1]).widget() - cur_label_item.setText(f"between £{int(prev_box_item.value())} and") + self.widgets_list_list.append(widgets) + cur_label_idx = self.gridLayout_2.indexOf(widgets[0]) + cur_box_idx = self.gridLayout_2.indexOf(widgets[1]) + cur_label_pos = list(self.gridLayout_2.getItemPosition(cur_label_idx))[:2] + cur_box_pos = list(self.gridLayout_2.getItemPosition(cur_box_idx))[:2] + + prev_box_row = cur_box_pos[0] - 1 + prev_box_item = self.gridLayout_2.itemAtPosition(prev_box_row, cur_box_pos[1]).widget() + cur_label_item = self.gridLayout_2.itemAtPosition(cur_label_pos[0], cur_label_pos[1]).widget() + cur_label_item.setText(f"between £{int(prev_box_item.value())} and") if self.fund_fee_rows > 1: self.del_row_but.setEnabled(True) @@ -267,8 +327,9 @@ class PlatformEdit(QWidget): self.add_row_but.setEnabled(False) self.check_valid() + self.update_tier_labels() - # TODO: Tab order + # TODO: Tab/focus order def remove_row(self): for widget in self.widgets_list_list[self.fund_fee_rows - 2]: @@ -286,31 +347,3 @@ class PlatformEdit(QWidget): self.check_valid() self.update_tier_labels() - - # Getter functions (is this necessary? maybe directly reading class vars would be best...) - def get_optional_boxes(self): - return self.check_boxes_ticked - - def get_plat_name(self): - return self.plat_name - - def get_fund_plat_fee(self): - return self.fund_plat_fee - - def get_fund_deal_fee(self): - return self.fund_deal_fee - - def get_share_plat_fee(self): - return self.share_plat_fee - - def get_share_plat_max_fee(self): - return self.share_plat_max_fee - - def get_share_deal_fee(self): - return self.share_deal_fee - - def get_share_deal_reduce_trades(self): - return self.share_deal_reduce_trades - - def get_share_deal_reduce_amount(self): - return self.share_deal_reduce_amount diff --git a/src/platform_list.py b/src/platform_list.py new file mode 100644 index 0000000..77574e1 --- /dev/null +++ b/src/platform_list.py @@ -0,0 +1,148 @@ +from PyQt6 import uic +from PyQt6.QtCore import QRegularExpression +from PyQt6.QtGui import QIcon, QRegularExpressionValidator, QFont +from PyQt6.QtWidgets import QWidget, QListWidgetItem, QDialog, QDialogButtonBox, QMessageBox + +import resource_finder +from db_handler import DBHandler +from data_struct import Platform +from platform_edit import PlatformEdit + + +class PlatformRename(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + # Import Qt Designer UI XML file + uic.loadUi(resource_finder.get_res_path("gui/dialogs/platform_rename.ui"), self) + self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) + + self.rename_plat_box.setFocus() + self.new_name = "" + + # Set validators + # Regex accepts any characters that match [a-Z], [0-9] or _ + self.rename_plat_box.setValidator( + QRegularExpressionValidator(QRegularExpression("\\w*")) + ) + + self.rename_plat_ok_but.clicked.connect(self.store_new_name) + + def store_new_name(self): + self.new_name = self.rename_plat_box.text() + + def closeEvent(self, event): + event.ignore() + self.reject() + + +class RemoveConfirm(QMessageBox): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) + self.setWindowTitle("Remove platform?") + self.setIcon(QMessageBox.Icon.Warning) + font = QFont() + font.setPointSize(11) + self.setFont(font) + self.setText("Are you sure you want to remove this platform?") + self.setInformativeText("This action is immediate and permanent") + + self.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel) + self.setDefaultButton(QMessageBox.StandardButton.Cancel) + font.setPointSize(10) + self.findChild(QDialogButtonBox).setFont(font) + + +class PlatformList(QWidget): + def __init__(self, db: DBHandler): + super().__init__() + # Import Qt Designer UI XML file + uic.loadUi(resource_finder.get_res_path("gui/platform_list.ui"), self) + self.setWindowIcon(QIcon(resource_finder.get_res_path("icon2.ico"))) + + # Initialise class variables + self.db = db + self.plat_edit_win = None + self.plat_list_dialog = PlatformRename() + self.del_plat_dialog = RemoveConfirm() + self.plat_list = [] + self.plat_name_list = [] + self.new_plat_name = "" + self.update_plat_list() + self.db_indices = [x for x in range(len(self.plat_name_list))] + + # Handle events + self.add_plat_but.clicked.connect(self.add_platform) + self.del_plat_but.clicked.connect(self.remove_platform) + self.edit_plat_but.clicked.connect(self.edit_platform) + self.plist_save_but.clicked.connect(self.save_platforms) + self.plat_enabled_check.checkStateChanged.connect(self.toggle_platform_state) + self.platListWidget.currentRowChanged.connect(self.get_enabled_state) + + def update_plat_list(self): + self.plat_name_list = self.db.retrieve_plat_list() + self.plat_list = self.db.retrieve_platforms() + self.platListWidget.clear() + + for i in range(len(self.plat_name_list)): + plat_name = self.plat_name_list[i] + item = QListWidgetItem() + if plat_name is not None: + item.setText(plat_name) + else: + item.setText(f"Unnamed [ID: {i}]") + + self.platListWidget.addItem(item) + + def add_platform(self): + name_dialog_res = self.plat_list_dialog.exec() + if name_dialog_res == QDialog.DialogCode.Accepted: + name = self.plat_list_dialog.new_name + index = self.platListWidget.count() + if name != "": + self.platListWidget.addItem(name) + name_param = name + else: + self.platListWidget.addItem(f"Unnamed [ID: {index}]") + name_param = None + + self.plat_list.append(Platform( + index, [[0], [0]], name_param, True, 0, 0, None, 0, None, None) + ) + self.plat_edit_win = PlatformEdit(self.plat_list[index]) + self.plat_edit_win.show() + + def get_enabled_state(self): + index = self.platListWidget.currentRow() + is_enabled = self.plat_list[index].enabled + if is_enabled: + self.plat_enabled_check.setChecked(True) + else: + self.plat_enabled_check.setChecked(False) + + if index in self.db_indices: + self.del_plat_but.setEnabled(True) + else: + self.del_plat_but.setEnabled(False) + + def edit_platform(self): + index = self.platListWidget.currentRow() + self.plat_edit_win = PlatformEdit(self.plat_list[index]) + self.plat_edit_win.show() + + def save_platforms(self): + self.db.write_platforms(self.plat_list) + self.update_plat_list() + self.db_indices = [x for x in range(len(self.plat_name_list))] + + def toggle_platform_state(self): + index = self.platListWidget.currentRow() + state = self.plat_enabled_check.isChecked() + self.plat_list[index].enabled = state + + def remove_platform(self): + index = self.platListWidget.currentRow() + del_dialog_res = self.del_plat_dialog.exec() + if del_dialog_res == QMessageBox.StandardButton.Yes: + self.db.remove_platform(index) + self.update_plat_list() diff --git a/src/resource_finder.py b/src/resource_finder.py index d0e006a..f2b33e2 100644 --- a/src/resource_finder.py +++ b/src/resource_finder.py @@ -10,4 +10,4 @@ def get_res_path(relative_path): except AttributeError: base_path = os.path.abspath(".") - return os.path.join(base_path, relative_path) \ No newline at end of file + return os.path.join(base_path, relative_path) diff --git a/src/widgets/fastedit_spinbox.py b/src/widgets/fastedit_spinbox.py index dcdc692..ae93506 100644 --- a/src/widgets/fastedit_spinbox.py +++ b/src/widgets/fastedit_spinbox.py @@ -7,6 +7,7 @@ class FastEditQDoubleSpinBox(QDoubleSpinBox): QTimer.singleShot(0, self.selectAll) super(FastEditQDoubleSpinBox, self).focusInEvent(e) + class FastEditQSpinBox(QSpinBox): def focusInEvent(self, e): QTimer.singleShot(0, self.selectAll) diff --git a/src/widgets/mpl_widget.py b/src/widgets/mpl_widget.py new file mode 100644 index 0000000..a5de8ac --- /dev/null +++ b/src/widgets/mpl_widget.py @@ -0,0 +1,13 @@ +from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg +from matplotlib.figure import Figure +from PyQt6.QtWidgets import QWidget, QVBoxLayout + + +class MplWidget(QWidget): + def __init__(self, parent=None): + super().__init__(parent) + self.canvas = FigureCanvasQTAgg(Figure(figsize=(15, 8), dpi=100)) + vertical_layout = QVBoxLayout() + vertical_layout.addWidget(self.canvas) + self.canvas.axes = self.canvas.figure.add_subplot(1, 1, 1) + self.setLayout(vertical_layout)