예제 #1
0
class OWEditDomain(widget.OWWidget):
    name = "Edit Domain"
    description = "Rename features and their values."
    icon = "icons/EditDomain.svg"
    priority = 3125

    inputs = [("Data", Orange.data.Table, "set_data")]
    outputs = [("Data", Orange.data.Table)]

    settingsHandler = settings.DomainContextHandler()

    domain_change_hints = settings.ContextSetting({})
    selected_index = settings.ContextSetting({})

    autocommit = settings.Setting(False)

    def __init__(self):
        super().__init__()

        self.data = None
        self.input_vars = ()
        self._invalidated = False

        box = gui.vBox(self.controlArea, "Domain Features")

        self.domain_model = itemmodels.VariableListModel()
        self.domain_view = QListView(
            selectionMode=QListView.SingleSelection
        )
        self.domain_view.setModel(self.domain_model)
        self.domain_view.selectionModel().selectionChanged.connect(
            self._on_selection_changed)
        box.layout().addWidget(self.domain_view)

        box = gui.hBox(self.controlArea)
        gui.button(box, self, "Reset Selected", callback=self.reset_selected)
        gui.button(box, self, "Reset All", callback=self.reset_all)

        gui.auto_commit(self.controlArea, self, "autocommit", "Apply")

        box = gui.vBox(self.mainArea, "Edit")
        self.editor_stack = QStackedWidget()

        self.editor_stack.addWidget(DiscreteVariableEditor())
        self.editor_stack.addWidget(ContinuousVariableEditor())
        self.editor_stack.addWidget(VariableEditor())

        box.layout().addWidget(self.editor_stack)

    @check_sql_input
    def set_data(self, data):
        """Set input data set."""
        self.closeContext()
        self.clear()
        self.data = data

        if self.data is not None:
            self._initialize()
            self.openContext(self.data)
            self._restore()

        self.unconditional_commit()

    def clear(self):
        """Clear the widget state."""
        self.data = None
        self.domain_model[:] = []
        self.input_vars = []
        self.domain_change_hints = {}
        self.selected_index = -1

    def reset_selected(self):
        """Reset the currently selected variable to its original state."""
        ind = self.selected_var_index()
        if ind >= 0:
            var = self.input_vars[ind]
            desc = variable_description(var, skip_attributes=True)
            if desc in self.domain_change_hints:
                del self.domain_change_hints[desc]

            self.domain_model[ind] = var
            self.editor_stack.currentWidget().set_data(var)
            self._invalidate()

    def reset_all(self):
        """Reset all variables to their original state."""
        self.domain_change_hints = {}
        if self.data is not None:
            # To invalidate stored hints
            self.domain_model[:] = self.input_vars
            itemmodels.select_row(self.domain_view, self.selected_index)
            self._invalidate()

    def selected_var_index(self):
        """Return the selected row in 'Domain Features' view."""
        rows = self.domain_view.selectedIndexes()
        assert len(rows) <= 1
        return rows[0].row() if rows else -1

    def _initialize(self):
        domain = self.data.domain
        self.input_vars = tuple(domain) + domain.metas
        self.domain_model[:] = list(self.input_vars)

    def _restore(self):
        # Restore the variable states from saved settings.
        def transform(var):
            vdesc = variable_description(var, skip_attributes=True)
            if vdesc in self.domain_change_hints:
                return variable_from_description(
                    self.domain_change_hints[vdesc],
                    compute_value=Orange.preprocess.transformation.Identity(var))
            else:
                return var

        self.domain_model[:] = map(transform, self.input_vars)

        # Restore the variable selection if possible
        index = self.selected_index
        if index >= len(self.input_vars):
            index = 0 if len(self.input_vars) else -1
        if index >= 0:
            itemmodels.select_row(self.domain_view, index)

    def _on_selection_changed(self):
        self.selected_index = self.selected_var_index()
        self.open_editor(self.selected_index)

    def open_editor(self, index):
        self.clear_editor()
        if index < 0:
            return

        var = self.domain_model[index]

        editor_index = 2
        if var.is_discrete:
            editor_index = 0
        elif var.is_continuous:
            editor_index = 1
        editor = self.editor_stack.widget(editor_index)
        self.editor_stack.setCurrentWidget(editor)

        editor.set_data(var)
        editor.variable_changed.connect(self._on_variable_changed)

    def clear_editor(self):
        current = self.editor_stack.currentWidget()
        try:
            current.variable_changed.disconnect(self._on_variable_changed)
        except Exception:
            pass
        current.set_data(None)

    def _on_variable_changed(self):
        """User edited the current variable in editor."""
        assert 0 <= self.selected_index <= len(self.domain_model)
        editor = self.editor_stack.currentWidget()

        # Replace the variable in the 'Domain Features' view/model
        old_var = self.input_vars[self.selected_index]
        new_var = editor.get_data().copy(compute_value=Orange.preprocess.transformation.Identity(old_var))
        self.domain_model[self.selected_index] = new_var


        # Store the transformation hint.
        old_var_desc = variable_description(old_var, skip_attributes=True)
        self.domain_change_hints[old_var_desc] = variable_description(new_var)

        self._invalidate()

    def _invalidate(self):
        self.commit()

    def commit(self):
        """Send the changed data to output."""
        new_data = None
        if self.data is not None:
            input_domain = self.data.domain
            n_attrs = len(input_domain.attributes)
            n_vars = len(input_domain.variables)
            n_class_vars = len(input_domain.class_vars)
            all_new_vars = list(self.domain_model)
            attrs = all_new_vars[: n_attrs]
            class_vars = all_new_vars[n_attrs: n_attrs + n_class_vars]
            new_metas = all_new_vars[n_attrs + n_class_vars:]
            new_domain = Orange.data.Domain(attrs, class_vars, new_metas)
            new_data = self.data.from_table(new_domain, self.data)

        self.send("Data", new_data)

    def sizeHint(self):
        sh = super().sizeHint()
        return sh.expandedTo(QSize(660, 550))

    def send_report(self):
        if self.data is not None:
            self.report_raw("", EditDomainReport(
                old_domain=chain(self.data.domain.variables, self.data.domain.metas),
                new_domain=self.domain_model).to_html())
        else:
            self.report_data(None)
