svghmi/widgets_common.ysl2
author Edouard Tisserant <edouard.tisserant@gmail.com>
Sat, 05 Mar 2022 11:14:00 +0100
branchwxPython4
changeset 3437 ce366d67a5b7
parent 3417 9b9775d230f5
child 3452 a0bd2c4cb8f7
permissions -rw-r--r--
Tests: Enhance robustness of stdout driven waiting state in Sikuli based tests.

Some tests were randomly passing, because from time to time waiting for idle was skiped. It was combination of multiple problems :
- buffering on stdout (now use readline + flush for each write to log)
- it is sometime required to wait for activity before waiting for timeout added "WaitForChangeAndIdle" to "stdoutIdleObserver"
// widgets_common.ysl2

in xsl decl labels(*ptr, name="defs_by_labels") alias call-template {
    with "hmi_element", "$hmi_element";
    with "labels"{text *ptr};
    content;
};

decl optional_labels(*ptr) alias - {
    /* TODO add some per label xslt variable to check if exist */
    labels(*ptr){
        with "mandatory","'no'";
        content;
    }
};

decl activable_labels(*ptr) alias - {
    optional_labels(*ptr) {
        with "subelements","'active inactive'";
        content;
    }
};

in xsl decl widget_desc(%name, match="widget[@type='%name']", mode="widget_desc") alias template {
    type > «@type»
    content;
};

in xsl decl widget_class(%name, *clsname="%nameWidget", match="widget[@type='%name']", mode="widget_class") alias template {
    | class `text **clsname` extends Widget{
    content;
    | }
};

in xsl decl widget_defs(%name, match="widget[@type='%name']", mode="widget_defs") alias template {
    param "hmi_element";
    content;
};

in xsl decl widget_page(%name, match="widget[@type='%name']", mode="widget_page") alias template {
    param "page_desc";
    content;
};

decl gen_index_xhtml alias - {
    content;
};

template "svg:*", mode="hmi_widgets" {
    const "widget", "func:widget(@id)";
    const "eltid","@id";
    const "args" foreach "$widget/arg" > "«func:escape_quotes(@value)»"`if "position()!=last()" > ,`
    const "indexes" foreach "$widget/path" {
        choose {
            when "not(@index)" {
                choose {
                    when "not(@type)" {
                        warning > Widget «$widget/@type» id="«$eltid»" : No match for path "«@value»" in HMI tree
                        > undefined
                    }
                    when "@type = 'PAGE_LOCAL'" 
                        > "«@value»"
                    when "@type = 'HMI_LOCAL'" 
                        > hmi_local_index("«@value»")
                    otherwise 
                        error > Internal error while processing widget's non indexed HMI tree path : unknown type
                }
            }
            otherwise {
                > «@index»
            }
        }
        if "position()!=last()" > ,
    }

    const "minmaxes" foreach "$widget/path" {
        choose {
            when "@min and @max"
                > [«@min»,«@max»]
            otherwise
                > undefined
        }
        if "position()!=last()" > ,
    }

    const "freq" choose {
        when "$widget/@freq"
            > «$widget/@freq»
        otherwise
            > undefined
    }

    |   "«@id»": new «$widget/@type»Widget ("«@id»",«$freq»,[«$args»],[«$indexes»],[«$minmaxes»],{
    apply "$widget", mode="widget_defs" with "hmi_element",".";
    |   })`if "position()!=last()" > ,`
}

emit "preamble:local-variable-indexes" {
    ||

    let hmi_locals = {};
    var last_remote_index = hmitree_types.length - 1;
    var next_available_index = hmitree_types.length;
    let cookies = new Map(document.cookie.split("; ").map(s=>s.split("=")));

    const local_defaults = {
    ||
    foreach "$parsed_widgets/widget[starts-with(@type,'VarInit')]"{
        if "count(path) != 1" error > VarInit «@id» must have only one variable given.
        if "path/@type != 'PAGE_LOCAL' and path/@type != 'HMI_LOCAL'" error > VarInit «@id» only applies to HMI variable.
        >     "«path/@value»":
        choose {
            when "@type = 'VarInitPersistent'" > cookies.has("«path/@value»")?cookies.get("«path/@value»"):«arg[1]/@value»
            otherwise > «arg[1]/@value»
        }
        > \n
        if "position()!=last()" > ,
    }
    ||
    };

    const persistent_locals = new Set([
    ||
    foreach "$parsed_widgets/widget[@type='VarInitPersistent']"{
    |    "«path/@value»"`if "position()!=last()" > ,`
    }
    ||
    ]);
    var persistent_indexes = new Map();
    var cache = hmitree_types.map(_ignored => undefined);
    var updates = new Map();

    function page_local_index(varname, pagename){
        let pagevars = hmi_locals[pagename];
        let new_index;
        if(pagevars == undefined){
            new_index = next_available_index++;
            hmi_locals[pagename] = {[varname]:new_index}
        } else {
            let result = pagevars[varname];
            if(result != undefined) {
                return result;
            }

            new_index = next_available_index++;
            pagevars[varname] = new_index;
        }
        let defaultval = local_defaults[varname];
        if(defaultval != undefined) {
            cache[new_index] = defaultval; 
            updates.set(new_index, defaultval);
            if(persistent_locals.has(varname))
                persistent_indexes.set(new_index, varname);
        }
        return new_index;
    }

    function hmi_local_index(varname){
        return page_local_index(varname, "HMI_LOCAL");
    }
    ||
}

