svghmi/detachable_pages.ysl2
author Edouard Tisserant <edouard.tisserant@gmail.com>
Tue, 03 Dec 2024 21:22:45 +0100
changeset 4057 cf42c3dbdd5c
parent 3923 77a1846390ef
permissions -rw-r--r--
IDE: Fix PLCOpenEditor (wxPython4 menus) and drop broken PDF doc support.
// detachable_pages.ysl2
//
// compute what elements are required by pages
// and decide where to cut when removing/attaching
// pages elements on page switch

const "hmi_pages_descs", "$parsed_widgets/widget[@type = 'Page']";
const "hmi_pages", "$hmi_elements[@id = $hmi_pages_descs/@id]";

const "default_page" choose {
    when "count($hmi_pages) > 1" {
        choose {
            when "$hmi_pages_descs/arg[1]/@value = 'Home'" > Home
            otherwise {
                error > No Home page defined!
            }
        }
    }
    when "count($hmi_pages) = 0" {
        error > No page defined!
    }
    otherwise > «func:widget($hmi_pages/@id)/arg[1]/@value»
}

emit "preamble:default-page" {
    |
    | var default_page = "«$default_page»";
    const "screensaverpage", "$hmi_pages_descs[arg[1]/@value = 'ScreenSaver']";
    const "delay" choose {
        when "$screensaverpage" {
            const "delaystr", "$screensaverpage/arg[2]/@value";
            if "not(regexp:test($delaystr,'^[0-9]+$'))"
                error > ScreenSaver page has missing or malformed delay argument.
            value "$delaystr";
        }
        otherwise > null
    }
    | var screensaver_delay = «$delay»;
}

const "keypads_descs", "$parsed_widgets/widget[@type = 'Keypad']";
const "keypads", "$hmi_elements[@id = $keypads_descs/@id]";

// returns all directly or indirectly refered elements
def "func:refered_elements" {
    param "elems";
    const "descend", "$elems/descendant-or-self::svg:*";
    const "clones", "$descend[self::svg:use]";
    const "originals", "//svg:*[concat('#',@id) = $clones/@xlink:href]";
    choose {
        when "$originals"
            result "$descend | func:refered_elements($originals)";
        otherwise
            result "$descend";
    }
}

// variable "overlapping_geometry" was added for optimization.
// It avoids calling func:overlapping_geometry 3 times for each page
// (apparently libxml doesn't cache exslt function results)
// in order to optimize further, func:overlapping_geometry 
// should be implemented in python or even C,
// as this is still the main bottleneck here
const "_overlapping_geometry" {
    foreach "$hmi_pages | $keypads" {
        const "k", "concat('overlapping:', @id)";
        value "ns:ProgressStart($k, concat('collecting membership of ', @inkscape:label))";
        elt {
            attrib "id" > «@id»
            copy "func:overlapping_geometry(.)";
        }
        value "ns:ProgressEnd($k)";
    }
}

const "overlapping_geometry", "exsl:node-set($_overlapping_geometry)";

def "func:all_related_elements" {
    param "page";
    const "page_overlapping_geometry", "$overlapping_geometry/elt[@id = $page/@id]/*";
    const "overlapping_candidates", "//svg:*[not(starts-with((ancestor::svg:g | .) /@inkscape:label, 'DISCARD:'))]";
    const "page_overlapping_elements", "$overlapping_candidates[@id = $page_overlapping_geometry/@Id]";
    const "page_widgets_elements", """
        $hmi_elements[not(@id=$page/@id)
                      and descendant-or-self::svg:*/@id = $page_overlapping_elements/@id]
        /descendant-or-self::svg:*""";
    const "page_sub_elements", "func:refered_elements($page | $page_overlapping_elements | $page_widgets_elements)";
    result "$page_sub_elements";
}


def "func:required_elements" {
    param "pages"; 
    choose{
        when "$pages"{
            result """func:all_related_elements($pages[1])
                      | func:required_elements($pages[position()!=1])""";
        }otherwise{
            result "/..";
        }
    }
}

const "required_page_elements",
    "func:required_elements($hmi_pages | $keypads)/ancestor-or-self::svg:*";

const "required_list_elements", "func:refered_elements(($hmi_lists | $hmi_textlists)[@id = $required_page_elements/@id])/ancestor-or-self::svg:*";

const "required_elements", "$defs | $required_list_elements | $required_page_elements";

const "discardable_elements", "//svg:*[not(@id = $required_elements/@id)]";

def "func:sumarized_elements" {
    param "elements";
    const "short_list", "$elements[not(ancestor::*/@id = $elements/@id)]";
    const "filled_groups", """$short_list/parent::svg:g[
        not(child::*[
            not(@id = $discardable_elements/@id) and
            not(@id = $short_list/@id)
        ])]""";
    const "groups_to_add", "$filled_groups[not(ancestor::*/@id = $filled_groups/@id)]";
    result "$groups_to_add | $short_list[not(ancestor::*/@id = $filled_groups/@id)]";
}