예제 #2
0
class OWEditDomain(widget.OWWidget):
    name = "Edit Domain"
    description = "Rename variables, edit categories and variable annotations."
    icon = "icons/EditDomain.svg"
    priority = 3125
    keywords = []

    class Inputs:
        data = Input("Data", Orange.data.Table)

    class Outputs:
        data = Output("Data", Orange.data.Table)

    class Error(widget.OWWidget.Error):
        duplicate_var_name = widget.Msg("A variable name is duplicated.")

    settingsHandler = settings.DomainContextHandler()
    settings_version = 2

    _domain_change_store = settings.ContextSetting({})
    _selected_item = settings.ContextSetting(None)  # type: Optional[str]

    want_control_area = False

    def __init__(self):
        super().__init__()
        self.data = None  # type: Optional[Orange.data.Table]
        #: The current selected variable index
        self.selected_index = -1
        self._invalidated = False

        mainlayout = self.mainArea.layout()
        assert isinstance(mainlayout, QVBoxLayout)
        layout = QHBoxLayout()
        mainlayout.addLayout(layout)
        box = QGroupBox("Variables")
        box.setLayout(QVBoxLayout())
        layout.addWidget(box)

        self.variables_model = VariableListModel(parent=self)
        self.variables_view = self.domain_view = QListView(
            selectionMode=QListView.SingleSelection,
            uniformItemSizes=True,
        )
        self.variables_view.setItemDelegate(VariableEditDelegate(self))
        self.variables_view.setModel(self.variables_model)
        self.variables_view.selectionModel().selectionChanged.connect(
            self._on_selection_changed)
        box.layout().addWidget(self.variables_view)

        box = QGroupBox("Edit", )
        box.setLayout(QVBoxLayout(margin=4))
        layout.addWidget(box)

        self.editor_stack = QStackedWidget()

        self.editor_stack.addWidget(DiscreteVariableEditor())
        self.editor_stack.addWidget(ContinuousVariableEditor())
        self.editor_stack.addWidget(TimeVariableEditor())
        self.editor_stack.addWidget(VariableEditor())

        box.layout().addWidget(self.editor_stack)

        bbox = QDialogButtonBox()
        bbox.setStyleSheet("button-layout: {:d};".format(
            QDialogButtonBox.MacLayout))
        bapply = QPushButton(
            "Apply",
            objectName="button-apply",
            toolTip="Apply changes and commit data on output.",
            default=True,
            autoDefault=False)
        bapply.clicked.connect(self.commit)
        breset = QPushButton(
            "Reset Selected",
            objectName="button-reset",
            toolTip="Rest selected variable to its input state.",
            autoDefault=False)
        breset.clicked.connect(self.reset_selected)
        breset_all = QPushButton(
            "Reset All",
            objectName="button-reset-all",
            toolTip="Reset all variables to their input state.",
            autoDefault=False)
        breset_all.clicked.connect(self.reset_all)

        bbox.addButton(bapply, QDialogButtonBox.AcceptRole)
        bbox.addButton(breset, QDialogButtonBox.ResetRole)
        bbox.addButton(breset_all, QDialogButtonBox.ResetRole)

        mainlayout.addWidget(bbox)
        self.variables_view.setFocus(Qt.NoFocusReason)  # initial focus

    @Inputs.data
    def set_data(self, data):
        """Set input dataset."""
        self.closeContext()
        self.clear()
        self.data = data

        if self.data is not None:
            self.set_domain(data.domain)
            self.openContext(self.data)
            self._restore()

        self.commit()

    def clear(self):
        """Clear the widget state."""
        self.data = None
        self.variables_model.clear()
        assert self.selected_index == -1
        self.selected_index = -1

        self._selected_item = None
        self._domain_change_store = {}

    def reset_selected(self):
        """Reset the currently selected variable to its original state."""
        ind = self.selected_var_index()
        if ind >= 0:
            model = self.variables_model
            midx = model.index(ind)
            var = midx.data(Qt.EditRole)
            tr = midx.data(TransformRole)
            if not tr:
                return  # nothing to reset
            editor = self.editor_stack.currentWidget()
            with disconnected(editor.variable_changed,
                              self._on_variable_changed):
                model.setData(midx, [], TransformRole)
                editor.set_data(var, [])
            self._invalidate()

    def reset_all(self):
        """Reset all variables to their original state."""
        self._domain_change_store = {}
        if self.data is not None:
            model = self.variables_model
            for i in range(model.rowCount()):
                midx = model.index(i)
                model.setData(midx, [], TransformRole)
            index = self.selected_var_index()
            if index >= 0:
                self.open_editor(index)
            self._invalidate()

    def selected_var_index(self):
        """Return the current selected variable index."""
        rows = self.variables_view.selectedIndexes()
        assert len(rows) <= 1
        return rows[0].row() if rows else -1

    def set_domain(self, domain):
        # type: (Orange.data.Domain) -> None
        self.variables_model[:] = [
            abstract(v) for v in domain.variables + domain.metas
        ]

    def _restore(self, ):
        """
        Restore the edit transform from saved state.
        """
        model = self.variables_model
        for i in range(model.rowCount()):
            midx = model.index(i, 0)
            var = model.data(midx, Qt.EditRole)
            tr = self._restore_transform(var)
            if tr:
                model.setData(midx, tr, TransformRole)

        # Restore the current variable selection
        i = -1
        if self._selected_item is not None:
            for i, var in enumerate(model):
                if var.name == self._selected_item:
                    break
        if i == -1 and model.rowCount():
            i = 0

        if i != -1:
            itemmodels.select_row(self.variables_view, i)

    def _on_selection_changed(self):
        self.selected_index = self.selected_var_index()
        if self.selected_index != -1:
            self._selected_item = self.variables_model[
                self.selected_index].name
        else:
            self._selected_item = None
        self.open_editor(self.selected_index)

    def open_editor(self, index):
        # type: (int) -> None
        self.clear_editor()
        model = self.variables_model
        if not 0 <= index < model.rowCount():
            return
        idx = model.index(index, 0)
        var = model.data(idx, Qt.EditRole)
        tr = model.data(idx, TransformRole)
        if tr is None:
            tr = []

        editors = {Categorical: 0, Real: 1, Time: 2, String: 3}

        editor_index = editors.get(type(var), 3)
        editor = self.editor_stack.widget(editor_index)
        self.editor_stack.setCurrentWidget(editor)
        editor.set_data(var, tr)
        editor.variable_changed.connect(self._on_variable_changed,
                                        Qt.UniqueConnection)

    def clear_editor(self):
        current = self.editor_stack.currentWidget()
        try:
            current.variable_changed.disconnect(self._on_variable_changed)
        except TypeError:
            pass
        current.set_data(None)

    @Slot()
    def _on_variable_changed(self):
        """User edited the current variable in editor."""
        assert 0 <= self.selected_index <= len(self.variables_model)
        editor = self.editor_stack.currentWidget()
        var, transform = editor.get_data()
        model = self.variables_model
        midx = model.index(self.selected_index, 0)
        model.setData(midx, transform, TransformRole)
        self._store_transform(var, transform)
        self._invalidate()

    def _store_transform(self, var, transform):
        # type: (Variable, List[Transform]) -> None
        self._domain_change_store[deconstruct(var)] = [
            deconstruct(t) for t in transform
        ]

    def _restore_transform(self, var):
        # type: (Variable) -> List[Transform]
        tr_ = self._domain_change_store.get(deconstruct(var), [])
        tr = []

        for t in tr_:
            try:
                tr.append(reconstruct(*t))
            except (NameError, TypeError) as err:
                warnings.warn("Failed to restore transform: {}, {!r}".format(
                    t, err),
                              UserWarning,
                              stacklevel=2)
        return tr

    def _invalidate(self):
        self._set_modified(True)

    def _set_modified(self, state):
        self._invalidated = state
        b = self.findChild(QPushButton, "button-apply")
        if isinstance(b, QPushButton):
            f = b.font()
            f.setItalic(state)
            b.setFont(f)

    def commit(self):
        """
        Apply the changes to the input data and send the changed data to output.
        """
        self._set_modified(False)
        self.Error.duplicate_var_name.clear()

        data = self.data
        if data is None:
            self.Outputs.data.send(None)
            return
        model = self.variables_model

        def state(i):
            # type: (int) -> Tuple[Variable, List[Transform]]
            midx = self.variables_model.index(i, 0)
            return (model.data(midx,
                               Qt.EditRole), model.data(midx, TransformRole))

        state = [state(i) for i in range(model.rowCount())]
        if all(tr is None or not tr for _, tr in state):
            self.Outputs.data.send(data)
            return

        output_vars = []
        input_vars = data.domain.variables + data.domain.metas
        assert all(v_.name == v.name for v, (v_, _) in zip(input_vars, state))
        for (_, tr), v in zip(state, input_vars):
            if tr is not None and len(tr) > 0:
                var = apply_transform(v, tr)
            else:
                var = v
            output_vars.append(var)

        if len(output_vars) != len({v.name for v in output_vars}):
            self.Error.duplicate_var_name()
            self.Outputs.data.send(None)
            return

        domain = data.domain
        nx = len(domain.attributes)
        ny = len(domain.class_vars)
        domain = Orange.data.Domain(output_vars[:nx], output_vars[nx:nx + ny],
                                    output_vars[nx + ny:])
        new_data = data.transform(domain)
        # print(new_data)
        self.Outputs.data.send(new_data)

    def sizeHint(self):
        sh = super().sizeHint()
        return sh.expandedTo(QSize(660, 550))

    def send_report(self):

        if self.data is not None:
            model = self.variables_model
            state = ((model.data(midx,
                                 Qt.EditRole), model.data(midx, TransformRole))
                     for i in range(model.rowCount())
                     for midx in [model.index(i)])
            parts = []
            for var, trs in state:
                if trs:
                    parts.append(report_transform(var, trs))
            if parts:
                html = ("<ul>" + "".join(map("<li>{}</li>".format, parts)) +
                        "</ul>")
            else:
                html = "No changes"
            self.report_raw("", html)
        else:
            self.report_data(None)

    @classmethod
    def migrate_context(cls, context, version):
        # pylint: disable=bad-continuation
        if version is None or version <= 1:
            hints_ = context.values.get("domain_change_hints", ({}, -2))[0]
            store = []
            ns = "Orange.data.variable"
            mapping = {
                "DiscreteVariable":
                lambda name, args, attrs:
                ("Categorical", (name, tuple(args[0][1]), None, ())),
                "TimeVariable":
                lambda name, _, attrs: ("Time", (name, ())),
                "ContinuousVariable":
                lambda name, _, attrs: ("Real", (name, (3, "f"), ())),
                "StringVariable":
                lambda name, _, attrs: ("String", (name, ())),
            }
            for (module, class_name, *rest), target in hints_.items():
                if module != ns:
                    continue
                f = mapping.get(class_name)
                if f is None:
                    continue
                trs = []
                key_mapped = f(*rest)
                item_mapped = f(*target[2:])
                src = reconstruct(*key_mapped)  # type: Variable
                dst = reconstruct(*item_mapped)  # type: Variable
                if src.name != dst.name:
                    trs.append(Rename(dst.name))
                if src.annotations != dst.annotations:
                    trs.append(Annotate(dst.annotations))
                if isinstance(src, Categorical):
                    if src.categories != dst.categories:
                        assert len(src.categories) == len(dst.categories)
                        trs.append(
                            CategoriesMapping(
                                list(zip(src.categories, dst.categories))))
                store.append(
                    (deconstruct(src), [deconstruct(tr) for tr in trs]))
            context.values["_domain_change_store"] = (dict(store), -2)