emit "preamble:widget-base-class" {
    ||
    var pending_widget_animates = [];

    class Widget {
        offset = 0;
        frequency = 10; /* FIXME arbitrary default max freq. Obtain from config ? */
        unsubscribable = false;
        pending_animate = false;

        constructor(elt_id, freq, args, indexes, minmaxes, members){
            this.element_id = elt_id;
            this.element = id(elt_id);
            this.args = args;
            this.indexes = indexes;
            this.minmaxes = minmaxes;
            Object.keys(members).forEach(prop => this[prop]=members[prop]);
            this.lastapply = indexes.map(() => undefined);
            this.inhibit = indexes.map(() => undefined);
            this.pending = indexes.map(() => undefined);
            this.bound_unhinibit = this.unhinibit.bind(this);
            this.forced_frequency = freq;
        }

        unsub(){
            /* remove subsribers */
            if(!this.unsubscribable)
                for(let i = 0; i < this.indexes.length; i++) {
                    /* flush updates pending because of inhibition */
                    let inhibition = this.inhibit[i];
                    if(inhibition != undefined){
                        clearTimeout(inhibition);
                        this.lastapply[i] = undefined;
                        this.unhinibit(i);
                    }
                    let index = this.indexes[i];
                    if(this.relativeness[i])
                        index += this.offset;
                    subscribers(index).delete(this);
                }
            this.offset = 0;
            this.relativeness = undefined;
        }

        sub(new_offset=0, relativeness, container_id){
            this.offset = new_offset;
            this.relativeness = relativeness;
            this.container_id = container_id ;
            /* add this's subsribers */
            if(!this.unsubscribable)
                for(let i = 0; i < this.indexes.length; i++) {
                    let index = this.get_variable_index(i);
                    if(index == undefined) continue;
                    subscribers(index).add(this);
                }
            need_cache_apply.push(this); 
        }

        apply_cache() {
            if(!this.unsubscribable) for(let index in this.indexes){
                /* dispatch current cache in newly opened page widgets */
                let realindex = this.get_variable_index(index);
                if(realindex == undefined) continue;
                let cached_val = cache[realindex];
                if(cached_val != undefined)
                    this._dispatch(cached_val, cached_val, index);
            }
        }

        get_variable_index(varnum) {
            let index = this.indexes[varnum];
            if(typeof(index) == "string"){
                index = page_local_index(index, this.container_id);
            } else {
                if(this.relativeness[varnum]){
                    index += this.offset;
                }
            }
            return index;
        }

        overshot(new_val, max) {
        }

        undershot(new_val, min) {
        }

        clip_min_max(index, new_val) {
            let minmax = this.minmaxes[index];
            if(minmax !== undefined && typeof new_val == "number") {
                let [min,max] = minmax;
                if(new_val < min){
                    this.undershot(new_val, min);
                    return min;
                }
                if(new_val > max){
                    this.overshot(new_val, max);
                    return max;
                }
            }
            return new_val;
        }

        change_hmi_value(index, opstr) {
            let realindex = this.get_variable_index(index);
            if(realindex == undefined) return undefined;
            let old_val = cache[realindex];
            let new_val = eval_operation_string(old_val, opstr);
            new_val = this.clip_min_max(index, new_val);
            return apply_hmi_value(realindex, new_val);
        }

        _apply_hmi_value(index, new_val) {
            let realindex = this.get_variable_index(index);
            if(realindex == undefined) return undefined;
            new_val = this.clip_min_max(index, new_val);
            return apply_hmi_value(realindex, new_val);
        }

        unhinibit(index){
            this.inhibit[index] = undefined;
            let new_val = this.pending[index];
            this.pending[index] = undefined;
            return this.apply_hmi_value(index, new_val);
        }

        apply_hmi_value(index, new_val) {
            if(this.inhibit[index] == undefined){
                let now = Date.now();
                let min_interval = 1000/this.frequency;
                let lastapply = this.lastapply[index];
                if(lastapply == undefined || now > lastapply + min_interval){
                    this.lastapply[index] = now;
                    return this._apply_hmi_value(index, new_val);
                }
                else {
                    let elapsed = now - lastapply;
                    this.pending[index] = new_val;
                    this.inhibit[index] = setTimeout(this.bound_unhinibit, min_interval - elapsed, index);
                }
            }
            else {
                this.pending[index] = new_val;
                return new_val;
            }
        }

        new_hmi_value(index, value, oldval) {
            // TODO avoid searching, store index at sub()
            for(let i = 0; i < this.indexes.length; i++) {
                let refindex = this.get_variable_index(i);
                if(refindex == undefined) continue;

                if(index == refindex) {
                    this._dispatch(value, oldval, i);
                    break;
                }
            }
        }
        
        _dispatch(value, oldval, varnum) {
            let dispatch = this.dispatch;
            if(dispatch != undefined){
                try {
                    dispatch.call(this, value, oldval, varnum);
                } catch(err) {
                    console.log(err);
                }
            }
        }

        _animate(){
            this.animate();
            this.pending_animate = false;
        }

        request_animate(){
            if(!this.pending_animate){
                pending_widget_animates.push(this);
                this.pending_animate = true;
                requestHMIAnimation();
            }

        }

        activate_activable(eltsub) {
            eltsub.inactive.style.display = "none";
            eltsub.active.style.display = "";
        }

        inactivate_activable(eltsub) {
            eltsub.active.style.display = "none";
            eltsub.inactive.style.display = "";
        }
    }
    ||
}