def "func:detachable_elements" {
    param "pages";
    choose{
        when "$pages"{
            result """func:sumarized_elements(func:all_related_elements($pages[1]))
                      | func:detachable_elements($pages[position()!=1])""";
        }otherwise{
            result "/..";
        }
    }
}

// Avoid nested detachables
const "_detachable_elements", "func:detachable_elements($hmi_pages | $keypads)";
const "detachable_elements", "$_detachable_elements[not(ancestor::*/@id = $_detachable_elements/@id)]";

emit "declarations:page-class" {
    | class PageWidget extends Widget{}
}

emit "declarations:detachable-elements" {
    |
    | var detachable_elements = {
    foreach "$detachable_elements"{
    |     "«@id»":[id("«@id»"), id("«../@id»")]`if "position()!=last()" > ,`
    }
    | }
}

const "forEach_widgets_ids", "$parsed_widgets/widget[@type = 'ForEach']/@id";
const "forEach_widgets", "$hmi_widgets[@id = $forEach_widgets_ids]";
const "in_forEach_widget_ids", "func:refered_elements($forEach_widgets)[not(@id = $forEach_widgets_ids)]/@id";

template "svg:*", mode="page_desc" {
    if "ancestor::*[@id = $hmi_pages/@id]" error > HMI:Page «@id» is nested in another HMI:Page


    const "desc", "func:widget(@id)";
    const "pagename", "$desc/arg[1]/@value";
    const "msg", "concat('generating page description ', $pagename)";
    value "ns:ProgressStart($pagename, $msg)";
    const "page", ".";
    const "p", "$geometry[@Id = $page/@id]";

    const "page_all_elements", "func:all_related_elements($page)";

    const "all_page_widgets","$hmi_widgets[@id = $page_all_elements/@id and @id != $page/@id]";
    const "page_managed_widgets","$all_page_widgets[not(@id=$in_forEach_widget_ids)]";

    const "page_root_path", "$desc/path[not(@assign)]";
    if "count($page_root_path)>1"
        error > Page id="«$page/@id»" : only one root path can be declared

    const "page_relative_widgets",
        "$page_managed_widgets[func:is_descendant_path(func:widget(@id)/path/@value, $page_root_path/@value)]";

    // Take closest ancestor in detachable_elements
    // since nested detachable elements are filtered out
    const "sumarized_page", 
        """func:sumarized_elements($page_all_elements)""";

    const "required_detachables", 
        """$sumarized_page/
           ancestor-or-self::*[@id = $detachable_elements/@id]""";

    |   "«$pagename»": {
    |     bbox: [«$p/@x», «$p/@y», «$p/@w», «$p/@h»],
    if "count($page_root_path)=1"{
        if "count($page_root_path/@index)=0"
            warning > Page id="«$page/@id»" : No match for path "«$page_root_path/@value»" in HMI tree
    |     page_index: «$page_root_path/@index»,
    |     page_class: "«$indexed_hmitree/*[@hmipath = $page_root_path/@value]/@class»",
    }
    |     widgets: [
    |         [hmi_widgets["«$page/@id»"], []],
    foreach "$page_managed_widgets" {
        const "widget_paths_relativeness" 
            foreach "func:widget(@id)/path" {
                value "func:is_descendant_path(@value, $page_root_path/@value)";
                if "position()!=last()" > ,
            }
    |         [hmi_widgets["«@id»"], [«$widget_paths_relativeness»]]`if "position()!=last()" > ,`
    }
    |     ],
    |     jumps: [
    foreach "$parsed_widgets/widget[@id = $all_page_widgets/@id and @type='Jump']" {
    |         hmi_widgets["«@id»"]`if "position()!=last()" > ,`
    }
    |     ],
    |     required_detachables: {
    foreach "$required_detachables" {
    |         "«@id»": detachable_elements["«@id»"]`if "position()!=last()" > ,`
    }
    |     }
    apply "$parsed_widgets/widget[@id = $all_page_widgets/@id]", mode="widget_page"{
        with "page_desc", "$desc";
    }
    |   }`if "position()!=last()" > ,`
    value "ns:ProgressEnd($pagename)";
}

emit "definitions:page-desc" {
    |
    | var page_desc = {
    apply "$hmi_pages", mode="page_desc";
    | }
}

template "*", mode="widget_page";


emit "debug:detachable-pages" {
    |
    | DETACHABLES:
    foreach "$detachable_elements"{
        |  «@id»
    }
    | DISCARDABLES:
    foreach "$discardable_elements"{
        |  «@id»
    }
    | In Foreach:
    foreach "$in_forEach_widget_ids"{
        |  «.»
    }
    | Overlapping 
    apply "$overlapping_geometry", mode="testtree";
}