class OWEditDomain(widget.OWWidget):
    name = "Edit Domain"
    description = "Rename features and their values."
    icon = "icons/EditDomain.svg"
    priority = 3125

    class Inputs:
        data = Input("Data", Orange.data.Table)

    class Outputs:
        data = Output("Data", Orange.data.Table)

    settingsHandler = settings.DomainContextHandler()

    domain_change_hints = settings.ContextSetting({})
    selected_index = settings.ContextSetting({})

    autocommit = settings.Setting(True)

    def __init__(self):
        super().__init__()

        self.data = None
        self.input_vars = ()
        self._invalidated = False

        box = gui.vBox(self.controlArea, "Domain Features")

        self.domain_model = itemmodels.VariableListModel()
        self.domain_view = QListView(selectionMode=QListView.SingleSelection,
                                     uniformItemSizes=True)
        self.domain_view.setModel(self.domain_model)
        self.domain_view.selectionModel().selectionChanged.connect(
            self._on_selection_changed)
        box.layout().addWidget(self.domain_view)

        box = gui.hBox(self.controlArea)
        gui.button(box, self, "Reset Selected", callback=self.reset_selected)
        gui.button(box, self, "Reset All", callback=self.reset_all)

        gui.auto_commit(self.controlArea, self, "autocommit", "Apply")

        box = gui.vBox(self.mainArea, "Edit")
        self.editor_stack = QStackedWidget()

        self.editor_stack.addWidget(DiscreteVariableEditor())
        self.editor_stack.addWidget(ContinuousVariableEditor())
        self.editor_stack.addWidget(VariableEditor())

        box.layout().addWidget(self.editor_stack)

        self.Error.add_message("duplicate_var_name",
                               "A variable name is duplicated.")

    @Inputs.data
    @check_sql_input
    def set_data(self, data):
        """Set input dataset."""
        self.closeContext()
        self.clear()
        self.data = data

        if self.data is not None:
            self._initialize()
            self.openContext(self.data)
            self._restore()

        self.unconditional_commit()

    def clear(self):
        """Clear the widget state."""
        self.data = None
        self.domain_model[:] = []
        self.input_vars = []
        self.domain_change_hints = {}
        self.selected_index = -1

    def reset_selected(self):
        """Reset the currently selected variable to its original state."""
        ind = self.selected_var_index()
        if ind >= 0:
            var = self.input_vars[ind]
            desc = variable_description(var, skip_attributes=True)
            if desc in self.domain_change_hints:
                del self.domain_change_hints[desc]

            self.domain_model[ind] = var
            self.editor_stack.currentWidget().set_data(var)
            self._invalidate()

    def reset_all(self):
        """Reset all variables to their original state."""
        self.domain_change_hints = {}
        if self.data is not None:
            # To invalidate stored hints
            self.domain_model[:] = self.input_vars
            itemmodels.select_row(self.domain_view, self.selected_index)
            self._invalidate()

    def selected_var_index(self):
        """Return the selected row in 'Domain Features' view."""
        rows = self.domain_view.selectedIndexes()
        assert len(rows) <= 1
        return rows[0].row() if rows else -1

    def _initialize(self):
        domain = self.data.domain
        self.input_vars = domain.variables + domain.metas
        self.domain_model[:] = list(self.input_vars)

    def _restore(self):
        # Restore the variable states from saved settings.
        def transform(var):
            vdesc = variable_description(var, skip_attributes=True)
            if vdesc in self.domain_change_hints:
                return variable_from_description(
                    self.domain_change_hints[vdesc],
                    compute_value=Orange.preprocess.transformation.Identity(
                        var),
                )
            else:
                return var

        self.domain_model[:] = map(transform, self.input_vars)

        # Restore the variable selection if possible
        index = self.selected_index
        if index >= len(self.input_vars):
            index = 0 if len(self.input_vars) else -1
        if index >= 0:
            itemmodels.select_row(self.domain_view, index)

    def _on_selection_changed(self):
        self.selected_index = self.selected_var_index()
        self.open_editor(self.selected_index)

    def open_editor(self, index):
        self.clear_editor()
        if index < 0:
            return

        var = self.domain_model[index]

        editor_index = 2
        if var.is_discrete:
            editor_index = 0
        elif var.is_continuous:
            editor_index = 1
        editor = self.editor_stack.widget(editor_index)
        self.editor_stack.setCurrentWidget(editor)

        editor.set_data(var)
        editor.variable_changed.connect(self._on_variable_changed)

    def clear_editor(self):
        current = self.editor_stack.currentWidget()
        try:
            current.variable_changed.disconnect(self._on_variable_changed)
        except Exception:
            pass
        current.set_data(None)

    def _on_variable_changed(self):
        """User edited the current variable in editor."""
        assert 0 <= self.selected_index <= len(self.domain_model)
        editor = self.editor_stack.currentWidget()

        # Replace the variable in the 'Domain Features' view/model
        old_var = self.input_vars[self.selected_index]
        new_var = editor.get_data().copy(
            compute_value=Orange.preprocess.transformation.Identity(old_var))
        self.domain_model[self.selected_index] = new_var

        # Store the transformation hint.
        old_var_desc = variable_description(old_var, skip_attributes=True)
        self.domain_change_hints[old_var_desc] = variable_description(new_var)

        self._invalidate()

    def _invalidate(self):
        self.commit()

    def commit(self):
        """Send the changed data to output."""
        new_data = None
        var_names = [vn.name for vn in self.domain_model]
        self.Error.duplicate_var_name.clear()
        if self.data is not None:
            if len(var_names) == len(set(var_names)):
                input_domain = self.data.domain
                n_attrs = len(input_domain.attributes)
                n_class_vars = len(input_domain.class_vars)
                all_new_vars = list(self.domain_model)
                attrs = all_new_vars[:n_attrs]
                class_vars = all_new_vars[n_attrs:n_attrs + n_class_vars]
                new_metas = all_new_vars[n_attrs + n_class_vars:]
                new_domain = Orange.data.Domain(attrs, class_vars, new_metas)
                new_data = self.data.transform(new_domain)
            else:
                self.Error.duplicate_var_name()

        self.Outputs.data.send(new_data)

    def sizeHint(self):
        sh = super().sizeHint()
        return sh.expandedTo(QSize(660, 550))

    def send_report(self):
        if self.data is not None:
            self.report_raw(
                "",
                EditDomainReport(
                    old_domain=chain(self.data.domain.variables,
                                     self.data.domain.metas),
                    new_domain=self.domain_model,
                ).to_html(),
            )
        else:
            self.report_data(None)
