From 2e4ca86edc746d6ce90893d03525cf6cdd9ba7df Mon Sep 17 00:00:00 2001 From: Hugo GIRARD Date: Sun, 26 Jan 2025 17:28:04 +0100 Subject: [PATCH 1/9] Add profile editor gui --- requirements.txt | 1 + src/gui/build_from_yaml.py | 151 ++++++++++++++++++++++++++ src/gui/d4lfitem.py | 164 +++++++++++++++++++++++++++++ src/gui/dialog.py | 95 +++++++++++++++++ src/gui/profile_tab.py | 210 +++++++++++++++++++++++++++++++++++++ src/gui/qt_gui.py | 6 +- 6 files changed, 625 insertions(+), 2 deletions(-) create mode 100644 src/gui/build_from_yaml.py create mode 100644 src/gui/d4lfitem.py create mode 100644 src/gui/dialog.py create mode 100644 src/gui/profile_tab.py diff --git a/requirements.txt b/requirements.txt index 3970b56..3219064 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,6 +25,7 @@ pytest-mock pytest-pythonpath pytest-xdist pytweening +pywinstyles pywin32 pyyaml rapidfuzz diff --git a/src/gui/build_from_yaml.py b/src/gui/build_from_yaml.py new file mode 100644 index 0000000..415a9be --- /dev/null +++ b/src/gui/build_from_yaml.py @@ -0,0 +1,151 @@ +import yaml +from typing import List + +class Affix: + def __init__(self, name: str): + self.name = name + + @classmethod + def from_dict(cls, data): + return cls(name=data['name']) + + def to_dict(self): + return {'name': self.name} + + def __str__(self): + return f"name: {self.name}" + + +class AffixPool: + def __init__(self, count: List[Affix], minCount: int, minGreaterAffixCount: int): + self.count = count + self.minCount = minCount + self.minGreaterAffixCount = minGreaterAffixCount + + @classmethod + def from_dict(cls, data): + count = [Affix.from_dict(affix) for affix in data['count']] + try: + minCount = data['minCount'] + except KeyError: + minCount = None + try: + minGreaterAffixCount = data['minGreaterAffixCount'] + except KeyError: + minGreaterAffixCount = None + return cls(count=count, minCount=minCount, minGreaterAffixCount=minGreaterAffixCount) + + def to_dict(self): + if self.minCount is None or self.minGreaterAffixCount is None: + return { + 'count': [affix.to_dict() for affix in self.count] + } + return { + 'count': [affix.to_dict() for affix in self.count], + 'minCount': self.minCount, + 'minGreaterAffixCount': self.minGreaterAffixCount + } + + def __str__(self): + return f"count: {self.count}\nminCount: {self.minCount}\nminGreaterAffixCount: {self.minGreaterAffixCount}" + + def set_count(self, newCount : List[str]): + self.count = [ Affix(name) for name in newCount] + + +class Item: + def __init__(self, itemName: str, itemType: List[str], minPower: int, affixPool: List[AffixPool], inherentPool: List[AffixPool] = None): + self.itemName = itemName + self.itemType = itemType + self.minPower = minPower + self.affixPool = affixPool + self.inherentPool = inherentPool if inherentPool else [] + + def set_affix_pool(self, newAffixPool: List[str], newMinCount : int, newGreaterCount: int): + self.affixPool[0].minCount = newMinCount + self.affixPool[0].minGreaterAffixCount = newGreaterCount + self.affixPool[0].set_count(newAffixPool) + + def set_inherent_pool(self, newInherentPool: List[str]): + self.inherentPool[0].set_count(newInherentPool) + + @classmethod + def from_dict(cls, data): + itemName = list(data.keys())[0] + affixPool = [AffixPool.from_dict(pool) for pool in data[itemName]['affixPool']] + try: + inherentPool = [AffixPool.from_dict(pool) for pool in data[itemName]['inherentPool']] + except: + inherentPool = [] + try: + minPower = data[itemName]['minPower'] + except KeyError as e: + minPower = 0 + return cls(itemName=itemName, itemType=data[itemName]['itemType'], minPower=minPower, affixPool=affixPool, inherentPool=inherentPool) + + def to_dict(self): + data = { f'{self.itemName}' : + { + 'itemType': self.itemType, + 'minPower': self.minPower, + 'affixPool': [pool.to_dict() for pool in self.affixPool] + } + } + if self.inherentPool: + data[self.itemName]['inherentPool'] = [pool.to_dict() for pool in self.inherentPool] + return data + + def __str__(self): + affixPoolStr = f"count:\n" + for affix in self.affixPool[0].count: + affixPoolStr += f"\t- {affix}\n" + affixPoolStr += f"minCount: {self.affixPool[0].minCount}\nminGreaterAffixCount: {self.affixPool[0].minGreaterAffixCount}" + inherentPoolStr = "" + if self.inherentPool: + inherentPoolStr = f"count:\n" + for inherent in self.inherentPool[0].count: + inherentPoolStr += f"\t-{inherent}\n" + inherentPoolStr += f"minCount: {self.inherentPool[0].minCount}\nminGreaterAffixCount: {self.inherentPool[0].minGreaterAffixCount}" + if inherentPoolStr == "": + return f"itemName: {self.itemName}\nitemType: {self.itemType}\nminPower: {self.minPower}\naffixPool: {affixPoolStr}\n" + return f"itemName: {self.itemName}\nitemType: {self.itemType}\nminPower: {self.minPower}\naffixPool: {affixPoolStr}\ninherentPool: {inherentPoolStr}\n" + + def set_minPower(self, minPower): + self.minPower = minPower + + def set_itemType(self, itemType): + self.itemType = itemType + + def set_itemName(self, itemName): + self.itemName = itemName + + def set_minGreaterAffix(self, minGreaterAffix): + self.affixPool[0].minGreaterAffixCount = minGreaterAffix + +class Root: + def __init__(self, affixes: List[Item], data): + self.affixes = affixes + self.data = data + + @classmethod + def from_dict(cls, data): + affixes = [Item.from_dict(item) for item in data['Affixes']] + return cls(affixes=affixes, data=data) + + def to_dict(self): + self.data['Affixes'] = [item.to_dict() for item in self.affixes] + return self.data + + def set_min_power(self, minPower): + for affix in self.affixes: + affix.set_minPower(minPower) + + @classmethod + def load_yaml(cls, file_path): + with open(file_path, 'r') as file: + data = yaml.safe_load(file) + return cls.from_dict(data) + + def save_yaml(self, file_path): + with open(file_path, 'w') as file: + yaml.safe_dump(self.to_dict(), file) \ No newline at end of file diff --git a/src/gui/d4lfitem.py b/src/gui/d4lfitem.py new file mode 100644 index 0000000..4507c00 --- /dev/null +++ b/src/gui/d4lfitem.py @@ -0,0 +1,164 @@ +from src.gui.build_from_yaml import * +from PyQt6.QtWidgets import QHeaderView, QTableView, QLabel, QVBoxLayout, QHBoxLayout, QSpinBox, QComboBox, QGroupBox, QSizePolicy, QFormLayout, QCompleter, QMessageBox +from PyQt6.QtCore import Qt + +class D4LFItem(QGroupBox): + def __init__(self, item : Item, affixesNames, itemTypes): + super().__init__() + self.setTitle(item.itemName) + self.setStyleSheet("QGroupBox {font-size: 10pt;} QLabel {font-size: 10pt;} QComboBox {font-size: 10pt;} QSpinBox {font-size: 10pt;}") + self.main_layout = QVBoxLayout() + self.main_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + self.item = item + self.changed = False + self.setMaximumSize(300, 500) + self.affixesNames = affixesNames + self.itemTypes = itemTypes + + self.minPowerEdit = QSpinBox(self) + self.minPowerEdit.setMaximum(800) + self.minPowerEdit.setMaximumWidth(100) + self.minPowerEdit.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.minPower_form = QFormLayout() + self.minPower_form.addRow(QLabel("minPower:"), self.minPowerEdit) + self.main_layout.addLayout(self.minPower_form) + + if item.affixPool: + self.affixes_label = QLabel("Affixes:") + self.affixes_label.setMaximumSize(200, 50) + self.affixes_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.main_layout.addWidget(self.affixes_label) + self.affixListLayout = QVBoxLayout() + self.main_layout.addLayout(self.affixListLayout) + + if item.inherentPool: + self.inherent_label = QLabel("Inherent:") + self.inherent_label.setMaximumSize(200, 50) + self.inherent_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.main_layout.addWidget(self.inherent_label) + self.inherentListLayout = QVBoxLayout() + self.main_layout.addLayout(self.inherentListLayout) + + self.load_item() + self.setLayout(self.main_layout) + + self.minPowerEdit.valueChanged.connect(self.item_changed) + + def load_item(self): + self.minPowerEdit.setValue(self.item.minPower) + for pool in self.item.affixPool: + for affix in pool.count: + affixComboBox = self.create_affix_combobox(affix.name) + self.affixListLayout.addWidget(affixComboBox) + if pool.minCount != None: + minCount = self.create_pair_label_spinbox("minCount:", 3, pool.minCount) + self.affixListLayout.addLayout(minCount) + if pool.minGreaterAffixCount != None: + minGreaterAffixCount = self.create_pair_label_spinbox("minGreaterAffixCount:", 3, pool.minGreaterAffixCount) + self.affixListLayout.addLayout(minGreaterAffixCount) + + for pool in self.item.inherentPool: + for affix in pool.count: + affixComboBox = self.create_affix_combobox(affix.name) + self.inherentListLayout.addWidget(affixComboBox) + + def create_affix_combobox(self, affix_name): + affixComboBox = QComboBox() + affixComboBox.setEditable(True) + affixComboBox.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) + affixComboBox.completer().setCompletionMode(QCompleter.CompletionMode.PopupCompletion) + + table_view = QTableView() + table_view.horizontalHeader().setVisible(False) + table_view.verticalHeader().setVisible(False) + table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + + affixComboBox.setView(table_view) + affixComboBox.addItems(self.affixesNames.values()) + + key_list = list(self.affixesNames.keys()) + try: + idx = key_list.index(affix_name) + except ValueError: + self.create_alert(f"{affix_name} is not a valid affix.") + return affixComboBox + affixComboBox.setCurrentIndex(idx) + affixComboBox.setMaximumWidth(250) + affixComboBox.currentTextChanged.connect(self.item_changed) + return affixComboBox + + def create_alert(self, msg: str): + reply = QMessageBox.warning(self, 'Alert', msg, QMessageBox.StandardButton.Ok) + if reply == QMessageBox.StandardButton.Ok: + return True + else: + return False + + def create_pair_label_spinbox(self, labelText, maxValue, value): + ret = QHBoxLayout() + ret.setContentsMargins(0, 0, 50, 0) + label = QLabel(labelText) + spinBox = QSpinBox() + spinBox.setMaximum(maxValue) + spinBox.setValue(value) + spinBox.setMaximumWidth(70) + label.setAlignment(Qt.AlignmentFlag.AlignLeft) + spinBox.setAlignment(Qt.AlignmentFlag.AlignLeft) + ret.addWidget(label) + ret.addWidget(spinBox) + spinBox.valueChanged.connect(self.item_changed) + return ret + + def set_minPower(self, minPower): + self.minPowerEdit.setValue(minPower) + + def set_minGreaterAffix(self, minGreaterAffix): + for i in range(self.affixListLayout.count()): + layout = self.affixListLayout.itemAt(i).layout() + if layout != None: + if isinstance(layout, QHBoxLayout): + if layout.itemAt(0).widget().text() == "minGreaterAffixCount:": + layout.itemAt(1).widget().setValue(minGreaterAffix) + + def set_minCount(self, minCount): + for i in range(self.affixListLayout.count()): + layout = self.affixListLayout.itemAt(i).layout() + if layout != None: + if isinstance(layout, QHBoxLayout): + if layout.itemAt(0).widget().text() == "minCount:": + layout.itemAt(1).widget().setValue(minCount) + + def find_key_from_value(self, target_value): + for key, value in self.affixesNames.items(): + if value == target_value: + return key + return None + + def save_item(self): + self.item.minPower = self.minPowerEdit.value() + for pool in self.item.affixPool: + for i in range(self.affixListLayout.count()): + widget = self.affixListLayout.itemAt(i).widget() + layout = self.affixListLayout.itemAt(i).layout() + if widget != None: + if isinstance(widget, QComboBox): + pool.count[i] = Affix(self.find_key_from_value(widget.currentText())) + elif layout != None: + if isinstance(layout, QHBoxLayout): + if layout.itemAt(0).widget().text() == "minCount:": + pool.minCount = layout.itemAt(1).widget().value() + elif layout.itemAt(0).widget().text() == "minGreaterAffixCount:": + pool.minGreaterAffixCount = layout.itemAt(1).widget().value() + + for pool in self.item.inherentPool: + for i in range(self.inherentListLayout.count()): + widget = self.inherentListLayout.itemAt(i).widget() + if isinstance(widget, QComboBox): + pool.count[i] = Affix(self.find_key_from_value(widget.currentText())) + self.changed = False + + def item_changed(self): + self.changed = True + + def has_changes(self): + return self.changed \ No newline at end of file diff --git a/src/gui/dialog.py b/src/gui/dialog.py new file mode 100644 index 0000000..af72aed --- /dev/null +++ b/src/gui/dialog.py @@ -0,0 +1,95 @@ +from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QSpinBox, QPushButton, QHBoxLayout, QGridLayout, QTextBrowser +from PyQt6.QtWidgets import QApplication, QMainWindow, QWidgetItem, QPushButton, QScrollArea, QFileDialog, QVBoxLayout, QWidget, QComboBox, QLineEdit, QLabel, QFormLayout, QHBoxLayout, QMessageBox, QMenuBar, QStatusBar, QSpinBox, QSizePolicy +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QAction, QFont, QIcon +import pywinstyles + +class MinPowerDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Set Min Power") + self.setFixedSize(250, 150) + pywinstyles.apply_style(self, "dark") + self.layout = QVBoxLayout() + + self.label = QLabel("Min Power:") + self.layout.addWidget(self.label) + + self.spinBox = QSpinBox() + self.spinBox.setRange(0, 800) + self.spinBox.setValue(800) + self.layout.addWidget(self.spinBox) + + self.buttonLayout = QHBoxLayout() + self.okButton = QPushButton("OK") + self.okButton.clicked.connect(self.accept) + self.cancelButton = QPushButton("Cancel") + self.cancelButton.clicked.connect(self.reject) + self.buttonLayout.addWidget(self.okButton) + self.buttonLayout.addWidget(self.cancelButton) + + self.layout.addLayout(self.buttonLayout) + self.setLayout(self.layout) + + def get_value(self): + return self.spinBox.value() + +class MinGreaterDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Set Min Greater Affix") + self.setFixedSize(250, 150) + pywinstyles.apply_style(self, "dark") + self.layout = QVBoxLayout() + + self.label = QLabel("Min Greater Affix:") + self.layout.addWidget(self.label) + + self.spinBox = QSpinBox() + self.spinBox.setRange(0, 3) + self.spinBox.setValue(0) + self.layout.addWidget(self.spinBox) + + self.buttonLayout = QHBoxLayout() + self.okButton = QPushButton("OK") + self.okButton.clicked.connect(self.accept) + self.cancelButton = QPushButton("Cancel") + self.cancelButton.clicked.connect(self.reject) + self.buttonLayout.addWidget(self.okButton) + self.buttonLayout.addWidget(self.cancelButton) + + self.layout.addLayout(self.buttonLayout) + self.setLayout(self.layout) + + def get_value(self): + return self.spinBox.value() + +class MinCountDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Set Min Count") + self.setFixedSize(250, 150) + pywinstyles.apply_style(self, "dark") + self.layout = QVBoxLayout() + + self.label = QLabel("Min Count:") + self.layout.addWidget(self.label) + + self.spinBox = QSpinBox() + self.spinBox.setRange(0, 3) + self.spinBox.setValue(0) + self.layout.addWidget(self.spinBox) + + self.buttonLayout = QHBoxLayout() + self.okButton = QPushButton("OK") + self.okButton.clicked.connect(self.accept) + self.cancelButton = QPushButton("Cancel") + self.cancelButton.clicked.connect(self.reject) + self.buttonLayout.addWidget(self.okButton) + self.buttonLayout.addWidget(self.cancelButton) + + self.layout.addLayout(self.buttonLayout) + self.setLayout(self.layout) + + def get_value(self): + return self.spinBox.value() \ No newline at end of file diff --git a/src/gui/profile_tab.py b/src/gui/profile_tab.py new file mode 100644 index 0000000..cd4bfc1 --- /dev/null +++ b/src/gui/profile_tab.py @@ -0,0 +1,210 @@ +import configparser +import sys +import os +import json +import time +from src.gui.build_from_yaml import * +from src.gui.dialog import * +from src.gui.d4lfitem import * +from src.config import BASE_DIR + +PROFILE_TABNAME = "Edit Profile" + +class ProfileTab(QWidget): + def __init__(self): + super().__init__() + + self.root = None + self.file_path = None + self.main_layout = QVBoxLayout(self) + + scroll_area = QScrollArea(self) + scroll_widget = QWidget(scroll_area) + scrollable_layout = QVBoxLayout(scroll_widget) + scroll_area.setWidgetResizable(True) + + + info_layout = QHBoxLayout() + info_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + profile_groupbox = QGroupBox("Profile Loaded") + profile_groupbox_layout = QVBoxLayout() + self.filenameLabel = QLabel("") + self.filenameLabel.setStyleSheet("font-size: 12pt;") + profile_groupbox_layout.addWidget(self.filenameLabel) + profile_groupbox.setLayout(profile_groupbox_layout) + info_layout.addWidget(profile_groupbox) + + tools_groupbox = QGroupBox("Tools") + tools_groupbox_layout = QHBoxLayout() + self.file_button = QPushButton("File") + self.save_button = QPushButton("Save") + self.file_button.clicked.connect(self.load_yaml) + self.save_button.clicked.connect(self.save_yaml) + tools_groupbox_layout.addWidget(self.file_button) + tools_groupbox_layout.addWidget(self.save_button) + + self.set_all_minGreaterAffix_button = QPushButton("Set all minGreaterAffix") + self.set_all_minPower_button = QPushButton("Set all minPower") + self.set_all_minCount_button = QPushButton("Set all minCount") + self.set_all_minGreaterAffix_button.clicked.connect(self.set_all_minGreaterAffix) + self.set_all_minPower_button.clicked.connect(self.set_all_minPower) + self.set_all_minCount_button.clicked.connect(self.set_all_minCount) + tools_groupbox_layout.addWidget(self.set_all_minGreaterAffix_button) + tools_groupbox_layout.addWidget(self.set_all_minPower_button) + tools_groupbox_layout.addWidget(self.set_all_minCount_button) + tools_groupbox.setLayout(tools_groupbox_layout) + info_layout.addWidget(tools_groupbox) + self.main_layout.addLayout(info_layout) + + self.itemTypes = None + with open(str(BASE_DIR / "assets/lang/enUS/item_types.json"), 'r') as f: + self.itemTypes = json.load(f) + + self.affixesNames = None + with open(str(BASE_DIR / "assets/lang/enUS/affixes.json"), 'r') as f: + self.affixesNames = json.load(f) + + self.item_widgets = QWidget() + self.item_widgets_layout = QGridLayout() + self.item_list = [] + self.item_widgets.setLayout(self.item_widgets_layout) + scrollable_layout.addWidget(self.item_widgets) + self.update_filename_label() + scroll_widget.setLayout(scrollable_layout) + scroll_area.setWidget(scroll_widget) + self.main_layout.addWidget(scroll_area) + instructions_label = QLabel("Instructions") + self.main_layout.addWidget(instructions_label) + + instructions_text = QTextBrowser() + instructions_text.append("The default profile loaded is the first profile in the params.ini file. You can change the default profile in the params.ini file by changing the 'profiles' value to the desired profile name.") + instructions_text.append("You can also load a profile by clicking the 'File' button.") + instructions_text.append("") + instructions_text.append("All values are not saved automatically immediately upon changing.") + instructions_text.append("You must click the save button to apply the changes to the profile.") + instructions_text.append("") + instructions_text.append("Note: You will need to restart d4lf after modifying these values. Modifying profile file manually while this gui is running is not supported (and really not necessary).") + + instructions_text.setFixedHeight(150) + self.main_layout.addWidget(instructions_text) + self.setLayout(self.main_layout) + + + def confirm_discard_changes(self): + reply = QMessageBox.warning(self, 'Unsaved Changes', + "You have unsaved changes. Do you want to save them before continuing?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel) + if reply == QMessageBox.StandardButton.Yes: + self.save_yaml() + return True + elif reply == QMessageBox.StandardButton.No: + return True + else: + return False + + def create_alert(self, msg: str): + reply = QMessageBox.warning(self, 'Alert', msg, QMessageBox.StandardButton.Ok) + if reply == QMessageBox.StandardButton.Ok: + return True + else: + return False + + def set_all_minGreaterAffix(self): + if self.file_path: + dialog = MinGreaterDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + minGreaterAffix = dialog.get_value() + for d4lf_item in self.item_list: + d4lf_item.set_minGreaterAffix(minGreaterAffix) + else: + self.create_alert("No file loaded") + + def set_all_minCount(self): + if self.file_path: + dialog = MinCountDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + minCount = dialog.get_value() + for d4lf_item in self.item_list: + d4lf_item.set_minCount(minCount) + else: + self.create_alert("No file loaded") + + + def set_all_minPower(self): + if self.file_path: + dialog = MinPowerDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + minPower = dialog.get_value() + for d4lf_item in self.item_list: + d4lf_item.set_minPower(minPower) + else: + self.create_alert("No file loaded") + + def load_items(self): + i = 0 + j = 0 + + while self.item_widgets_layout.count(): + item = self.item_widgets_layout.takeAt(0) + item.widget().deleteLater() + + self.item_list = [] + + for item in self.root.affixes: + d4lf_item = D4LFItem(item, self.affixesNames, self.itemTypes) + self.item_list.append(d4lf_item) + if i % 4 == 0 and i != 0: + i = 0 + j += 1 + self.item_widgets_layout.addWidget(d4lf_item, j, i) + i += 1 + + def load_yaml(self): + if not self.file_path: # at start, set default file to build in params.ini + params_file = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "params.ini") + params_data = configparser.ConfigParser() + params_data.read(params_file) + profile_names = params_data.get('general', 'profiles').split(',') + if profile_names[0]: + file_path = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "profiles", f"{profile_names[0]}.yaml") + else: + base_dir = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "profiles") + file_path, _ = QFileDialog.getOpenFileName(self, "Open YAML File", base_dir, "YAML Files (*.yaml *.yml)") + else: + base_dir = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "profiles") + file_path, _ = QFileDialog.getOpenFileName(self, "Open YAML File", base_dir, "YAML Files (*.yaml *.yml)") + if file_path: + self.root = Root.load_yaml(file_path) + self.file_path = os.path.abspath(file_path) + self.update_filename_label() + self.load_items() + return True + return False + + def update_filename_label(self, close=False): + if close: + self.filenameLabel.setText("No file loaded") + return + if not self.file_path: + if not self.load_yaml(): + self.filenameLabel.setText("No file loaded") + return + + if self.file_path: + filename = self.file_path.split("\\")[-1] # Get the filename from the full path + filename_without_extension = filename.rsplit(".", 1)[0] # Remove the extension + display_name = filename_without_extension.replace("_", " ") # Replace underscores with spaces + self.filenameLabel.setText(display_name) + + def save_yaml(self): + for d4lf_item in self.item_list: + d4lf_item.save_item() + if self.root: + self.root.save_yaml(self.file_path) + + def check_close_save(self): + if self.item_list: + for d4lf_item in self.item_list: + if d4lf_item.has_changes(): + return self.confirm_discard_changes() \ No newline at end of file diff --git a/src/gui/qt_gui.py b/src/gui/qt_gui.py index e9b3209..af5a3c0 100644 --- a/src/gui/qt_gui.py +++ b/src/gui/qt_gui.py @@ -25,6 +25,7 @@ from src.config.helper import singleton from src.config.loader import IniConfigLoader from src.gui import config_tab +from src.gui import profile_tab from src.gui.importer.d4builds import import_d4builds from src.gui.importer.diablo_trade import import_diablo_trade from src.gui.importer.maxroll import import_maxroll @@ -69,7 +70,8 @@ def __init__(self): self._maxroll_or_d4builds_tab() self._diablo_trade_tab() self.tab_widget.addTab(config_tab.ConfigTab(), config_tab.CONFIG_TABNAME) - + self.profile_tab_widget = profile_tab.ProfileTab() + self.tab_widget.addTab(self.profile_tab_widget, profile_tab.PROFILE_TABNAME) LOGGER.root.addHandler(self.maxroll_log_handler) self.tab_widget.currentChanged.connect(self._handle_tab_changed) self._toggle_dark_mode() @@ -80,7 +82,7 @@ def closeEvent(self, e): self.settings.setValue("size", self.size()) self.settings.setValue("pos", self.pos()) self.settings.setValue("maximized", self.isMaximized()) - + self.profile_tab_widget.check_close_save() e.accept() def _diablo_trade_tab(self): From 01faa14466bf8e7468eab91b170efbab872d8842 Mon Sep 17 00:00:00 2001 From: CJ Shrader Date: Mon, 27 Jan 2025 12:54:21 -0500 Subject: [PATCH 2/9] - Added pitfighters_gull unique (#416) --- src/gui/importer/maxroll.py | 21 +++++++++++++++++---- src/tools/data/custom_uniques_enUS.json | 6 ++++++ 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/gui/importer/maxroll.py b/src/gui/importer/maxroll.py index 149c61b..600f5e9 100644 --- a/src/gui/importer/maxroll.py +++ b/src/gui/importer/maxroll.py @@ -60,11 +60,14 @@ def import_maxroll(url: str): unique_model = UniqueModel() unique_name = mapping_data["items"][resolved_item["id"]]["name"] try: + unique_name = _unique_name_special_handling(unique_name) unique_model.aspect = AspectUniqueFilterModel(name=unique_name) - unique_model.affix = [ - AffixFilterModel(name=x.name) - for x in _find_item_affixes(mapping_data=mapping_data, item_affixes=resolved_item["explicits"]) - ] + # Maxroll's uniques are all over the place in quality when it comes to affixes and names. + # Removing support for this for now. + # unique_model.affix = [ + # AffixFilterModel(name=x.name) + # for x in _find_item_affixes(mapping_data=mapping_data, item_affixes=resolved_item["explicits"]) + # ] unique_filters.append(unique_model) except Exception: LOGGER.exception(f"Unexpected error importing unique {unique_name}, please report a bug.") @@ -195,6 +198,16 @@ def _attr_desc_special_handling(affix_id: str) -> str: return "" +def _unique_name_special_handling(unique_name: str) -> str: + match unique_name: + case "[PH] Season 7 Necro Pants": + return "kessimes_legacy" + case "[PH] Season 7 Barb Chest": + return "mantle_of_mountains_fury" + case _: + return unique_name + + def _find_item_type(mapping_data: dict, value: str) -> ItemType | None: for d_key, d_value in mapping_data.items(): if d_key == value: diff --git a/src/tools/data/custom_uniques_enUS.json b/src/tools/data/custom_uniques_enUS.json index ccb3639..d089be3 100644 --- a/src/tools/data/custom_uniques_enUS.json +++ b/src/tools/data/custom_uniques_enUS.json @@ -29,6 +29,12 @@ 0 ] }, + "pitfighters_gull": { + "desc": "seconds and leaves behind a cloud of shadows. While within the cloud", + "num_idx": [ + 0 + ] + }, "rakanoths_wake": { "desc": "when you cast a skill with a cooldown you explode dealing fire damage", "num_idx": [ From bf24a1d92981cdae7b9a873e1533bfc83582bd16 Mon Sep 17 00:00:00 2001 From: CJ Shrader Date: Tue, 28 Jan 2025 07:54:31 -0500 Subject: [PATCH 3/9] Release (#417) --- src/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__init__.py b/src/__init__.py index 02281d7..5d23f9f 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -2,4 +2,4 @@ TP = concurrent.futures.ThreadPoolExecutor() -__version__ = "5.8.11" +__version__ = "5.8.12" From 20ef565a43085654eeae75616b45593e1f6d2c3f Mon Sep 17 00:00:00 2001 From: Hugo GIRARD Date: Sun, 26 Jan 2025 17:28:04 +0100 Subject: [PATCH 4/9] Add profile editor gui --- requirements.txt | 1 + src/gui/build_from_yaml.py | 151 ++++++++++++++++++++++++++ src/gui/d4lfitem.py | 164 +++++++++++++++++++++++++++++ src/gui/dialog.py | 95 +++++++++++++++++ src/gui/profile_tab.py | 210 +++++++++++++++++++++++++++++++++++++ src/gui/qt_gui.py | 6 +- 6 files changed, 625 insertions(+), 2 deletions(-) create mode 100644 src/gui/build_from_yaml.py create mode 100644 src/gui/d4lfitem.py create mode 100644 src/gui/dialog.py create mode 100644 src/gui/profile_tab.py diff --git a/requirements.txt b/requirements.txt index 3970b56..3219064 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,6 +25,7 @@ pytest-mock pytest-pythonpath pytest-xdist pytweening +pywinstyles pywin32 pyyaml rapidfuzz diff --git a/src/gui/build_from_yaml.py b/src/gui/build_from_yaml.py new file mode 100644 index 0000000..415a9be --- /dev/null +++ b/src/gui/build_from_yaml.py @@ -0,0 +1,151 @@ +import yaml +from typing import List + +class Affix: + def __init__(self, name: str): + self.name = name + + @classmethod + def from_dict(cls, data): + return cls(name=data['name']) + + def to_dict(self): + return {'name': self.name} + + def __str__(self): + return f"name: {self.name}" + + +class AffixPool: + def __init__(self, count: List[Affix], minCount: int, minGreaterAffixCount: int): + self.count = count + self.minCount = minCount + self.minGreaterAffixCount = minGreaterAffixCount + + @classmethod + def from_dict(cls, data): + count = [Affix.from_dict(affix) for affix in data['count']] + try: + minCount = data['minCount'] + except KeyError: + minCount = None + try: + minGreaterAffixCount = data['minGreaterAffixCount'] + except KeyError: + minGreaterAffixCount = None + return cls(count=count, minCount=minCount, minGreaterAffixCount=minGreaterAffixCount) + + def to_dict(self): + if self.minCount is None or self.minGreaterAffixCount is None: + return { + 'count': [affix.to_dict() for affix in self.count] + } + return { + 'count': [affix.to_dict() for affix in self.count], + 'minCount': self.minCount, + 'minGreaterAffixCount': self.minGreaterAffixCount + } + + def __str__(self): + return f"count: {self.count}\nminCount: {self.minCount}\nminGreaterAffixCount: {self.minGreaterAffixCount}" + + def set_count(self, newCount : List[str]): + self.count = [ Affix(name) for name in newCount] + + +class Item: + def __init__(self, itemName: str, itemType: List[str], minPower: int, affixPool: List[AffixPool], inherentPool: List[AffixPool] = None): + self.itemName = itemName + self.itemType = itemType + self.minPower = minPower + self.affixPool = affixPool + self.inherentPool = inherentPool if inherentPool else [] + + def set_affix_pool(self, newAffixPool: List[str], newMinCount : int, newGreaterCount: int): + self.affixPool[0].minCount = newMinCount + self.affixPool[0].minGreaterAffixCount = newGreaterCount + self.affixPool[0].set_count(newAffixPool) + + def set_inherent_pool(self, newInherentPool: List[str]): + self.inherentPool[0].set_count(newInherentPool) + + @classmethod + def from_dict(cls, data): + itemName = list(data.keys())[0] + affixPool = [AffixPool.from_dict(pool) for pool in data[itemName]['affixPool']] + try: + inherentPool = [AffixPool.from_dict(pool) for pool in data[itemName]['inherentPool']] + except: + inherentPool = [] + try: + minPower = data[itemName]['minPower'] + except KeyError as e: + minPower = 0 + return cls(itemName=itemName, itemType=data[itemName]['itemType'], minPower=minPower, affixPool=affixPool, inherentPool=inherentPool) + + def to_dict(self): + data = { f'{self.itemName}' : + { + 'itemType': self.itemType, + 'minPower': self.minPower, + 'affixPool': [pool.to_dict() for pool in self.affixPool] + } + } + if self.inherentPool: + data[self.itemName]['inherentPool'] = [pool.to_dict() for pool in self.inherentPool] + return data + + def __str__(self): + affixPoolStr = f"count:\n" + for affix in self.affixPool[0].count: + affixPoolStr += f"\t- {affix}\n" + affixPoolStr += f"minCount: {self.affixPool[0].minCount}\nminGreaterAffixCount: {self.affixPool[0].minGreaterAffixCount}" + inherentPoolStr = "" + if self.inherentPool: + inherentPoolStr = f"count:\n" + for inherent in self.inherentPool[0].count: + inherentPoolStr += f"\t-{inherent}\n" + inherentPoolStr += f"minCount: {self.inherentPool[0].minCount}\nminGreaterAffixCount: {self.inherentPool[0].minGreaterAffixCount}" + if inherentPoolStr == "": + return f"itemName: {self.itemName}\nitemType: {self.itemType}\nminPower: {self.minPower}\naffixPool: {affixPoolStr}\n" + return f"itemName: {self.itemName}\nitemType: {self.itemType}\nminPower: {self.minPower}\naffixPool: {affixPoolStr}\ninherentPool: {inherentPoolStr}\n" + + def set_minPower(self, minPower): + self.minPower = minPower + + def set_itemType(self, itemType): + self.itemType = itemType + + def set_itemName(self, itemName): + self.itemName = itemName + + def set_minGreaterAffix(self, minGreaterAffix): + self.affixPool[0].minGreaterAffixCount = minGreaterAffix + +class Root: + def __init__(self, affixes: List[Item], data): + self.affixes = affixes + self.data = data + + @classmethod + def from_dict(cls, data): + affixes = [Item.from_dict(item) for item in data['Affixes']] + return cls(affixes=affixes, data=data) + + def to_dict(self): + self.data['Affixes'] = [item.to_dict() for item in self.affixes] + return self.data + + def set_min_power(self, minPower): + for affix in self.affixes: + affix.set_minPower(minPower) + + @classmethod + def load_yaml(cls, file_path): + with open(file_path, 'r') as file: + data = yaml.safe_load(file) + return cls.from_dict(data) + + def save_yaml(self, file_path): + with open(file_path, 'w') as file: + yaml.safe_dump(self.to_dict(), file) \ No newline at end of file diff --git a/src/gui/d4lfitem.py b/src/gui/d4lfitem.py new file mode 100644 index 0000000..4507c00 --- /dev/null +++ b/src/gui/d4lfitem.py @@ -0,0 +1,164 @@ +from src.gui.build_from_yaml import * +from PyQt6.QtWidgets import QHeaderView, QTableView, QLabel, QVBoxLayout, QHBoxLayout, QSpinBox, QComboBox, QGroupBox, QSizePolicy, QFormLayout, QCompleter, QMessageBox +from PyQt6.QtCore import Qt + +class D4LFItem(QGroupBox): + def __init__(self, item : Item, affixesNames, itemTypes): + super().__init__() + self.setTitle(item.itemName) + self.setStyleSheet("QGroupBox {font-size: 10pt;} QLabel {font-size: 10pt;} QComboBox {font-size: 10pt;} QSpinBox {font-size: 10pt;}") + self.main_layout = QVBoxLayout() + self.main_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + self.item = item + self.changed = False + self.setMaximumSize(300, 500) + self.affixesNames = affixesNames + self.itemTypes = itemTypes + + self.minPowerEdit = QSpinBox(self) + self.minPowerEdit.setMaximum(800) + self.minPowerEdit.setMaximumWidth(100) + self.minPowerEdit.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.minPower_form = QFormLayout() + self.minPower_form.addRow(QLabel("minPower:"), self.minPowerEdit) + self.main_layout.addLayout(self.minPower_form) + + if item.affixPool: + self.affixes_label = QLabel("Affixes:") + self.affixes_label.setMaximumSize(200, 50) + self.affixes_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.main_layout.addWidget(self.affixes_label) + self.affixListLayout = QVBoxLayout() + self.main_layout.addLayout(self.affixListLayout) + + if item.inherentPool: + self.inherent_label = QLabel("Inherent:") + self.inherent_label.setMaximumSize(200, 50) + self.inherent_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.main_layout.addWidget(self.inherent_label) + self.inherentListLayout = QVBoxLayout() + self.main_layout.addLayout(self.inherentListLayout) + + self.load_item() + self.setLayout(self.main_layout) + + self.minPowerEdit.valueChanged.connect(self.item_changed) + + def load_item(self): + self.minPowerEdit.setValue(self.item.minPower) + for pool in self.item.affixPool: + for affix in pool.count: + affixComboBox = self.create_affix_combobox(affix.name) + self.affixListLayout.addWidget(affixComboBox) + if pool.minCount != None: + minCount = self.create_pair_label_spinbox("minCount:", 3, pool.minCount) + self.affixListLayout.addLayout(minCount) + if pool.minGreaterAffixCount != None: + minGreaterAffixCount = self.create_pair_label_spinbox("minGreaterAffixCount:", 3, pool.minGreaterAffixCount) + self.affixListLayout.addLayout(minGreaterAffixCount) + + for pool in self.item.inherentPool: + for affix in pool.count: + affixComboBox = self.create_affix_combobox(affix.name) + self.inherentListLayout.addWidget(affixComboBox) + + def create_affix_combobox(self, affix_name): + affixComboBox = QComboBox() + affixComboBox.setEditable(True) + affixComboBox.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) + affixComboBox.completer().setCompletionMode(QCompleter.CompletionMode.PopupCompletion) + + table_view = QTableView() + table_view.horizontalHeader().setVisible(False) + table_view.verticalHeader().setVisible(False) + table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + + affixComboBox.setView(table_view) + affixComboBox.addItems(self.affixesNames.values()) + + key_list = list(self.affixesNames.keys()) + try: + idx = key_list.index(affix_name) + except ValueError: + self.create_alert(f"{affix_name} is not a valid affix.") + return affixComboBox + affixComboBox.setCurrentIndex(idx) + affixComboBox.setMaximumWidth(250) + affixComboBox.currentTextChanged.connect(self.item_changed) + return affixComboBox + + def create_alert(self, msg: str): + reply = QMessageBox.warning(self, 'Alert', msg, QMessageBox.StandardButton.Ok) + if reply == QMessageBox.StandardButton.Ok: + return True + else: + return False + + def create_pair_label_spinbox(self, labelText, maxValue, value): + ret = QHBoxLayout() + ret.setContentsMargins(0, 0, 50, 0) + label = QLabel(labelText) + spinBox = QSpinBox() + spinBox.setMaximum(maxValue) + spinBox.setValue(value) + spinBox.setMaximumWidth(70) + label.setAlignment(Qt.AlignmentFlag.AlignLeft) + spinBox.setAlignment(Qt.AlignmentFlag.AlignLeft) + ret.addWidget(label) + ret.addWidget(spinBox) + spinBox.valueChanged.connect(self.item_changed) + return ret + + def set_minPower(self, minPower): + self.minPowerEdit.setValue(minPower) + + def set_minGreaterAffix(self, minGreaterAffix): + for i in range(self.affixListLayout.count()): + layout = self.affixListLayout.itemAt(i).layout() + if layout != None: + if isinstance(layout, QHBoxLayout): + if layout.itemAt(0).widget().text() == "minGreaterAffixCount:": + layout.itemAt(1).widget().setValue(minGreaterAffix) + + def set_minCount(self, minCount): + for i in range(self.affixListLayout.count()): + layout = self.affixListLayout.itemAt(i).layout() + if layout != None: + if isinstance(layout, QHBoxLayout): + if layout.itemAt(0).widget().text() == "minCount:": + layout.itemAt(1).widget().setValue(minCount) + + def find_key_from_value(self, target_value): + for key, value in self.affixesNames.items(): + if value == target_value: + return key + return None + + def save_item(self): + self.item.minPower = self.minPowerEdit.value() + for pool in self.item.affixPool: + for i in range(self.affixListLayout.count()): + widget = self.affixListLayout.itemAt(i).widget() + layout = self.affixListLayout.itemAt(i).layout() + if widget != None: + if isinstance(widget, QComboBox): + pool.count[i] = Affix(self.find_key_from_value(widget.currentText())) + elif layout != None: + if isinstance(layout, QHBoxLayout): + if layout.itemAt(0).widget().text() == "minCount:": + pool.minCount = layout.itemAt(1).widget().value() + elif layout.itemAt(0).widget().text() == "minGreaterAffixCount:": + pool.minGreaterAffixCount = layout.itemAt(1).widget().value() + + for pool in self.item.inherentPool: + for i in range(self.inherentListLayout.count()): + widget = self.inherentListLayout.itemAt(i).widget() + if isinstance(widget, QComboBox): + pool.count[i] = Affix(self.find_key_from_value(widget.currentText())) + self.changed = False + + def item_changed(self): + self.changed = True + + def has_changes(self): + return self.changed \ No newline at end of file diff --git a/src/gui/dialog.py b/src/gui/dialog.py new file mode 100644 index 0000000..af72aed --- /dev/null +++ b/src/gui/dialog.py @@ -0,0 +1,95 @@ +from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QSpinBox, QPushButton, QHBoxLayout, QGridLayout, QTextBrowser +from PyQt6.QtWidgets import QApplication, QMainWindow, QWidgetItem, QPushButton, QScrollArea, QFileDialog, QVBoxLayout, QWidget, QComboBox, QLineEdit, QLabel, QFormLayout, QHBoxLayout, QMessageBox, QMenuBar, QStatusBar, QSpinBox, QSizePolicy +from PyQt6.QtCore import Qt +from PyQt6.QtGui import QAction, QFont, QIcon +import pywinstyles + +class MinPowerDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Set Min Power") + self.setFixedSize(250, 150) + pywinstyles.apply_style(self, "dark") + self.layout = QVBoxLayout() + + self.label = QLabel("Min Power:") + self.layout.addWidget(self.label) + + self.spinBox = QSpinBox() + self.spinBox.setRange(0, 800) + self.spinBox.setValue(800) + self.layout.addWidget(self.spinBox) + + self.buttonLayout = QHBoxLayout() + self.okButton = QPushButton("OK") + self.okButton.clicked.connect(self.accept) + self.cancelButton = QPushButton("Cancel") + self.cancelButton.clicked.connect(self.reject) + self.buttonLayout.addWidget(self.okButton) + self.buttonLayout.addWidget(self.cancelButton) + + self.layout.addLayout(self.buttonLayout) + self.setLayout(self.layout) + + def get_value(self): + return self.spinBox.value() + +class MinGreaterDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Set Min Greater Affix") + self.setFixedSize(250, 150) + pywinstyles.apply_style(self, "dark") + self.layout = QVBoxLayout() + + self.label = QLabel("Min Greater Affix:") + self.layout.addWidget(self.label) + + self.spinBox = QSpinBox() + self.spinBox.setRange(0, 3) + self.spinBox.setValue(0) + self.layout.addWidget(self.spinBox) + + self.buttonLayout = QHBoxLayout() + self.okButton = QPushButton("OK") + self.okButton.clicked.connect(self.accept) + self.cancelButton = QPushButton("Cancel") + self.cancelButton.clicked.connect(self.reject) + self.buttonLayout.addWidget(self.okButton) + self.buttonLayout.addWidget(self.cancelButton) + + self.layout.addLayout(self.buttonLayout) + self.setLayout(self.layout) + + def get_value(self): + return self.spinBox.value() + +class MinCountDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Set Min Count") + self.setFixedSize(250, 150) + pywinstyles.apply_style(self, "dark") + self.layout = QVBoxLayout() + + self.label = QLabel("Min Count:") + self.layout.addWidget(self.label) + + self.spinBox = QSpinBox() + self.spinBox.setRange(0, 3) + self.spinBox.setValue(0) + self.layout.addWidget(self.spinBox) + + self.buttonLayout = QHBoxLayout() + self.okButton = QPushButton("OK") + self.okButton.clicked.connect(self.accept) + self.cancelButton = QPushButton("Cancel") + self.cancelButton.clicked.connect(self.reject) + self.buttonLayout.addWidget(self.okButton) + self.buttonLayout.addWidget(self.cancelButton) + + self.layout.addLayout(self.buttonLayout) + self.setLayout(self.layout) + + def get_value(self): + return self.spinBox.value() \ No newline at end of file diff --git a/src/gui/profile_tab.py b/src/gui/profile_tab.py new file mode 100644 index 0000000..cd4bfc1 --- /dev/null +++ b/src/gui/profile_tab.py @@ -0,0 +1,210 @@ +import configparser +import sys +import os +import json +import time +from src.gui.build_from_yaml import * +from src.gui.dialog import * +from src.gui.d4lfitem import * +from src.config import BASE_DIR + +PROFILE_TABNAME = "Edit Profile" + +class ProfileTab(QWidget): + def __init__(self): + super().__init__() + + self.root = None + self.file_path = None + self.main_layout = QVBoxLayout(self) + + scroll_area = QScrollArea(self) + scroll_widget = QWidget(scroll_area) + scrollable_layout = QVBoxLayout(scroll_widget) + scroll_area.setWidgetResizable(True) + + + info_layout = QHBoxLayout() + info_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) + + profile_groupbox = QGroupBox("Profile Loaded") + profile_groupbox_layout = QVBoxLayout() + self.filenameLabel = QLabel("") + self.filenameLabel.setStyleSheet("font-size: 12pt;") + profile_groupbox_layout.addWidget(self.filenameLabel) + profile_groupbox.setLayout(profile_groupbox_layout) + info_layout.addWidget(profile_groupbox) + + tools_groupbox = QGroupBox("Tools") + tools_groupbox_layout = QHBoxLayout() + self.file_button = QPushButton("File") + self.save_button = QPushButton("Save") + self.file_button.clicked.connect(self.load_yaml) + self.save_button.clicked.connect(self.save_yaml) + tools_groupbox_layout.addWidget(self.file_button) + tools_groupbox_layout.addWidget(self.save_button) + + self.set_all_minGreaterAffix_button = QPushButton("Set all minGreaterAffix") + self.set_all_minPower_button = QPushButton("Set all minPower") + self.set_all_minCount_button = QPushButton("Set all minCount") + self.set_all_minGreaterAffix_button.clicked.connect(self.set_all_minGreaterAffix) + self.set_all_minPower_button.clicked.connect(self.set_all_minPower) + self.set_all_minCount_button.clicked.connect(self.set_all_minCount) + tools_groupbox_layout.addWidget(self.set_all_minGreaterAffix_button) + tools_groupbox_layout.addWidget(self.set_all_minPower_button) + tools_groupbox_layout.addWidget(self.set_all_minCount_button) + tools_groupbox.setLayout(tools_groupbox_layout) + info_layout.addWidget(tools_groupbox) + self.main_layout.addLayout(info_layout) + + self.itemTypes = None + with open(str(BASE_DIR / "assets/lang/enUS/item_types.json"), 'r') as f: + self.itemTypes = json.load(f) + + self.affixesNames = None + with open(str(BASE_DIR / "assets/lang/enUS/affixes.json"), 'r') as f: + self.affixesNames = json.load(f) + + self.item_widgets = QWidget() + self.item_widgets_layout = QGridLayout() + self.item_list = [] + self.item_widgets.setLayout(self.item_widgets_layout) + scrollable_layout.addWidget(self.item_widgets) + self.update_filename_label() + scroll_widget.setLayout(scrollable_layout) + scroll_area.setWidget(scroll_widget) + self.main_layout.addWidget(scroll_area) + instructions_label = QLabel("Instructions") + self.main_layout.addWidget(instructions_label) + + instructions_text = QTextBrowser() + instructions_text.append("The default profile loaded is the first profile in the params.ini file. You can change the default profile in the params.ini file by changing the 'profiles' value to the desired profile name.") + instructions_text.append("You can also load a profile by clicking the 'File' button.") + instructions_text.append("") + instructions_text.append("All values are not saved automatically immediately upon changing.") + instructions_text.append("You must click the save button to apply the changes to the profile.") + instructions_text.append("") + instructions_text.append("Note: You will need to restart d4lf after modifying these values. Modifying profile file manually while this gui is running is not supported (and really not necessary).") + + instructions_text.setFixedHeight(150) + self.main_layout.addWidget(instructions_text) + self.setLayout(self.main_layout) + + + def confirm_discard_changes(self): + reply = QMessageBox.warning(self, 'Unsaved Changes', + "You have unsaved changes. Do you want to save them before continuing?", + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No | QMessageBox.StandardButton.Cancel) + if reply == QMessageBox.StandardButton.Yes: + self.save_yaml() + return True + elif reply == QMessageBox.StandardButton.No: + return True + else: + return False + + def create_alert(self, msg: str): + reply = QMessageBox.warning(self, 'Alert', msg, QMessageBox.StandardButton.Ok) + if reply == QMessageBox.StandardButton.Ok: + return True + else: + return False + + def set_all_minGreaterAffix(self): + if self.file_path: + dialog = MinGreaterDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + minGreaterAffix = dialog.get_value() + for d4lf_item in self.item_list: + d4lf_item.set_minGreaterAffix(minGreaterAffix) + else: + self.create_alert("No file loaded") + + def set_all_minCount(self): + if self.file_path: + dialog = MinCountDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + minCount = dialog.get_value() + for d4lf_item in self.item_list: + d4lf_item.set_minCount(minCount) + else: + self.create_alert("No file loaded") + + + def set_all_minPower(self): + if self.file_path: + dialog = MinPowerDialog(self) + if dialog.exec() == QDialog.DialogCode.Accepted: + minPower = dialog.get_value() + for d4lf_item in self.item_list: + d4lf_item.set_minPower(minPower) + else: + self.create_alert("No file loaded") + + def load_items(self): + i = 0 + j = 0 + + while self.item_widgets_layout.count(): + item = self.item_widgets_layout.takeAt(0) + item.widget().deleteLater() + + self.item_list = [] + + for item in self.root.affixes: + d4lf_item = D4LFItem(item, self.affixesNames, self.itemTypes) + self.item_list.append(d4lf_item) + if i % 4 == 0 and i != 0: + i = 0 + j += 1 + self.item_widgets_layout.addWidget(d4lf_item, j, i) + i += 1 + + def load_yaml(self): + if not self.file_path: # at start, set default file to build in params.ini + params_file = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "params.ini") + params_data = configparser.ConfigParser() + params_data.read(params_file) + profile_names = params_data.get('general', 'profiles').split(',') + if profile_names[0]: + file_path = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "profiles", f"{profile_names[0]}.yaml") + else: + base_dir = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "profiles") + file_path, _ = QFileDialog.getOpenFileName(self, "Open YAML File", base_dir, "YAML Files (*.yaml *.yml)") + else: + base_dir = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "profiles") + file_path, _ = QFileDialog.getOpenFileName(self, "Open YAML File", base_dir, "YAML Files (*.yaml *.yml)") + if file_path: + self.root = Root.load_yaml(file_path) + self.file_path = os.path.abspath(file_path) + self.update_filename_label() + self.load_items() + return True + return False + + def update_filename_label(self, close=False): + if close: + self.filenameLabel.setText("No file loaded") + return + if not self.file_path: + if not self.load_yaml(): + self.filenameLabel.setText("No file loaded") + return + + if self.file_path: + filename = self.file_path.split("\\")[-1] # Get the filename from the full path + filename_without_extension = filename.rsplit(".", 1)[0] # Remove the extension + display_name = filename_without_extension.replace("_", " ") # Replace underscores with spaces + self.filenameLabel.setText(display_name) + + def save_yaml(self): + for d4lf_item in self.item_list: + d4lf_item.save_item() + if self.root: + self.root.save_yaml(self.file_path) + + def check_close_save(self): + if self.item_list: + for d4lf_item in self.item_list: + if d4lf_item.has_changes(): + return self.confirm_discard_changes() \ No newline at end of file diff --git a/src/gui/qt_gui.py b/src/gui/qt_gui.py index e9b3209..af5a3c0 100644 --- a/src/gui/qt_gui.py +++ b/src/gui/qt_gui.py @@ -25,6 +25,7 @@ from src.config.helper import singleton from src.config.loader import IniConfigLoader from src.gui import config_tab +from src.gui import profile_tab from src.gui.importer.d4builds import import_d4builds from src.gui.importer.diablo_trade import import_diablo_trade from src.gui.importer.maxroll import import_maxroll @@ -69,7 +70,8 @@ def __init__(self): self._maxroll_or_d4builds_tab() self._diablo_trade_tab() self.tab_widget.addTab(config_tab.ConfigTab(), config_tab.CONFIG_TABNAME) - + self.profile_tab_widget = profile_tab.ProfileTab() + self.tab_widget.addTab(self.profile_tab_widget, profile_tab.PROFILE_TABNAME) LOGGER.root.addHandler(self.maxroll_log_handler) self.tab_widget.currentChanged.connect(self._handle_tab_changed) self._toggle_dark_mode() @@ -80,7 +82,7 @@ def closeEvent(self, e): self.settings.setValue("size", self.size()) self.settings.setValue("pos", self.pos()) self.settings.setValue("maximized", self.isMaximized()) - + self.profile_tab_widget.check_close_save() e.accept() def _diablo_trade_tab(self): From 8e94c6b1c9e09425fcf22c7b4cf6dc73f50e7f84 Mon Sep 17 00:00:00 2001 From: Hugo GIRARD Date: Sun, 26 Jan 2025 18:21:16 +0100 Subject: [PATCH 5/9] =?UTF-8?q?Revert=20"Added=20use=5Ftts=20to=20deprecat?= =?UTF-8?q?ed=20keys=20so=20that=20people=20who=20are=20swapping=20back?= =?UTF-8?q?=E2=80=A6=20(#412)"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit d890d4a854651d2c7e18a5243b0702b0cc88efa9. --- src/config/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/config/models.py b/src/config/models.py index d63f092..7ed9445 100644 --- a/src/config/models.py +++ b/src/config/models.py @@ -16,7 +16,11 @@ HIDE_FROM_GUI_KEY = "hide_from_gui" IS_HOTKEY_KEY = "is_hotkey" -DEPRECATED_INI_KEYS = ["import_build", "local_prefs_path", "move_item_type", "use_tts"] +DEPRECATED_INI_KEYS = [ + "import_build", + "local_prefs_path", + "move_item_type", +] class AspectFilterType(enum.StrEnum): From 1da09f10bb108df97ffeb2fee59df1ce3a14ba49 Mon Sep 17 00:00:00 2001 From: Hugo GIRARD Date: Fri, 31 Jan 2025 23:12:41 +0100 Subject: [PATCH 6/9] Using existing models, refresh, save alert, create/delete item --- src/gui/build_from_yaml.py | 151 ---------------------------- src/gui/d4lfitem.py | 178 ++++++++++++++++++++++----------- src/gui/dialog.py | 198 +++++++++++++++++++++++++++++++------ src/gui/profile_tab.py | 181 ++++++++++++++++++++++++--------- src/gui/qt_gui.py | 6 +- 5 files changed, 426 insertions(+), 288 deletions(-) delete mode 100644 src/gui/build_from_yaml.py diff --git a/src/gui/build_from_yaml.py b/src/gui/build_from_yaml.py deleted file mode 100644 index 415a9be..0000000 --- a/src/gui/build_from_yaml.py +++ /dev/null @@ -1,151 +0,0 @@ -import yaml -from typing import List - -class Affix: - def __init__(self, name: str): - self.name = name - - @classmethod - def from_dict(cls, data): - return cls(name=data['name']) - - def to_dict(self): - return {'name': self.name} - - def __str__(self): - return f"name: {self.name}" - - -class AffixPool: - def __init__(self, count: List[Affix], minCount: int, minGreaterAffixCount: int): - self.count = count - self.minCount = minCount - self.minGreaterAffixCount = minGreaterAffixCount - - @classmethod - def from_dict(cls, data): - count = [Affix.from_dict(affix) for affix in data['count']] - try: - minCount = data['minCount'] - except KeyError: - minCount = None - try: - minGreaterAffixCount = data['minGreaterAffixCount'] - except KeyError: - minGreaterAffixCount = None - return cls(count=count, minCount=minCount, minGreaterAffixCount=minGreaterAffixCount) - - def to_dict(self): - if self.minCount is None or self.minGreaterAffixCount is None: - return { - 'count': [affix.to_dict() for affix in self.count] - } - return { - 'count': [affix.to_dict() for affix in self.count], - 'minCount': self.minCount, - 'minGreaterAffixCount': self.minGreaterAffixCount - } - - def __str__(self): - return f"count: {self.count}\nminCount: {self.minCount}\nminGreaterAffixCount: {self.minGreaterAffixCount}" - - def set_count(self, newCount : List[str]): - self.count = [ Affix(name) for name in newCount] - - -class Item: - def __init__(self, itemName: str, itemType: List[str], minPower: int, affixPool: List[AffixPool], inherentPool: List[AffixPool] = None): - self.itemName = itemName - self.itemType = itemType - self.minPower = minPower - self.affixPool = affixPool - self.inherentPool = inherentPool if inherentPool else [] - - def set_affix_pool(self, newAffixPool: List[str], newMinCount : int, newGreaterCount: int): - self.affixPool[0].minCount = newMinCount - self.affixPool[0].minGreaterAffixCount = newGreaterCount - self.affixPool[0].set_count(newAffixPool) - - def set_inherent_pool(self, newInherentPool: List[str]): - self.inherentPool[0].set_count(newInherentPool) - - @classmethod - def from_dict(cls, data): - itemName = list(data.keys())[0] - affixPool = [AffixPool.from_dict(pool) for pool in data[itemName]['affixPool']] - try: - inherentPool = [AffixPool.from_dict(pool) for pool in data[itemName]['inherentPool']] - except: - inherentPool = [] - try: - minPower = data[itemName]['minPower'] - except KeyError as e: - minPower = 0 - return cls(itemName=itemName, itemType=data[itemName]['itemType'], minPower=minPower, affixPool=affixPool, inherentPool=inherentPool) - - def to_dict(self): - data = { f'{self.itemName}' : - { - 'itemType': self.itemType, - 'minPower': self.minPower, - 'affixPool': [pool.to_dict() for pool in self.affixPool] - } - } - if self.inherentPool: - data[self.itemName]['inherentPool'] = [pool.to_dict() for pool in self.inherentPool] - return data - - def __str__(self): - affixPoolStr = f"count:\n" - for affix in self.affixPool[0].count: - affixPoolStr += f"\t- {affix}\n" - affixPoolStr += f"minCount: {self.affixPool[0].minCount}\nminGreaterAffixCount: {self.affixPool[0].minGreaterAffixCount}" - inherentPoolStr = "" - if self.inherentPool: - inherentPoolStr = f"count:\n" - for inherent in self.inherentPool[0].count: - inherentPoolStr += f"\t-{inherent}\n" - inherentPoolStr += f"minCount: {self.inherentPool[0].minCount}\nminGreaterAffixCount: {self.inherentPool[0].minGreaterAffixCount}" - if inherentPoolStr == "": - return f"itemName: {self.itemName}\nitemType: {self.itemType}\nminPower: {self.minPower}\naffixPool: {affixPoolStr}\n" - return f"itemName: {self.itemName}\nitemType: {self.itemType}\nminPower: {self.minPower}\naffixPool: {affixPoolStr}\ninherentPool: {inherentPoolStr}\n" - - def set_minPower(self, minPower): - self.minPower = minPower - - def set_itemType(self, itemType): - self.itemType = itemType - - def set_itemName(self, itemName): - self.itemName = itemName - - def set_minGreaterAffix(self, minGreaterAffix): - self.affixPool[0].minGreaterAffixCount = minGreaterAffix - -class Root: - def __init__(self, affixes: List[Item], data): - self.affixes = affixes - self.data = data - - @classmethod - def from_dict(cls, data): - affixes = [Item.from_dict(item) for item in data['Affixes']] - return cls(affixes=affixes, data=data) - - def to_dict(self): - self.data['Affixes'] = [item.to_dict() for item in self.affixes] - return self.data - - def set_min_power(self, minPower): - for affix in self.affixes: - affix.set_minPower(minPower) - - @classmethod - def load_yaml(cls, file_path): - with open(file_path, 'r') as file: - data = yaml.safe_load(file) - return cls.from_dict(data) - - def save_yaml(self, file_path): - with open(file_path, 'w') as file: - yaml.safe_dump(self.to_dict(), file) \ No newline at end of file diff --git a/src/gui/d4lfitem.py b/src/gui/d4lfitem.py index 4507c00..625d107 100644 --- a/src/gui/d4lfitem.py +++ b/src/gui/d4lfitem.py @@ -1,29 +1,46 @@ -from src.gui.build_from_yaml import * -from PyQt6.QtWidgets import QHeaderView, QTableView, QLabel, QVBoxLayout, QHBoxLayout, QSpinBox, QComboBox, QGroupBox, QSizePolicy, QFormLayout, QCompleter, QMessageBox +from src.config.models import DynamicItemFilterModel, AffixFilterModel, AffixFilterCountModel, ItemFilterModel, ItemType +from src.gui.dialog import IgnoreScrollWheelComboBox, IgnoreScrollWheelSpinBox +from PyQt6.QtWidgets import QHeaderView, QTableView, QLabel, QVBoxLayout, QHBoxLayout, QComboBox, QGroupBox, QSizePolicy, QFormLayout, QCompleter, QMessageBox from PyQt6.QtCore import Qt class D4LFItem(QGroupBox): - def __init__(self, item : Item, affixesNames, itemTypes): + def __init__(self, item : DynamicItemFilterModel, affixesNames, itemTypes): super().__init__() - self.setTitle(item.itemName) - self.setStyleSheet("QGroupBox {font-size: 10pt;} QLabel {font-size: 10pt;} QComboBox {font-size: 10pt;} QSpinBox {font-size: 10pt;}") - self.main_layout = QVBoxLayout() - self.main_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + self.item_name = list(item.root.keys())[0] self.item = item + self.item_type = self.item.root[self.item_name].itemType[0].value + self.affix_pool = self.item.root[self.item_name].affixPool + self.inherent_pool = self.item.root[self.item_name].inherentPool + self.min_power = self.item.root[self.item_name].minPower + self.changed = False - self.setMaximumSize(300, 500) self.affixesNames = affixesNames self.itemTypes = itemTypes - self.minPowerEdit = QSpinBox(self) + self.setTitle(self.item_name) + self.setStyleSheet("QGroupBox {font-size: 10pt;} QLabel {font-size: 10pt;} IgnoreScrollWheelComboBox {font-size: 10pt;} IgnoreScrollWheelSpinBox {font-size: 10pt;}") + self.setMaximumSize(300, 500) + + self.main_layout = QVBoxLayout() + self.main_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + self.form_layout = QFormLayout() + + self.item_type_label = QLabel("Item Type:") + self.item_type_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.item_type_label_info = QLabel(self.find_item_from_value(self.item_type)) + self.item_type_label_info.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.form_layout.addRow(self.item_type_label, self.item_type_label_info) + + self.minPowerEdit = IgnoreScrollWheelSpinBox() self.minPowerEdit.setMaximum(800) - self.minPowerEdit.setMaximumWidth(100) + self.minPowerEdit.setMaximumWidth(75) self.minPowerEdit.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) - self.minPower_form = QFormLayout() - self.minPower_form.addRow(QLabel("minPower:"), self.minPowerEdit) - self.main_layout.addLayout(self.minPower_form) - - if item.affixPool: + self.form_layout.addRow(QLabel("minPower:"), self.minPowerEdit) + self.main_layout.addLayout(self.form_layout) + self.affixListLayout = None + self.inherentListLayout = None + if self.affix_pool: self.affixes_label = QLabel("Affixes:") self.affixes_label.setMaximumSize(200, 50) self.affixes_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) @@ -31,7 +48,7 @@ def __init__(self, item : Item, affixesNames, itemTypes): self.affixListLayout = QVBoxLayout() self.main_layout.addLayout(self.affixListLayout) - if item.inherentPool: + if self.inherent_pool: self.inherent_label = QLabel("Inherent:") self.inherent_label.setMaximumSize(200, 50) self.inherent_label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) @@ -45,25 +62,22 @@ def __init__(self, item : Item, affixesNames, itemTypes): self.minPowerEdit.valueChanged.connect(self.item_changed) def load_item(self): - self.minPowerEdit.setValue(self.item.minPower) - for pool in self.item.affixPool: + self.minPowerEdit.setValue(self.min_power) + for pool in self.affix_pool: for affix in pool.count: affixComboBox = self.create_affix_combobox(affix.name) self.affixListLayout.addWidget(affixComboBox) - if pool.minCount != None: - minCount = self.create_pair_label_spinbox("minCount:", 3, pool.minCount) - self.affixListLayout.addLayout(minCount) - if pool.minGreaterAffixCount != None: - minGreaterAffixCount = self.create_pair_label_spinbox("minGreaterAffixCount:", 3, pool.minGreaterAffixCount) - self.affixListLayout.addLayout(minGreaterAffixCount) - - for pool in self.item.inherentPool: + if pool.minCount != None and pool.minGreaterAffixCount != None: + layout = self.create_form_layout(pool.minCount, pool.minGreaterAffixCount) + self.affixListLayout.addLayout(layout) + + for pool in self.inherent_pool: for affix in pool.count: affixComboBox = self.create_affix_combobox(affix.name) self.inherentListLayout.addWidget(affixComboBox) def create_affix_combobox(self, affix_name): - affixComboBox = QComboBox() + affixComboBox = IgnoreScrollWheelComboBox() affixComboBox.setEditable(True) affixComboBox.setInsertPolicy(QComboBox.InsertPolicy.NoInsert) affixComboBox.completer().setCompletionMode(QCompleter.CompletionMode.PopupCompletion) @@ -75,6 +89,8 @@ def create_affix_combobox(self, affix_name): affixComboBox.setView(table_view) affixComboBox.addItems(self.affixesNames.values()) + for i, affixes in enumerate(self.affixesNames.values()): + affixComboBox.setItemData(i, affixes, Qt.ItemDataRole.ToolTipRole) key_list = list(self.affixesNames.keys()) try: @@ -94,19 +110,22 @@ def create_alert(self, msg: str): else: return False - def create_pair_label_spinbox(self, labelText, maxValue, value): - ret = QHBoxLayout() - ret.setContentsMargins(0, 0, 50, 0) - label = QLabel(labelText) - spinBox = QSpinBox() - spinBox.setMaximum(maxValue) - spinBox.setValue(value) - spinBox.setMaximumWidth(70) - label.setAlignment(Qt.AlignmentFlag.AlignLeft) - spinBox.setAlignment(Qt.AlignmentFlag.AlignLeft) - ret.addWidget(label) - ret.addWidget(spinBox) - spinBox.valueChanged.connect(self.item_changed) + def create_form_layout(self, minCount, minGreaterAffixCount): + ret = QFormLayout() + mincount_label = QLabel("minCount:") + mincount_spinBox = IgnoreScrollWheelSpinBox() + mincount_spinBox.setMaximum(3) + mincount_spinBox.setValue(minCount) + mincount_spinBox.setMaximumWidth(60) + mincount_spinBox.valueChanged.connect(self.item_changed) + ret.addRow(mincount_label, mincount_spinBox) + mingreater_label = QLabel("minGreaterAffixCount:") + mingreater_spinBox = IgnoreScrollWheelSpinBox() + mingreater_spinBox.setMaximum(3) + mingreater_spinBox.setValue(minGreaterAffixCount) + mingreater_spinBox.setMaximumWidth(60) + mingreater_spinBox.valueChanged.connect(self.item_changed) + ret.addRow(mingreater_label, mingreater_spinBox) return ret def set_minPower(self, minPower): @@ -116,46 +135,87 @@ def set_minGreaterAffix(self, minGreaterAffix): for i in range(self.affixListLayout.count()): layout = self.affixListLayout.itemAt(i).layout() if layout != None: - if isinstance(layout, QHBoxLayout): - if layout.itemAt(0).widget().text() == "minGreaterAffixCount:": - layout.itemAt(1).widget().setValue(minGreaterAffix) + if isinstance(layout, QFormLayout): + layout.itemAt(3).widget().setValue(minGreaterAffix) def set_minCount(self, minCount): for i in range(self.affixListLayout.count()): layout = self.affixListLayout.itemAt(i).layout() if layout != None: - if isinstance(layout, QHBoxLayout): - if layout.itemAt(0).widget().text() == "minCount:": - layout.itemAt(1).widget().setValue(minCount) + if isinstance(layout, QFormLayout): + layout.itemAt(1).widget().setValue(minCount) - def find_key_from_value(self, target_value): + def find_affix_from_value(self, target_value): for key, value in self.affixesNames.items(): if value == target_value: return key return None + def find_item_from_value(self, target_value): + for key, value in self.itemTypes.items(): + if value == target_value: + return key + return None + def save_item(self): - self.item.minPower = self.minPowerEdit.value() - for pool in self.item.affixPool: + self.min_power = self.minPowerEdit.value() + for pool in self.affix_pool: for i in range(self.affixListLayout.count()): widget = self.affixListLayout.itemAt(i).widget() layout = self.affixListLayout.itemAt(i).layout() if widget != None: - if isinstance(widget, QComboBox): - pool.count[i] = Affix(self.find_key_from_value(widget.currentText())) + if isinstance(widget, IgnoreScrollWheelComboBox): + pool.count[i] = AffixFilterModel(name=self.find_affix_from_value(widget.currentText())) elif layout != None: - if isinstance(layout, QHBoxLayout): - if layout.itemAt(0).widget().text() == "minCount:": - pool.minCount = layout.itemAt(1).widget().value() - elif layout.itemAt(0).widget().text() == "minGreaterAffixCount:": - pool.minGreaterAffixCount = layout.itemAt(1).widget().value() + if isinstance(layout, QFormLayout): + pool.minCount = layout.itemAt(1).widget().value() + pool.minGreaterAffixCount = layout.itemAt(3).widget().value() - for pool in self.item.inherentPool: + for pool in self.inherent_pool: for i in range(self.inherentListLayout.count()): widget = self.inherentListLayout.itemAt(i).widget() - if isinstance(widget, QComboBox): - pool.count[i] = Affix(self.find_key_from_value(widget.currentText())) + if isinstance(widget, IgnoreScrollWheelComboBox): + pool.count[i] = AffixFilterModel(name=self.find_affix_from_value(widget.currentText())) self.changed = False + self.item.root[self.item_name].affixPool = self.affix_pool + if self.inherent_pool: + self.item.root[self.item_name].inherentPool = self.inherent_pool + self.item.root[self.item_name].minPower= self.min_power + return self.item + + def save_item_create(self): + new_item = ItemFilterModel() + new_item.itemType = [ItemType(self.item_type)] + new_item.minPower = self.minPowerEdit.value() + new_item.affixPool = [] + new_item.inherentPool = [] + affix_filter_count_list = [] + minCount = 0 + minGreaterAffixCount = 0 + + for i in range(self.affixListLayout.count()): + widget = self.affixListLayout.itemAt(i).widget() + layout = self.affixListLayout.itemAt(i).layout() + if widget != None: + if isinstance(widget, IgnoreScrollWheelComboBox): + affix_filter_count_list.append(AffixFilterModel(name=self.find_affix_from_value(widget.currentText()))) + elif layout != None: + if isinstance(layout, QFormLayout): + minCount = layout.itemAt(1).widget().value() + minGreaterAffixCount = layout.itemAt(3).widget().value() + affix_filter_count = AffixFilterCountModel(minCount=minCount, minGreaterAffixCount=minGreaterAffixCount, count=affix_filter_count_list) + new_item.affixPool.append(affix_filter_count) + + if self.inherentListLayout: + inherent_filter_count_list = [] + for i in range(self.inherentListLayout.count()): + widget = self.inherentListLayout.itemAt(i).widget() + if isinstance(widget, IgnoreScrollWheelComboBox): + inherent_filter_count_list.append(AffixFilterModel(name=self.find_affix_from_value(widget.currentText()))) + inherent_filter_count = AffixFilterCountModel(count=inherent_filter_count_list) + new_item.inherentPool.append(inherent_filter_count) + + return DynamicItemFilterModel(**{self.item_name: new_item}) def item_changed(self): self.changed = True diff --git a/src/gui/dialog.py b/src/gui/dialog.py index af72aed..eb6c6bf 100644 --- a/src/gui/dialog.py +++ b/src/gui/dialog.py @@ -1,24 +1,35 @@ -from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QSpinBox, QPushButton, QHBoxLayout, QGridLayout, QTextBrowser -from PyQt6.QtWidgets import QApplication, QMainWindow, QWidgetItem, QPushButton, QScrollArea, QFileDialog, QVBoxLayout, QWidget, QComboBox, QLineEdit, QLabel, QFormLayout, QHBoxLayout, QMessageBox, QMenuBar, QStatusBar, QSpinBox, QSizePolicy +from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QSpinBox, QPushButton, QHBoxLayout, QGridLayout, QTextBrowser, QGroupBox, QFormLayout, QComboBox, QLineEdit, QCheckBox +from PyQt6.QtWidgets import QApplication, QMainWindow, QWidgetItem, QPushButton, QScrollArea, QFileDialog, QVBoxLayout, QWidget, QLineEdit, QLabel, QFormLayout, QHBoxLayout, QMessageBox, QMenuBar, QStatusBar, QSizePolicy from PyQt6.QtCore import Qt from PyQt6.QtGui import QAction, QFont, QIcon -import pywinstyles +from src.gui.config_tab import IgnoreScrollWheelComboBox +from src.config.models import AffixFilterCountModel, AffixFilterModel, ItemFilterModel, DynamicItemFilterModel, ItemType + +class IgnoreScrollWheelSpinBox(QSpinBox): + def __init__(self): + super().__init__() + self.setFocusPolicy(Qt.FocusPolicy.StrongFocus) + + def wheelEvent(self, event): + if self.hasFocus(): + return QSpinBox.wheelEvent(self, event) + + return event.ignore() class MinPowerDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Set Min Power") self.setFixedSize(250, 150) - pywinstyles.apply_style(self, "dark") - self.layout = QVBoxLayout() + self.main_layout = QVBoxLayout() + self.form_layout = QFormLayout() self.label = QLabel("Min Power:") - self.layout.addWidget(self.label) - - self.spinBox = QSpinBox() + self.spinBox = IgnoreScrollWheelSpinBox() self.spinBox.setRange(0, 800) self.spinBox.setValue(800) - self.layout.addWidget(self.spinBox) + self.form_layout.addRow(self.label, self.spinBox) + self.main_layout.addLayout(self.form_layout) self.buttonLayout = QHBoxLayout() self.okButton = QPushButton("OK") @@ -28,8 +39,8 @@ def __init__(self, parent=None): self.buttonLayout.addWidget(self.okButton) self.buttonLayout.addWidget(self.cancelButton) - self.layout.addLayout(self.buttonLayout) - self.setLayout(self.layout) + self.main_layout.addLayout(self.buttonLayout) + self.setLayout(self.main_layout) def get_value(self): return self.spinBox.value() @@ -39,16 +50,15 @@ def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Set Min Greater Affix") self.setFixedSize(250, 150) - pywinstyles.apply_style(self, "dark") - self.layout = QVBoxLayout() + self.main_layout = QVBoxLayout() + self.form_layout = QFormLayout() self.label = QLabel("Min Greater Affix:") - self.layout.addWidget(self.label) - - self.spinBox = QSpinBox() + self.spinBox = IgnoreScrollWheelSpinBox() self.spinBox.setRange(0, 3) self.spinBox.setValue(0) - self.layout.addWidget(self.spinBox) + self.form_layout.addRow(self.label, self.spinBox) + self.main_layout.addLayout(self.form_layout) self.buttonLayout = QHBoxLayout() self.okButton = QPushButton("OK") @@ -58,8 +68,8 @@ def __init__(self, parent=None): self.buttonLayout.addWidget(self.okButton) self.buttonLayout.addWidget(self.cancelButton) - self.layout.addLayout(self.buttonLayout) - self.setLayout(self.layout) + self.main_layout.addLayout(self.buttonLayout) + self.setLayout(self.main_layout) def get_value(self): return self.spinBox.value() @@ -69,27 +79,157 @@ def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Set Min Count") self.setFixedSize(250, 150) - pywinstyles.apply_style(self, "dark") - self.layout = QVBoxLayout() + self.main_layout = QVBoxLayout() - self.label = QLabel("Min Count:") - self.layout.addWidget(self.label) - - self.spinBox = QSpinBox() + self.form_layout = QFormLayout() + self.label = QLabel("Item Name:") + self.spinBox = IgnoreScrollWheelSpinBox() self.spinBox.setRange(0, 3) self.spinBox.setValue(0) - self.layout.addWidget(self.spinBox) + self.form_layout.addRow(self.label, self.spinBox) + self.main_layout.addLayout(self.form_layout) + + self.buttonLayout = QHBoxLayout() + self.okButton = QPushButton("OK") + self.okButton.clicked.connect(self.accept) + self.cancelButton = QPushButton("Cancel") + self.cancelButton.clicked.connect(self.reject) + self.buttonLayout.addWidget(self.okButton) + self.buttonLayout.addWidget(self.cancelButton) + + self.main_layout.addLayout(self.buttonLayout) + self.setLayout(self.main_layout) + + def get_value(self): + return self.spinBox.value() +class CreateItem(QDialog): + def __init__(self, item_types, parent=None): + super().__init__(parent) + self.setWindowTitle("Create Item") + self.setFixedSize(300, 150) + self.main_layout = QVBoxLayout() + + self.form_layout = QFormLayout() + + self.name_label = QLabel("Item Name:") + self.name_input = QLineEdit() + self.form_layout.addRow(self.name_label, self.name_input) + + self.type_label = QLabel("Item Type:") + self.type_input = IgnoreScrollWheelComboBox() + self.type_input.addItems(item_types.values()) + self.type_input.setCurrentIndex(0) + self.form_layout.addRow(self.type_label, self.type_input) + + self.affixes_label = QLabel("Affixes number:") + self.affixes_number = IgnoreScrollWheelSpinBox() + self.affixes_number.setRange(0, 3) + self.affixes_number.setValue(0) + self.affixes_number.setMaximumWidth(60) + self.form_layout.addRow(self.affixes_label, self.affixes_number) + + self.inherent_label = QLabel("Inherent number:") + self.inherent_number = IgnoreScrollWheelSpinBox() + self.inherent_number.setRange(0, 3) + self.inherent_number.setValue(0) + self.inherent_number.setMaximumWidth(60) + self.form_layout.addRow(self.inherent_label, self.inherent_number) + + self.buttonLayout = QHBoxLayout() + self.okButton = QPushButton("OK") + self.okButton.clicked.connect(self.accept) + self.cancelButton = QPushButton("Cancel") + self.cancelButton.clicked.connect(self.reject) + + self.buttonLayout.addWidget(self.okButton) + self.buttonLayout.addWidget(self.cancelButton) + + self.main_layout.addLayout(self.form_layout) + self.main_layout.addLayout(self.buttonLayout) + + self.setLayout(self.main_layout) + + def accept(self): + if self.name_input.text() == "": + QMessageBox.warning(self, "Warning", "Item name cannot be empty") + return + affixes_number = self.affixes_number.value() + if affixes_number == 0: + QMessageBox.warning(self, "Warning", "Affixes number cannot be 0") + return + super().accept() + + def get_value(self): + item_name = self.name_input.text() + item_type = self.type_input.currentText() + affixes_number = self.affixes_number.value() + inherent_number = self.inherent_number.value() + dummy_affixes = ['attack_speed', 'critical_strike_chance', 'maximum_life'] + item = ItemFilterModel() + item.itemType = [ItemType(item_type)] + item.affixPool = [ + AffixFilterCountModel( + count=[ + AffixFilterModel(name=x) + for x in dummy_affixes[:affixes_number] + ], + minCount=2, + minGreaterAffixCount=0, + ) + ] + if inherent_number > 0: + item.inherentPool = [ + AffixFilterCountModel( + count=[ + AffixFilterModel(name=x) + for x in dummy_affixes[:inherent_number] + ] + ) + ] + item.minPower = 100 + return DynamicItemFilterModel(**{item_name: item}) + +class DeleteItem(QDialog): + def __init__(self, item_names, parent=None): + super().__init__(parent) + self.setWindowTitle("Delete Items") + self.setFixedSize(300, 200) + self.main_layout = QVBoxLayout() + self.main_layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + self.groupbox = QGroupBox("Items") + scroll_area = QScrollArea(self) + scroll_widget = QWidget(scroll_area) + scrollable_layout = QVBoxLayout(scroll_widget) + self.groupbox_layout = QVBoxLayout() + + label = QLabel("Select items to delete:") + label.setSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum) + self.groupbox_layout.addWidget(label) + + self.checkbox_list = [] + for name in item_names: + checkbox = QCheckBox(name) + scrollable_layout.addWidget(checkbox) + self.checkbox_list.append(checkbox) + scroll_widget.setLayout(scrollable_layout) + scroll_area.setWidget(scroll_widget) + self.groupbox_layout.addWidget(scroll_area) + self.groupbox.setLayout(self.groupbox_layout) self.buttonLayout = QHBoxLayout() self.okButton = QPushButton("OK") self.okButton.clicked.connect(self.accept) self.cancelButton = QPushButton("Cancel") self.cancelButton.clicked.connect(self.reject) + self.buttonLayout.addWidget(self.okButton) self.buttonLayout.addWidget(self.cancelButton) - self.layout.addLayout(self.buttonLayout) - self.setLayout(self.layout) + self.main_layout.addWidget(self.groupbox) + self.main_layout.addLayout(self.buttonLayout) + + self.setLayout(self.main_layout) def get_value(self): - return self.spinBox.value() \ No newline at end of file + return [checkbox.text() for checkbox in self.checkbox_list if checkbox.isChecked()] \ No newline at end of file diff --git a/src/gui/profile_tab.py b/src/gui/profile_tab.py index cd4bfc1..81efee3 100644 --- a/src/gui/profile_tab.py +++ b/src/gui/profile_tab.py @@ -1,12 +1,22 @@ -import configparser -import sys import os import json -import time -from src.gui.build_from_yaml import * +import logging +import yaml +from pydantic import ValidationError + +from typing import List +from src.config import BASE_DIR +from src.config.loader import IniConfigLoader +from src.config.models import DynamicItemFilterModel, SigilFilterModel, UniqueModel, ConfigDict, AffixFilterCountModel +from src.gui.importer.common import ProfileModel, save_as_profile, _to_yaml_str +from src.gui.importer.maxroll import import_maxroll from src.gui.dialog import * from src.gui.d4lfitem import * -from src.config import BASE_DIR +from src.item.filter import _UniqueKeyLoader + +from PyQt6.QtCore import Qt + +LOGGER = logging.getLogger(__name__) PROFILE_TABNAME = "Edit Profile" @@ -23,7 +33,6 @@ def __init__(self): scrollable_layout = QVBoxLayout(scroll_widget) scroll_area.setWidgetResizable(True) - info_layout = QHBoxLayout() info_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) @@ -39,10 +48,21 @@ def __init__(self): tools_groupbox_layout = QHBoxLayout() self.file_button = QPushButton("File") self.save_button = QPushButton("Save") - self.file_button.clicked.connect(self.load_yaml) + self.refresh_button = QPushButton("Refresh") + self.file_button.clicked.connect(self.load) self.save_button.clicked.connect(self.save_yaml) + self.refresh_button.clicked.connect(self.refresh) tools_groupbox_layout.addWidget(self.file_button) tools_groupbox_layout.addWidget(self.save_button) + tools_groupbox_layout.addWidget(self.refresh_button) + + self.create_item_button = QPushButton("Create Item") + self.create_item_button.clicked.connect(self.create_item) + tools_groupbox_layout.addWidget(self.create_item_button) + + self.delete_item_button = QPushButton("Delete Item") + self.delete_item_button.clicked.connect(self.delete_items) + tools_groupbox_layout.addWidget(self.delete_item_button) self.set_all_minGreaterAffix_button = QPushButton("Set all minGreaterAffix") self.set_all_minPower_button = QPushButton("Set all minPower") @@ -67,10 +87,10 @@ def __init__(self): self.item_widgets = QWidget() self.item_widgets_layout = QGridLayout() - self.item_list = [] + self.item_widgets_layout.setDefaultPositioning(4, Qt.Orientation.Horizontal) + self.item_list : List[D4LFItem] = [] self.item_widgets.setLayout(self.item_widgets_layout) scrollable_layout.addWidget(self.item_widgets) - self.update_filename_label() scroll_widget.setLayout(scrollable_layout) scroll_area.setWidget(scroll_widget) self.main_layout.addWidget(scroll_area) @@ -84,11 +104,12 @@ def __init__(self): instructions_text.append("All values are not saved automatically immediately upon changing.") instructions_text.append("You must click the save button to apply the changes to the profile.") instructions_text.append("") - instructions_text.append("Note: You will need to restart d4lf after modifying these values. Modifying profile file manually while this gui is running is not supported (and really not necessary).") + instructions_text.append("Note: Modifying profile file manually while this gui is running is not supported (and really not necessary).") instructions_text.setFixedHeight(150) self.main_layout.addWidget(instructions_text) self.setLayout(self.main_layout) + self.load() def confirm_discard_changes(self): @@ -142,69 +163,135 @@ def set_all_minPower(self): self.create_alert("No file loaded") def load_items(self): - i = 0 - j = 0 + row = 0 + col = 0 + while self.item_widgets_layout.count(): item = self.item_widgets_layout.takeAt(0) item.widget().deleteLater() self.item_list = [] - - for item in self.root.affixes: + for item in self.root.Affixes: d4lf_item = D4LFItem(item, self.affixesNames, self.itemTypes) self.item_list.append(d4lf_item) - if i % 4 == 0 and i != 0: - i = 0 - j += 1 - self.item_widgets_layout.addWidget(d4lf_item, j, i) - i += 1 + if col % 4 == 0 and col != 0: + col = 0 + row += 1 + self.item_widgets_layout.addWidget(d4lf_item, row, col) + col += 1 - def load_yaml(self): + def load(self): + profiles: list[str] = IniConfigLoader().general.profiles + custom_profile_path = IniConfigLoader().user_dir / "profiles" if not self.file_path: # at start, set default file to build in params.ini - params_file = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "params.ini") - params_data = configparser.ConfigParser() - params_data.read(params_file) - profile_names = params_data.get('general', 'profiles').split(',') - if profile_names[0]: - file_path = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "profiles", f"{profile_names[0]}.yaml") + if profiles[0]: + custom_file_path = custom_profile_path / f"{profiles[0]}.yaml" + if custom_file_path.is_file(): + file_path = custom_file_path + else: + LOGGER.error(f"Could not load profile {profiles[0]}. Checked: {custom_file_path}") else: - base_dir = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "profiles") - file_path, _ = QFileDialog.getOpenFileName(self, "Open YAML File", base_dir, "YAML Files (*.yaml *.yml)") + file_path, _ = QFileDialog.getOpenFileName(self, "Open YAML File", str(custom_profile_path), "YAML Files (*.yaml *.yml)") else: - base_dir = os.path.join(os.getenv("USERPROFILE"), ".d4lf", "profiles") - file_path, _ = QFileDialog.getOpenFileName(self, "Open YAML File", base_dir, "YAML Files (*.yaml *.yml)") + file_path, _ = QFileDialog.getOpenFileName(self, "Open YAML File", str(custom_profile_path), "YAML Files (*.yaml *.yml)") + if file_path: - self.root = Root.load_yaml(file_path) - self.file_path = os.path.abspath(file_path) + self.file_path = file_path + if not self.load_yaml(): + return False self.update_filename_label() self.load_items() return True return False - def update_filename_label(self, close=False): - if close: - self.filenameLabel.setText("No file loaded") - return - if not self.file_path: - if not self.load_yaml(): - self.filenameLabel.setText("No file loaded") - return + def load_yaml(self): + filename = os.path.basename(self.file_path) # Get the filename from the full path + filename_without_extension = filename.rsplit(".", 1)[0] # Remove the extension + profile_str = filename_without_extension.replace("_", " ") # Replace underscores with spaces + self.root = None + with open(self.file_path, encoding="utf-8") as f: + try: + config = yaml.load(stream=f, Loader=_UniqueKeyLoader) + except Exception as e: + LOGGER.error(f"Error in the YAML file {self.file_path}: {e}") + return False + if config is None: + LOGGER.error(f"Empty YAML file {self.file_path}, please remove it") + return False + try: + self.root = ProfileModel(name=profile_str, **config) + except ValidationError as e: + LOGGER.error(f"Validation errors in {self.file_path}") + LOGGER.error(e) + return False + return True + def update_filename_label(self): if self.file_path: - filename = self.file_path.split("\\")[-1] # Get the filename from the full path + filename = os.path.basename(self.file_path) # Get the filename from the full path filename_without_extension = filename.rsplit(".", 1)[0] # Remove the extension display_name = filename_without_extension.replace("_", " ") # Replace underscores with spaces self.filenameLabel.setText(display_name) def save_yaml(self): + new_profile_affixes = [] for d4lf_item in self.item_list: - d4lf_item.save_item() + new_profile_affixes.append(d4lf_item.save_item()) if self.root: - self.root.save_yaml(self.file_path) + p = ProfileModel(name="imported profile", Affixes=new_profile_affixes, Uniques=self.root.Uniques) + save_as_profile(self.filenameLabel.text(), p, "custom") def check_close_save(self): - if self.item_list: - for d4lf_item in self.item_list: - if d4lf_item.has_changes(): - return self.confirm_discard_changes() \ No newline at end of file + new_profile_affixes = [] + for d4lf_item in self.item_list: + new_profile_affixes.append(d4lf_item.save_item_create()) + if self.root: + p = ProfileModel(name=self.filenameLabel.text(), Affixes=new_profile_affixes, Uniques=self.root.Uniques) + if p != self.root: + return self.confirm_discard_changes() + return True + + def check_item_name(self, name): + for d4lf_item in self.item_list: + if d4lf_item.item_name == name: + return False + return True + + def create_item(self): + dialog = CreateItem(self.itemTypes, self) + if dialog.exec() == QDialog.DialogCode.Accepted: + item = dialog.get_value() + if not self.check_item_name(list(item.root.keys())[0]): + self.create_alert("An item with the same already exists, please choose a different name.") + return + item_widget = D4LFItem(item, self.affixesNames, self.itemTypes) + item_widget.item_changed() + self.item_list.append(item_widget) + nb_item = self.item_widgets_layout.count() + row = nb_item // 4 + col = nb_item % 4 + self.item_widgets_layout.addWidget(item_widget, row, col) + return + + def delete_items(self): + item_names = [item.item_name for item in self.item_list] + dialog = DeleteItem(item_names, self) + if dialog.exec() == QDialog.DialogCode.Accepted: + for item_name in dialog.get_value(): + for i, item in enumerate(self.item_list): + if item.item_name == item_name: + self.item_list.pop(i) + to_delete = self.item_widgets_layout.takeAt(i) + to_delete.widget().deleteLater() + break + return + + def refresh(self): + self.item_list = [] + + if not self.load_yaml(): + return + + self.update_filename_label() + self.load_items() \ No newline at end of file diff --git a/src/gui/qt_gui.py b/src/gui/qt_gui.py index af5a3c0..31f35b4 100644 --- a/src/gui/qt_gui.py +++ b/src/gui/qt_gui.py @@ -82,8 +82,10 @@ def closeEvent(self, e): self.settings.setValue("size", self.size()) self.settings.setValue("pos", self.pos()) self.settings.setValue("maximized", self.isMaximized()) - self.profile_tab_widget.check_close_save() - e.accept() + if self.profile_tab_widget.check_close_save(): + e.accept() + else: + e.ignore() def _diablo_trade_tab(self): tab_diablo_trade = QWidget(self) From e3fe8f50e1168f5951c415af2e9a034620adde9b Mon Sep 17 00:00:00 2001 From: Hugo GIRARD Date: Fri, 31 Jan 2025 23:42:16 +0100 Subject: [PATCH 7/9] Remove pywinstyles from requirements as it's not needed anymore --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 3219064..3970b56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,7 +25,6 @@ pytest-mock pytest-pythonpath pytest-xdist pytweening -pywinstyles pywin32 pyyaml rapidfuzz From 29a0ef1cdf583f1b5cc3f00c9ac346fc05f61782 Mon Sep 17 00:00:00 2001 From: Hugo GIRARD Date: Sat, 1 Feb 2025 00:30:56 +0100 Subject: [PATCH 8/9] Sorting and removing unused imports --- src/gui/d4lfitem.py | 2 +- src/gui/dialog.py | 5 ++--- src/gui/profile_tab.py | 7 +++---- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/gui/d4lfitem.py b/src/gui/d4lfitem.py index 625d107..52a37bc 100644 --- a/src/gui/d4lfitem.py +++ b/src/gui/d4lfitem.py @@ -1,6 +1,6 @@ from src.config.models import DynamicItemFilterModel, AffixFilterModel, AffixFilterCountModel, ItemFilterModel, ItemType from src.gui.dialog import IgnoreScrollWheelComboBox, IgnoreScrollWheelSpinBox -from PyQt6.QtWidgets import QHeaderView, QTableView, QLabel, QVBoxLayout, QHBoxLayout, QComboBox, QGroupBox, QSizePolicy, QFormLayout, QCompleter, QMessageBox +from PyQt6.QtWidgets import QHeaderView, QTableView, QLabel, QVBoxLayout, QComboBox, QGroupBox, QSizePolicy, QFormLayout, QCompleter, QMessageBox from PyQt6.QtCore import Qt class D4LFItem(QGroupBox): diff --git a/src/gui/dialog.py b/src/gui/dialog.py index eb6c6bf..ee00bb7 100644 --- a/src/gui/dialog.py +++ b/src/gui/dialog.py @@ -1,7 +1,6 @@ -from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QSpinBox, QPushButton, QHBoxLayout, QGridLayout, QTextBrowser, QGroupBox, QFormLayout, QComboBox, QLineEdit, QCheckBox -from PyQt6.QtWidgets import QApplication, QMainWindow, QWidgetItem, QPushButton, QScrollArea, QFileDialog, QVBoxLayout, QWidget, QLineEdit, QLabel, QFormLayout, QHBoxLayout, QMessageBox, QMenuBar, QStatusBar, QSizePolicy +from PyQt6.QtWidgets import QDialog, QVBoxLayout, QLabel, QSpinBox, QPushButton, QHBoxLayout, QGroupBox, QFormLayout, QLineEdit, QCheckBox +from PyQt6.QtWidgets import QPushButton, QScrollArea, QVBoxLayout, QWidget, QLineEdit, QLabel, QFormLayout, QHBoxLayout, QMessageBox, QSizePolicy from PyQt6.QtCore import Qt -from PyQt6.QtGui import QAction, QFont, QIcon from src.gui.config_tab import IgnoreScrollWheelComboBox from src.config.models import AffixFilterCountModel, AffixFilterModel, ItemFilterModel, DynamicItemFilterModel, ItemType diff --git a/src/gui/profile_tab.py b/src/gui/profile_tab.py index b8e880e..95b276b 100644 --- a/src/gui/profile_tab.py +++ b/src/gui/profile_tab.py @@ -2,14 +2,13 @@ import json import logging import yaml -from pydantic import ValidationError +from pydantic import ValidationError +from PyQt6.QtWidgets import QGridLayout, QTextBrowser, QFileDialog from typing import List from src.config import BASE_DIR from src.config.loader import IniConfigLoader -from src.config.models import DynamicItemFilterModel, SigilFilterModel, UniqueModel, ConfigDict, AffixFilterCountModel -from src.gui.importer.common import ProfileModel, save_as_profile, _to_yaml_str -from src.gui.importer.maxroll import import_maxroll +from src.gui.importer.common import ProfileModel, save_as_profile from src.gui.dialog import * from src.gui.d4lfitem import * from src.item.filter import _UniqueKeyLoader From 11da39d0c5ba614762fc6cadb38dae63aa9ab102 Mon Sep 17 00:00:00 2001 From: Hugo GIRARD Date: Sun, 2 Feb 2025 15:30:52 +0100 Subject: [PATCH 9/9] Fix merge error in models.py --- src/config/models.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/config/models.py b/src/config/models.py index 7ed9445..d63f092 100644 --- a/src/config/models.py +++ b/src/config/models.py @@ -16,11 +16,7 @@ HIDE_FROM_GUI_KEY = "hide_from_gui" IS_HOTKEY_KEY = "is_hotkey" -DEPRECATED_INI_KEYS = [ - "import_build", - "local_prefs_path", - "move_item_type", -] +DEPRECATED_INI_KEYS = ["import_build", "local_prefs_path", "move_item_type", "use_tts"] class AspectFilterType(enum.StrEnum):