Skip to content

Commit bf2280c

Browse files
committed
Add conditional sub config UI support
1 parent de811a6 commit bf2280c

3 files changed

Lines changed: 265 additions & 15 deletions

File tree

docs/api_doc/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -677,11 +677,13 @@ def get_global_config_desc(self, option) -> str
677677
#### 显示指定配置类型 (self.config_type)
678678

679679
如果需要更复杂的控件(如下拉菜单、多选框或按钮),可以使用 `self.config_type` 进行显式定义。
680+
`type` 是可选的;当配置项提供 `options` 时,会根据默认值自动推断为下拉框或多选框。
680681

681682
目前支持以下类型:
682683

683684
- **`drop_down`**: 下拉选择框。
684685
- **参数:** `options` (list[str]): 选项列表。
686+
- **可选参数:** `sub_configs` (dict[str, list[str]]): 根据当前下拉选项控制其他配置项是否显示。key 是下拉选项值,value 是需要显示的配置项名称列表;这些配置项会按照列表顺序显示在当前配置项下方,未包含在当前选项列表中的配置项会被隐藏。
685687
- **`multi_selection`**: 多选列表。
686688
- **参数:** `options` (list[str]): 选项列表。
687689
- **`text_edit`**: 强制使用多行文本框。
@@ -707,8 +709,10 @@ class MyTask(BaseTask):
707709
}
708710
self.config_type = {
709711
'Mode': {
710-
'type': 'drop_down',
711-
'options': ['Default', 'Fast']
712+
'options': ['Default', 'Fast'],
713+
'sub_configs': {
714+
'Fast': ['Advanced Tool']
715+
}
712716
},
713717
'Advanced Tool': {
714718
'type': 'button',

ok/gui/tasks/ConfigCard.py

Lines changed: 235 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from PySide6.QtCore import Qt
2-
from PySide6.QtWidgets import QHBoxLayout
2+
from PySide6.QtWidgets import QFrame, QHBoxLayout
33
from qfluentwidgets import FluentIcon, ExpandSettingCard, PushButton
44

55
from ok import og
@@ -14,9 +14,14 @@ def __init__(self, task, name, config, description, default_config, config_descr
1414
super().__init__(config_icon or FluentIcon.INFO, og.app.tr(name), og.app.tr(description))
1515
self.config = config
1616
self.config_widgets = []
17+
self.config_widget_by_key = {}
18+
self.config_keys = []
1719
self.default_config = default_config
1820
self.config_description = config_description
1921
self.config_type = config_type
22+
self.sub_configs_rules = {}
23+
self.sub_configs_controlled_keys = {}
24+
self.sub_configs_dividers = {}
2025
self.task = task
2126
self.reset_config = None
2227
self.__initWidget()
@@ -48,28 +53,252 @@ def __initWidget(self):
4853
self.viewLayout.setSpacing(0)
4954
self.viewLayout.setAlignment(Qt.AlignTop)
5055
self.viewLayout.setContentsMargins(10, 0, 10, 0)
56+
self.sub_configs_rules = self.__collect_sub_configs_rules()
57+
self.sub_configs_controlled_keys = self.__collect_sub_configs_controlled_keys()
5158
if not self.config or not (self.config.has_user_config() or self.default_config or self.config_type):
5259
self.card.expandButton.hide()
5360
else:
5461
added_keys = set()
5562
for key, value in self.config.items():
56-
if not key.startswith('_'):
57-
self.__addConfig(key, value)
58-
added_keys.add(key)
63+
if not key.startswith('_') and not self.__is_sub_config_key(key):
64+
self.__addConfigWithSubConfigs(key, value, added_keys, set())
5965
if self.config_type:
6066
for key, the_type in self.config_type.items():
6167
if key not in added_keys and not key.startswith('_'):
62-
if isinstance(the_type, dict) and the_type.get('type') == 'button':
63-
self.__addConfig(key, None)
68+
if self.__is_button_config(the_type) and not self.__is_sub_config_key(key):
69+
self.__addConfigWithSubConfigs(key, None, added_keys, set())
70+
self.__setup_sub_configs()
6471
self.add_buttons()
6572
self._adjustViewSize()
6673

74+
def __addConfigWithSubConfigs(self, key: str, value, added_keys, adding_keys):
75+
if key in added_keys or key in adding_keys:
76+
return
77+
78+
adding_keys.add(key)
79+
has_sub_configs = self.__has_renderable_sub_configs(key)
80+
if has_sub_configs:
81+
self.__add_sub_configs_divider(key, 'top')
82+
83+
self.__addConfig(key, value)
84+
added_keys.add(key)
85+
86+
for sub_config_key in self.__get_sub_config_keys(key):
87+
if sub_config_key.startswith('_'):
88+
continue
89+
90+
sub_config_value = self.__get_config_value(sub_config_key)
91+
if not self.__can_render_config(sub_config_key, sub_config_value):
92+
continue
93+
94+
self.__addConfigWithSubConfigs(sub_config_key, sub_config_value, added_keys, adding_keys)
95+
96+
if has_sub_configs:
97+
self.__add_sub_configs_divider(key, 'bottom')
98+
99+
adding_keys.remove(key)
100+
67101
def __addConfig(self, key: str, value):
68102
widget = config_widget(self.config_type, self.config_description, self.config, key, value, self.task)
69103
self.config_widgets.append(widget)
104+
self.config_widget_by_key[key] = widget
105+
self.config_keys.append(key)
70106
self.viewLayout.addWidget(widget)
71107

108+
def __add_sub_configs_divider(self, key, position):
109+
divider = QFrame()
110+
divider.setFrameShape(QFrame.HLine)
111+
divider.setFrameShadow(QFrame.Plain)
112+
divider.setObjectName('subConfigsDivider')
113+
divider.setFixedHeight(1)
114+
divider.setStyleSheet("color: rgba(128, 128, 128, 90); background-color: rgba(128, 128, 128, 90);")
115+
self.sub_configs_dividers.setdefault(key, {})[position] = divider
116+
self.viewLayout.addWidget(divider)
117+
118+
def __is_button_config(self, the_type):
119+
return (
120+
isinstance(the_type, dict)
121+
and (
122+
the_type.get('type') == 'button'
123+
or ('type' not in the_type and ('buttons' in the_type or 'callback' in the_type))
124+
)
125+
)
126+
127+
def __setup_sub_configs(self):
128+
if not self.sub_configs_rules:
129+
return
130+
131+
for key in self.sub_configs_rules:
132+
widget = self.config_widget_by_key.get(key)
133+
combo_box = getattr(widget, 'combo_box', None)
134+
if combo_box is not None:
135+
combo_box.currentTextChanged.connect(self.__apply_sub_config_visibility)
136+
137+
self.__apply_sub_config_visibility()
138+
139+
def __collect_sub_configs_rules(self):
140+
rules = {}
141+
if not self.config_type:
142+
return rules
143+
144+
for key, the_type in self.config_type.items():
145+
if not isinstance(the_type, dict):
146+
continue
147+
148+
sub_configs = the_type.get('sub_configs')
149+
if not isinstance(sub_configs, dict):
150+
continue
151+
152+
rules[key] = {
153+
choice: self.__normalize_sub_config_keys(config_keys)
154+
for choice, config_keys in sub_configs.items()
155+
}
156+
157+
return rules
158+
159+
def __collect_sub_configs_controlled_keys(self):
160+
return {
161+
key: set().union(*rule.values()) if rule else set()
162+
for key, rule in self.sub_configs_rules.items()
163+
}
164+
165+
def __normalize_sub_config_keys(self, config_keys):
166+
if config_keys is None:
167+
return []
168+
if isinstance(config_keys, str):
169+
return [config_keys]
170+
return list(config_keys)
171+
172+
def __is_sub_config_key(self, key):
173+
return any(key in keys for keys in self.sub_configs_controlled_keys.values())
174+
175+
def __get_config_type(self, key):
176+
if self.config_type is None:
177+
return None
178+
return self.config_type.get(key)
179+
180+
def __get_config_value(self, key):
181+
if self.config is not None and key in self.config:
182+
return self.config.get(key)
183+
return None
184+
185+
def __can_render_config(self, key, value):
186+
return value is not None or self.__is_button_config(self.__get_config_type(key))
187+
188+
def __has_renderable_sub_configs(self, key):
189+
for sub_config_key in self.__get_sub_config_keys(key):
190+
if sub_config_key.startswith('_'):
191+
continue
192+
if self.__can_render_config(sub_config_key, self.__get_config_value(sub_config_key)):
193+
return True
194+
return False
195+
196+
def __get_sub_config_keys(self, key):
197+
keys = []
198+
for config_keys in self.sub_configs_rules.get(key, {}).values():
199+
for config_key in config_keys:
200+
if config_key not in keys:
201+
keys.append(config_key)
202+
return keys
203+
204+
def __get_active_sub_config_keys(self, key):
205+
try:
206+
config_keys = self.sub_configs_rules.get(key, {}).get(self.config.get(key), [])
207+
except TypeError:
208+
return []
209+
return [
210+
config_key for config_key in config_keys
211+
if config_key in self.config_widget_by_key
212+
]
213+
214+
def __apply_sub_config_visibility(self, *args):
215+
self.__sync_sub_config_order()
216+
for key, widget in self.config_widget_by_key.items():
217+
widget.setVisible(self.__is_config_visible(key, set()))
218+
for key, dividers in self.sub_configs_dividers.items():
219+
visible = self.__is_sub_configs_group_visible(key)
220+
for divider in dividers.values():
221+
divider.setVisible(visible)
222+
self._adjustViewSize()
223+
224+
def __sync_sub_config_order(self):
225+
for widget in self.config_widget_by_key.values():
226+
self.viewLayout.removeWidget(widget)
227+
for dividers in self.sub_configs_dividers.values():
228+
for divider in dividers.values():
229+
self.viewLayout.removeWidget(divider)
230+
231+
insert_index = 0
232+
for key in self.config_keys:
233+
if self.__is_sub_config_key(key):
234+
continue
235+
insert_index = self.__insert_config_group(key, insert_index, set())
236+
237+
def __insert_config_group(self, key, insert_index, inserting_keys):
238+
if key in inserting_keys or key not in self.config_widget_by_key:
239+
return insert_index
240+
241+
inserting_keys.add(key)
242+
active_sub_config_keys = self.__get_active_sub_config_keys(key)
243+
has_visible_sub_configs = any(
244+
self.__is_config_visible(sub_config_key, set())
245+
for sub_config_key in active_sub_config_keys
246+
)
247+
248+
if has_visible_sub_configs:
249+
insert_index = self.__insert_sub_configs_divider(key, 'top', insert_index)
250+
251+
self.viewLayout.insertWidget(insert_index, self.config_widget_by_key[key])
252+
insert_index += 1
253+
254+
for sub_config_key in active_sub_config_keys:
255+
insert_index = self.__insert_config_group(sub_config_key, insert_index, inserting_keys)
256+
257+
if has_visible_sub_configs:
258+
insert_index = self.__insert_sub_configs_divider(key, 'bottom', insert_index)
259+
260+
inserting_keys.remove(key)
261+
return insert_index
262+
263+
def __insert_sub_configs_divider(self, key, position, insert_index):
264+
divider = self.sub_configs_dividers.get(key, {}).get(position)
265+
if divider is None:
266+
return insert_index
267+
268+
self.viewLayout.insertWidget(insert_index, divider)
269+
return insert_index + 1
270+
271+
def __is_sub_configs_group_visible(self, key):
272+
if not self.__is_config_visible(key, set()):
273+
return False
274+
for sub_config_key in self.__get_active_sub_config_keys(key):
275+
if sub_config_key in self.config_widget_by_key and self.__is_config_visible(sub_config_key, set()):
276+
return True
277+
return False
278+
279+
def __is_config_visible(self, key, checking):
280+
if key in checking:
281+
return False
282+
283+
checking = checking | {key}
284+
for parent_key, rule in self.sub_configs_rules.items():
285+
if key not in self.sub_configs_controlled_keys.get(parent_key, set()):
286+
continue
287+
288+
if not self.__is_config_visible(parent_key, checking):
289+
return False
290+
291+
try:
292+
visible_config_keys = rule.get(self.config.get(parent_key), [])
293+
except TypeError:
294+
visible_config_keys = []
295+
296+
if key not in visible_config_keys:
297+
return False
298+
299+
return True
72300

73301
def update_config(self):
74302
for widget in self.config_widgets:
75303
widget.update_value()
304+
self.__apply_sub_config_visibility()

ok/gui/tasks/ConfigItemFactory.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,44 @@
1010
from ok.gui.tasks.ModifyListItem import ModifyListItem
1111

1212

13+
def _resolve_type(the_type, default_value):
14+
if not isinstance(the_type, dict):
15+
return None
16+
17+
resolved_type = the_type.get('type')
18+
if resolved_type:
19+
return resolved_type
20+
if 'buttons' in the_type or 'callback' in the_type:
21+
return 'button'
22+
if 'options' in the_type:
23+
if isinstance(default_value, list):
24+
return 'multi_selection'
25+
return 'drop_down'
26+
return None
27+
28+
1329
def config_widget(config_type, config_desc, config, key, value, task):
1430
the_type = config_type.get(key) if config_type is not None else None
15-
if the_type:
16-
if the_type['type'] == 'drop_down':
31+
value = config.get_default(key)
32+
resolved_type = _resolve_type(the_type, value)
33+
if resolved_type:
34+
if resolved_type == 'drop_down':
1735
return LabelAndDropDown(config_desc, the_type['options'], config, key)
18-
elif the_type['type'] == 'multi_selection':
36+
elif resolved_type == 'multi_selection':
1937
return LabelAndMultiSelection(config_desc, the_type['options'], config, key)
20-
elif the_type['type'] == 'global':
38+
elif resolved_type == 'global':
2139
config = task.get_global_config(key)
2240
desc = task.get_global_config_desc(key)
2341
return LabelAndGlobal(desc, config, key)
24-
elif the_type['type'] == 'text_edit':
42+
elif resolved_type == 'text_edit':
2543
return LabelAndTextEdit(config_desc, config, key)
26-
elif the_type['type'] == 'button':
44+
elif resolved_type == 'button':
2745
buttons = the_type.get('buttons')
2846
if not buttons:
2947
buttons = [the_type]
3048
return LabelAndButtons(config_desc, key, buttons)
3149
else:
3250
raise Exception('Unknown config type')
33-
value = config.get_default(key)
3451
if isinstance(value, bool):
3552
return LabelAndSwitchButton(config_desc, config, key)
3653
elif isinstance(value, list):

0 commit comments

Comments
 (0)