const "excluded_types", "str:split('Page VarInit VarInitPersistent')";

// Key to filter unique types
key "TypesKey", "widget", "@type";

emit "declarations:hmi-classes" {
    const "used_widget_types", """$parsed_widgets/widget[
                                    generate-id() = generate-id(key('TypesKey', @type)) and 
                                    not(@type = $excluded_types)]""";
    apply "$used_widget_types", mode="widget_class";
}

template "widget", mode="widget_class"
||
class «@type»Widget extends Widget{
    /* empty class, as «@type» widget didn't provide any */
}
||

const "included_ids","$parsed_widgets/widget[not(@type = $excluded_types) and not(@id = $discardable_elements/@id)]/@id";
const "hmi_widgets","$hmi_elements[@id = $included_ids]";
const "result_widgets","$result_svg_ns//*[@id = $hmi_widgets/@id]";

emit "declarations:hmi-elements" {
    | var hmi_widgets = {
    apply "$hmi_widgets", mode="hmi_widgets";
    | }
}

function "defs_by_labels" {
    param "labels","''";
    param "mandatory","'yes'";
    param "subelements","/..";
    param "hmi_element";
    const "widget_type","@type";
    foreach "str:split($labels)" {
        const "name",".";
        const "elt","$result_widgets[@id = $hmi_element/@id]//*[@inkscape:label=$name][1]";
        choose {
            when "not($elt/@id)" {
                if "$mandatory='yes'" {
                    error > «$widget_type» widget must have a «$name» element
                }
                // otherwise produce nothing
            }
            otherwise {
                |     «$name»_elt: id("«$elt/@id»"),
                if "$subelements" {
                |     «$name»_sub: {
                    foreach "str:split($subelements)" {
                        const "subname",".";
                        const "subelt","$elt/*[@inkscape:label=$subname][1]";
                        choose {
                            when "not($subelt/@id)" {
                                if "$mandatory='yes'" {
                                    error > «$widget_type» widget must have a «$name»/«$subname» element
                                }
                |         /* missing «$name»/«$subname» element */
                            }
                            otherwise {
                |         "«$subname»": id("«$subelt/@id»")`if "position()!=last()" > ,`
                            }
                        }
                    }
                |     },
                }
            }
        }
    }
}

def "func:escape_quotes" {
    param "txt";
    // have to use a python string to enter escaped quote
    // const "frstln", "string-length($frst)";
    choose {
        when !"contains($txt,'\"')"! {
            result !"concat(substring-before($txt,'\"'),'\\\"',func:escape_quotes(substring-after($txt,'\"')))"!;
        }
        otherwise {
            result "$txt";
        }
    }
}