From e39dd31d39b12b9674d355aada169b70c0403883 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Fri, 2 Feb 2024 15:41:25 +0100 Subject: [PATCH 1/5] first working prototype --- Orange/widgets/settings.py | 93 ++++++++++++++++++++++++++++++-------- 1 file changed, 75 insertions(+), 18 deletions(-) diff --git a/Orange/widgets/settings.py b/Orange/widgets/settings.py index 3bbe31eb2c0..1d1d4e67f4e 100644 --- a/Orange/widgets/settings.py +++ b/Orange/widgets/settings.py @@ -38,10 +38,11 @@ import warnings from orangewidget.settings import ( - Setting, SettingProvider, SettingsHandler, ContextSetting, + Setting, SettingProvider, SettingsHandler, ContextHandler, Context, IncompatibleContext, SettingsPrinter, rename_setting, widget_settings_dir ) +from orangewidget.settings import ContextSetting as WBContextSetting from orangewidget.settings import _apply_setting from Orange.data import Domain, Variable @@ -62,6 +63,19 @@ ] +class ContextSetting(WBContextSetting): + """Description of a context dependent setting""" + + def __init__(self, default, *, required=2, + exclude_attributes=False, exclude_class_vars=False, exclude_metas=False, + **data): + super().__init__(default, required=required, **data) + self.exclude_attributes = exclude_attributes + self.exclude_class_vars = exclude_class_vars + self.exclude_metas = exclude_metas + self.required = required + + class DomainContextHandler(ContextHandler): """Context handler for widgets with settings that depend on the input dataset. Suitable settings are selected based on the @@ -124,18 +138,18 @@ def open_context(self, widget, domain): domain = domain.domain super().open_context(widget, domain, *self.encode_domain(domain)) - def filter_value(self, setting, data, domain, attrs, metas): + def filter_value(self, setting, data, domain, *args): value = data.get(setting.name, None) if isinstance(value, list): new_value = [item for item in value - if self.is_valid_item(setting, item, attrs, metas)] + if self.is_valid_item(setting, item, *args)] data[setting.name] = new_value elif isinstance(value, dict): new_value = {item: val for item, val in value.items() - if self.is_valid_item(setting, item, attrs, metas)} + if self.is_valid_item(setting, item, *args)} data[setting.name] = new_value elif self.is_encoded_var(value) \ - and not self._var_exists(setting, value, attrs, metas): + and not self._var_exists(setting, value, *args): del data[setting.name] @staticmethod @@ -159,7 +173,7 @@ def encode_setting(cls, context, setting, value): -4) if isinstance(value, Variable): - if isinstance(setting, ContextSetting): + if isinstance(setting, WBContextSetting): return cls.encode_variable(value) else: raise ValueError("Variables must be stored as ContextSettings; " @@ -205,28 +219,30 @@ def _var_exists(cls, setting, value, attributes, metas): def match(self, context, domain, attrs, metas): if context.attributes == attrs and context.metas == metas: return self.PERFECT_MATCH + return self._match(context, domain, attrs, metas) + def _match(self, context, domain, *args): matches = [] try: for setting, data, _ in \ self.provider.traverse_settings(data=context.values): - if not isinstance(setting, ContextSetting): + if not isinstance(setting, WBContextSetting): continue value = data.get(setting.name, None) if isinstance(value, list): matches.append( - self.match_list(setting, value, context, attrs, metas)) + self.match_list(setting, value, context, *args)) # type check is a (not foolproof) check in case of a pair that # would, by conincidence, have -3 or -4 as the second element elif isinstance(value, tuple) and len(value) == 2 \ and (value[1] == -3 and isinstance(value[0], list) or (value[1] == -4 and isinstance(value[0], dict))): matches.append(self.match_list(setting, value[0], context, - attrs, metas)) + *args)) elif value is not None: matches.append( - self.match_value(setting, value, attrs, metas)) + self.match_value(setting, value, *args)) except IncompatibleContext: return self.NO_MATCH @@ -237,31 +253,31 @@ def match(self, context, domain, attrs, metas): matched, available = [sum(m) for m in zip(*matches)] return matched / available if available else 0.1 - def match_list(self, setting, value, context, attrs, metas): + def match_list(self, setting, value, context, *args): """Match a list of values with the given context. returns a tuple containing number of matched and all values. """ matched = 0 for item in value: - if self.is_valid_item(setting, item, attrs, metas): + if self.is_valid_item(setting, item, *args): matched += 1 - elif setting.required == ContextSetting.REQUIRED: + elif setting.required == WBContextSetting.REQUIRED: raise IncompatibleContext() return matched, len(value) - def match_value(self, setting, value, attrs, metas): + def match_value(self, setting, value, *args): """Match a single value """ if value[1] < 0: return 0, 0 - if self._var_exists(setting, value, attrs, metas): + if self._var_exists(setting, value, *args): return 1, 1 elif setting.required == setting.OPTIONAL: return 0, 1 else: raise IncompatibleContext() - def is_valid_item(self, setting, item, attrs, metas): + def is_valid_item(self, setting, item, *args): """Return True if given item can be used with attrs and metas Subclasses can override this method to checks data in alternative @@ -269,7 +285,7 @@ def is_valid_item(self, setting, item, attrs, metas): """ if not isinstance(item, tuple): return True - return self._var_exists(setting, item, attrs, metas) + return self._var_exists(setting, item, *args) @staticmethod def is_encoded_var(value): @@ -278,6 +294,47 @@ def is_encoded_var(value): and isinstance(value[0], str) and isinstance(value[1], int) \ and value[1] >= 0 + +class DomainContextHandlerPosition(DomainContextHandler): + + def __init__(self, *, match_values=0, first_match=True): + super().__init__(match_values=match_values, first_match=first_match) + + def encode_domain(self, domain): + match = self.match_values + encode = self.encode_variables + attributes = encode(domain.attributes, match == self.MATCH_VALUES_ALL) + class_vars = encode(domain.class_vars, + match in (self.MATCH_VALUES_ALL, self.MATCH_VALUES_CLASS)) + metas = encode(domain.metas, match == self.MATCH_VALUES_ALL) + + return attributes, class_vars, metas + + new_context = ContextHandler.new_context + + @classmethod + def _var_exists(cls, setting, value, attributes, class_vars, metas): + assert isinstance(setting, ContextSetting) + + if not cls.is_encoded_var(value): + return False + + attr_name, attr_type = value + # attr_type used to be either 1-4 for variables stored as string + # settings, and 101-104 for variables stored as variables. The former is + # no longer supported, but we play it safe and still handle both here. + attr_type %= 100 + return (not setting.exclude_attributes and + attributes.get(attr_name, -1) == attr_type or + not setting.exclude_class_vars and + class_vars.get(attr_name, -1) == attr_type or + not setting.exclude_metas and + metas.get(attr_name, -1) == attr_type) + + def match(self, context, domain, attrs, class_vars, metas): + return self._match(context, domain, attrs, class_vars, metas) + + class ClassValuesContextHandler(ContextHandler): """Context handler used for widgets that work with a single discrete variable""" @@ -358,7 +415,7 @@ def encode_setting(self, context, setting, value): """Same as is domain context handler, but handles separately stored class_vars.""" - if isinstance(setting, ContextSetting) and isinstance(value, str): + if isinstance(setting, WBContextSetting) and isinstance(value, str): def _candidate_variables(): if not setting.exclude_attributes: From 5695cb304a78e6c8ffde15059d5a5f401255967a Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Mon, 19 Feb 2024 13:58:00 +0100 Subject: [PATCH 2/5] owheatmap: Fix inconsistent row_color_annotations A combo box for choosing row annotation color is limited to metas and class vars, and thus, when a selected feature was moved from metas to attributes, an incompatible context would match. That did not trigger any crashes because the combo box was updated through special functions, but it did leave the UI in an inconsistent state: an element with index of -1 was selected in the combo (""), and, the saved color was still shown in the map. --- Orange/widgets/visualize/owheatmap.py | 6 +++--- Orange/widgets/visualize/tests/test_owheatmap.py | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/Orange/widgets/visualize/owheatmap.py b/Orange/widgets/visualize/owheatmap.py index 53d1845331d..4ba24e8db84 100644 --- a/Orange/widgets/visualize/owheatmap.py +++ b/Orange/widgets/visualize/owheatmap.py @@ -146,9 +146,9 @@ class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) - settings_version = 3 + settings_version = 4 - settingsHandler = settings.DomainContextHandler() + settingsHandler = settings.DomainContextHandlerPosition() # Disable clustering for inputs bigger than this MaxClustering = 25000 @@ -170,7 +170,7 @@ class Outputs: #: text row annotation (row names) annotation_var = settings.ContextSetting(None) #: color row annotation - annotation_color_var = settings.ContextSetting(None) + annotation_color_var = settings.ContextSetting(None, exclude_attributes=True) column_annotation_color_key: Optional[Tuple[str, str]] = settings.ContextSetting(None) # Discrete variable used to split that data/heatmaps (vertically) diff --git a/Orange/widgets/visualize/tests/test_owheatmap.py b/Orange/widgets/visualize/tests/test_owheatmap.py index d6f42f29154..7649a3299ef 100644 --- a/Orange/widgets/visualize/tests/test_owheatmap.py +++ b/Orange/widgets/visualize/tests/test_owheatmap.py @@ -365,6 +365,20 @@ def test_row_color_annotations_with_na(self): widget.set_annotation_color_var(None) self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible()) + def test_row_color_annotations_invalid_context(self): + widget = self.widget + data = self.brown_selected[::5] + self.send_signal(widget.Inputs.data, data, widget=widget) + widget.set_annotation_color_var(data.domain["function"]) + self.assertTrue(widget.scene.widget.right_side_colors[0].isVisible()) + # attributes are ignored in for annotation_color_var, so the following + # data should not match any context + data_attributes = self.brown_selected.transform( + Domain(data.domain.attributes + data.domain.class_vars)) + self.send_signal(widget.Inputs.data, data_attributes, widget=widget) + self.assertEqual(widget.row_side_color_cb.currentText(), "(None)") + self.assertFalse(widget.scene.widget.right_side_colors[0].isVisible()) + def test_col_color_annotations(self): widget = self.widget data = self._brown_selected_10() From 9a2b01a2b1b69b3e7ea0fbe80805fcd69bdce170 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Fri, 2 Aug 2024 14:34:03 +0200 Subject: [PATCH 3/5] SimpleDomainContextHandler: match directly to the domain --- Orange/widgets/settings.py | 54 +++++++++++++-------------- Orange/widgets/visualize/owheatmap.py | 2 +- 2 files changed, 27 insertions(+), 29 deletions(-) diff --git a/Orange/widgets/settings.py b/Orange/widgets/settings.py index 1d1d4e67f4e..73868993ada 100644 --- a/Orange/widgets/settings.py +++ b/Orange/widgets/settings.py @@ -138,7 +138,7 @@ def open_context(self, widget, domain): domain = domain.domain super().open_context(widget, domain, *self.encode_domain(domain)) - def filter_value(self, setting, data, domain, *args): + def _filter_value(self, setting, data, *args): value = data.get(setting.name, None) if isinstance(value, list): new_value = [item for item in value @@ -152,6 +152,9 @@ def filter_value(self, setting, data, domain, *args): and not self._var_exists(setting, value, *args): del data[setting.name] + def filter_value(self, setting, data, domain, *args): + self._filter_value(setting, data, *args) + @staticmethod def encode_variable(var): return var.name, 100 + vartype(var) @@ -219,9 +222,9 @@ def _var_exists(cls, setting, value, attributes, metas): def match(self, context, domain, attrs, metas): if context.attributes == attrs and context.metas == metas: return self.PERFECT_MATCH - return self._match(context, domain, attrs, metas) + return self._match(context, attrs, metas) - def _match(self, context, domain, *args): + def _match(self, context, *args): matches = [] try: for setting, data, _ in \ @@ -295,44 +298,39 @@ def is_encoded_var(value): and value[1] >= 0 -class DomainContextHandlerPosition(DomainContextHandler): - - def __init__(self, *, match_values=0, first_match=True): - super().__init__(match_values=match_values, first_match=first_match) +class SimpleDomainContextHandler(DomainContextHandler): def encode_domain(self, domain): - match = self.match_values - encode = self.encode_variables - attributes = encode(domain.attributes, match == self.MATCH_VALUES_ALL) - class_vars = encode(domain.class_vars, - match in (self.MATCH_VALUES_ALL, self.MATCH_VALUES_CLASS)) - metas = encode(domain.metas, match == self.MATCH_VALUES_ALL) - - return attributes, class_vars, metas + return tuple() new_context = ContextHandler.new_context @classmethod - def _var_exists(cls, setting, value, attributes, class_vars, metas): + def _var_exists(cls, setting, value, domain): assert isinstance(setting, ContextSetting) if not cls.is_encoded_var(value): return False attr_name, attr_type = value - # attr_type used to be either 1-4 for variables stored as string - # settings, and 101-104 for variables stored as variables. The former is - # no longer supported, but we play it safe and still handle both here. - attr_type %= 100 - return (not setting.exclude_attributes and - attributes.get(attr_name, -1) == attr_type or - not setting.exclude_class_vars and - class_vars.get(attr_name, -1) == attr_type or - not setting.exclude_metas and - metas.get(attr_name, -1) == attr_type) - def match(self, context, domain, attrs, class_vars, metas): - return self._match(context, domain, attrs, class_vars, metas) + if attr_name not in domain: + return False + + candidate = domain[attr_name] + idx = domain.index(candidate) + if (0 <= idx < len(domain.attributes) and setting.exclude_attributes + or idx >= len(domain.attributes) and setting.exclude_class_vars + or idx < 0 and setting.exclude_metas): + return False + + return cls.encode_variable(candidate)[1] == attr_type + + def filter_value(self, setting, data, domain): + self._filter_value(setting, data, domain) + + def match(self, context, domain): + return self._match(context, domain) class ClassValuesContextHandler(ContextHandler): diff --git a/Orange/widgets/visualize/owheatmap.py b/Orange/widgets/visualize/owheatmap.py index 4ba24e8db84..e2d7f3986fd 100644 --- a/Orange/widgets/visualize/owheatmap.py +++ b/Orange/widgets/visualize/owheatmap.py @@ -148,7 +148,7 @@ class Outputs: settings_version = 4 - settingsHandler = settings.DomainContextHandlerPosition() + settingsHandler = settings.SimpleDomainContextHandler() # Disable clustering for inputs bigger than this MaxClustering = 25000 From 761506b2d750f599b65a0c71b30801b6094f331c Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Fri, 2 Aug 2024 15:39:47 +0200 Subject: [PATCH 4/5] DomainContextHandler: refactor into mixins --- Orange/widgets/settings.py | 226 ++++++++++++++++++++----------------- 1 file changed, 122 insertions(+), 104 deletions(-) diff --git a/Orange/widgets/settings.py b/Orange/widgets/settings.py index 73868993ada..fdc9066e98e 100644 --- a/Orange/widgets/settings.py +++ b/Orange/widgets/settings.py @@ -76,69 +76,9 @@ def __init__(self, default, *, required=2, self.required = required -class DomainContextHandler(ContextHandler): - """Context handler for widgets with settings that depend on - the input dataset. Suitable settings are selected based on the - data domain.""" - - MATCH_VALUES_NONE, MATCH_VALUES_CLASS, MATCH_VALUES_ALL = range(3) - - def __init__(self, *, match_values=0, first_match=True, **kwargs): - super().__init__() - self.match_values = match_values - self.first_match = first_match - - for name in kwargs: - warnings.warn( - "{} is not a valid parameter for DomainContextHandler" - .format(name), OrangeDeprecationWarning - ) - - def encode_domain(self, domain): - """ - domain: Orange.data.domain to encode - return: dict mapping attribute name to type or list of values - (based on the value of self.match_values attribute) - """ - - match = self.match_values - encode = self.encode_variables - if match == self.MATCH_VALUES_CLASS: - attributes = encode(domain.attributes, False) - attributes.update(encode(domain.class_vars, True)) - else: - attributes = encode(domain.variables, match == self.MATCH_VALUES_ALL) - - metas = encode(domain.metas, match == self.MATCH_VALUES_ALL) - - return attributes, metas - - @staticmethod - def encode_variables(attributes, encode_values): - """Encode variables to a list mapping name to variable type - or a list of values.""" - - if not encode_values: - return {v.name: vartype(v) for v in attributes} - - return {v.name: v.values if v.is_discrete else vartype(v) - for v in attributes} - - def new_context(self, domain, attributes, metas): - """Create a new context.""" - context = super().new_context() - context.attributes = attributes - context.metas = metas - return context +class StructuredVariableSettingMixin: - def open_context(self, widget, domain): - if domain is None: - return - if not isinstance(domain, Domain): - domain = domain.domain - super().open_context(widget, domain, *self.encode_domain(domain)) - - def _filter_value(self, setting, data, *args): + def filter_value(self, setting, data, *args): value = data.get(setting.name, None) if isinstance(value, list): new_value = [item for item in value @@ -149,22 +89,14 @@ def _filter_value(self, setting, data, *args): if self.is_valid_item(setting, item, *args)} data[setting.name] = new_value elif self.is_encoded_var(value) \ - and not self._var_exists(setting, value, *args): + and not self.match_variable(setting, value, *args): del data[setting.name] - def filter_value(self, setting, data, domain, *args): - self._filter_value(setting, data, *args) - - @staticmethod - def encode_variable(var): - return var.name, 100 + vartype(var) - - @classmethod - def encode_setting(cls, context, setting, value): + def encode_setting(self, context, setting, value): if isinstance(value, list): if all(e is None or isinstance(e, Variable) for e in value) \ and any(e is not None for e in value): - return ([None if e is None else cls.encode_variable(e) + return ([None if e is None else self.encode_variable(e) for e in value], -3) else: @@ -172,12 +104,12 @@ def encode_setting(cls, context, setting, value): elif isinstance(value, dict) \ and all(isinstance(e, Variable) for e in value): - return ({cls.encode_variable(e): val for e, val in value.items()}, + return ({self.encode_variable(e): val for e, val in value.items()}, -4) if isinstance(value, Variable): if isinstance(setting, WBContextSetting): - return cls.encode_variable(value) + return self.encode_variable(value) else: raise ValueError("Variables must be stored as ContextSettings; " f"change {setting.name} to ContextSetting.") @@ -204,27 +136,7 @@ def get_var(name): else: return value - @classmethod - def _var_exists(cls, setting, value, attributes, metas): - if not cls.is_encoded_var(value): - return False - - attr_name, attr_type = value - # attr_type used to be either 1-4 for variables stored as string - # settings, and 101-104 for variables stored as variables. The former is - # no longer supported, but we play it safe and still handle both here. - attr_type %= 100 - return (not setting.exclude_attributes and - attributes.get(attr_name, -1) == attr_type or - not setting.exclude_metas and - metas.get(attr_name, -1) == attr_type) - - def match(self, context, domain, attrs, metas): - if context.attributes == attrs and context.metas == metas: - return self.PERFECT_MATCH - return self._match(context, attrs, metas) - - def _match(self, context, *args): + def match(self, context, *args): matches = [] try: for setting, data, _ in \ @@ -273,7 +185,7 @@ def match_value(self, setting, value, *args): if value[1] < 0: return 0, 0 - if self._var_exists(setting, value, *args): + if self.match_variable(setting, value, *args): return 1, 1 elif setting.required == setting.OPTIONAL: return 0, 1 @@ -288,7 +200,19 @@ def is_valid_item(self, setting, item, *args): """ if not isinstance(item, tuple): return True - return self._var_exists(setting, item, *args) + return self.match_variable(setting, item, *args) + + @classmethod + def match_variable(cls, setting, value, *args): + """ Return if variable described with value can be matched to *args. """ + raise NotImplementedError + + +class VariableEncoderMixin: + + @staticmethod + def encode_variable(var): + return var.name, 100 + vartype(var) @staticmethod def is_encoded_var(value): @@ -298,6 +222,101 @@ def is_encoded_var(value): and value[1] >= 0 +class DomainContextHandler(ContextHandler, StructuredVariableSettingMixin, + VariableEncoderMixin): + """Context handler for widgets with settings that depend on + the input dataset. Suitable settings are selected based on the + data domain.""" + + MATCH_VALUES_NONE, MATCH_VALUES_CLASS, MATCH_VALUES_ALL = range(3) + + def __init__(self, *, match_values=0, first_match=True, **kwargs): + super().__init__() + self.match_values = match_values + self.first_match = first_match + + for name in kwargs: + warnings.warn( + "{} is not a valid parameter for DomainContextHandler" + .format(name), OrangeDeprecationWarning + ) + + def encode_domain(self, domain): + """ + domain: Orange.data.domain to encode + return: dict mapping attribute name to type or list of values + (based on the value of self.match_values attribute) + """ + + match = self.match_values + encode = self.encode_variables + if match == self.MATCH_VALUES_CLASS: + attributes = encode(domain.attributes, False) + attributes.update(encode(domain.class_vars, True)) + else: + attributes = encode(domain.variables, match == self.MATCH_VALUES_ALL) + + metas = encode(domain.metas, match == self.MATCH_VALUES_ALL) + + return attributes, metas + + @staticmethod + def encode_variables(attributes, encode_values): + """Encode variables to a list mapping name to variable type + or a list of values.""" + + if not encode_values: + return {v.name: vartype(v) for v in attributes} + + return {v.name: v.values if v.is_discrete else vartype(v) + for v in attributes} + + def new_context(self, domain, attributes, metas): + """Create a new context.""" + context = super().new_context() + context.attributes = attributes + context.metas = metas + return context + + def open_context(self, widget, domain): + if domain is None: + return + if not isinstance(domain, Domain): + domain = domain.domain + super().open_context(widget, domain, *self.encode_domain(domain)) + + def filter_value(self, setting, data, domain, *args): + StructuredVariableSettingMixin.filter_value(self, setting, data, *args) + + def encode_setting(self, context, setting, value): + return StructuredVariableSettingMixin.encode_setting( + self, context, setting, value) + + # backward compatibility, pylint: disable=keyword-arg-before-vararg + def decode_setting(self, setting, value, domain=None, *args): + return StructuredVariableSettingMixin.decode_setting( + self, setting, value, domain, *args) + + def match_variable(self, setting, value, attributes, metas): + if not self.is_encoded_var(value): + return False + + attr_name, attr_type = value + # attr_type used to be either 1-4 for variables stored as string + # settings, and 101-104 for variables stored as variables. The former is + # no longer supported, but we play it safe and still handle both here. + attr_type %= 100 + return (not setting.exclude_attributes and + attributes.get(attr_name, -1) == attr_type or + not setting.exclude_metas and + metas.get(attr_name, -1) == attr_type) + + def match(self, context, domain, attrs, metas): + if context.attributes == attrs and context.metas == metas: + return self.PERFECT_MATCH + return StructuredVariableSettingMixin.match(self, context, attrs, metas) + + class SimpleDomainContextHandler(DomainContextHandler): def encode_domain(self, domain): @@ -305,11 +324,10 @@ def encode_domain(self, domain): new_context = ContextHandler.new_context - @classmethod - def _var_exists(cls, setting, value, domain): + def match_variable(self, setting, value, domain): assert isinstance(setting, ContextSetting) - if not cls.is_encoded_var(value): + if not self.is_encoded_var(value): return False attr_name, attr_type = value @@ -324,13 +342,13 @@ def _var_exists(cls, setting, value, domain): or idx < 0 and setting.exclude_metas): return False - return cls.encode_variable(candidate)[1] == attr_type + return self.encode_variable(candidate)[1] == attr_type def filter_value(self, setting, data, domain): - self._filter_value(setting, data, domain) + StructuredVariableSettingMixin.filter_value(self, setting, data, domain) def match(self, context, domain): - return self._match(context, domain) + return StructuredVariableSettingMixin.match(self, context, domain) class ClassValuesContextHandler(ContextHandler): From f18722eee1af3cb2a46b068c17c42a2f801a03f2 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Mon, 5 Aug 2024 12:42:00 +0200 Subject: [PATCH 5/5] SimpleDomainContextHandler does not subclass DomainContextHandler --- Orange/widgets/settings.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/Orange/widgets/settings.py b/Orange/widgets/settings.py index fdc9066e98e..1e8bcd25ffd 100644 --- a/Orange/widgets/settings.py +++ b/Orange/widgets/settings.py @@ -317,12 +317,29 @@ def match(self, context, domain, attrs, metas): return StructuredVariableSettingMixin.match(self, context, attrs, metas) -class SimpleDomainContextHandler(DomainContextHandler): +class SimpleDomainContextHandler(ContextHandler, StructuredVariableSettingMixin, + VariableEncoderMixin): + def __init__(self): + super().__init__() + self.first_match = True - def encode_domain(self, domain): - return tuple() + def open_context(self, widget, domain): + if domain is None: + return + if not isinstance(domain, Domain): + domain = domain.domain + super().open_context(widget, domain) + + def filter_value(self, setting, data, domain, *args): + StructuredVariableSettingMixin.filter_value(self, setting, data, *args) + + def encode_setting(self, context, setting, value): + return StructuredVariableSettingMixin.encode_setting( + self, context, setting, value) - new_context = ContextHandler.new_context + def decode_setting(self, setting, value, domain, *args): + return StructuredVariableSettingMixin.decode_setting( + self, setting, value, domain, *args) def match_variable(self, setting, value, domain): assert isinstance(setting, ContextSetting)