예제 #4
0
class OWEditDomain(widget.OWWidget):
    name = "Edit Domain"
    description = "Rename variables, edit categories and variable annotations."
    icon = "icons/EditDomain.svg"
    priority = 3125
    keywords = []

    class Inputs:
        data = Input("Data", Orange.data.Table)

    class Outputs:
        data = Output("Data", Orange.data.Table)

    class Error(widget.OWWidget.Error):
        duplicate_var_name = widget.Msg("A variable name is duplicated.")

    settingsHandler = settings.DomainContextHandler()
    settings_version = 2

    _domain_change_store = settings.ContextSetting({})
    _selected_item = settings.ContextSetting(None)  # type: Optional[str]

    want_control_area = False

    def __init__(self):
        super().__init__()
        self.data = None  # type: Optional[Orange.data.Table]
        #: The current selected variable index
        self.selected_index = -1
        self._invalidated = False

        mainlayout = self.mainArea.layout()
        assert isinstance(mainlayout, QVBoxLayout)
        layout = QHBoxLayout()
        mainlayout.addLayout(layout)
        box = QGroupBox("Variables")
        box.setLayout(QVBoxLayout())
        layout.addWidget(box)

        self.variables_model = VariableListModel(parent=self)
        self.variables_view = self.domain_view = QListView(
            selectionMode=QListView.SingleSelection,
            uniformItemSizes=True,
        )
        self.variables_view.setItemDelegate(VariableEditDelegate(self))
        self.variables_view.setModel(self.variables_model)
        self.variables_view.selectionModel().selectionChanged.connect(
            self._on_selection_changed
        )
        box.layout().addWidget(self.variables_view)

        box = QGroupBox("Edit", )
        box.setLayout(QVBoxLayout(margin=4))
        layout.addWidget(box)

        self.editor_stack = QStackedWidget()

        self.editor_stack.addWidget(DiscreteVariableEditor())
        self.editor_stack.addWidget(ContinuousVariableEditor())
        self.editor_stack.addWidget(TimeVariableEditor())
        self.editor_stack.addWidget(VariableEditor())

        box.layout().addWidget(self.editor_stack)

        bbox = QDialogButtonBox()
        bbox.setStyleSheet(
            "button-layout: {:d};".format(QDialogButtonBox.MacLayout))
        bapply = QPushButton(
            "Apply",
            objectName="button-apply",
            toolTip="Apply changes and commit data on output.",
            default=True,
            autoDefault=False
        )
        bapply.clicked.connect(self.commit)
        breset = QPushButton(
            "Reset Selected",
            objectName="button-reset",
            toolTip="Rest selected variable to its input state.",
            autoDefault=False
        )
        breset.clicked.connect(self.reset_selected)
        breset_all = QPushButton(
            "Reset All",
            objectName="button-reset-all",
            toolTip="Reset all variables to their input state.",
            autoDefault=False
        )
        breset_all.clicked.connect(self.reset_all)

        bbox.addButton(bapply, QDialogButtonBox.AcceptRole)
        bbox.addButton(breset, QDialogButtonBox.ResetRole)
        bbox.addButton(breset_all, QDialogButtonBox.ResetRole)

        mainlayout.addWidget(bbox)
        self.variables_view.setFocus(Qt.NoFocusReason)  # initial focus

    @Inputs.data
    def set_data(self, data):
        """Set input dataset."""
        self.closeContext()
        self.clear()
        self.data = data

        if self.data is not None:
            self.set_domain(data.domain)
            self.openContext(self.data)
            self._restore()

        self.commit()

    def clear(self):
        """Clear the widget state."""
        self.data = None
        self.variables_model.clear()
        assert self.selected_index == -1
        self.selected_index = -1

        self._selected_item = None
        self._domain_change_store = {}

    def reset_selected(self):
        """Reset the currently selected variable to its original state."""
        ind = self.selected_var_index()
        if ind >= 0:
            model = self.variables_model
            midx = model.index(ind)
            var = midx.data(Qt.EditRole)
            tr = midx.data(TransformRole)
            if not tr:
                return  # nothing to reset
            editor = self.editor_stack.currentWidget()
            with disconnected(editor.variable_changed,
                              self._on_variable_changed):
                model.setData(midx, [], TransformRole)
                editor.set_data(var, [])
            self._invalidate()

    def reset_all(self):
        """Reset all variables to their original state."""
        self._domain_change_store = {}
        if self.data is not None:
            model = self.variables_model
            for i in range(model.rowCount()):
                midx = model.index(i)
                model.setData(midx, [], TransformRole)
            index = self.selected_var_index()
            if index >= 0:
                self.open_editor(index)
            self._invalidate()

    def selected_var_index(self):
        """Return the current selected variable index."""
        rows = self.variables_view.selectedIndexes()
        assert len(rows) <= 1
        return rows[0].row() if rows else -1

    def set_domain(self, domain):
        # type: (Orange.data.Domain) -> None
        self.variables_model[:] = [abstract(v)
                                   for v in domain.variables + domain.metas]

    def _restore(self, ):
        """
        Restore the edit transform from saved state.
        """
        model = self.variables_model
        for i in range(model.rowCount()):
            midx = model.index(i, 0)
            var = model.data(midx, Qt.EditRole)
            tr = self._restore_transform(var)
            if tr:
                model.setData(midx, tr, TransformRole)

        # Restore the current variable selection
        i = -1
        if self._selected_item is not None:
            for i, var in enumerate(model):
                if var.name == self._selected_item:
                    break
        if i == -1 and model.rowCount():
            i = 0

        if i != -1:
            itemmodels.select_row(self.variables_view, i)

    def _on_selection_changed(self):
        self.selected_index = self.selected_var_index()
        if self.selected_index != -1:
            self._selected_item = self.variables_model[self.selected_index].name
        else:
            self._selected_item = None
        self.open_editor(self.selected_index)

    def open_editor(self, index):
        # type: (int) -> None
        self.clear_editor()
        model = self.variables_model
        if not 0 <= index < model.rowCount():
            return
        idx = model.index(index, 0)
        var = model.data(idx, Qt.EditRole)
        tr = model.data(idx, TransformRole)
        if tr is None:
            tr = []

        editors = {
            Categorical: 0,
            Real: 1,
            Time: 2,
            String: 3
        }

        editor_index = editors.get(type(var), 3)
        editor = self.editor_stack.widget(editor_index)
        self.editor_stack.setCurrentWidget(editor)
        editor.set_data(var, tr)
        editor.variable_changed.connect(
            self._on_variable_changed, Qt.UniqueConnection
        )

    def clear_editor(self):
        current = self.editor_stack.currentWidget()
        try:
            current.variable_changed.disconnect(self._on_variable_changed)
        except TypeError:
            pass
        current.set_data(None)

    @Slot()
    def _on_variable_changed(self):
        """User edited the current variable in editor."""
        assert 0 <= self.selected_index <= len(self.variables_model)
        editor = self.editor_stack.currentWidget()
        var, transform = editor.get_data()
        model = self.variables_model
        midx = model.index(self.selected_index, 0)
        model.setData(midx, transform, TransformRole)
        self._store_transform(var, transform)
        self._invalidate()

    def _store_transform(self, var, transform):
        # type: (Variable, List[Transform]) -> None
        self._domain_change_store[deconstruct(var)] = [deconstruct(t) for t in transform]

    def _restore_transform(self, var):
        # type: (Variable) -> List[Transform]
        tr_ = self._domain_change_store.get(deconstruct(var), [])
        tr = []

        for t in tr_:
            try:
                tr.append(reconstruct(*t))
            except (NameError, TypeError) as err:
                warnings.warn(
                    "Failed to restore transform: {}, {!r}".format(t, err),
                    UserWarning, stacklevel=2
                )
        return tr

    def _invalidate(self):
        self._set_modified(True)

    def _set_modified(self, state):
        self._invalidated = state
        b = self.findChild(QPushButton, "button-apply")
        if isinstance(b, QPushButton):
            f = b.font()
            f.setItalic(state)
            b.setFont(f)

    def commit(self):
        """
        Apply the changes to the input data and send the changed data to output.
        """
        self._set_modified(False)
        self.Error.duplicate_var_name.clear()

        data = self.data
        if data is None:
            self.Outputs.data.send(None)
            return
        model = self.variables_model

        def state(i):
            # type: (int) -> Tuple[Variable, List[Transform]]
            midx = self.variables_model.index(i, 0)
            return (model.data(midx, Qt.EditRole),
                    model.data(midx, TransformRole))

        state = [state(i) for i in range(model.rowCount())]
        if all(tr is None or not tr for _, tr in state):
            self.Outputs.data.send(data)
            return

        output_vars = []
        input_vars = data.domain.variables + data.domain.metas
        assert all(v_.name == v.name
                   for v, (v_, _) in zip(input_vars, state))
        for (_, tr), v in zip(state, input_vars):
            if tr is not None and len(tr) > 0:
                var = apply_transform(v, tr)
            else:
                var = v
            output_vars.append(var)

        if len(output_vars) != len({v.name for v in output_vars}):
            self.Error.duplicate_var_name()
            self.Outputs.data.send(None)
            return

        domain = data.domain
        nx = len(domain.attributes)
        ny = len(domain.class_vars)
        domain = Orange.data.Domain(
            output_vars[:nx], output_vars[nx: nx + ny], output_vars[nx + ny:]
        )
        new_data = data.transform(domain)
        # print(new_data)
        self.Outputs.data.send(new_data)

    def sizeHint(self):
        sh = super().sizeHint()
        return sh.expandedTo(QSize(660, 550))

    def send_report(self):

        if self.data is not None:
            model = self.variables_model
            state = ((model.data(midx, Qt.EditRole),
                      model.data(midx, TransformRole))
                     for i in range(model.rowCount())
                     for midx in [model.index(i)])
            parts = []
            for var, trs in state:
                if trs:
                    parts.append(report_transform(var, trs))
            if parts:
                html = ("<ul>" +
                        "".join(map("<li>{}</li>".format, parts)) +
                        "</ul>")
            else:
                html = "No changes"
            self.report_raw("", html)
        else:
            self.report_data(None)

    @classmethod
    def migrate_context(cls, context, version):
        # pylint: disable=bad-continuation
        if version is None or version <= 1:
            hints_ = context.values.get("domain_change_hints", ({}, -2))[0]
            store = []
            ns = "Orange.data.variable"
            mapping = {
                "DiscreteVariable":
                    lambda name, args, attrs:
                        ("Categorical", (name, tuple(args[0][1]), None, ())),
                "TimeVariable":
                    lambda name, _, attrs:
                        ("Time", (name, ())),
                "ContinuousVariable":
                    lambda name, _, attrs:
                        ("Real", (name, (3, "f"), ())),
                "StringVariable":
                    lambda name, _, attrs:
                        ("String", (name, ())),
            }
            for (module, class_name, *rest), target in hints_.items():
                if module != ns:
                    continue
                f = mapping.get(class_name)
                if f is None:
                    continue
                trs = []
                key_mapped = f(*rest)
                item_mapped = f(*target[2:])
                src = reconstruct(*key_mapped)   # type: Variable
                dst = reconstruct(*item_mapped)  # type: Variable
                if src.name != dst.name:
                    trs.append(Rename(dst.name))
                if src.annotations != dst.annotations:
                    trs.append(Annotate(dst.annotations))
                if isinstance(src, Categorical):
                    if src.categories != dst.categories:
                        assert len(src.categories) == len(dst.categories)
                        trs.append(CategoriesMapping(
                            list(zip(src.categories, dst.categories))))
                store.append((deconstruct(src), [deconstruct(tr) for tr in trs]))
            context.values["_domain_change_store"] = (dict(store), -2)