From 64dce721146c931ffe670d30f3644129f8f757a2 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Wed, 13 Jun 2018 15:44:01 +0200 Subject: [PATCH] canvas/readwrite: Serialize window group presets to workflow file --- Orange/canvas/scheme/readwrite.py | 186 ++++++++++++++++++++++-------- 1 file changed, 137 insertions(+), 49 deletions(-) diff --git a/Orange/canvas/scheme/readwrite.py b/Orange/canvas/scheme/readwrite.py index fd1cd7e4625..f2ea0d1142b 100644 --- a/Orange/canvas/scheme/readwrite.py +++ b/Orange/canvas/scheme/readwrite.py @@ -2,13 +2,14 @@ Scheme save/load routines. """ -import base64 import sys import warnings +import base64 +import binascii from xml.etree.ElementTree import TreeBuilder, Element, ElementTree, parse -from collections import defaultdict, namedtuple +from collections import defaultdict from itertools import chain, count import pickle as pickle @@ -20,6 +21,8 @@ import logging +from typing import NamedTuple, List, Dict, Tuple, Union + from . import SchemeNode, SchemeLink from .annotations import SchemeTextAnnotation, SchemeArrowAnnotation from .errors import IncompatibleChannelTypeError @@ -316,7 +319,7 @@ def parse_scheme_v_2_0(etree, scheme, error_handler, widget_registry=None, def parse_scheme_v_1_0(etree, scheme, error_handler, widget_registry=None, allow_pickle_data=False): """ - ElementTree Instance of an old .ows scheme format. + ElementTree Instance of an old .ows scheme format (Orange < 2.7). """ if widget_registry is None: widget_registry = global_registry() @@ -397,38 +400,86 @@ def parse_scheme_v_1_0(etree, scheme, error_handler, widget_registry=None, # Intermediate scheme representation -_scheme = namedtuple( - "_scheme", - ["title", "version", "description", "nodes", "links", "annotations"]) - -_node = namedtuple( - "_node", - ["id", "title", "name", "position", "project_name", "qualified_name", - "version", "data"]) - -_data = namedtuple( - "_data", - ["format", "data"]) +_scheme = NamedTuple( + "_scheme", [ + ("title", str), + ("version", str), + ("description", str), + ("nodes", 'List[_node]'), + ("links", 'List[_link]'), + ("annotations", 'List[_annotation]'), + ("session_state", '_session_data') + ] +) + +_node = NamedTuple( + "_node", [ + ("id", str), + ("title", str), + ("name", str), + ("position", 'Tuple[float, float]'), + ("project_name", str), + ("qualified_name", str), + ("version", str), + ("data", '_data') + ] +) + +_data = NamedTuple( + "_data", [ + ("format", str), + ("data", bytes) + ] +) + +_link = NamedTuple( + "_link", [ + ("id", str), + ("source_node_id", str), + ("sink_node_id", str), + ("source_channel", str), + ("sink_channel", str), + ("enabled", bool), + ] +) + +_annotation = NamedTuple( + "_annotation", [ + ("id", str), + ("type", str), + ("params", 'Union[_text_params, _arrow_params]'), + ] +) + +_text_params = NamedTuple( + "_text_params", [ + ("geometry", str), + ("text", str), + ("font", 'Dict[str, Any]'), + ("content_type", str), + ] +) + +_arrow_params = NamedTuple( + "_arrow_params", [ + ("geometry", str), + ("color", str), + ]) + + +_session_data = NamedTuple( + "_session_data", [ + ("groups", 'List[Tuple[str, List[Tuple[str, bytes]]]]') + ] +) -_link = namedtuple( - "_link", - ["id", "source_node_id", "sink_node_id", "source_channel", "sink_channel", - "enabled"]) -_annotation = namedtuple( - "_annotation", - ["id", "type", "params"]) - -_text_params = namedtuple( - "_text_params", - ["geometry", "text", "font", "content_type"]) - -_arrow_params = namedtuple( - "_arrow_params", - ["geometry", "color"]) +def parse_ows_etree_v_2_0(tree): + return parse_ows_etree_v_2_0_1(tree) -def parse_ows_etree_v_2_0(tree): +def parse_ows_etree_v_2_0_1(tree): + # type: (ElementTree) -> _scheme scheme = tree.getroot() nodes, links, annotations = [], [], [] @@ -499,15 +550,36 @@ def parse_ows_etree_v_2_0(tree): type="arrow", params=_arrow_params((start, end), color) ) + else: + log.warn("Unknown annotation '%s'. Skipping.", annot.tag) + continue annotations.append(annotation) + window_presets = [] + for window_group in tree.findall("session_state/window_groups/group"): + name = window_group.get("name") # type: str + state = [] + for state_ in window_group.findall("window_state"): + node_id = state_.get("node_id") # type: str + try: + data = base64.decodebytes(state_.text.encode("ascii")) + except binascii.Error: + data = b'' + except UnicodeDecodeError: + data = b'' + state.append((node_id, data)) + window_presets.append((name, state)) + + session_state = _session_data(window_presets) + return _scheme( version=scheme.get("version"), title=scheme.get("title", ""), description=scheme.get("description"), nodes=nodes, links=links, - annotations=annotations + annotations=annotations, + session_state=session_state, ) @@ -568,6 +640,7 @@ def parse_ows_etree_v_1_0(tree): def parse_ows_stream(stream): + # type: (...) -> _scheme doc = parse(stream) scheme_el = doc.getroot() version = scheme_el.get("version", None) @@ -659,7 +732,7 @@ def resolve_replaced(scheme_desc, registry): def scheme_load(scheme, stream, registry=None, error_handler=None): - desc = parse_ows_stream(stream) + desc = parse_ows_stream(stream) # type: _scheme if registry is None: registry = global_registry() @@ -735,6 +808,7 @@ def error_handler(exc): else: log.warning("Ignoring unknown annotation type: %r", annot_d.type) + continue annotations.append(annot) for node in nodes: @@ -746,17 +820,17 @@ def error_handler(exc): for annot in annotations: scheme.add_annotation(annot) + if desc.session_state.groups: + # resolve node_id -> node + groups = [] + for name, state in desc.session_state.groups: + state = [(nodes_by_id[node_id], data) + for node_id, data in state if node_id in nodes_by_id] + groups.append((name, state)) + scheme.setProperty("_presets", groups) return scheme -def inf_range(start=0, step=1): - """Return an infinite range iterator. - """ - while True: - yield start - start += step - - def scheme_to_etree(scheme, data_format="literal", pickle_fallback=False): """ Return an `xml.etree.ElementTree` representation of the `scheme. @@ -766,10 +840,10 @@ def scheme_to_etree(scheme, data_format="literal", pickle_fallback=False): "title": scheme.title or "", "description": scheme.description or ""}) - ## Nodes - node_ids = defaultdict(inf_range().__next__) + # Nodes + node_ids = defaultdict(count().__next__) builder.start("nodes", {}) - for node in scheme.nodes: + for node in scheme.nodes: # type: SchemeNode desc = node.description attrs = {"id": str(node_ids[node]), "name": desc.name, @@ -789,10 +863,10 @@ def scheme_to_etree(scheme, data_format="literal", pickle_fallback=False): builder.end("nodes") - ## Links - link_ids = defaultdict(inf_range().__next__) + # Links + link_ids = defaultdict(count().__next__) builder.start("links", {}) - for link in scheme.links: + for link in scheme.links: # type: SchemeLink source = link.source_node sink = link.sink_node source_id = node_ids[source] @@ -809,8 +883,8 @@ def scheme_to_etree(scheme, data_format="literal", pickle_fallback=False): builder.end("links") - ## Annotations - annotation_ids = defaultdict(inf_range().__next__) + # Annotations + annotation_ids = defaultdict(count().__next__) builder.start("annotations", {}) for annotation in scheme.annotations: annot_id = annotation_ids[annotation] @@ -867,6 +941,20 @@ def scheme_to_etree(scheme, data_format="literal", pickle_fallback=False): builder.end("properties") builder.end("node_properties") + builder.start("session_state", {}) + builder.start("window_groups", {}) + + for name, state in scheme.property("_presets") or []: + builder.start("group", {"name": name}) + for node, data in state: + if node not in node_ids: + continue + builder.start("window_state", {"node_id": str(node_ids[node])}) + builder.data(base64.encodebytes(data).decode("ascii")) + builder.end("window_state") + builder.end("group") + builder.end("window_group") + builder.end("session_state") builder.end("scheme") root = builder.close() tree = ElementTree(root)