1import sys 2from typing import List, Iterable, Tuple, Callable, Union, Dict 3from functools import singledispatch 4 5from AnyQt.QtCore import Qt, pyqtSignal as Signal, QStringListModel, \ 6 QAbstractItemModel 7from AnyQt.QtWidgets import QDialog, QVBoxLayout, QComboBox, QCheckBox, \ 8 QDialogButtonBox, QSpinBox, QWidget, QApplication, QFormLayout, QLineEdit 9 10from orangewidget import gui 11from orangewidget.utils.combobox import _ComboBoxListDelegate 12from orangewidget.widget import OWBaseWidget 13 14KeyType = Tuple[str, str, str] 15ValueType = Union[str, int, bool] 16ValueRangeType = Union[Iterable, None] 17SettingsType = Dict[str, Tuple[ValueRangeType, ValueType]] 18 19 20class SettingsDialog(QDialog): 21 """ A dialog for settings manipulation. 22 23 Attributes 24 ---------- 25 master : Union[QWidget, None] 26 Parent widget. 27 28 settings : Dict[str, Dict[str, SettingsType]] 29 Collection of box names, label texts, parameter names, 30 initial control values and possible control values. 31 32 """ 33 setting_changed = Signal(object, object) 34 35 def __init__(self, master: Union[QWidget, None], 36 settings: Dict[str, Dict[str, SettingsType]]): 37 super().__init__(master, windowTitle="Visual Settings") 38 self.__controls: Dict[KeyType, Tuple[QWidget, ValueType]] = {} 39 self.__changed_settings: Dict[KeyType, ValueType] = {} 40 self.setting_changed.connect(self.__on_setting_changed) 41 42 layout = QVBoxLayout() 43 layout.setContentsMargins(0, 0, 0, 0) 44 self.setLayout(layout) 45 46 self.main_box = gui.vBox(self, box=None) # type: QWidget 47 48 buttons = QDialogButtonBox( 49 orientation=Qt.Horizontal, 50 standardButtons=QDialogButtonBox.Close | QDialogButtonBox.Reset, 51 ) 52 closeButton = buttons.button(QDialogButtonBox.Close) 53 closeButton.clicked.connect(self.close) 54 55 resetButton = buttons.button(QDialogButtonBox.Reset) 56 resetButton.clicked.connect(self.__reset) 57 resetButton.setAutoDefault(False) 58 59 layout.addWidget(buttons) 60 61 self.__initialize(settings) 62 63 @property 64 def changed_settings(self) -> Dict[KeyType, ValueType]: 65 """ 66 Keys (box, label, parameter) and values for changed settings. 67 68 Returns 69 ------- 70 settings : Dict[KeyType, ValueType] 71 """ 72 return self.__changed_settings 73 74 def __on_setting_changed(self, key: KeyType, value: ValueType): 75 self.__changed_settings[key] = value 76 77 def __reset(self): 78 for key in self.__changed_settings: 79 _set_control_value(*self.__controls[key]) 80 self.__changed_settings = {} 81 82 def __initialize(self, settings: Dict[str, Dict[str, SettingsType]]): 83 for box_name in settings: 84 box = gui.vBox(self.main_box, box=box_name) 85 form = QFormLayout() 86 form.setFormAlignment(Qt.AlignLeft | Qt.AlignTop) 87 form.setLabelAlignment(Qt.AlignLeft) 88 box.layout().addLayout(form) 89 for label, values in settings[box_name].items(): 90 self.__add_row(form, box_name, label, values) 91 self.main_box.adjustSize() 92 93 def __add_row(self, form: QFormLayout, box_name: str, 94 label: str, settings: SettingsType): 95 box = gui.hBox(None, box=None) 96 for parameter, (values, default_value) in settings.items(): 97 key = (box_name, label, parameter) 98 control = _add_control(values or default_value, default_value, key, 99 self.setting_changed) 100 control.setToolTip(parameter) 101 box.layout().addWidget(control) 102 self.__controls[key] = (control, default_value) 103 form.addRow(f"{label}:", box) 104 105 def apply_settings(self, settings: Iterable[Tuple[KeyType, ValueType]]): 106 """ Assign values to controls. 107 108 Parameters 109 ---------- 110 settings : Iterable[Tuple[KeyType, ValueType] 111 Collection of box names, label texts, parameter names 112 and control values. 113 """ 114 for key, value in settings: 115 _set_control_value(self.__controls[key][0], value) 116 117 def show_dlg(self): 118 """ Open the dialog. """ 119 self.show() 120 self.raise_() 121 self.activateWindow() 122 123 124class VisualSettingsDialog(SettingsDialog): 125 """ A dialog for visual settings manipulation, that can be used along 126 OWBaseWidget. 127 128 The OWBaseWidget should implement set_visual_settings. 129 If the OWBaseWidget has visual_settings property as Setting({}), 130 the saved settings are applied. 131 132 Attributes 133 ---------- 134 master : OWBaseWidget 135 Parent widget. 136 137 settings : Dict[str, Dict[str, SettingsType]] 138 Collection of box names, label texts, parameter names, 139 initial control values and possible control values. 140 """ 141 142 def __init__(self, master: OWBaseWidget, 143 settings: Dict[str, Dict[str, SettingsType]]): 144 super().__init__(master, settings) 145 self.setting_changed.connect(master.set_visual_settings) 146 if hasattr(master, "visual_settings"): 147 self.apply_settings(master.visual_settings.items()) 148 master.openVisualSettingsClicked.connect(self.show_dlg) 149 150 151@singledispatch 152def _add_control(*_): 153 raise NotImplementedError 154 155 156@_add_control.register(list) 157def _(values: List[str], value: str, key: KeyType, signal: Callable) \ 158 -> QComboBox: 159 combo = QComboBox() 160 combo.addItems(values) 161 combo.setCurrentText(value) 162 combo.currentTextChanged.connect(lambda text: signal.emit(key, text)) 163 return combo 164 165 166class FontList(list): 167 pass 168 169 170@_add_control.register(FontList) 171def _(values: FontList, value: str, key: KeyType, signal: Callable) \ 172 -> QComboBox: 173 class FontModel(QStringListModel): 174 def data(self, index, role=Qt.DisplayRole): 175 if role == Qt.AccessibleDescriptionRole \ 176 and super().data(index, Qt.DisplayRole) == "": 177 return "separator" 178 179 value = super().data(index, role) 180 if role == Qt.DisplayRole and value.startswith("."): 181 value = value[1:] 182 return value 183 184 def flags(self, index): 185 if index.data(Qt.DisplayRole) == "separator": 186 return Qt.NoItemFlags 187 else: 188 return super().flags(index) 189 190 combo = QComboBox() 191 model = FontModel(values) 192 combo.setModel(model) 193 combo.setCurrentIndex(values.index(value)) 194 combo.currentIndexChanged.connect(lambda i: signal.emit(key, values[i])) 195 combo.setItemDelegate(_ComboBoxListDelegate()) 196 return combo 197 198 199@_add_control.register(range) 200def _(values: Iterable[int], value: int, key: KeyType, signal: Callable) \ 201 -> QSpinBox: 202 spin = QSpinBox(minimum=values.start, maximum=values.stop, 203 singleStep=values.step, value=value) 204 spin.valueChanged.connect(lambda val: signal.emit(key, val)) 205 return spin 206 207 208@_add_control.register(bool) 209def _(_: bool, value: bool, key: KeyType, signal: Callable) -> QCheckBox: 210 check = QCheckBox(text=f"{key[-1]} ", checked=value) 211 check.stateChanged.connect(lambda val: signal.emit(key, bool(val))) 212 return check 213 214 215@_add_control.register(str) 216def _(_: str, value: str, key: KeyType, signal: Callable) -> QLineEdit: 217 line_edit = QLineEdit(value) 218 line_edit.textChanged.connect(lambda text: signal.emit(key, text)) 219 return line_edit 220 221 222@singledispatch 223def _set_control_value(*_): 224 raise NotImplementedError 225 226 227@_set_control_value.register(QComboBox) 228def _(combo: QComboBox, value: str): 229 model: QAbstractItemModel = combo.model() 230 values = [model.data(model.index(i, 0), role=Qt.EditRole) 231 for i in range(model.rowCount())] 232 combo.setCurrentIndex(values.index(value)) 233 234 235@_set_control_value.register(QSpinBox) 236def _(spin: QSpinBox, value: int): 237 spin.setValue(value) 238 239 240@_set_control_value.register(QCheckBox) 241def _(check: QCheckBox, value: bool): 242 check.setChecked(value) 243 244 245@_set_control_value.register(QLineEdit) 246def _(edit: QLineEdit, value: str): 247 edit.setText(value) 248 249 250if __name__ == "__main__": 251 from AnyQt.QtWidgets import QPushButton 252 253 app = QApplication(sys.argv) 254 w = QDialog() 255 w.setFixedSize(400, 200) 256 257 _items = ["Foo", "Bar", "Baz", "Foo Bar", "Foo Baz", "Bar Baz"] 258 _settings = { 259 "Box 1": { 260 "Item 1": { 261 "Parameter 1": (_items[:10], _items[0]), 262 "Parameter 2": (_items[:10], _items[0]), 263 "Parameter 3": (range(4, 20), 5) 264 }, 265 "Item 2": { 266 "Parameter 1": (_items[:10], _items[1]), 267 "Parameter 2": (range(4, 20), 6), 268 "Parameter 3": (range(4, 20), 7) 269 }, 270 "Item 3": { 271 "Parameter 1": (_items[:10], _items[1]), 272 "Parameter 2": (range(4, 20), 8) 273 }, 274 }, 275 "Box 2": { 276 "Item 1": { 277 "Parameter 1": (_items[:10], _items[0]), 278 "Parameter 2": (None, True) 279 }, 280 "Item 2": { 281 "Parameter 1": (_items[:10], _items[1]), 282 "Parameter 2": (None, False) 283 }, 284 "Item 3": { 285 "Parameter 1": (None, False), 286 "Parameter 2": (None, True) 287 }, 288 "Item 4": { 289 "Parameter 1": (_items[:10], _items[0]), 290 "Parameter 2": (None, False) 291 }, 292 "Item 5": { 293 "Parameter 1": ("", "Foo"), 294 "Parameter 2": (None, False) 295 }, 296 "Item 6": { 297 "Parameter 1": ("", ""), 298 "Parameter 2": (None, False) 299 }, 300 }, 301 } 302 303 dlg = SettingsDialog(w, _settings) 304 dlg.setting_changed.connect(lambda *res: print(*res)) 305 dlg.finished.connect(lambda res: print(res, dlg.changed_settings)) 306 307 btn = QPushButton(w) 308 btn.setText("Open dialog") 309 btn.clicked.connect(dlg.show_dlg) 310 311 w.show() 312 sys.exit(app.exec_()) 313