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