From 28379b664a7167f5fa9625a9c1b24b5366b23d5d Mon Sep 17 00:00:00 2001 From: Alexandre Schulz Date: Wed, 17 Nov 2021 06:54:50 +0100 Subject: [PATCH] copied from amda_xml_manager project --- xsd2tkform/core/annotation.py | 2 ++ xsd2tkform/core/choice.py | 2 +- xsd2tkform/core/element.py | 87 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- xsd2tkform/core/parser.py | 11 +++++++---- xsd2tkform/core/restriction.py | 2 ++ xsd2tkform/core/schema.py | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------- xsd2tkform/core/sequence.py | 8 ++++++-- xsd2tkform/core/type.py | 53 +++++++++++++++++++++++++++++++++++++++++++---------- xsd2tkform/core/utils.py | 36 ++++++++++++++++++++++++++++++------ xsd2tkform/factory.py | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++------------------ xsd2tkform/ui/adaptivetextentry.py | 34 ++++++++++++++++++++++++++++++++++ xsd2tkform/ui/attributeframe.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ xsd2tkform/ui/autocompleteentry.py | 152 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ xsd2tkform/ui/buttoncontainer.py | 20 ++++++++++++++++++++ xsd2tkform/ui/choice.py | 101 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------------------- xsd2tkform/ui/complextype.py | 639 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------------------------------------------------------------------------------------------------------------------------------- xsd2tkform/ui/datetime_selector.py | 24 +++++++++++++++++++++++- xsd2tkform/ui/entrywidgets.py | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ xsd2tkform/ui/field.py | 10 +++++++++- xsd2tkform/ui/optional.py | 151 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------------- xsd2tkform/ui/scrollframe.py | 27 +++++++++++++++++---------- xsd2tkform/ui/simpletype.py | 239 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------- 22 files changed, 1683 insertions(+), 294 deletions(-) create mode 100644 xsd2tkform/ui/adaptivetextentry.py create mode 100644 xsd2tkform/ui/attributeframe.py create mode 100644 xsd2tkform/ui/autocompleteentry.py create mode 100644 xsd2tkform/ui/buttoncontainer.py create mode 100644 xsd2tkform/ui/entrywidgets.py diff --git a/xsd2tkform/core/annotation.py b/xsd2tkform/core/annotation.py index f86a231..488f32c 100644 --- a/xsd2tkform/core/annotation.py +++ b/xsd2tkform/core/annotation.py @@ -7,6 +7,8 @@ class Annotation: self.documentation = docs # dictionary where keys are language tags and values are documentation text def languages(self): return [k for k in self.documentation] + def __eq__(self, e): + return self.docs==e.docs def __str__(self): return "Annotation(languages={})".format(self.languages()) diff --git a/xsd2tkform/core/choice.py b/xsd2tkform/core/choice.py index 47608b8..fd79427 100644 --- a/xsd2tkform/core/choice.py +++ b/xsd2tkform/core/choice.py @@ -4,7 +4,7 @@ from .element import Element class Choice: - def __init__(self, min_occurs=0, max_occurs=0): + def __init__(self, min_occurs=1, max_occurs=1): self.min_occurs=min_occurs self.max_occurs=max_occurs self.elements=[] diff --git a/xsd2tkform/core/element.py b/xsd2tkform/core/element.py index 4dde339..db2112b 100644 --- a/xsd2tkform/core/element.py +++ b/xsd2tkform/core/element.py @@ -1,15 +1,54 @@ """Element definition """ +from lxml import etree + +class Attribute: + def __init__(self, name=None, type="string", use=None, default=None): + self.name=name + self.type=type + self.use=use + self.default=default + def mandatory(self): + return self.use=="required" + def __eq__(self, e): + if not isinstance(e, Attribute): + return False + return self.name==e.name and self.type==e.type and self.use==e.use and self.default==e.default + def __str__(self): + return "Attribute (name={}, type={}, use={}, default={})".format(self.name, self.type, self.use, self.default) + @staticmethod + def from_element(element): + att=dict(element.attrib) + return Attribute(**att) + +class AnyElement: + def __init__(self, **kwargs): + self.attributes=kwargs + def __str__(self, *args, **kwargs): + return "AnyElement" class Element: - def __init__(self, name=None, etype=None, min_occurs=0, max_occurs=0): + def __init__(self, name=None, etype=None, min_occurs=1, max_occurs=1, abstract=False, typedef=None, ref=None, substitution_group=None,**kwargs): self.name=name self.type=etype self.min_occurs=min_occurs self.max_occurs=max_occurs + self.typedef = typedef + self.abstract=abstract + self.ref=ref + self.substitution_group=substitution_group + if self.type is None: + if self.typedef is None: + self.type = self.name + else: + self.type = self.typedef.name + def __eq__(self, e): + return self.name==e.name and self.type==e.type and self.min_occurs==e.min_occurs and \ + self.max_occurs==e.max_occurs and self.typedef==e.typedef and self.abstract==e.abstract and \ + self.ref==e.ref and self.substitution_group==e.substitution_group def __str__(self, tab_n=0): - return "\t"*tab_n+"Element(name={}, type={}, min_occurs={}, max_occurs={})".format(self.name, - self.type,self.min_occurs, self.max_occurs) + return "\t"*tab_n+"Element(name={}, type={}, min_occurs={}, max_occurs={}, abstract={}, typedef={}, ref={})".format(self.name, + self.type,self.min_occurs, self.max_occurs, self.abstract, self.typedef, self.ref) @staticmethod def from_element(element): att=dict(element.attrib) @@ -23,4 +62,44 @@ class Element: att["max_occurs"]=att.pop("maxOccurs") if att["max_occurs"].isdigit(): att["max_occurs"]=int(att["max_occurs"]) - return Element(**att) + if "substitutionGroup" in att: + att["substitution_group"]=att.pop("substitutionGroup") + if "abstract" in att: + print("ABSTRACT val ", att["abstract"], type(att["abstract"])) + if att["abstract"]=="true": + att["abstract"]=True + if att["abstract"]=="false": + att["abstract"]=False + # check if element contains type definition + ct=None + for child in element: + if child.tag.endswith("complexType"): + from .type import ComplexType, SimpleType + from .utils import get_sequence, get_extension + from .sequence import Sequence + sequence = get_sequence(child) + attributes= get_attributes(child) + extension=get_extension(child) + if extension is None: + ct=ComplexType(att["name"], annotation=None, sequence=sequence, attributes=attributes, extension=extension) + elif isinstance(extension[1], Sequence): + ct=ComplexType(att["name"], annotation=None, sequence=sequence, attributes=attributes, extension=extension) + elif isinstance(extension[1], list): + from .restriction import Restriction + ct=SimpleType(att["name"], restriction=Restriction(base=extension[0]), attributes=extension[1]) + else: + ct=ComplexType(att["name"], annotation=None, sequence=sequence, attributes=attributes, extension=extension) + continue + + + + return Element(typedef = ct, **att) + +def get_attributes(element): + attributes=[] + for child in element: + if isinstance(child, etree._Comment): + continue + if child.tag.endswith("}attribute"): + attributes.append(Attribute.from_element(child)) + return attributes diff --git a/xsd2tkform/core/parser.py b/xsd2tkform/core/parser.py index 00ef2b0..c0bd63f 100644 --- a/xsd2tkform/core/parser.py +++ b/xsd2tkform/core/parser.py @@ -4,9 +4,9 @@ For now we suppose the file contains a single schema """ from lxml import etree -from xsd2tkform.core.type import SimpleType, ComplexType, Group, from_element -from xsd2tkform.core.annotation import Annotation -from xsd2tkform.core.schema import Schema +from .type import SimpleType, ComplexType, Group, from_element +from .annotation import Annotation +from .schema import Schema class XSDParser: def __init__(self, filename=None): @@ -21,7 +21,10 @@ class XSDParser: ns=root.nsmap # if root is a schema if root.tag.endswith("schema"): - self.schemas.append(Schema.from_element(root)) + self.schemas.append(Schema.from_element(root, filename)) + # apply substitutions + self.schemas[-1].apply_substitutions() + self.schemas[-1].apply_extensions() return def get(self, typename): for schema in self.schemas: diff --git a/xsd2tkform/core/restriction.py b/xsd2tkform/core/restriction.py index bde51c6..0770ace 100644 --- a/xsd2tkform/core/restriction.py +++ b/xsd2tkform/core/restriction.py @@ -6,6 +6,8 @@ class Restriction: def __init__(self, base=None, enum=[]): self.base=base self.enum=enum + def __eq__(self, e): + return self.base==e.base and self.enum==e.enum def __str__(self): return "Restriction(base={}, enum={})".format(self.base, self.enum) def possible_values(self): diff --git a/xsd2tkform/core/schema.py b/xsd2tkform/core/schema.py index d29503d..29e6ae0 100644 --- a/xsd2tkform/core/schema.py +++ b/xsd2tkform/core/schema.py @@ -2,27 +2,116 @@ """ from lxml import etree -from xsd2tkform.core.type import SimpleType, ComplexType, Group, from_element -from xsd2tkform.core.element import Element +from .type import SimpleType, ComplexType, Group, from_element +from .element import Element class Schema: - def __init__(self): + def __init__(self, filename): + self.filename=filename # need filename to manage include statements + self.root_items = [] self.items=[] + #self.elements={} # map element names to their type self.simple_types={} self.complex_types={} self.groups={} - def parse_element(self, element): + def find_ref(self, name): + for i in self.items: + if i.name==name: + return i + def find_substitutions(self, name): + return [e for e in self.items if e.substitution_group==name] + def include_schema(self, element): + schema_file = element.attrib["schemaLocation"] + from .parser import XSDParser + import os + path = os.path.join(os.path.dirname(self.filename), schema_file) + parser = XSDParser(path) + for schema in parser.schemas: + self.add_schema(schema) + def add_schema(self, schema): + for i in schema.items: + if not i in self.items: + self.items.append(i) + for ri in schema.root_items: + if not ri in self.root_items: + self.root_items.append(ri) + # simple types + for k in schema.simple_types: + self.simple_types[k]=schema.simple_types[k] + # complex types + for k in schema.complex_types: + self.complex_types[k]=schema.complex_types[k] + # groups + for k in schema.groups: + self.groups[k]=schema.groups[k] + + + + def parse_element(self, element, root=True, name=None): + # if element is an include statement + if element.tag.endswith("}include"): + self.include_schema(element) td = from_element(element) + if td is not None: + if td.name=="get": + pass if isinstance(td, SimpleType): self.simple_types[td.name]=td elif isinstance(td, ComplexType): self.complex_types[td.name]=td elif isinstance(td, Group): self.groups[td.name]=td + elif isinstance(td, Element): + if td.type is None: + # get type definition in this element + td.type=td.name + ct = td.typedef + self.complex_types[ct.name]=ct + #for child in element: + # self.parse_element(child, name=td.name, root=False) + #td.type = td.name + self.items.append(td) + if root: + self.root_items.append(td) else: - print("Unknown type defined : ", element, element.attrib, element.tag) + pass + + # WARNING : should not do this + for child in element: + if isinstance(child, etree._Comment): + continue + self.parse_element(child, root=False) + def apply_substitutions(self): + for i in self.items: + if i.substitution_group is not None: + # get the current object type definition + td = self.get(i.type) + # get the base type + bases = [e for e in self.items if e.name==i.substitution_group] + if len(bases): + # get the corresponding type + base_t = self.get(bases[0].type) + for att in base_t.attributes: + if not att in td.attributes: + td.attributes.append(att) + self.complex_types[td.name]=td + def apply_extensions(self): + for k in self.complex_types: + t = self.complex_types[k] + if t.extension is not None: + # get base type + base_type = self.get(t.extension[0]) + # add attributes and sequence element + for att in base_type.attributes: + if not att in t.attributes: + t.attributes.append(att) + for item in base_type.sequence.items: + if not item in t.sequence.items: + t.sequence.add(item) + t.sequence = t.extension[1] + t.extension=None def get(self, typename): if ":" in typename: typename = typename.split(":")[-1] @@ -32,6 +121,10 @@ class Schema: return self.complex_types[typename] if typename in self.groups: return self.groups[typename] + for item in self.items: + if typename==item.name: + return item + def __contains__(self, typename): if typename in self.simple_types: return True @@ -39,13 +132,16 @@ class Schema: return True if typename in self.groups: return True + item_names = [i.name for i in self.items] + if typename in item_names: + return True return False @staticmethod - def from_element(element): + def from_element(element, filename): if not element.tag.endswith("schema"): raise Exception("Cannot initialize Schema from {}".format(element)) - schema = Schema() + schema = Schema(filename=filename) # load type definitions and components for child in element: # ignore comments diff --git a/xsd2tkform/core/sequence.py b/xsd2tkform/core/sequence.py index 91ab174..fdc100c 100644 --- a/xsd2tkform/core/sequence.py +++ b/xsd2tkform/core/sequence.py @@ -2,14 +2,16 @@ """ -from xsd2tkform.core.element import Element -from xsd2tkform.core.choice import Choice +from .element import Element +from .choice import Choice class Sequence: def __init__(self, items=[]): self.items = items def add(self, i): self.items.append(i) + def __eq__(self, e): + return self.items==e.items def __str__(self, tab_n=0): r="\t"*tab_n+"Sequence" if len(self.items): @@ -28,6 +30,8 @@ class Sequence: s.add(Element.from_element(child)) elif child.tag.endswith("choice"): s.add(Choice.from_element(child)) + elif child.tag.endswith("any"): + s.add(AnyElement(**dict(child.attrib))) else: pass return s diff --git a/xsd2tkform/core/type.py b/xsd2tkform/core/type.py index fc91bad..7310cea 100644 --- a/xsd2tkform/core/type.py +++ b/xsd2tkform/core/type.py @@ -2,47 +2,68 @@ XSDType class is the base class for all XSD types, inherited by the XSDSimpleType, XSDComplexType classes""" -from xsd2tkform.core.element import Element -from xsd2tkform.core.annotation import Annotation -from xsd2tkform.core.restriction import Restriction -from xsd2tkform.core.list import List -from xsd2tkform.core.utils import get_annotation, get_restriction, get_list, get_sequence +from .element import Element, get_attributes +from .annotation import Annotation +from .restriction import Restriction +from .list import List +from .utils import get_annotation, get_restriction, get_list, get_sequence, get_extension class XSDType: def __init__(self, name=None, annotation=None): self.name = name self.annotation = annotation + def __eq__(self, e): + if not isinstance(e, XSDType): + return False + return self.name==e.name and self.annotation==e.annotation def __str__(self): return "XSDType(name={})".format(self.name) class SimpleType(XSDType): - def __init__(self, name=None, restriction=None, item_list=None, *args, **kwargs): + def __init__(self, name=None, restriction=None, item_list=None, attributes=[], *args, **kwargs): XSDType.__init__(self, name=name, *args, **kwargs) self.restriction=restriction self.item_list = item_list + self.attributes=attributes + def __eq__(self, e): + if not isinstance(e, SimpleType): + return False + return super().__eq__(e) and self.restriction==e.restriction and self.item_list==e.item_list and self.attributes==e.attributes def __str__(self): att=["name={})".format(self.name)] if self.restriction is not None: att.append("\n\trestriction={}".format(str(self.restriction))) if self.item_list is not None: att.append("\n\tlist={}".format(self.item_list)) + if len(self.attributes): + att.append("\n\tattributes={}".format(self.attributes)) a = "SimpleType({}".format(", ".join(att)) return a class ComplexType(XSDType): - def __init__(self, name=None, sequence=None, *args, **kwargs): + def __init__(self, name=None, sequence=None, attributes=[], extension=None, *args, **kwargs): XSDType.__init__(self, name=name, *args, **kwargs) self.sequence=sequence + self.attributes=attributes + self.extension=extension + def __eq__(self, e): + if not isinstance(e, ComplexType): + return False + return super().__eq__(e) and self.sequence==e.sequence def __str__(self, tab_n=0): r = "ComplexType(name={})".format(self.name) if len(self.sequence): r+="\n"+self.sequence.__str__(tab_n=tab_n+1) + if len(self.attributes): + r+="\nAttributes {}".format(self.attributes) return r class Group(XSDType): def __init__(self, name=None, sequence=None, *args, **kwargs): self.name = name self.sequence=sequence + def __eq__(self, e): + return super().__eq__(e) and self.name==e.name and self.sequence==e.sequence def __str__(self, tab_n=0): r="Group (name={})".format(self.name) if len(self.sequence): @@ -53,19 +74,31 @@ class Group(XSDType): def from_element(element): """Parse a XML element to XSDType object """ + if "name" not in element.attrib: + return name = element.attrib["name"] annotation = get_annotation(element) + attributes = get_attributes(element) if element.tag.endswith("simpleType"): restriction = get_restriction(element) l= get_list(element) - return SimpleType(name=name, annotation=annotation, restriction=restriction, item_list=l) + return SimpleType(name=name, annotation=annotation, restriction=restriction, item_list=l, attributes=attributes) elif element.tag.endswith("complexType"): sequence = get_sequence(element) - return ComplexType(name, annotation=annotation, sequence=sequence) + extension = get_extension(element) + if extension is None: + return ComplexType(name, annotation=annotation, sequence=sequence, attributes=attributes) + from .sequence import Sequence + if isinstance(extension[1], Sequence): + return ComplexType(name, annotation=annotation, sequence=sequence, attributes=attributes, extension=extension) + if isinstance(extension[1], list): + r=SimpleType(name, type=extensions[0], attributes=extension[1]) + return r elif element.tag.endswith("group"): sequence = get_sequence(element) return Group(name, annotation=annotation, sequence=sequence) elif element.tag.endswith("element"): - return Element.from_element(element) + e= Element.from_element(element) + return e else: return None diff --git a/xsd2tkform/core/utils.py b/xsd2tkform/core/utils.py index 1a6b1c8..2560032 100644 --- a/xsd2tkform/core/utils.py +++ b/xsd2tkform/core/utils.py @@ -1,9 +1,11 @@ -from xsd2tkform.core.annotation import Annotation -from xsd2tkform.core.restriction import Restriction -from xsd2tkform.core.list import List -from xsd2tkform.core.sequence import Sequence -from xsd2tkform.core.choice import Choice -from xsd2tkform.core.element import Element +from lxml import etree + +from .annotation import Annotation +from .restriction import Restriction +from .list import List +from .sequence import Sequence +from .choice import Choice +from .element import Element, AnyElement def get_annotation(element): """Get annotations from a type definition element @@ -39,6 +41,8 @@ def get_list(element): def get_sequence(element): sequence = [] for child in element: + if isinstance(child, etree._Comment): + continue if child.tag.endswith("sequence"): # children should be elements of choices for gchild in child: @@ -46,8 +50,28 @@ def get_sequence(element): sequence.append(Element.from_element(gchild)) elif gchild.tag.endswith("choice"): sequence.append(Choice.from_element(gchild)) + elif gchild.tag.endswith("any"): + sequence.append(AnyElement(**dict(gchild.attrib))) else: pass return Sequence(sequence) +def get_extension(element): + from .element import get_attributes + for child in element: + if isinstance(child, etree._Comment): + continue + if child.tag.endswith("}complexContent"): + # extension + if child[0].tag.endswith("}extension"): + base_type = child[0].attrib["base"] + sequence = get_sequence(child[0]) + return (base_type, sequence) + if child.tag.endswith("}simpleContent"): + # extension + if child[0].tag.endswith("}extension"): + base_type = child[0].attrib["base"].split(":")[-1] + attributes = get_attributes(child[0]) + return (base_type, attributes) + return None diff --git a/xsd2tkform/factory.py b/xsd2tkform/factory.py index aaf305f..8989cd4 100644 --- a/xsd2tkform/factory.py +++ b/xsd2tkform/factory.py @@ -2,25 +2,41 @@ """ import tkinter as tk -from xsd2tkform.form import XSDForm -from xsd2tkform.core.parser import XSDParser -from xsd2tkform.core.type import SimpleType, ComplexType +from .form import XSDForm +from .core.parser import XSDParser +from .core.type import SimpleType, ComplexType -from xsd2tkform.ui.simpletype import XSDSimpleTypeFrame -from xsd2tkform.ui.complextype import XSDComplexTypeFrame -from xsd2tkform.ui.scrollframe import ScrollFrame +from .ui.simpletype import XSDSimpleTypeFrame +from .ui.complextype import XSDComplexTypeFrame +from .ui.scrollframe import ScrollFrame class XSDFormFactory: - def __init__(self, parser=None): + def __init__(self, parser=None, widget_config={}): + self.widget_config = widget_config self.parser = parser - - def get_form(self, typename, parent): + def get_schema_form(self, parent): + # get form for schema + schema = self.parser.schemas[0] + root_frame = tk.Frame(parent) + for item in schema.root_items: + item_form = self.get_form(item.name, parent=root_frame) + item_form.pack(side=tk.TOP, fill=tk.X, expand=1) + return root_frame + def get_form(self, typename, parent, filename=None): if not typename in self.parser.schemas[0]: - raise Exception("Type {} not found") + raise Exception("Type '{}' not found".format(typename)) typedef = self.parser.get(typename) if isinstance(typedef, SimpleType): - return XSDSimpleTypeFrame(typename, schema=self.parser.schemas[0], parent=parent, delete_button=False) + return XSDSimpleTypeFrame(typename, schema=self.parser.schemas[0], parent=parent, delete_button=False, filename=filename, widget_config=self.widget_config) if isinstance(typedef, ComplexType): - return XSDComplexTypeFrame(typename, schema=self.parser.schemas[0], parent=parent, delete_button=False) + return XSDComplexTypeFrame(typename, schema=self.parser.schemas[0], parent=parent, delete_button=False, filename=filename, widget_config=self.widget_config) + from .core.element import Element + if isinstance(typedef, Element): + if typedef.typedef is None: + return self.get_form(typedef.type, parent=parent, filename=filename) + if isinstance(typedef.typedef, ComplexType): + return XSDComplexTypeFrame(typedef.name, schema=self.parser.schemas[0], parent=parent, delete_button=False, filename=filename, widget_config=self.widget_config, typedef=typedef) + if isinstance(typedef.typedef, SimpleType): + return XSDSimpleTypeFrame(typedef.name, schema=self.parser.schemas[0], parent=parent, delete_button=False, filename=filename, widget_config=self.widget_config, typedef=typedef.typedef) # example app @@ -42,28 +58,46 @@ if __name__=="__main__": parser = XSDParser(xsd_filename) # list complex types typenames = [k for k in parser.schemas[0].complex_types] + print("Simple types : \n", [k for k in parser.schemas[0].simple_types]) + print("Complex types : \n", [k for k in parser.schemas[0].complex_types]) + print("Elements : \n", [e.type for e in parser.schemas[0].items]) + print("Root elements : \n", parser.schemas[0].root_items) print("Creating form for type : {}".format(typenames[0])) - print(typenames) - + print() + for e in parser.schemas[0].items: + if e.type=="get": + print(e) + print() + print(parser.schemas[0].complex_types["GetterType"]) + #exit() # form factory factory=XSDFormFactory(parser) + app = ExampleApp() mainframe = tk.Frame(app) scrollframe = ScrollFrame(mainframe) # add a new scrollable frame. # populate window with all simpleTypes found in the XSD schema + #form_frame = factory.get_schema_form(parent = scrollframe.viewPort) + #form_frame.pack(side=tk.TOP, fill=tk.X, expand=1) + + tk.Label(scrollframe.viewPort, text="FILLED").pack(side=tk.TOP, fill=tk.X, expand=1) + + filled_form = factory.get_form("param", parent=scrollframe.viewPort, filename="ace_r.xml") + filled_form.pack(side=tk.TOP, fill=tk.X, expand=1) #for t in typenames: - form_frame = factory.get_form(typename = typenames[0], parent = scrollframe.viewPort) - form_frame.pack(side=tk.TOP, fill=tk.X, expand=1) + # #form_frame = factory.get_form(typename = t, parent = scrollframe.viewPort) + # form_frame = factory.get_schema_form(parent = scrollframe.viewPort) + # form_frame.pack(side=tk.TOP, fill=tk.X, expand=1) # add a submit and cancel button def save_form(): - tree = etree.ElementTree(form_frame.get_content()) + tree = etree.ElementTree(filled_form.get_content()) from tkinter.filedialog import askopenfilename filename = askopenfilename() - tree.write(filename, pretty_print=True) + tree.write(filename, pretty_print=True, xml_declaration=True, encoding="utf-8") submit_button = tk.Button(scrollframe.viewPort, text="Submit", command=save_form) cancel_button = tk.Button(scrollframe.viewPort, text="Cancel", command=app.quit) submit_button.pack(side=tk.LEFT, fill=tk.X, expand=1) diff --git a/xsd2tkform/ui/adaptivetextentry.py b/xsd2tkform/ui/adaptivetextentry.py new file mode 100644 index 0000000..d7a43ca --- /dev/null +++ b/xsd2tkform/ui/adaptivetextentry.py @@ -0,0 +1,34 @@ +"""Text input widget with size adaptive height +""" + +import tkinter as tk +from tkinter import ttk + +class AdaptiveHeightText(tk.Text): + def __init__(self, parent, height=None, width=None, *args, **kwargs): + tk.Text.__init__(self, parent, height=height, width=width, *args, **kwargs) + self.bind("", self.update_height) + def update_height(self, event=None): + text = self.get("1.0", tk.END) + n_lines = len(text.split("\n"))+1 + self.configure(height=n_lines) + def insert(self, *args, **kwargs): + super().insert(*args, **kwargs) + self.update_height() + def delete(self, *args, **kwargs): + super().delete(*args, **kwargs) + self.update_height() + +if __name__=="__main__": + root = tk.Tk() + mainframe = tk.Frame(root) + + text=AdaptiveHeightText(mainframe, height=2) + text.pack(side=tk.TOP, fill=tk.X, expand=1) + + text.insert("1.0", "a\nb\nc\n") + + mainframe.pack(side=tk.TOP, fill=tk.BOTH, expand=1) + + root.mainloop() + diff --git a/xsd2tkform/ui/attributeframe.py b/xsd2tkform/ui/attributeframe.py new file mode 100644 index 0000000..860b028 --- /dev/null +++ b/xsd2tkform/ui/attributeframe.py @@ -0,0 +1,126 @@ +"""Frame for containing list of attributes +""" + +import tkinter as tk +from tkinter import ttk + +from .simpletype import XSDAttributeFrame +from .entrywidgets import FloatEntry, IntEntry, BoolEntry +from amda_xml_manager import collapse_image_file, expand_image_file + +class AttributeButtonFrame(tk.Frame): + def __init__(self, parent): + tk.Frame.__init__(self, parent) + tk.Label(self, text="Add :", anchor=tk.W).grid(row=0, sticky=tk.W) + def count_visible_children(self): + c=0 + for child in self.winfo_children(): + ginfo=child.grid_info() + if "row" in ginfo: + c+=1 + return c + +class AttributeFrame(tk.Frame): + def __init__(self, parent, attributes=[]): + tk.Frame.__init__(self, parent, highlightbackground="black", highlightthickness=1) + + #tk.Label(self, text="Attributes").grid(row=0, columnspan=2, sticky=tk.W) + self.grid_columnconfigure(0, weight=0) + self.grid_columnconfigure(1, weight=1) + + #images + self.collapse_img = tk.PhotoImage(file=collapse_image_file) + self.expand_img = tk.PhotoImage(file=expand_image_file) + + # add header + self.get_header_frame() + + # content frame : content that is hidden when collapsed + self.content_frame = tk.Frame(self) + self.content_frame.grid_columnconfigure(0, weight=0) + self.content_frame.grid_columnconfigure(1, weight=1) + + + self.attributes=attributes + # attribute counts + self.counts={a.name:0 for a in self.attributes} + + # button container + self.button_frame = AttributeButtonFrame(self.content_frame) + + self.attribute_inputs=[XSDAttributeFrame(a, parent=self.content_frame,\ + on_delete=lambda t=a.name: self.delete_attribute(t),\ + on_add=self.update_grid) for a in self.attributes] + + self.add_attribute_widgets() + + self.content_frame.grid(row=1, columnspan=2, sticky=tk.EW) + def set_attribute_content(self, name, value): + if "}" in name: + name = name.split("}")[-1] + print("attrib Setting {} : {}".format(name, value)) + for i in range(len(self.attributes)): + if name==self.attributes[i].name: + A=self.attribute_inputs[i] + A.set(value) + + ginfo=self.attribute_inputs[i].grid_info() + if not "row" in ginfo: + self.attribute_inputs[i].grid() + def get_attribute_values(self): + r={} + for inp in self.attribute_inputs: + if inp.is_visible(): + v=inp.get() + if len(v): + r[inp.name]=v + return r + def add_attribute_widgets(self): + # add button for each non required attributes + row,col=0,1 + for att in self.attribute_inputs: + att.grid(row=row, column=1, sticky=tk.EW) + l=att.get_label(self.content_frame) + l.grid(row=row, column=0, sticky=tk.W) + if not att.mandatory: + att.grid_remove() + l.grid_remove() + + b=att.get_add_button(parent=self.button_frame) + b.grid(row=0, column=col) + row+=1 + col+=1 + + def get_header_frame(self): + self.header_frame=tk.Frame(self) + self.header_label=tk.Label(self.header_frame, text="Attributes", anchor=tk.W) + self.header_label.pack(side=tk.LEFT, fill=tk.X, expand=1) + self.collapse_button=tk.Button(self.header_frame, image=self.collapse_img, command=self.collapse) + self.collapse_button.pack(side=tk.RIGHT, expand=0) + + self.header_frame.grid(row=0, columnspan=2, sticky=tk.EW) + def collapse(self): + self.content_frame.grid_remove() + #for c in self.winfo_children(): + # c.grid_remove() + #self.header_frame.grid() + self.collapse_button.configure(image=self.expand_img, command=self.expand) + def expand(self): + self.content_frame.grid() + self.collapse_button.configure(image=self.collapse_img, command=self.collapse) + def delete_attribute(self, name): + self.counts[name]-=1 + #self.update_grid() + def update_grid(self): + row=1 + for f in self.attribute_inputs: + row+=1 + ttk.Separator(self, orient="horizontal").grid(row=row, columnspan=2, sticky=tk.EW) + row+=1 + if self.button_frame.count_visible_children()>1: + self.button_frame.grid(row=row, columnspan=2, sticky=tk.EW) + else: + self.button_frame.grid_remove() + + def delete_attribute(self, name): + self.update_grid() diff --git a/xsd2tkform/ui/autocompleteentry.py b/xsd2tkform/ui/autocompleteentry.py new file mode 100644 index 0000000..72023f9 --- /dev/null +++ b/xsd2tkform/ui/autocompleteentry.py @@ -0,0 +1,152 @@ +""" +tkentrycomplete.py + +A Tkinter widget that features autocompletion. + +Created by Mitja Martini on 2008-11-29. +Updated by Russell Adams, 2011/01/24 to support Python 3 and Combobox. +Updated by Dominic Kexel to use Tkinter and ttk instead of tkinter and tkinter.ttk + Licensed same as original (not specified?), or public domain, whichever is less restrictive. + +""" +import sys +import os +import tkinter +from tkinter import ttk + +__version__ = "1.1" + +# I may have broken the unicode... +tkinter_umlauts=['odiaeresis', 'adiaeresis', 'udiaeresis', 'Odiaeresis', 'Adiaeresis', 'Udiaeresis', 'ssharp'] + +class AutocompleteEntry(tkinter.Entry): + """ + Subclass of Tkinter.Entry that features autocompletion. + + To enable autocompletion use set_completion_list(list) to define + a list of possible strings to hit. + To cycle through hits use down and up arrow keys. + """ + def set_completion_list(self, completion_list): + self._completion_list = sorted(completion_list, key=str.lower) # Work with a sorted list + self._hits = [] + self._hit_index = 0 + self.position = 0 + self.bind('', self.handle_keyrelease) + + def autocomplete(self, delta=0): + """autocomplete the Entry, delta may be 0/1/-1 to cycle through possible hits""" + if delta: # need to delete selection otherwise we would fix the current position + self.delete(self.position, tkinter.END) + else: # set position to end so selection starts where textentry ended + self.position = len(self.get()) + # collect hits + _hits = [] + for element in self._completion_list: + if element.lower().startswith(self.get().lower()): # Match case-insensitively + _hits.append(element) + # if we have a new hit list, keep this in mind + if _hits != self._hits: + self._hit_index = 0 + self._hits=_hits + # only allow cycling if we are in a known hit list + if _hits == self._hits and self._hits: + self._hit_index = (self._hit_index + delta) % len(self._hits) + # now finally perform the auto completion + if self._hits: + self.delete(0,tkinter.END) + self.insert(0,self._hits[self._hit_index]) + self.select_range(self.position,tkinter.END) + + def handle_keyrelease(self, event): + """event handler for the keyrelease event on this widget""" + if event.keysym == "BackSpace": + self.delete(self.index(tkinter.INSERT), tkinter.END) + self.position = self.index(tkinter.END) + if event.keysym == "Left": + if self.position < self.index(tkinter.END): # delete the selection + self.delete(self.position, tkinter.END) + else: + self.position = self.position-1 # delete one character + self.delete(self.position, tkinter.END) + if event.keysym == "Right": + self.position = self.index(tkinter.END) # go to end (no selection) + if event.keysym == "Down": + self.autocomplete(1) # cycle to next hit + if event.keysym == "Up": + self.autocomplete(-1) # cycle to previous hit + if len(event.keysym) == 1 or event.keysym in tkinter_umlauts: + self.autocomplete() + +class AutocompleteCombobox(ttk.Combobox): + + def set_completion_list(self, completion_list): + """Use our completion list as our drop down selection menu, arrows move through menu.""" + self._completion_list = sorted(completion_list, key=str.lower) # Work with a sorted list + self._hits = [] + self._hit_index = 0 + self.position = 0 + self.bind('', self.handle_keyrelease) + self['values'] = self._completion_list # Setup our popup menu + + def autocomplete(self, delta=0): + """autocomplete the Combobox, delta may be 0/1/-1 to cycle through possible hits""" + if delta: # need to delete selection otherwise we would fix the current position + self.delete(self.position, tkinter.END) + else: # set position to end so selection starts where textentry ended + self.position = len(self.get()) + # collect hits + _hits = [] + for element in self._completion_list: + if element.lower().startswith(self.get().lower()): # Match case insensitively + _hits.append(element) + # if we have a new hit list, keep this in mind + if _hits != self._hits: + self._hit_index = 0 + self._hits=_hits + # only allow cycling if we are in a known hit list + if _hits == self._hits and self._hits: + self._hit_index = (self._hit_index + delta) % len(self._hits) + # now finally perform the auto completion + if self._hits: + self.delete(0,tkinter.END) + self.insert(0,self._hits[self._hit_index]) + self.select_range(self.position,tkinter.END) + + def handle_keyrelease(self, event): + """event handler for the keyrelease event on this widget""" + if event.keysym == "BackSpace": + self.delete(self.index(tkinter.INSERT), tkinter.END) + self.position = self.index(tkinter.END) + if event.keysym == "Left": + if self.position < self.index(tkinter.END): # delete the selection + self.delete(self.position, tkinter.END) + else: + self.position = self.position-1 # delete one character + self.delete(self.position, tkinter.END) + if event.keysym == "Right": + self.position = self.index(tkinter.END) # go to end (no selection) + if len(event.keysym) == 1: + self.autocomplete() + # No need for up/down, we'll jump to the popup + # list at the position of the autocompletion + +def test(test_list): + """Run a mini application to test the AutocompleteEntry Widget.""" + root = tkinter.Tk(className=' AutocompleteEntry demo') + entry = AutocompleteEntry(root) + entry.set_completion_list(test_list) + entry.pack() + entry.focus_set() + combo = AutocompleteCombobox(root) + combo.set_completion_list(test_list) + combo.pack() + combo.focus_set() + # I used a tiling WM with no controls, added a shortcut to quit + root.bind('', lambda event=None: root.destroy()) + root.bind('', lambda event=None: root.destroy()) + root.mainloop() + +if __name__ == '__main__': + test_list = ('apple', 'banana', 'CranBerry', 'dogwood', 'alpha', 'Acorn', 'Anise' ) + test(test_list) diff --git a/xsd2tkform/ui/buttoncontainer.py b/xsd2tkform/ui/buttoncontainer.py new file mode 100644 index 0000000..f009230 --- /dev/null +++ b/xsd2tkform/ui/buttoncontainer.py @@ -0,0 +1,20 @@ +"""Frame for containing optional field buttons +""" +import tkinter as tk + +class ButtonContainer(tk.Frame): + def __init__(self, parent, *args, **kwargs): + #tk.Frame.__init__(self, parent, highlightbackground="magenta", highlightthickness=3, *args, **kwargs) + tk.Frame.__init__(self, parent, highlightbackground=None, highlightthickness=None, *args, **kwargs) + + self.buttons = [] + + self.bind("", self.update_layout) + def add_button(self, button): + self.buttons.append(button) + def update_layout(self, event): + pass + #print("Update layout {}".format(event)) + + + diff --git a/xsd2tkform/ui/choice.py b/xsd2tkform/ui/choice.py index 214099b..4ac73fb 100644 --- a/xsd2tkform/ui/choice.py +++ b/xsd2tkform/ui/choice.py @@ -1,21 +1,22 @@ import tkinter as tk from tkinter import ttk -from xsd2tkform.core.type import SimpleType, ComplexType +from ..core.type import SimpleType, ComplexType -from xsd2tkform.ui.simpletype import XSDSimpleTypeFrame -from xsd2tkform.ui.complextype import XSDComplexTypeFrame +from .simpletype import XSDSimpleTypeFrame +from .complextype import XSDComplexTypeFrame -from xsd2tkform.ui.field import XSDInputField +from .field import XSDInputField class ChoiceInput(XSDInputField): def __init__(self, parent, choice, schema, *args, **kwargs): - tk.Frame.__init__(self, parent, *args, **kwargs) + XSDInputField.__init__(self, parent, highlightbackground="green", highlightthickness=2, *args, **kwargs) self.schema=schema # get the choice types self.choice_inputs=[] # get the list of choice type - choice_types = [t.type for t in choice.elements] + self.choice_types = [t.type for t in choice.elements] + self.choice_names = [t.name for t in choice.elements] # get occurence bounds for choices #choice_occ_bounds = self.get_element_occurence_limits(item) choice_occ_bounds = choice.min_occurs, choice.max_occurs @@ -23,7 +24,7 @@ class ChoiceInput(XSDInputField): self.field_counts={} self.field_min_counts={} self.field_max_counts={} - for _type in choice_types: + for _type in self.choice_types: # TODO : check if bounds are correct, if choice type is present somewhere else in the type definition then the bounds are overwritten here, which can be bad if _type in self.field_min_counts: print("Not good if you see this") @@ -38,9 +39,13 @@ class ChoiceInput(XSDInputField): choice_select_frame = tk.Frame(self) # add a choice selector : combobox and button - self.choice_type = ttk.Combobox(choice_select_frame, values=choice_types, state="readonly") + #self.choice_type = ttk.Combobox(choice_select_frame, values=self.choice_types, state="readonly") + self.choice_type = ttk.Combobox(choice_select_frame, values=self.choice_names, state="readonly") + self.choice_type.current(0) - choice_add = tk.Button(choice_select_frame, text="Add", command=lambda: self.add_field()) + from amda_xml_manager import add_image_file + self.add_img = tk.PhotoImage(file=add_image_file) + choice_add = tk.Button(choice_select_frame, image=self.add_img, command=lambda: self.add_field()) self.choice_type.pack(side=tk.LEFT, fill=tk.X, expand=1) choice_add.pack(side=tk.RIGHT, expand=0) @@ -48,16 +53,40 @@ class ChoiceInput(XSDInputField): ##choice_frame.pack(side=tk.TOP, fill=tk.X, expand=1) #sequence_inputs.append(choice_inputs) #return choice_frame - def add_field(self): - _type = self.choice_type.get() + def has_type(self, t): + # TODO watch out + if t in self.choice_names: + return True + for _type in self.choice_types: + if t==_type or t==_type.split(":")[-1]: + return True + return False + def set_content(self, content, update_grid=True): + # get the name of the item + ct=content.tag.split("}")[-1] + + for c in self.choice_names: + #for c in self.choice_types: + ctt=c.split(":")[-1] + if ct==ctt: + self.choice_type.set(c) + self.add_field(content) + + #self.add_field(content) + + def add_field(self, content=None): + _name = self.choice_type.get() + #_type = self.choice_type.get() + _type = self.choice_types[self.choice_names.index(_name)] # add the frame - self.add_frame_by_type(_type) - def add_frame_by_type(self, t): + self.add_frame_by_type(_type, content=content) + def add_frame_by_type(self, t, content=None): # check if the maximum occurences of this field is achieved if self.field_max_counts[t]=="unbounded": - print("No limit on {} fields".format(t)) + pass + #print("No limit on {} fields".format(t)) elif t in self.field_counts: if self.get_field_count_by_type(t)==self.field_max_counts[t]: showerror(title="{} maximum occurences reached".format(t), message="Type {} supports a maximum of {} occurences of type {}".format(self.type, self.field_max_counts[t], t)) @@ -67,27 +96,38 @@ class ChoiceInput(XSDInputField): self.increment_field_count_by_type(t) - f = self.get_frame_by_type(t, parent=self, delete_button=True) + f = self.get_frame_by_type(t, parent=self, delete_button=True, content=content) + f.configure(highlightbackground="blue", highlightthickness=2) f.pack(side=tk.TOP, fill=tk.X, expand=1) - #self.choice_inputs.append(f.winfo_children()[0]) - print("in choice_input append : {}".format(type(f))) self.choice_inputs.append(f) #if container is not None: # container.append(f.winfo_children()[0]) - def get_frame_by_type(self, t, parent=None, delete_button=False): + #self.master.update_grid() + + def get_frame_by_type(self, t, parent=None, delete_button=False, content=None): if parent is None: parent = self td=self.schema.get(t) + nn=self.choice_names[self.choice_types.index(t)] + print("IN CHOICE type={}, name={}".format(t, nn)) if isinstance(td, SimpleType):# in self.simple_types: return XSDSimpleTypeFrame(t, parent=parent,\ + name=nn, schema=self.schema, - delete_button=delete_button) + delete_button=delete_button, + content=content, + widget_config=self.widget_config, + on_delete=lambda x=t: self.delete_field(x)) elif isinstance(td, ComplexType): return XSDComplexTypeFrame(t, parent=parent,\ + name=nn, schema=self.schema, delete_button=delete_button, - collapsable=True) + collapsable=True, + content=content, + widget_config=self.widget_config, + on_delete=lambda x=t: self.delete_field(x)) else: # TODO : add Group support print("Group support not yet implemented") @@ -102,20 +142,10 @@ class ChoiceInput(XSDInputField): def decrement_field_count_by_type(self, t): self.field_counts[t]=self.get_field_count_by_type(t)-1 - def delete_field(self, t, field, container=None): - field_dims = (field.winfo_width(), field.winfo_height()) - field.destroy() + def delete_field(self, t, field=None, container=None): self.decrement_field_count_by_type(t) - current_scroll_region=self.master.master.bbox("all") - new_scrollregion= (current_scroll_region[0], - current_scroll_region[1], - current_scroll_region[2]-field_dims[0], - current_scroll_region[3]-field_dims[1]) - - # WORKS - self.master.master.master.configure(scrollregion=new_scrollregion) - - + self.choice_inputs=[w for w in self.choice_inputs if w.winfo_exists()] + def add_content(self, root, content): if isinstance(content, list): for item in content: @@ -123,5 +153,6 @@ class ChoiceInput(XSDInputField): else: if content is not None: root.append(content) - def get_content(self): - return [i.get_content(i) for i in self.choice_inputs] + def get_content(self, nsmap=None, qname_attrs=None): + return [i.get_content(nsmap=nsmap, qname_attrs=qname_attrs) for i in self.choice_inputs] + #return [i.get_content(i, nsmap=nsmap, qname_attrs=qname_attrs) for i in self.choice_inputs] diff --git a/xsd2tkform/ui/complextype.py b/xsd2tkform/ui/complextype.py index 40908f0..4e17fbf 100644 --- a/xsd2tkform/ui/complextype.py +++ b/xsd2tkform/ui/complextype.py @@ -4,173 +4,488 @@ from tkinter.messagebox import showerror from lxml import etree -from xsd2tkform.core.type import SimpleType, ComplexType -from xsd2tkform.core.element import Element -from xsd2tkform.core.choice import Choice +from ..core.type import SimpleType, ComplexType +from ..core.element import Element, AnyElement +from ..core.choice import Choice -from xsd2tkform.ui.simpletype import XSDSimpleTypeFrame -from xsd2tkform.ui.tooltip import ToolTip +from .simpletype import XSDSimpleTypeFrame, XSDAttributeFrame, XSDAnyInputFrame +from .tooltip import ToolTip from .field import XSDInputField +from amda_xml_manager import add_image_file, delete_image_file, collapse_image_file, expand_image_file + +from .attributeframe import AttributeFrame + class XSDComplexTypeFrame(XSDInputField): - def __init__(self, typename, parent=None, schema=None, delete_button=False, collapsable=False, *args, **kwargs): + def __init__(self, typename, parent=None, schema=None, delete_button=False, collapsable=False, filename=None, content=None, name=None, widget_config={}, typedef=None, *args, **kwargs): XSDInputField.__init__(self, parent, borderwidth=1,\ highlightbackground="black",\ - highlightthickness=2,\ + highlightthickness=1,\ + widget_config=widget_config,\ *args, **kwargs) + # flag indicating that the fields content has been set + self.content_has_been_set = False + + self.grid_columnconfigure(0, weight=1) + #self.grid_columnconfigure(1, weight=1) + + # collapsed state + self.collapsed = False + # store type of the current field + self.name = name self.type = typename self.schema = schema - + + # keep reference to the label + self.label=None # store the number of fields of each type, and min and max self.field_counts={} self.field_min_counts={} self.field_max_counts={} - # storage for optional frame - self.subframes=[] - # frame for containing label, collapse and delete buttons - lf = tk.Frame(self) - self.label=tk.Label(lf, text=self.sanitize_type(typename), font="bold") - self.label.pack(side=tk.LEFT, fill=tk.X, expand=1) - + self.header_frame = self.get_header_frame() + self.delete_img = None if delete_button: - tk.Button(lf, text="X", command=self.delete).pack(side=tk.RIGHT) + #tk.Button(self.header_frame, text="X", command=self.delete).pack(side=tk.RIGHT) + self.delete_img = tk.PhotoImage(file=delete_image_file) + tk.Button(self.header_frame, image=self.delete_img, command=self.delete).pack(side=tk.RIGHT) + + # add a collapse button + self.expand_img = None + self.collapse_img = None self.collapse_button=None if collapsable: - self.collapse_button = tk.Button(lf, text="_", command=self.collapse) + self.expand_img = tk.PhotoImage(file=expand_image_file) + self.collapse_img = tk.PhotoImage(file=collapse_image_file) + #self.collapse_button = tk.Button(self.header_frame, text="_", command=self.collapse) + self.collapse_button = tk.Button(self.header_frame, image=self.collapse_img, command=self.collapse) self.collapse_button.pack(side=tk.RIGHT) - - lf.pack(side=tk.TOP, fill=tk.X, expand=1) self.inputs=[] # list of inputs used for constructing the tree + # option button frame + #self.option_button_frame = tk.Frame(self) + from .buttoncontainer import ButtonContainer + self.option_button_frame2 = ButtonContainer(self) + + + + #tk.Label(self.option_button_frame, text="Add :").grid(row=0, column=0) + tk.Label(self.option_button_frame2, text="Add :").grid(row=0, column=0) + + # get type definition - self.typedef = schema.get(typename) + if typedef is None: + self.typedef = schema.get(typename) + else: + self.typedef = typedef + #from ..core.element import Element + if isinstance(typedef, Element): + if typedef.typedef is not None: + self.typedef = typedef.typedef if self.typedef is None: raise Exception("Typename {} not found".format(typename)) + + # attribute container + if len(self.typedef.attributes): + self.attribute_frame = AttributeFrame(self, self.typedef.attributes) + self.attribute_frame.collapse() + else: + self.attribute_frame = None + # field container + #self.field_frame = tk.Frame(self, highlightbackground="red", highlightthickness=2) + self.field_frame = tk.Frame(self, highlightbackground=None, highlightthickness=None) + self.field_frame.grid_columnconfigure(0, weight=0) + self.field_frame.grid_columnconfigure(1, weight=1) + + + self.set_tooltip() - sequence_inputs=[] + + # grid content + #c=0 + #self.next_row = 1 + option_button_column=1 + # attributes first + for item in self.typedef.sequence.items: + #print("item type {} {}".format(type(item), type(item.typedef))) if isinstance(item, Element): + if item.ref is not None: + # there can be only ONE + refered_element = self.schema.find_ref(item.ref) + print("Refered element : {}".format(refered_element)) + if refered_element.abstract: + # find substitutions + substitutions = self.schema.find_substitutions(refered_element.name) + print("Substitutions : {}".format(substitutions)) + print("\tnames : {}".format([s.name for s in substitutions])) + if len(substitutions)==1: + item = substitutions[0] + else: + #create a choice item + item = Choice() + for s in substitutions: + item.add(s) + from .choice import ChoiceInput + chh = ChoiceInput(self.field_frame, item, self.schema, widget_config=self.widget_config) + #self.grid_contents.append(chh) + self.inputs.append(chh) + continue + self.set_occurrence_bounds(item) - element_field = self.get_element_field(item, sequence_inputs) - element_field.pack(side=tk.TOP, fill=tk.X, expand=1) + + for element_field in self.get_element_field(item, self.inputs): + # add to grid contents + #self.grid_contents.append(element_field) + + from .optional import OptionalInput + if isinstance(element_field, OptionalInput): + # pack the button at the bottom of the field + but = element_field.get_add_button(parent = self.option_button_frame2) + self.option_button_frame2.add_button(but) + but.grid(row=0, column=option_button_column) + option_button_column+=1 + elif isinstance(item, Choice): - from xsd2tkform.ui.choice import ChoiceInput - chh = ChoiceInput(self, item, self.schema) - chh.pack(side=tk.TOP, fill=tk.X, expand=1) - sequence_inputs.append(chh) + from .choice import ChoiceInput + chh = ChoiceInput(self.field_frame, item, self.schema, widget_config=self.widget_config) + #self.grid_contents.append(chh) + self.inputs.append(chh) + elif isinstance(item, AnyElement): + from .adaptivetextentry import AdaptiveHeightText + r=XSDAnyInputFrame(parent=self.field_frame,\ + input_widget_type=lambda p: AdaptiveHeightText(p, height=3)) + self.inputs.append(r) + else: # TODO : add groups print("Group support not implemeneted yet") - self.inputs.append(sequence_inputs) - def collapse(self): - # hide all inputs - for i in self.inputs: - if isinstance(i, list): - for e in i: - e.pack_forget() + + #c+=1 + + row=1 + if self.attribute_frame is not None: + self.attribute_frame.grid(row=1, sticky=tk.EW) + row+=1 + self.field_frame.grid(row=row, sticky=tk.EW) + + + + # if filename or content was given + if filename is not None: + print("Setting content from {}".format(filename)) + self.set_content(filename=filename) + if content is not None: + self.set_content(content=content) + + def get_header_frame(self): + hf=tk.Frame(self) + if self.name is None: + label_text=self.sanitize_type(self.type) + else: + label_text=self.name + + #self.header_label=tk.Label(hf, text=label_text, font="bold", anchor="w") + self.header_label=tk.Label(hf, text=label_text, font=("Helvetica",11,"bold"), anchor="w") + + self.header_label.pack(side=tk.LEFT, fill=tk.X, expand=1) + + self.header_label.bind("", self.collapse) + return hf + + + def clear_grid(self): + for child in self.field_frame.winfo_children(): + # delete labels + if isinstance(child, tk.Label): + child.destroy() + elif isinstance(child, ttk.Separator): + child.destroy() else: - i.pack_forget() + child.grid_forget() + def get_label(self, parent): + print(" ------> ComplexType name={}, type={}".format(self.name, self.type)) + label_text=self.sanitize_type(self.type) + if isinstance(self.typedef, Element): + label_text=self.typedef.name + if self.name is not None: + label_text=self.name + + self.label = tk.Label(parent, text=label_text+" :") + self.set_tooltip() + return self.label + def get_fields(self): + from .optional import OptionalInput + l=[] + #for item in self.grid_contents: + for item in self.inputs: + if isinstance(item, OptionalInput): + l+=[w for w in item.get_fields()] + else: + l.append(item) + return l + def remove_separators(self): + for child in self.field_frame.winfo_children(): + if isinstance(child, ttk.Separator): + child.destroy() + def count_grid_contents(self): + c={} + for child in self.field_frame.winfo_children(): + if type(child) in c: + c[type(child)]+=1 + else: + c[type(child)]=1 + for k in c: + print("{} : {}".format(k, c[k])) + + def update_attribute_grid(self): + if self.attribute_frame is not None: + self.attribute_frame.update_grid() + + def update_grid(self): + print("update_grid name={}, type={}, self.type={}".format(self.name, self.type, type(self))) + from .optional import OptionalInput + from .choice import ChoiceInput + + self.remove_separators() + # attribute grid update + self.update_attribute_grid() + # get fields + new_fields = self.get_fields() + + # add the contents of the grid + self.header_frame.grid(row=0, columnspan=2, sticky=tk.EW) + if not self.collapsed: + #ttk.Separator(self, orient="horizontal").grid(row=1, columnspan=2, sticky=tk.EW) + row=2 + for f in new_fields: + # add the input field + grid_info = f.grid_info() + if "row" in grid_info: + if grid_info["row"]!=row: + f.grid_forget() # remove the label too + f.grid(row=row, column=1, sticky=tk.EW) + else: + f.grid(row=row, column=1, sticky=tk.EW) + + # check the label + if not isinstance(f, ChoiceInput): + if f.label is None: + f.get_label(self.field_frame).grid(row=row, column=0, sticky=tk.NW) + else: + label_grid_info = f.label.grid_info() + if "row" in label_grid_info: + if label_grid_info["row"]!=row: + f.label.grid_forget() + f.label.grid(row=row, column=0, sticky=tk.NW) + else: + f.label.grid(row=row, column=0, sticky=tk.NW) + row+=1 + ttk.Separator(self.field_frame, orient="horizontal").grid(row=row, columnspan=2, sticky=tk.EW) + row+=1 + n=len(list(self.option_button_frame2.winfo_children())) + if n>1: + self.option_button_frame2.grid(row=row, columnspan=2, sticky=tk.EW) + + def grid(self, *args, **kwargs): + self.update_grid() + return super().grid(pady=1,*args, **kwargs) + def pack(self, *args, **kwargs): + self.update_grid() + return super().pack(pady=1, *args, **kwargs) + if isinstance(self.master, XSDComplexTypeFrame): + #super().pack(*args, padx=(100,0), **kwargs) + super().pack(*args, **kwargs) + + else: + super().pack(*args, **kwargs) + def iter_inputs(self): + for e in self.inputs: + if isinstance(e, list): + for el in e: + yield el + else: + yield e + def set_content(self, filename=None, content=None, update_grid=True): + if filename is not None: + root = etree.parse(filename).getroot() + return self.set_content(content=root, update_grid=update_grid) + if content is not None: + # set attribute content + for att in content.attrib: + self.attribute_frame.set_attribute_content(att, content.attrib[att]) + # go over children of content + for child in content: + if isinstance(child, etree._Comment): + continue + self.set_child_content(child, update_grid=True) + self.content_has_been_set=True + if update_grid: + self.update_grid() + def is_full(self): + # check if this field is full : all mandatory fields are filled + return self.content_has_been_set + def set_child_content(self, child, update_grid=True): + ct=child.tag.split("}")[-1] + ctype = self.schema.get(ct) + from ..core.element import Element + for minp in self.iter_inputs(): + if isinstance(minp, XSDSimpleTypeFrame) or isinstance(minp, XSDComplexTypeFrame): + if minp.name is None: + ctt=minp.type.split(":")[-1] + else: + ctt=minp.name + if ct==ctt: + # if the field is already filled skip + if minp.is_full(): + continue + minp.set_content(content=child, update_grid=True) + if isinstance(minp, XSDComplexTypeFrame): + minp.collapse() + return + from .optional import OptionalInput + if isinstance(minp, OptionalInput): + if minp.is_full(): + continue + ctt=minp.type.split(":")[-1] + if ct==ctt: + minp.set_content(content=child, update_grid=True) + return + from .choice import ChoiceInput + if isinstance(minp, ChoiceInput): + if minp.has_type(ct): + minp.set_content(content=child, update_grid=True) + return + + def collapse(self, event=None): + self.collapsed = True + if self.attribute_frame is not None: + self.attribute_frame.grid_remove() + for item in self.field_frame.winfo_children(): + if isinstance(item, tk.Label): + item.grid_remove() + if isinstance(item, XSDInputField): + item.grid_remove() + # collapse the optional button frame + self.option_button_frame2.grid_remove() + # change button action to expand - self.collapse_button.configure(text="+", command=self.expand) + self.collapse_button.configure(image=self.expand_img, command=self.expand) # change the label text in_val=[] for i in self.inputs: - if isinstance(i, list): - for e in i: - if isinstance(e, XSDSimpleTypeFrame): - in_val+=["{}:{}".format(self.sanitize_type(e.type),e.get_value())] - new_lab = "{}({})".format(self.sanitize_type(self.type), + if isinstance(i, XSDSimpleTypeFrame): + tval = str(i.get_value()) + if len(tval)>100: + tval=tval[:30]+"..." + in_val+=["{}:{}".format(self.sanitize_type(i.type),tval)] + l=self.sanitize_type(self.type) + if self.name is not None: + l=self.name + new_lab = "{}({})".format(l, ",".join(in_val)) - self.label.configure(text=new_lab) - def expand(self): - # hide all inputs - for i in self.inputs: - if isinstance(i, list): - for e in i: - e.pack(side=tk.TOP, fill=tk.X, expand=1) - else: - i.pack(side=tk.TOP, fill=tk.X, expand=1) + w=int(self.winfo_width()*.8) + self.header_label.configure(text=new_lab, wraplength=w, justify="left") + self.header_label.bind("", self.expand) + def expand(self, event=None): + #self.update_grid() + from .optional import OptionalInput + self.collapsed = False + if self.attribute_frame is not None: + self.attribute_frame.grid() + for item in self.field_frame.winfo_children(): + if isinstance(item, tk.Label): + item.grid() + if isinstance(item, XSDInputField): + if isinstance(item, OptionalInput): + continue + item.grid() + + + # option button frame + #self.option_button_frame.pack(side=tk.BOTTOM, fill=tk.X, expand=1) + self.option_button_frame2.grid() + # change button action to collapse - self.collapse_button.configure(text="_", command=self.collapse) + self.collapse_button.configure(image=self.collapse_img, command=self.collapse) # set original lable - self.label.configure(text=self.sanitize_type(self.type)) + l=self.sanitize_type(self.type) + if self.name is not None: + l=self.name + self.header_label.configure(text=l, justify="center") + self.header_label.bind("", self.collapse) + self.update_grid() + def delete(self): - self.master.decrement_field_count_by_type(self.type) + #self.master.decrement_field_count_by_type(self.type) self.destroy() - def get_choice_field(self, item, sequence_inputs): - choice_inputs=[] - # get the list of choice type - choice_types = [t.type for t in item.elements] - # get occurence bounds for choices - #choice_occ_bounds = self.get_element_occurence_limits(item) - choice_occ_bounds = item.min_occurs, item.max_occurs - # set those bounds for all types - for _type in choice_types: - # TODO : check if bounds are correct, if choice type is present somewhere else in the type definition then the bounds are overwritten here, which can be bad - if _type in self.field_min_counts: - print("Not good if you see this") - if _type in self.field_max_counts: - print("Not good if you see this") - self.field_min_counts[_type]=choice_occ_bounds[0] - self.field_max_counts[_type]=choice_occ_bounds[1] - # frame for storing the selector and choices - choice_frame = tk.Frame(self) - # create a frame to store the choice selector - choice_select_frame = tk.Frame(choice_frame) + if self.on_delete is not None: + self.on_delete() + if self.label is not None: + self.label.destroy() - # add a choice selector : combobox and button - choice_type = ttk.Combobox(choice_select_frame, values=choice_types, state="readonly") - choice_type.current(0) - choice_add = tk.Button(choice_select_frame, text="Add", command=lambda w=choice_type, frame=choice_frame, container=choice_inputs: self.add_frame_by_type(w.get(), frame, container)) - - choice_type.pack(side=tk.LEFT, fill=tk.X, expand=1) - choice_add.pack(side=tk.RIGHT, expand=0) - choice_select_frame.pack(side=tk.TOP, fill=tk.X, expand=1) - ##choice_frame.pack(side=tk.TOP, fill=tk.X, expand=1) - sequence_inputs.append(choice_inputs) - return choice_frame def get_element_field(self, item, sequence_inputs): - if item.min_occurs==0: - # optional field, add a frame to contain the eventual fields, with a button - b_frame= tk.Frame(self) - b=tk.Button(b_frame, text="Add {}".format(item.type), command=lambda t=item.type, frame=b_frame, container=sequence_inputs: self.add_frame_by_type(t, frame, container)) - # add tooltip - doc_str = self.get_type_docstring(item.type) - if len(doc_str): - ttt = ToolTip(b, doc_str) - b.pack(side=tk.TOP, fill=tk.X, expand=1) + from .optional import OptionalInput + if isinstance(item, AnyElement): + of = self.get_frame_by_type(item) + sequence_inputs.append(of) # store the field for later use + yield of + if item.min_occurs==0: # temp - from xsd2tkform.ui.optional import OptionalInput bounds=(self.field_min_counts[item.type], self.field_max_counts[item.type]) - of=OptionalInput(self, item, self.schema, bounds=bounds) - of.pack(side=tk.TOP, fill=tk.X, expand=1) + of=OptionalInput(self.field_frame, item, self.schema, bounds=bounds, widget_config=self.widget_config, on_add_field=self.update_grid,\ + on_delete_field=lambda t=item.type: self.decrement_field_count_by_type(t)\ + ) + sequence_inputs.append(of) - return of - return b_frame + yield of else: - # mandatory field - f = self.get_frame_by_type(item.type) - sequence_inputs.append(f) # store the field for later use - return f + # yield mandatory items + for i in range(item.min_occurs): + of = self.get_frame_by_type(item) + if of is None: + continue + sequence_inputs.append(of) # store the field for later use + yield of + # get optional bounds + bounds=[0, self.field_max_counts[item.type]] + if bounds[1]!="unbounded": + bounds[1]=bounds[1]-item.min_occurs + a = (bounds[1]=="unbounded") + b = False + if not a: + b = bounds[1] + + if a or b: + # yield optional items + of=OptionalInput(self.field_frame, item, self.schema, bounds=tuple(bounds), widget_config=self.widget_config, on_add_field=self.update_grid, \ + on_delete_field=lambda t=item.type: self.decrement_field_count_by_type(t)\ + ) + sequence_inputs.append(of) + yield of + def set_occurrence_bounds(self, item): self.field_min_counts[item.type]=item.min_occurs self.field_max_counts[item.type]=item.max_occurs def set_tooltip(self): + if self.label is None or self.typedef is None: + return + if self.typedef.annotation is None: + return if len(self.typedef.annotation.documentation): langs = [k for k in self.typedef.annotation.documentation] - tt = ToolTip(self.label, self.typedef.annotation.documentation[langs[0]]) + tt = ToolTip(self.header_label, self.typedef.annotation.documentation[langs[0]]) def get_type_docstring(self, t): td=self.schema.get(t) @@ -209,8 +524,6 @@ class XSDComplexTypeFrame(XSDInputField): def increment_field_count_by_type(self, t): self.field_counts[t]=self.get_field_count_by_type(t)+1 def decrement_field_count_by_type(self, t): - print("Current {} field count {}".format(t, self.get_field_count_by_type(t))) - print([k for k in self.field_counts]) self.field_counts[t]=self.get_field_count_by_type(t)-1 def delete_field(self, t, field, container=None): field_dims = (field.winfo_width(), field.winfo_height()) @@ -226,47 +539,64 @@ class XSDComplexTypeFrame(XSDInputField): self.master.master.configure(scrollregion=new_scrollregion) - def add_frame_by_type(self, t, frame=None, container=None): - print("in add frame by type : " , t) - # check if the maximum occurences of this field is achieved - - if self.field_max_counts[t]=="unbounded": - print("No limit on {} fields".format(t)) - elif t in self.field_counts: - if self.get_field_count_by_type(t)==self.field_max_counts[t]: - showerror(title="{} maximum occurences reached".format(t), message="Type {} supports a maximum of {} occurences of type {}".format(self.type, self.field_max_counts[t], t)) - return - else: - pass - - self.increment_field_count_by_type(t) - - if frame is None: - f = self.get_frame_by_type(t, delete_button=True) - f.pack(side=tk.TOP, fill=tk.X, expand=1) - else: - f = self.get_frame_by_type(t, parent=frame, delete_button=True) - f.pack(side=tk.TOP, fill=tk.X, expand=1) - if container is not None: - container.append(f) - #container.append(f.winfo_children()[0]) def get_frame_by_type(self, t, parent=None, delete_button=False): + #from ..core.type import SimpleType if parent is None: - parent = self - - td=self.schema.get(t) + parent = self.field_frame + if isinstance(t, AnyElement): + from .adaptivetextentry import AdaptiveHeightText + return XSDAnyInputFrame(parent=parent,\ + input_widget=AdaptiveHeightText) + + + if t.type is None: + td=self.schema.get(t.name) + else: + td=self.schema.get(t.type) + if td is None: + # if type is native type + if t.type.split(":")[-1]=="string": + # return a SimpleType object + from ..core.restriction import Restriction + #from ..core.type import SimpleType + td=SimpleType(t.name, restriction=Restriction(base="string")) + return XSDSimpleTypeFrame(t.name, name=t.name, parent=parent,\ + schema=self.schema, delete_button=delete_button,\ + widget_config=self.widget_config,\ + typedef=td) + if isinstance(td, SimpleType):# in self.simple_types: - return XSDSimpleTypeFrame(t, parent=parent,\ + return XSDSimpleTypeFrame(t.type, name=t.name, parent=parent,\ schema=self.schema, - delete_button=delete_button) + delete_button=delete_button, + widget_config=self.widget_config) elif isinstance(td, ComplexType): - return XSDComplexTypeFrame(t, parent=parent,\ + return XSDComplexTypeFrame(t.type, name=t.name, parent=parent,\ schema=self.schema, delete_button=delete_button, - collapsable=True) + collapsable=True, + widget_config=self.widget_config) else: # TODO : add Group support print("Group support not yet implemented") + #if td.typedef is None: + # return None + if td.ref is not None: + return None + if isinstance(td.typedef, SimpleType): + return XSDSimpleTypeFrame(t.type, name=t.name, parent=parent,\ + schema=self.schema, + delete_button=delete_button, + widget_config=self.widget_config, typedef=td.typedef) + + + return XSDComplexTypeFrame(td.type, name=td.name, parent=parent,\ + schema=self.schema, + delete_button=delete_button, + collapsable=True, + widget_config=self.widget_config, + typedef=td.typedef) + def get_value(self): return "" def add_content(self, root, content): @@ -276,31 +606,46 @@ class XSDComplexTypeFrame(XSDInputField): else: if content is not None: root.append(content) - def get_content(self, obj=None): + def get_attribute_values(self): + if self.attribute_frame is None: + return {} + return self.attribute_frame.get_attribute_values() + def get_content(self, obj=None, nsmap=None, qname_attrs=None): if obj is not None: - from xsd2tkform.ui.choice import ChoiceInput + from .choice import ChoiceInput if isinstance(obj, list): - return [self.get_content(i) for i in obj] - if isinstance(obj, XSDSimpleTypeFrame): + return [self.get_content(i, nsmap=nsmap) for i in obj] + if isinstance(obj, XSDAnyInputFrame): if obj.winfo_exists(): return obj.get_content() + if isinstance(obj, XSDSimpleTypeFrame): + if obj.winfo_exists(): + return obj.get_content(nsmap=nsmap) if isinstance(obj, XSDComplexTypeFrame): if obj.winfo_exists(): - return obj.get_content() + return obj.get_content(nsmap=nsmap) if isinstance(obj, ChoiceInput): if obj.winfo_exists(): - return obj.get_content() - from xsd2tkform.ui.optional import OptionalInput + return obj.get_content(nsmap=nsmap) + from .optional import OptionalInput if isinstance(obj, OptionalInput): if obj.winfo_exists(): - return obj.get_content() + return obj.get_content(nsmap=nsmap) return - - root = etree.Element(self.sanitize_type(self.type)) + if nsmap is None: + root = etree.Element(self.sanitize_type(self.type)) + else: + root = etree.Element(self.sanitize_type(self.type), qname_attrs, nsmap=nsmap) + attrib_values=self.get_attribute_values() + for k in attrib_values: + root.set(k, attrib_values[k]) # returns tree type - for c in self.get_content(self.inputs): + for c in self.get_content(self.inputs, nsmap=nsmap): + if isinstance(c, str): + root.text=c + continue self.add_content(root, c) return root diff --git a/xsd2tkform/ui/datetime_selector.py b/xsd2tkform/ui/datetime_selector.py index 6425ad2..ac6ee87 100644 --- a/xsd2tkform/ui/datetime_selector.py +++ b/xsd2tkform/ui/datetime_selector.py @@ -31,5 +31,27 @@ class DatetimeEntry(tk.Frame): def get(self): # return time value return "{}T{}:{}:{}Z".format(self.date.get(), self.hours.get(), self.minutes.get(), self.seconds.get()) - + def set(self, content): + content=content.strip() + from datetime import datetime + if content.endswith("Z"): + if "." in content: + d=datetime.strptime(content, "%Y-%m-%dT%H:%M:%S.%fZ") + else: + d=datetime.strptime(content, "%Y-%m-%dT%H:%M:%SZ") + else: + if "." in content: + d=datetime.strptime(content, "%Y-%m-%dT%H:%M:%S.%f") + else: + d=datetime.strptime(content, "%Y-%m-%dT%H:%M:%S") + + + self.date.set_date(d) + t=d.time() + self.hours.delete(0,"end") + self.minutes.delete(0,"end") + self.seconds.delete(0,"end") + self.hours.insert(0, "{0:02d}".format(t.hour)) + self.minutes.insert(0, "{0:02d}".format(t.minute)) + self.seconds.insert(0, "{0:02d}".format(t.second)) diff --git a/xsd2tkform/ui/entrywidgets.py b/xsd2tkform/ui/entrywidgets.py new file mode 100644 index 0000000..f072918 --- /dev/null +++ b/xsd2tkform/ui/entrywidgets.py @@ -0,0 +1,73 @@ +"""Some Entry widgets with validation depending on type +""" +import tkinter as tk + + +class FloatEntry(tk.Entry): + def __init__(self, parent): + tk.Entry.__init__(self, parent) + vcmd = (self.register(self.validate), + '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W') + self.configure(validate="all", validatecommand=vcmd) + + def validate(self, *args): + try: + i=float(args[2]) + self.config(fg="black") + return True + except: + self.config(fg="red") + return True + def get(self, *args, **kwargs): + return float(self.get(*args, **kwargs)) + def set(self, value): + self.delete(0, tk.END) + self.insert(0, value) + + +class IntEntry(tk.Entry): + def __init__(self, parent): + tk.Entry.__init__(self, parent) + vcmd = (self.register(self.validate), + '%d', '%i', '%P', '%s', '%S', '%v', '%V', '%W') + self.configure(validate="all", validatecommand=vcmd) + def set(self, value): + self.delete(0, tk.END) + self.insert(0, value) + def get(self, *args, **kwargs): + return int(self.get(*args, **kwargs)) + + + def validate(self, *args): + try: + i=int(args[2]) + self.config(fg="black") + return True + except: + self.config(fg="red") + return True + +class BoolEntry(tk.Frame): + def __init__(self, parent): + tk.Frame.__init__(self, parent)#highlightbackground="bleu", highlightthickness=2) + vals = ["true", "false"] + labels=["True", "False"] + self.var = tk.StringVar() + self.var.set(vals[0]) + for i in range(2): + tk.Radiobutton(self, variable=self.var, text=labels[i], value=vals[i]).pack(side=tk.LEFT, fill=tk.X, expand=1) + def get(self, *args): + return self.var.get() + def set(self, value): + self.var.set(value) + +if __name__=="__main__": + root = tk.Tk() + + float_entry = FloatEntry(root) + float_entry.pack(side=tk.TOP, fill=tk.X, expand=1) + + int_entry = IntEntry(root) + int_entry.pack(side=tk.TOP, fill=tk.X, expand=1) + + root.mainloop() diff --git a/xsd2tkform/ui/field.py b/xsd2tkform/ui/field.py index 938c523..bd29045 100644 --- a/xsd2tkform/ui/field.py +++ b/xsd2tkform/ui/field.py @@ -4,10 +4,18 @@ import tkinter as tk class XSDInputField(tk.Frame): - def __init__(self, parent, *args, **kwargs): + def __init__(self, parent, content=None, widget_config={}, on_delete=None, *args, **kwargs): tk.Frame.__init__(self, parent, *args, **kwargs) + self.widget_config=widget_config + self.on_delete = on_delete + if content is not None: + self.set_content(content) def sanitize_type(self, t): + if t is None: + return "?" if ":" in t: return t.split(":")[-1] return t + def set_content(self, content=None): + print("XSDInputField::set_content {}".format(content)) diff --git a/xsd2tkform/ui/optional.py b/xsd2tkform/ui/optional.py index b4da957..7b856c9 100644 --- a/xsd2tkform/ui/optional.py +++ b/xsd2tkform/ui/optional.py @@ -1,75 +1,164 @@ import tkinter as tk from tkinter.messagebox import showerror -from xsd2tkform.ui.field import XSDInputField -from xsd2tkform.ui.tooltip import ToolTip +from .field import XSDInputField +from .tooltip import ToolTip -from xsd2tkform.core.type import SimpleType, ComplexType +from ..core.type import SimpleType, ComplexType -from xsd2tkform.ui.simpletype import XSDSimpleTypeFrame -from xsd2tkform.ui.complextype import XSDComplexTypeFrame +from .simpletype import XSDSimpleTypeFrame +from .complextype import XSDComplexTypeFrame class OptionalInput(XSDInputField): - def __init__(self, parent, item, schema, bounds=None, *args, **kwargs): - XSDInputField.__init__(self, parent , *args, **kwargs) + def __init__(self, parent, item, schema, bounds=None, on_add_field=None, on_delete_field=None, *args, **kwargs): + XSDInputField.__init__(self, parent , \ + highlightbackground="red",\ + highlightthickness=1,\ + + *args, **kwargs) + self.grid_columnconfigure(0, weight=1) self.count=0 self.bounds=bounds self.type = item.type + self.item=item self.schema = schema + self.fields=[] - b=tk.Button(self, text="Add {}".format(self.sanitize_type(item.type)), command=self.add_field) - # add tooltip - doc_str = self.get_type_docstring(item.type) + self.add_button2=None + self.next_row=0 + self.label_frame=None + self.label=None + + self.on_add_field=on_add_field + self.on_delete_field=on_delete_field + def is_full(self): + return self.count == self.bounds[1] + def remove_label(self): + if self.label is not None: + self.label.destroy() + self.label=None + + def set_label_frame(self, frame): + self.label_frame=frame + + def get_add_button(self, parent): + button_text=self.sanitize_type(self.type) + if button_text in ["float", "string", "integer", "?"]: + button_text=self.item.name + self.add_button2= tk.Button(parent, text=button_text, command=self.add_field) + doc_str = self.get_type_docstring(self.type) if len(doc_str): - ttt = ToolTip(b, doc_str) - b.pack(side=tk.TOP, fill=tk.X, expand=1) - def add_field(self): + ttt = ToolTip(self.add_button2, doc_str) + + + return self.add_button2 + def set_content(self, content, update_grid=True): + self.add_field(content=content, update_grid=update_grid) + def add_label(self): + if self.label_frame is not None and self.label is None: + self.label=tk.Label(self.label_frame, text=self.sanitize_type(self.type)+":") + self.label.grid() + def get_fields(self): + self.fields = [w for w in self.fields if w.winfo_exists()] + return self.fields + def add_field(self, content=None, update_grid=True): if self.bounds[1]=="unbounded": - print("No limit on {} fields".format(self.type)) + pass elif self.count==self.bounds[1]: showerror(title="{} maximum occurences reached".format(self.type), message="A maximum of {} occurences of type {}".format(self.bounds[1], self.type)) return else: pass - + self.add_label() self.count+=1 - - f=self.get_frame_by_type(self.type, delete_button=True) - f.pack(side=tk.TOP, fill=tk.X, expand=1) - #self.fields.append(f.winfo_children()[0]) + + if self.count <= self.bounds[0]: + f=self.get_frame_by_type(self.type, delete_button=False, content=content, parent=self.master, update_grid=update_grid) + else: + f=self.get_frame_by_type(self.type, delete_button=True, content=content, parent=self.master, update_grid=update_grid) + if f is None: + return self.fields.append(f) - def get_frame_by_type(self, t, parent=None, delete_button=False): + if isinstance(f, XSDComplexTypeFrame): + f.collapse() + + + # if the maximum number of fields of this type is attained then remove the add button + if self.count == self.bounds[1] and self.add_button2 is not None: + self.add_button2.grid_remove() + + # update grid in parent + if content is None: + if self.on_add_field is not None: + self.on_add_field() + #self.master.update_grid() + + def get_frame_by_type(self, t, parent=None, delete_button=False, content=None, update_grid=True): if parent is None: parent = self td=self.schema.get(t) + if td is None: + # if type is native type + ttt=t.split(":")[-1] + if ttt=="string" or ttt=="float" or ttt=="integer" or ttt=="int": + # return a SimpleType object + from ..core.restriction import Restriction + #from ..core.type import SimpleType + td=SimpleType(self.item.name, restriction=Restriction(base="string")) + #return XSDSimpleTypeFrame(self.item.name, name=self.item.name, parent=parent,\ + return XSDSimpleTypeFrame(ttt, name=self.item.name, parent=parent,\ + schema=self.schema, delete_button=delete_button,\ + widget_config=self.widget_config,\ + typedef=td,\ + on_delete=self.delete_field) + + if isinstance(td, SimpleType):# in self.simple_types: return XSDSimpleTypeFrame(t, parent=parent,\ schema=self.schema, - delete_button=delete_button) + delete_button=delete_button, + content=content, + widget_config=self.widget_config, + on_delete=self.delete_field) elif isinstance(td, ComplexType): return XSDComplexTypeFrame(t, parent=parent,\ schema=self.schema, - delete_button=delete_button) + delete_button=delete_button, + content=content, + collapsable=True, + widget_config=self.widget_config, + on_delete=self.delete_field) else: # TODO : add Group support print("Group support not yet implemented") - def delete_field(self, t, widget): - print("in delete_field : {}".format(type(widget))) - print(self.fields) - widget.destroy() - self.count-=1 - def get_content(self): - return [i.get_content() for i in self.fields if i.winfo_exists()] + def delete_field(self, t=None, widget=None): + # doesn't seem to be used + self.fields = [w for w in self.fields if w.winfo_exists()] + self.decrement_field_count_by_type(self.type) + #self.count-=1 + + if self.bounds[1]!="unbounded": + if self.bounds[1]-self.count == 1: + #self.add_button2.pack(side=tk.LEFT) + self.add_button2.grid() + if self.on_delete_field is not None: + self.on_delete_field() + #self.master.update_grid() + def get_content(self, nsmap=None, qname_attrs=None): + return [i.get_content(nsmap=nsmap, qname_attrs=qname_attrs) for i in self.fields if i.winfo_exists()] def get_type_docstring(self, t): + if t is None: + return "" td=self.schema.get(t) + if td is None or (not hasattr(td, "annotation")): + return "" a=td.annotation.documentation if len(a): ls=[l for l in a] return td.annotation.documentation[ls[0]] return "" - def decrement_field_count_by_type(self, t): + def decrement_field_count_by_type(self, t, widget=None): self.count-=1 - diff --git a/xsd2tkform/ui/scrollframe.py b/xsd2tkform/ui/scrollframe.py index ac0f0d8..040adf7 100644 --- a/xsd2tkform/ui/scrollframe.py +++ b/xsd2tkform/ui/scrollframe.py @@ -7,7 +7,7 @@ import platform class ScrollFrame(tk.Frame): def __init__(self, parent): super().__init__(parent) # create a frame (self) - + self.bind_ids=[] self.canvas = tk.Canvas(self, borderwidth=0, background="#ffffff") #place canvas on self self.viewPort = tk.Frame(self.canvas, background="#ffffff") #place a frame on the canvas, this frame will hold the child widgets self.vsb = tk.Scrollbar(self, orient="vertical", command=self.canvas.yview) #place a scrollbar on self @@ -18,11 +18,11 @@ class ScrollFrame(tk.Frame): self.canvas_window = self.canvas.create_window((4,4), window=self.viewPort, anchor="nw", #add view port frame to canvas tags="self.viewPort") - self.viewPort.bind("", self.onFrameConfigure) #bind an event whenever the size of the viewPort frame changes. - self.canvas.bind("", self.onCanvasConfigure) #bind an event whenever the size of the canvas frame changes. + self.bind_ids.append((self.viewPort, self.viewPort.bind("", self.onFrameConfigure))) #bind an event whenever the size of the viewPort frame changes. + self.bind_ids.append((self.canvas, self.canvas.bind("", self.onCanvasConfigure))) #bind an event whenever the size of the canvas frame changes. - self.viewPort.bind('', self.onEnter) # bind wheel events when the cursor enters the control - self.viewPort.bind('', self.onLeave) # unbind wheel events when the cursorl leaves the control + self.bind_ids.append((self.viewPort, self.viewPort.bind('', self.onEnter))) # bind wheel events when the cursor enters the control + self.bind_ids.append((self.viewPort, self.viewPort.bind('', self.onLeave))) # unbind wheel events when the cursorl leaves the control self.onFrameConfigure(None) #perform an initial stretch on render, otherwise the scroll region has a tiny border until the first resize @@ -49,18 +49,25 @@ class ScrollFrame(tk.Frame): def onEnter(self, event): # bind wheel events when the cursor enters the control if platform.system() == 'Linux': - self.canvas.bind_all("", self.onMouseWheel) - self.canvas.bind_all("", self.onMouseWheel) + self.bind_ids.append((self.canvas,self.canvas.bind_all("", self.onMouseWheel))) + self.bind_ids.append((self.canvas,self.canvas.bind_all("", self.onMouseWheel))) else: - self.canvas.bind_all("", self.onMouseWheel) + self.bind_ids.append((self.canvas,self.canvas.bind_all("", self.onMouseWheel))) - def onLeave(self, event): # unbind wheel events when the cursorl leaves the control + def onLeave(self, event): # unbind wheel events when the cursorl leaves the control if platform.system() == 'Linux': self.canvas.unbind_all("") self.canvas.unbind_all("") else: self.canvas.unbind_all("") - + def destroy(self): + # unbind all + for w,fid in self.bind_ids: + w.unbind(fid) + self.canvas.unbind_all("") + self.canvas.unbind_all("") + self.canvas.unbind_all("") + super().destroy() # ******************************** diff --git a/xsd2tkform/ui/simpletype.py b/xsd2tkform/ui/simpletype.py index 43c845b..c8ac9e1 100644 --- a/xsd2tkform/ui/simpletype.py +++ b/xsd2tkform/ui/simpletype.py @@ -3,22 +3,142 @@ from tkinter import ttk from lxml import etree -from xsd2tkform.ui.tooltip import ToolTip +from .tooltip import ToolTip from tkcalendar import DateEntry -from xsd2tkform.ui.datetime_selector import DatetimeEntry +from .datetime_selector import DatetimeEntry from .field import XSDInputField + +from amda_xml_manager import delete_image_file + +from .entrywidgets import IntEntry, FloatEntry, BoolEntry + +class XSDAnyInputFrame(XSDInputField): + def __init__(self, parent, input_widget_type, content=None): + XSDInputField.__init__(self, parent) + + self.label=None + self.add_button=None + + self.grid_columnconfigure(0, weight=1) + self.input_widget = input_widget_type(self) + self.input_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=1) + + self.set_content(content) + def set_content(self, content=None): + if content is None: + return + if isinstance(content, etree._Element): + self.input_widget.insert(0, etree.tostring(content, pretty_print=True)) + return + self.input_widget.insert(0, str(content)) + + + def get_label(self, parent): + label_text="any :" + self.label = tk.Label(parent, text=label_text) + return self.label + def get_content(self): + return self.input_widget.get("1.0", tk.END) + + +class XSDAttributeFrame(XSDInputField): + def __init__(self, attribute, parent, on_delete=None, on_add=None): + XSDInputField.__init__(self, parent) + + self.on_delete=on_delete + self.on_add=on_add + self.name = attribute.name + self.type = attribute.type + self.attribute = attribute + if attribute.use=="required": + self.mandatory = True + else: + self.mandatory = False + self.label=None + self.add_button=None + + self.grid_columnconfigure(0, weight=1) + if self.type.endswith("float"): + self.input_widget=FloatEntry(self) + elif self.type.endswith("int") or self.type.endswith("integer"): + self.input_widget=IntEntry(self) + elif self.type.endswith("boolean"): + self.input_widget=BoolEntry(self) + else: + self.input_widget=tk.Entry(self) + self.input_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=1) + + if not self.mandatory: + self.delete_img = tk.PhotoImage(file=delete_image_file) + #self.db = tk.Button(self, text="X", command=self.delete) + self.db = tk.Button(self, image=self.delete_img, command=self.delete) + self.db.pack(side=tk.RIGHT, expand=0) + def is_visible(self): + return "row" in self.grid_info() + def get(self): + return self.input_widget.get() + def set(self, value): + if isinstance(self.input_widget, FloatEntry) or isinstance(self.input_widget, IntEntry) or \ + isinstance(self.input_widget, BoolEntry): + self.input_widget.set(value) + else: + self.input_widget.insert(0, value) + def get_add_button(self, parent): + self.add_button = tk.Button(parent, text=self.name, command=self.add) + return self.add_button + def add(self): + # remove the button + self.grid() + if self.label is not None: + self.label.grid() + if self.add_button is not None: + self.add_button.grid_remove() + if self.on_add is not None: + self.on_add() + def delete(self, *args): + self.grid_remove() + if self.label is not None: + self.label.grid_remove() + # replace the add button + if self.add_button is not None: + self.add_button.grid() + + if self.on_delete is not None: + self.on_delete() + + def get_label(self, parent): + label_text=self.sanitize_type(self.name) + label_text+=" :" + self.label = tk.Label(parent, text=label_text) + return self.label + class XSDSimpleTypeFrame(XSDInputField): - def __init__(self, typename, parent=None, schema=None, delete_button=False, *args, **kwargs): - XSDInputField.__init__(self, parent, *args, **kwargs) + def __init__(self, typename, parent=None, schema=None, delete_button=False, filename=None, content=None, name=None, widget_config={}, typedef=None, input_widget=None,*args, **kwargs): + XSDInputField.__init__(self, parent, widget_config=widget_config,\ + # highlightbackground="blue",\ + # highlightthickness=1,\ + *args, **kwargs) + # flag indicating that the content has been set + self.content_has_been_set = False + + self.grid_columnconfigure(0, weight=1) + self.input_widget_type=input_widget self.type = typename - self.label = tk.Label(self, text="{}: ".format(self.sanitize_type(typename))) - self.label.pack(side=tk.LEFT, expand=0) + self.name = name + + print("XSDSimpleTypeFrame name={}, type={}".format(self.name, self.type)) + + # label reference + self.label=None - self.typedef = schema.get(typename) + if typedef is None: + self.typedef = schema.get(typename) + else: + self.typedef = typedef self.set_tooltip() @@ -27,41 +147,126 @@ class XSDSimpleTypeFrame(XSDInputField): self.input_widget.pack(side=tk.LEFT, fill=tk.BOTH, expand=1) # delete button + self.delete_img = None if delete_button: - self.db = tk.Button(self, text="X", command=self.delete) + self.delete_img = tk.PhotoImage(file=delete_image_file) + #self.db = tk.Button(self, text="X", command=self.delete) + self.db = tk.Button(self, image=self.delete_img, command=self.delete) self.db.pack(side=tk.RIGHT, expand=0) + + # if a filename was given then load the content + if filename is not None: + print("Setting SimpleType content from file : {}".format(filename)) + if content is not None: + self.set_content(content) + def get_label(self, parent): + label_text=self.sanitize_type(self.type) + if label_text in ["string", "float", "integer"]: + label_text=self.name + + if self.name is not None: + label_text=self.name + else: + label_text=self.sanitize_type(self.type) + if label_text in ["string", "float", "integer"]: + label_text=self.name + + label_text+=" :" + self.label = tk.Label(parent, text=label_text) + self.set_tooltip() + return self.label def delete(self): # destroy this widget and decrement the field count in # the master frame - print("self id : ", id(self)) - print([id(e) for e in self.master.fields]) - print(self.master.fields) - self.master.decrement_field_count_by_type(self.type) + #self.master.decrement_field_count_by_type(self.type) self.destroy() + if self.on_delete is not None: + self.on_delete() + if self.label is not None: + self.label.destroy() + def is_full(self): + return self.content_has_been_set + def set_content(self, content, update_grid=True): + text=content.text + if text is None: + text="" + if isinstance(self.input_widget, ttk.Combobox): + if not content.text is None: + self.input_widget.set(content.text) + elif isinstance(self.input_widget, tk.Text): + n_lines = len(text.split("\n"))+1 + self.input_widget.insert("1.0", text) + #self.input_widget.configure(height=n_lines) + + elif isinstance(self.input_widget, tk.Entry): + self.input_widget.insert(0, text) + else: + self.input_widget.set(content.text) + self.content_has_been_set = True def set_tooltip(self): + if self.label is None: + return + if self.typedef is None: + return + if self.typedef.annotation is None: + return if len(self.typedef.annotation.documentation): langs = [k for k in self.typedef.annotation.documentation] - tt = ToolTip(self.label, self.typedef.annotation.documentation[langs[0]]) + tt=ToolTip(self.label, self.typedef.annotation.documentation[langs[0]]) def get_input_widget(self): + if self.input_widget_type is not None: + return self.input_widget_type(self) + if self.sanitize_type(self.type) in self.widget_config: + persot = self.widget_config[self.sanitize_type(self.type)] + if isinstance(persot, tuple): + return persot[0](self, *persot[1]) + return self.widget_config[self.sanitize_type(self.type)](self) if self.typedef.restriction is not None: if self.typedef.restriction.base=="xsd:string": if len(self.typedef.restriction.enum): - b=ttk.Combobox(self, values=self.typedef.restriction.enum, state="readonly") + #b=ttk.Combobox(self, values=self.typedef.restriction.enum, state="readonly") + from .autocompleteentry import AutocompleteCombobox + b=AutocompleteCombobox(self) + b.set_completion_list(self.typedef.restriction.enum) + b.current(0) + b.unbind_class("TCombobox","") + b.unbind_class("TCombobox","") + b.unbind_class("TCombobox","") + return b + #if "Description" in self.type: + # return tk.Text(self) return tk.Entry(self) if self.typedef.restriction.base=="xsd:dateTime": return DatetimeEntry(self) if len(self.typedef.restriction.enum): return ttk.Combobox(self, values=self.typedef.restriction.enum, state="readonly") + if self.type.endswith("float"): + return FloatEntry(self) + if self.type.endswith("int") or self.type.endswith("integer"): + return IntEntry(self) return tk.Entry(self) def get_value(self): + if isinstance(self.input_widget, tk.Text): + return self.input_widget.get("1.0",tk.END).strip() return self.input_widget.get() - def get_content(self): + def get_attribute_values(self): + return {} + def get_content(self, nsmap=None, qname_attrs=None): # returns tree type - t = self.sanitize_type(self.type) - root = etree.Element(t) + if self.name is None: + t = self.sanitize_type(self.type) + else: + t=self.name + if nsmap is not None: + root = etree.Element(t,qname_attrs, nsmap=nsmap) + else: + root = etree.Element(t) + attrib_values=self.get_attribute_values() + for k in attrib_values: + root.set(k, attrib_values[k]) root.text = self.get_value() return root -- libgit2 0.21.2