svghmi/widgets_common.ysl2
author Edouard Tisserant <edouard.tisserant@gmail.com>
Tue, 29 Dec 2020 15:35:39 +0100
branchsvghmi
changeset 3099 c7d14130401f
parent 3098 5823b73b132f
child 3101 4cbf024a6640
permissions -rw-r--r--
SVGHMI: Added clipping to min and max value for all widget using @path,min,max syntax. Added overshot() and undershot() methods to widget base class, called when value is limited to max or min respectively.
// 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;
    }
};

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`if "position()!=last()" > ,`
                    }
                    when "@type = 'PAGE_LOCAL'" 
                        > "«@value»"`if "position()!=last()" > ,`
                    when "@type = 'HMI_LOCAL'" 
                        > hmi_local_index("«@value»")`if "position()!=last()" > ,`
                }
            }
            otherwise {
                > «@index»`if "position()!=last()" > ,`
            }
        }
    }

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

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

def "func:unique_types" {
    param "elts_with_type";
    choose {
        when "count($elts_with_type) > 1" {
            const "prior_results","func:unique_types($elts_with_type[position()!=last()])";
            choose {
                when "$elts_with_type[last()][@type = $prior_results/@type]"{
                    // type already in
                    result "$prior_results";
                }
                otherwise {
                    result "$prior_results | $elts_with_type[last()]";
                }
            }
        }
        otherwise {
            result "$elts_with_type";
        }
    }
}

emit "preamble:local-variable-indexes" {
    ||
    let hmi_locals = {};
    var last_remote_index = hmitree_types.length - 1;
    var next_available_index = hmitree_types.length;

    const local_defaults = {
    ||
    foreach "$parsed_widgets/widget[@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»":«arg[1]/@value»`if "position()!=last()" > ,`
    }
    ||
    };
    var cache = hmitree_types.map(_ignored => undefined);

    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; 
        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,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]);
        }

        unsub(){
            /* remove subsribers */
            if(!this.unsubscribable)
                for(let i = 0; i < this.indexes.length; 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);
        }

        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();
            }

        }
    }
    ||
}

emit "declarations:hmi-classes" {
    const "used_widget_types", "func:unique_types($parsed_widgets/widget)";
    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 "excluded_types", "str:split('Page Lang VarInit')";
const "included_ids","$parsed_widgets/widget[not(@type = $excluded_types)]/@id";

emit "declarations:hmi-elements" {
    | var hmi_widgets = {
    apply "$hmi_elements[@id = $included_ids]", 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_svg_ns//*[@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";
        }
    }
}