diff --git a/ok/gui/tasks/ConfigItemFactory.py b/ok/gui/tasks/ConfigItemFactory.py index 0160df7f..176a3eff 100644 --- a/ok/gui/tasks/ConfigItemFactory.py +++ b/ok/gui/tasks/ConfigItemFactory.py @@ -5,6 +5,8 @@ from ok.gui.tasks.LabelAndGlobal import LabelAndGlobal from ok.gui.tasks.LabelAndLineEdit import LabelAndLineEdit from ok.gui.tasks.LabelAndMultiSelection import LabelAndMultiSelection +from ok.gui.tasks.LabelAndRadioGroup import LabelAndRadioGroup # 新增导入 +from ok.gui.tasks.LabelAndPresetManager import LabelAndPresetManager # 新增总控导入 from ok.gui.tasks.LabelAndSpinBox import LabelAndSpinBox from ok.gui.tasks.LabelAndSwitchButton import LabelAndSwitchButton from ok.gui.tasks.LabelAndTextEdit import LabelAndTextEdit @@ -31,6 +33,7 @@ def config_widget(config_type, config_desc, config, key, value, task): the_type = config_type.get(key) if config_type is not None else None value = config.get_default(key) resolved_type = _resolve_type(the_type, value) + if resolved_type: if resolved_type == 'drop_down': if isinstance(value, list) and 'options_available' in the_type: @@ -41,6 +44,10 @@ def config_widget(config_type, config_desc, config, key, value, task): return LabelAndDropDown(config_desc, the_type['options'], config, key) elif resolved_type == 'multi_selection': return LabelAndMultiSelection(config_desc, the_type['options'], config, key) + elif resolved_type == 'radio_group': # 补回单选组组件 + return LabelAndRadioGroup(config_desc, the_type['options'], config, key) + elif resolved_type == 'preset_manager': # 桥接总控方案管理类 + return LabelAndPresetManager(config_desc, config, key, task, the_type.get('linked_keys', [])) elif resolved_type == 'global': config = task.get_global_config(key) desc = task.get_global_config_desc(key) @@ -58,6 +65,7 @@ def config_widget(config_type, config_desc, config, key, value, task): return LabelAndButtons(config_desc, key, buttons) else: raise Exception('Unknown config type') + if isinstance(value, bool): return LabelAndSwitchButton(config_desc, config, key) elif isinstance(value, list): diff --git a/ok/gui/tasks/LabelAndPresetManager.py b/ok/gui/tasks/LabelAndPresetManager.py new file mode 100644 index 00000000..f97cc502 --- /dev/null +++ b/ok/gui/tasks/LabelAndPresetManager.py @@ -0,0 +1,167 @@ +from PySide6.QtWidgets import QWidget, QHBoxLayout, QCompleter +from PySide6.QtCore import Qt, QPoint +from PySide6.QtGui import QAction # 使用 PySide6 原生 Action +from qfluentwidgets import LineEdit, ToolButton, PushButton, FluentIcon, RoundMenu # 使用 RoundMenu + +from ok.gui.tasks.ConfigLabelAndWidget import ConfigLabelAndWidget + + +class LabelAndPresetManager(ConfigLabelAndWidget): + def __init__(self, config_desc, config, key: str, task, linked_keys: list): + super().__init__(config_desc, config, key) + self.key = key + self.task = task + self.linked_keys = linked_keys + self.suppress_guard = False + + self.default_presets = { + "经典双爆方案": { + '必须词条': ['暴击', '暴击伤害'], + '可选词条': ['攻击百分比'], + '可选词条数量 >=': 3, + '前置检查': '在双爆出现前', + '暴击数值 >=': '6.3%', + '爆伤数值 >=': '12.6%', + '双爆总计 >=': 13.8, + 'Pause after Success': True, + } + } + + self.container = QWidget() + self.container_layout = QHBoxLayout(self.container) + self.container_layout.setContentsMargins(0, 0, 0, 0) + self.container_layout.setSpacing(8) + + self.line_edit = LineEdit() + self.line_edit.setPlaceholderText("选择或输入新方案名") + self.line_edit.setFixedWidth(180) + + self.menu_btn = ToolButton(FluentIcon.DOWN, self.container) + self.menu_btn.setFixedWidth(32) + self.menu_btn.clicked.connect(self._show_presets_menu) + + self.btn_add = PushButton("新增", self.container) + self.btn_save = PushButton("覆盖", self.container) + self.btn_del = PushButton("删除", self.container) + + self.btn_add.clicked.connect(self._add_preset) + self.btn_save.clicked.connect(self._overwrite_preset) + self.btn_del.clicked.connect(self._delete_preset) + + self.container_layout.addWidget(self.line_edit) + self.container_layout.addWidget(self.menu_btn) + self.container_layout.addWidget(self.btn_add) + self.container_layout.addWidget(self.btn_save) + self.container_layout.addWidget(self.btn_del) + self.container_layout.addStretch(1) # 强制靠左紧凑排列 + + self.add_widget(self.container, stretch=1) + + self.update_value() + self._rebuild_menu_and_completer() + + def _show_presets_menu(self): + """点击下拉箭头时,在按钮下方弹出方案菜单""" + if hasattr(self, 'menu'): + pos = self.menu_btn.mapToGlobal(QPoint(0, self.menu_btn.height())) + self.menu.exec(pos) + + def _rebuild_menu_and_completer(self): + """重新生成菜单和输入框过滤器项""" + if hasattr(self, 'menu'): + self.menu.deleteLater() + + self.menu = RoundMenu(parent=self) + presets = self.config.get('_presets_data', self.default_presets) + + for name in presets.keys(): + action = QAction(name, self) + action.triggered.connect(lambda checked=False, n=name: self._load_preset(n)) + self.menu.addAction(action) + + preset_names = list(presets.keys()) + self.completer = QCompleter(preset_names, self) + self.completer.setFilterMode(Qt.MatchContains) + self.completer.activated.connect(self._load_preset) + self.line_edit.setCompleter(self.completer) + + def _load_preset(self, preset_name): + """明确点选了某个方案:载入该方案数据并动态重绘全面板组件""" + if not preset_name: + return + presets = self.config.get('_presets_data', self.default_presets) + if preset_name in presets: + preset_values = presets[preset_name] + + self.line_edit.setText(preset_name) + self.update_config(preset_name) + + for key in self.linked_keys: + if key in preset_values: + self.config[key] = preset_values[key] + + self._broadcast_ui_refresh() + + def _add_preset(self): + """点击新增:获取当前面板的值并储存为非同名新方案""" + name = self.line_edit.text().strip() + if not name: + name = "未命名方案" + + presets = dict(self.config.get('_presets_data', self.default_presets)) + + while name in presets: + name += "1" + + presets[name] = {} + for key in self.linked_keys: + presets[name][key] = self.config.get(key) + + self.config['_presets_data'] = presets + self.line_edit.setText(name) + self.update_config(name) + self._rebuild_menu_and_completer() + + def _overwrite_preset(self): + """点击覆盖:把当前面板更改后的数值,更新写入到当前输入的方案名中""" + name = self.line_edit.text().strip() + if not name: + return + + presets = dict(self.config.get('_presets_data', self.default_presets)) + presets[name] = {} + for key in self.linked_keys: + presets[name][key] = self.config.get(key) + + self.config['_presets_data'] = presets + self.update_config(name) + self._rebuild_menu_and_completer() + + def _delete_preset(self): + """点击删除:移除当前方案""" + name = self.line_edit.text().strip() + presets = dict(self.config.get('_presets_data', self.default_presets)) + + if name in presets: + del presets[name] + self.config['_presets_data'] = presets + self.line_edit.clear() + self.update_config("") + self._rebuild_menu_and_completer() + + def _broadcast_ui_refresh(self): + """跨组件总控:向上溯源并遍历刷新其他关联配置项的 UI 显示""" + curr_parent = self.parent() + while curr_parent: + children = curr_parent.findChildren(ConfigLabelAndWidget) + if children: + for child in children: + if child != self and hasattr(child, 'update_value') and child.key in self.linked_keys: + child.update_value() + break + curr_parent = curr_parent.parent() + + def update_value(self): + """数据拉取刷新""" + val = self.config.get(self.key, "") + self.line_edit.setText(val) diff --git a/ok/gui/tasks/LabelAndRadioGroup b/ok/gui/tasks/LabelAndRadioGroup new file mode 100644 index 00000000..ddfbaf5f --- /dev/null +++ b/ok/gui/tasks/LabelAndRadioGroup @@ -0,0 +1,57 @@ +from PySide6.QtWidgets import QWidget, QHBoxLayout, QButtonGroup +from qfluentwidgets import RadioButton + +from ok.gui.tasks.ConfigLabelAndWidget import ConfigLabelAndWidget + + +class LabelAndRadioGroup(ConfigLabelAndWidget): + def __init__(self, config_desc, options, config, key: str): + super().__init__(config_desc, config, key) + + self.key = key + self.options = list(options) + self.suppress_guard = False + + self.radio_container = QWidget() + self.radio_layout = QHBoxLayout(self.radio_container) + self.radio_layout.setContentsMargins(0, 0, 0, 0) + self.radio_layout.setSpacing(16) + + self.button_group = QButtonGroup(self) + self.buttons = [] + + current_val = self.config.get(self.key) + + for index, opt in enumerate(self.options): + btn = RadioButton(opt) + self.button_group.addButton(btn, index) + self.radio_layout.addWidget(btn) + self.buttons.append(btn) + + if opt == current_val: + btn.setChecked(True) + + self.button_group.idClicked.connect(self._on_button_clicked) + + self.radio_layout.addStretch(1) + self.add_widget(self.radio_container, stretch=1) + + def _on_button_clicked(self, btn_id: int): + """当单选组内任意按钮被点击时触发(仅触发一次)""" + if self.suppress_guard: + return + + # 通过 idClicked 传进来的 btn_id 就是我们在上面 addButton 时绑定的 index + if 0 <= btn_id < len(self.options): + target_value = self.options[btn_id] + self.update_config(target_value) + + def update_value(self): + """外部数据变动时同步更新 UI 状态""" + self.suppress_guard = True + current_val = self.config.get(self.key) + for i, opt in enumerate(self.options): + if opt == current_val: + self.buttons[i].setChecked(True) + break + self.suppress_guard = False