diff --git a/ltk 01 -project starter/README.md b/ltk 01 -project starter/README.md new file mode 100644 index 0000000..d6285cf --- /dev/null +++ b/ltk 01 -project starter/README.md @@ -0,0 +1,22 @@ +When developing an application which is larger than a demo, its useful to have a better structure. + +This series is about presenting a better project-based structure. + +# Part 1: + +## Pyodide (full python) or Micropython +We want to delay the decision of choosing between these two as long as possible. +To this end the index.html has both possible with Pyodide initially disabled. + +## Loading animation +The very first time a user hits your page, there will be a delay. +This delay will always be longer for Pyodide than for Micropython. +The index.html has a preloading section for required css and a modal dialog. + +## Terminal +When initially developing it's often useful to have the python terminal available. +The terminal takes up a bit of room but it can be resized. + +## Initial user interaction +Dom manipulation is supported via pyscript.web +Next steps are to go to the ltk for all screen UI diff --git a/ltk 01 -project starter/index.html b/ltk 01 -project starter/index.html new file mode 100644 index 0000000..db2ceeb --- /dev/null +++ b/ltk 01 -project starter/index.html @@ -0,0 +1,90 @@ + + + + Newtitle + + + + + + + + + + + + + + +
+

Building:

+

Newtitle

+

Assembling bits and pieces...

+

Almost there...

+
+ + + + + + + + + \ No newline at end of file diff --git a/ltk 01 -project starter/main.css b/ltk 01 -project starter/main.css new file mode 100644 index 0000000..3869831 --- /dev/null +++ b/ltk 01 -project starter/main.css @@ -0,0 +1,3 @@ +.css-class1 { + background: #f8f6e7; +} \ No newline at end of file diff --git a/ltk 01 -project starter/main.py b/ltk 01 -project starter/main.py new file mode 100644 index 0000000..31799f7 --- /dev/null +++ b/ltk 01 -project starter/main.py @@ -0,0 +1,34 @@ +from pyscript.web import page, div, button, span, br +from pyscript import when, window, config +MICROPYTHON = config["type"] == "mpy" # if we need to know if we're running pyodide or micropython + +if '__terminal__' in locals(): + __terminal__.resize(60, 10) + +def add_ui(toplevelpage): + toplevelpage.append( + div( + button(span("Hello! "), span("World!")), + br(), + button("Click me!", id="my-button"), + classes=["css-class1", "css-class2"], + style={"border": "2px dashed red"} + )) + +def my_button_click_handler(event): + print("The button has been clicked!") + +if __name__ == "__main__": + print(f"Printing to Terminal: ({"mpy" if MICROPYTHON else "pyodide"})") + add_ui(page) + # UI built + when("click", "#my-button", handler=my_button_click_handler) + # explain + explanation = ["Index example:","- Specify micropython or pyodide in index.html", + " - two manual changes needed in index.html to swap", + "- Add a loading animation", + "- Include a resized Terminal for manual debugging", + "- basic pyscript.web based dom manipulation" + ] + print("\n".join(explanation)) + \ No newline at end of file diff --git a/ltk 01 -project starter/pyscript_mpy.toml b/ltk 01 -project starter/pyscript_mpy.toml new file mode 100644 index 0000000..9c34853 --- /dev/null +++ b/ltk 01 -project starter/pyscript_mpy.toml @@ -0,0 +1 @@ +name = "Newtitle" diff --git a/ltk 01 -project starter/pyscript_py.toml b/ltk 01 -project starter/pyscript_py.toml new file mode 100644 index 0000000..9c34853 --- /dev/null +++ b/ltk 01 -project starter/pyscript_py.toml @@ -0,0 +1 @@ +name = "Newtitle" diff --git a/ltk 02 -project starter/README.md b/ltk 02 -project starter/README.md new file mode 100644 index 0000000..aa3f604 --- /dev/null +++ b/ltk 02 -project starter/README.md @@ -0,0 +1,18 @@ +When developing an application which is larger than a demo, its useful to have a better structure. + +This series is about presenting a better project-based structure. + +# Part 2: +Building on ideas described in Part1. + - Pyodide (full python) or Micropython + - "Loading" animation + +## Initial user interaction +We are now using the Ltk for Dom manipulation and all screen UI. +Ltk has jQuery base, so can also use that approach for Selectors and dom manipulation + +## Terminal +The Ltk allows us to move the terminal into a sliding section. +This means we can minimise the terminal on-screen while still having full access. + + diff --git a/ltk 02 -project starter/index.html b/ltk 02 -project starter/index.html new file mode 100644 index 0000000..29dff4e --- /dev/null +++ b/ltk 02 -project starter/index.html @@ -0,0 +1,96 @@ + + + + Newtitle + + + + + + + + + + + + + + + + + + + + +
+

Building:

+

Newtitle

+

Assembling bits and pieces...

+

Almost there...

+
+ + + + + + + + + \ No newline at end of file diff --git a/ltk 02 -project starter/main.css b/ltk 02 -project starter/main.css new file mode 100644 index 0000000..3fb2c0f --- /dev/null +++ b/ltk 02 -project starter/main.css @@ -0,0 +1,8 @@ +.description { + font-size: 1.0rem; + font-style: light; + border: 2px dashed red; +} +#graphic { + border: 1px dashed #0d720d; +} \ No newline at end of file diff --git a/ltk 02 -project starter/main.py b/ltk 02 -project starter/main.py new file mode 100644 index 0000000..4c4190a --- /dev/null +++ b/ltk 02 -project starter/main.py @@ -0,0 +1,93 @@ +#imports +import ltk +import svg as SVG +import random +from pyscript import config +MICROPYTHON = config["type"] == "mpy" # if we need to know if we're running pyodide or micropython + +if '__terminal__' in locals(): + __terminal__.resize(60, 30) + +# for SVG +solid_style = {"fill":"#AA0", "stroke":"#A00", "stroke-width":"1px"} + +def create_svg_box(size=5): + overall = 100 + box_size = 100 / size + highlight_svg = SVG.svg(width=overall, height=overall, + preserveAspectRatio="xMidYMid meet", + viewBox=f"0 0 {overall+2} {overall+2}") + # Transform the group to move its children + grp = SVG.g(id="highlight", transform='translate(0 0)', style=solid_style) + highlight_svg.appendChild(grp) + grp.appendChild(SVG.rect(x=0, y=0, width=box_size, height=box_size)) + # + return highlight_svg + +def button_click_handler(event): + print("The button has been clicked, the svg transformed") + # move the svg child + x,y = random.randrange(0, 100-20), random.randrange(0, 100-20) + ltk.find("#highlight").attr("transform", f"translate({x} {y})") + +class UI_widget(object): + """ + Top level object that shows App. + """ + + def __init__(self): + self.explanation = ["Ltk project example:","- Swap between micropython or pyodide in index.html", + "- Everything from starter_01 but using the Ltk for UI instead", + "- 'Ltk' used instead of direct dom manipulation", + "- Ltk has jQuery base, so can also use that approach for Selectors and dom manipulation", + "- The Terminal is in a top level slider. Try moving it out of the way", + "- SVG is included for more complex UIs" + ] + + def create(self): + info_paragraph = ltk.Paragraph("
".join(self.explanation)).attr("id","explanation").addClass("description") + return (ltk.VBox( + ltk.HorizontalSplitPane( # or VerticalSplitPane if your UI is better that way + ltk.Div( + # put your UI in here + # - can easily just remove these dummy panes when you no longer need the terminal + ltk.Div( + ltk.Button("Click me!", button_click_handler).attr("id","my-button") + ), + ltk.Div().attr("id", "graphic"), + # overall description + info_paragraph + ).attr("id", "mydiv").css("border", "2px solid red"), + + # For the terminal + ltk.Div().attr("id", "forterm").css("border", "2px solid green"), + "Temp-split-pane" + ) + )) + + +if __name__ == "__main__": + w = UI_widget() + widget = w.create() + widget.appendTo(ltk.window.document.body) + + # move terminal to sliding pane using one of these methods: + # - the usual Javascript mechanism + term = ltk.window.document.getElementsByTagName("py-terminal")[0] + term_box = ltk.window.document.getElementById("forterm") + # - OR + # Using ltk.find - jquery style selectors and python lists + term = ltk.find("py-terminal")[0] + term_box = ltk.find("#forterm")[0] + # + term_box.appendChild(term) + # - OR + # Using the jQuery interface with all Jquery chaining flexibility + ltk.jQuery("#forterm")[0].append(ltk.jQuery("py-terminal")[0]) + # remind us of py/mpy + print(f"The Terminal: ({"mpy" if MICROPYTHON else "pyodide"})") + # svg insert into Div + somesvg = create_svg_box() + ltk.find("#graphic").html(somesvg.outerHTML) + + \ No newline at end of file diff --git a/ltk 02 -project starter/pyscript_mpy.toml b/ltk 02 -project starter/pyscript_mpy.toml new file mode 100644 index 0000000..22fdb2e --- /dev/null +++ b/ltk 02 -project starter/pyscript_mpy.toml @@ -0,0 +1,12 @@ +name = "Newtitle" + +[files] +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/jquery.py" = "ltk/jquery.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/widgets.py" = "ltk/widgets.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/pubsub.py" = "ltk/pubsub.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/__init__.py" = "ltk/__init__.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/logger.py" = "ltk/logger.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css" + +"./svg.py" = "" \ No newline at end of file diff --git a/ltk 02 -project starter/pyscript_py.toml b/ltk 02 -project starter/pyscript_py.toml new file mode 100644 index 0000000..5388c0f --- /dev/null +++ b/ltk 02 -project starter/pyscript_py.toml @@ -0,0 +1,9 @@ +name = "Newtitle" + +packages = [ "pyscript-ltk==0.2.20"] + +[files] +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css" + +"./svg.py" = "" \ No newline at end of file diff --git a/ltk 03 -project starter/README.md b/ltk 03 -project starter/README.md new file mode 100644 index 0000000..bf42d7d --- /dev/null +++ b/ltk 03 -project starter/README.md @@ -0,0 +1,20 @@ +When developing an application which is larger than a demo, its useful to have a better structure. + +This series is about presenting a better project-based structure. + +# Part 3: +Building on ideas described in Parts 1,2,3. + - Pyodide (full python) or Micropython + - "Loading" animation + +Now adding: +## Reactive UI +The Ltk has a Reaactive parameter capability where named instance variables become reactive in the UI. +As the user changes the UI, the python variable will update and vice versa. +There is a simple mechanism to manage race conditions when changing one reactive variable affects any number of other reactive variables. + +## Interactive SVG controls +An SVG module has been added so SVG style UI can be added and full interaction is available. +Either by using transforms to existing SVG elements or replacing the SVG in whole or in part. + +In this demo we transform an svg rect to a random position when the button is clicked. \ No newline at end of file diff --git a/ltk 03 -project starter/index.html b/ltk 03 -project starter/index.html new file mode 100644 index 0000000..29dff4e --- /dev/null +++ b/ltk 03 -project starter/index.html @@ -0,0 +1,96 @@ + + + + Newtitle + + + + + + + + + + + + + + + + + + + + +
+

Building:

+

Newtitle

+

Assembling bits and pieces...

+

Almost there...

+
+ + + + + + + + + \ No newline at end of file diff --git a/ltk 03 -project starter/main.css b/ltk 03 -project starter/main.css new file mode 100644 index 0000000..a4bff9c --- /dev/null +++ b/ltk 03 -project starter/main.css @@ -0,0 +1,23 @@ +.description { + font-size: 1.0rem; + font-style: light; + border: 2px dashed #e232ca; +} +.basetext { + font-size: 1.4rem; + padding: 3px; +} +.short { + width: 60px; + align-self: center; + padding: 3px; +} +#graphic { + border: 1px dashed #0d720d; +} + +/* ltk overrides */ +.ltk-input { + font-size: 1.4rem; + padding: 3px; +} \ No newline at end of file diff --git a/ltk 03 -project starter/main.py b/ltk 03 -project starter/main.py new file mode 100644 index 0000000..1a8077c --- /dev/null +++ b/ltk 03 -project starter/main.py @@ -0,0 +1,150 @@ +#imports +import ltk +import svg as SVG +import random +from pyscript import config +MICROPYTHON = config["type"] == "mpy" # if we need to know if we're running pyodide or micropython + +if '__terminal__' in locals(): # resize terminal if started in index.html + __terminal__.resize(60, 10) + +# globals +g_widget = None # Hold ref to widget for nested components +# for SVG +solid_style = {"fill":"#AA0", "stroke":"#A00", "stroke-width":"1px"} + +def create_svg_box(size=5): + overall = 100 + box_size = 100 / size + highlight_svg = SVG.svg(width=overall, height=overall, + preserveAspectRatio="xMidYMid meet", + viewBox=f"0 0 {overall+2} {overall+2}") + # Transform the group to move its children + grp = SVG.g(id="highlight", transform='translate(0 0)', style=solid_style) + highlight_svg.appendChild(grp) + grp.appendChild(SVG.rect(x=0, y=0, width=box_size, height=box_size)) + # + return highlight_svg + +## Events +def button_click_handler(event): + print("The button has been clicked, the svg transformed") + print(f"- checkbox clicked {g_widget.params.count_check_ticks} times.") + # move the svg child + x,y = random.randrange(0, 100-20), random.randrange(0, 100-20) + ltk.find("#highlight").attr("transform", f"translate({x} {y})") + + +### Reactive UI variables and behaviour +class Reactive_params(ltk.Model): + """ + """ + # These variables are all reactive + check_me = False + Drawin = 12 # Can use Drawin: int = 12 but python runtime does not check + Draw_label = "Subtract 2" + + def __init__(self): + """ + variables instanced here will not be reactive (only the classvars above) + but its a good place to put all the other variables your widget needs. + """ + super().__init__() + self.inhibit_update = False + self.count_check_ticks = 0 # as a useless example + + def changed(self, name, value): + """ + Called whenever a UI element is changed by user. + - name is always a string. + Do whatever changes are required in here + Also invoked for internal changes + - inhibit_update is used to prevent needless rippling + """ + #print("- Change requested:",name,value) + if not self.inhibit_update: + self.inhibit_update = True # inhibit rippling calls while we update + print("Changing:", name, value) + if name == "check_me": + # changing another reactive value does not ripple because of global inhibit var + if self.check_me: + self.Draw_label = "Add 2 extra" + self.Drawin -= 2 + else: + self.Draw_label = "Subtract 2" + self.Drawin += 2 + # local iv + self.count_check_ticks += 1 + # reset ripple updates + self.inhibit_update = False + + +### UI +class UI_widget(object): + """ + Top level object that shows App. + """ + + def __init__(self): + self.params = Reactive_params() + self.explanation = ["Ltk project example:", + "- Everything from starter_02", + "- Adding Reactive UI components", + "(Use ctrl-shift while moving the mouse over ltk UI elements for descriptions.)" + ] + + def label_box(self): + """ + Can make parts of the UI like this + """ + return (ltk.HBox( + ltk.Label(self.params.Draw_label), + ltk.Checkbox(self.params.check_me).attr("id", "check_1") + ).css("border","1px dashed cyan") + ) + + def create(self): + """ + Return the UI structure + """ + info_paragraph = ltk.Paragraph("
".join(self.explanation)).attr("id","explanation").addClass("description") + return ( + ltk.VBox( + ltk.HorizontalSplitPane( # or VerticalSplitPane if your UI is better that way + ltk.Div( # put your UI in here + # - can easily just remove these dummy panes when you no longer need the terminal + ltk.VBox( + ltk.Button("Click me!", button_click_handler).attr("id","my-button"), + self.label_box(), + ltk.HBox( + ltk.Label("Draw-in").addClass("basetext"), + ltk.Input(self.params.Drawin).attr("type","number").attr("id","drawin").addClass("short") + ) + ), + ltk.Div().attr("id", "graphic"), + # overall description + info_paragraph + ).attr("id", "mydiv").css("border", "2px solid red"), + # Other side for the terminal + ltk.Div().attr("id", "forterm").css("border", "2px solid green"), + "Temp-split-pane" + ) + ) + ) + + +if __name__ == "__main__": + g_widget = UI_widget() + widget = g_widget.create() + widget.appendTo(ltk.window.document.body) # ui is now running + # + # move terminal to sliding pane + term = ltk.find("py-terminal")[0] + term_box = ltk.find("#forterm")[0] + term_box.appendChild(term) + # + print(f"The Terminal: ({"mpy" if MICROPYTHON else "pyodide"})") + # svg insert into Div + somesvg = create_svg_box() + ltk.find("#graphic").html(somesvg.outerHTML) + \ No newline at end of file diff --git a/ltk 03 -project starter/pyscript_mpy.toml b/ltk 03 -project starter/pyscript_mpy.toml new file mode 100644 index 0000000..22fdb2e --- /dev/null +++ b/ltk 03 -project starter/pyscript_mpy.toml @@ -0,0 +1,12 @@ +name = "Newtitle" + +[files] +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/jquery.py" = "ltk/jquery.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/widgets.py" = "ltk/widgets.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/pubsub.py" = "ltk/pubsub.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/__init__.py" = "ltk/__init__.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/logger.py" = "ltk/logger.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css" + +"./svg.py" = "" \ No newline at end of file diff --git a/ltk 03 -project starter/pyscript_py.toml b/ltk 03 -project starter/pyscript_py.toml new file mode 100644 index 0000000..f32e3b2 --- /dev/null +++ b/ltk 03 -project starter/pyscript_py.toml @@ -0,0 +1,9 @@ +name = "Newtitle" + +packages = [ "pyscript-ltk==0.2.30"] + +[files] +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css" + +"./svg.py" = "" diff --git a/ltk 03 -project starter/svg.py b/ltk 03 -project starter/svg.py new file mode 100644 index 0000000..05c94ed --- /dev/null +++ b/ltk 03 -project starter/svg.py @@ -0,0 +1,179 @@ +""" +A rewrite of Brython's SVG module, to remove JavaScript / document related +interactions (so this can be used within a web worker, where document is not +conveniently available). + +Author: Nicholas H.Tollervey (ntollervey@anaconda.com) +Based on original work by: Romain Casati +Mods for class,todom from Neon22(https://github.com/Neon22) + +License: GPL v3 or higher. +""" +from pyscript import document + +def todom(node): + """ + Get back entity we can jam into the dom live. + - If your inserted svg is being ignored then use this as a wrapper before inserting + """ + svgnode = document.createElementNS('http://www.w3.org/2000/svg', node.tagName) + for a in node.attributes: + svgnode.setAttributeNS(None,a, node.attributes[a]) + return svgnode + + +class Node: + """ + Represents a node in the DOM. + """ + + def __init__(self, **kwargs): + self._node = kwargs + self.parent = kwargs.get("parent") + + @property + def outerHTML(self): + """ + Get a string representation of the element's outer HTML. + """ + return NotImplemented + + +class ElementNode(Node): + """ + An element defined by a tag, may have attributes and children. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.tagName = kwargs["tagName"] + self.attributes = kwargs.get("attributes", {}) + self.value = kwargs.get("value") + self.childNodes = [] + + def appendChild(self, child): + """ + Add a child node to the children of this node. Using DOM API naming + conventions. + """ + child.parent = self + self.childNodes.append(child) + + def setAttribute(self, key, value): + """ + Sets an attribute on the node. + """ + self.attributes[key] = value + + @property + def outerHTML(self): + """ + Get a string representation of the element's outer HTML. Using DOM API + naming conventions. + """ + result = "<" + self.tagName + for attr, val in self.attributes.items(): + result += " " + attr + '="' + str(val) + '"' + result += ">" + result += self.innerHTML + result += "" + return result + + @property + def innerHTML(self): + """ + Get a string representation of the element's inner HTML. Using DOM API + naming conventions. + """ + result = "" + for child in self.childNodes: + result += child.outerHTML + return result + + +class TextNode(Node): + """ + Textual content inside an ElementNode. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.nodeValue = kwargs.get("nodeValue") + + @property + def outerHTML(self): + """ + Get a string representation of the element's outer HTML. + """ + return self.nodeValue + + +_svg_ns = "http://www.w3.org/2000/svg" +_xlink_ns = "http://www.w3.org/1999/xlink" + + +def _tag_func(tag): + def func(*args, **kwargs): + node = ElementNode(tagName=tag) + # this is mandatory to display svg properly + if tag == "svg": + node.setAttribute("xmlns", _svg_ns) + for arg in args: + if isinstance(arg, (str, int, float)): + arg = TextNode(nodeValue=str(arg)) + node.appendChild(arg) + for key, value in kwargs.items(): + key = key.lower() + if key[0:2] == "on": + # Ignore event handlers within the SVG. This shouldn't happen. + pass + elif key == "style": + node.setAttribute( + "style", "; ".join(f"{k}: {v}" for k, v in value.items()) + ) + elif key == "_class": + node.setAttribute( + "class", str(value)) + elif value is not False: + node.setAttribute(key.replace("_", "-"), str(value)) + return node + + return func + + +a = _tag_func("a") +altGlyph = _tag_func("altGlyph") +altGlyphDef = _tag_func("altGlyphDef") +altGlyphItem = _tag_func("altGlyphItem") +animate = _tag_func("animate") +animateColor = _tag_func("animateColor") +animateMotion = _tag_func("animateMotion") +animateTransform = _tag_func("animateTransform") +circle = _tag_func("circle") +clipPath = _tag_func("clipPath") +color_profile = _tag_func("color_profile") +cursor = _tag_func("cursor") +defs = _tag_func("defs") +desc = _tag_func("desc") +ellipse = _tag_func("ellipse") +feBlend = _tag_func("feBlend") +foreignObject = _tag_func("foreignObject") +g = _tag_func("g") +image = _tag_func("image") +line = _tag_func("line") +linearGradient = _tag_func("linearGradient") +marker = _tag_func("marker") +mask = _tag_func("mask") +path = _tag_func("path") +pattern = _tag_func("pattern") +polygon = _tag_func("polygon") +polyline = _tag_func("polyline") +radialGradient = _tag_func("radialGradient") +rect = _tag_func("rect") +set = _tag_func("set") +stop = _tag_func("stop") +svg = _tag_func("svg") +text = _tag_func("text") +tref = _tag_func("tref") +tspan = _tag_func("tspan") +use = _tag_func("use") \ No newline at end of file diff --git a/ltk 04 -project starter/README.md b/ltk 04 -project starter/README.md new file mode 100644 index 0000000..615b8c6 --- /dev/null +++ b/ltk 04 -project starter/README.md @@ -0,0 +1,27 @@ +When developing an application which is larger than a demo, its useful to have a better structure. + +This series is about presenting a better project-based structure. + +# Part 4: +Building on ideas described in Parts 1,2,3. + - Pyodide (full python) or Micropython + - "Loading" animation + - Reactive UI + - Interactive SVG controls + +## Single File Components +We are now building a UI element from a separate component file. +This means you can construct standalone UI components to include when needed. + - Making single file components that can be reused is critical for large projects. + +In the index.html - you can uncomment script lines to run: + - the base module that is imported 'util_units.py' which runs a test, + - the single file component (with tests) 'dimension_component.py' which also tests/demonstrates itself, + - or the main.py which uses the single file component twice + +## All events under Reactive (Model) class +Moving all toplevel events under the Model class (which enables reactivity) means we can use this as a single file component also. +This enables anything to be used in this way. +The testing under '__main__' means a component can be tested/checked on its own and then used as a component elsewhere without further modification. + +(You can use ctrl-shift while moving the mouse over ltk UI elements for descriptions of those elements.) \ No newline at end of file diff --git a/ltk 04 -project starter/components/dimension_component.py b/ltk 04 -project starter/components/dimension_component.py new file mode 100644 index 0000000..4877352 --- /dev/null +++ b/ltk 04 -project starter/components/dimension_component.py @@ -0,0 +1,124 @@ +# Dimension Component using ltk's reactive MVP model + +import ltk +from util_units import * + +# Debug +def print(*args): + ltk.find("body").append(" ".join(str(a) for a in args), "
") +if '__terminal__' in locals(): # resize terminal if started in index.html + __terminal__.resize(60, 6) + +class DC_params(ltk.Model): + """ + Reactive UI parameters + """ + measure: str = "250yd" + units: str = "yd" + altunits: str = "229m" + + def __init__(self, parent, defaults=["20","wpi","wpcm"], inch_base=False): + super().__init__() + self.parent = parent # not strictly required at this child level but + # needed for UI components with operationally dependent children. + # + self.defaults = defaults + self.inch_base = inch_base + self.inhibit_update = False + # + self.set_to_defaults() + + def set_to_defaults(self): + self.measure = self.defaults[0] + self.defaults[1] + self.units = self.defaults[1] + self.altunits = convert_imp(int(self.defaults[0]), self.defaults[2], self.inch_base) + + def changed(self, name, value): + """ + For this component: + - toggle units btwn metric/imperial + - allow overiding units + - allow numeric only or numeric+units entry + - show the alt units to be helpful eh + """ + #print(f"DC:Change req: {name} to {value}") + if not self.inhibit_update: + #print(f"DC:Changing: {name} to {value}") + self.inhibit_update = True # inhibit rippling calls while we update + if name == "measure": + self.update_measure(value) + elif name == "units": + self.update_units(value) + # reset ripple updates + self.inhibit_update = False + #else: + # print("- DC:Skipping change") + + def update_measure(self, value): + num, unit = parse_units(value) + if num and num > -1: + # got a valid measure but maybe no units + if not unit: + # use on-screen units + units = f"{self.units}" + self.measure = f"{format_nice(num)}{units}" + if units in metric_units: + num = num * convert_factors[units] + self.altunits = convert_imp(num, unit_swaps[units], self.inch_base) + else: # unit from measure + if unit not in convert_factors.keys(): + unit = self.defaults[1] + self.units = unit + usnum,_ = parse_to_US(value) + self.measure = convert_imp(usnum, unit) + self.altunits = convert_imp(usnum, unit_swaps[unit]) + else: # not valid so set default + self.set_to_defaults() + + def update_units(self, newunit): + num, altunits = parse_units(self.altunits) + usnum, _ = parse_to_US(self.measure) + if altunits == newunit: # straight swap + self.update_measure(f"{num}{newunit}") + elif newunit in unit_swaps: # convert to newunit + self.update_measure(convert_imp(usnum, newunit)) + else: # not a unit + self.update_measure(f"{usnum}{newunit}") # will be defaulted + + +class Component(object): + """ + Create an instance using: Component(label, defaults, force_inches) + Add to UI by using: .create() + Read/write values by using .params.name_of_var + - Default is [value, unit, altunit] all strings + """ + def __init__(self, label="Foo", defaults=["250","yd","m"], inch_base=False): + self.label = label + self.params = DC_params(self, defaults=defaults, inch_base=inch_base) + + def toggle_units(self, event, var): + self.params.changed(var.name, unit_swaps[f"{var}"]) + + def create(self): + """ + Present the value, units as buttons and + altunits as a label + """ + return ( + ltk.VBox( + ltk.Label(self.label).addClass("Item paramtitle"), + ltk.HBox( + ltk.Input(self.params.measure).addClass("measure"), + ltk.Input(self.params.units).addClass("measure short").on("click", ltk.proxy(lambda event: self.toggle_units(event, self.params.units))), + ltk.Label(self.params.altunits).addClass("measure altlabel noborder") + ) + ).addClass("param") + ) + +# standalone development/examination +if __name__ == "__main__": + w = Component() + widget = w.create() + widget.appendTo(ltk.window.document.body) + ltk.find(".param").css("border 2px dashed red") \ No newline at end of file diff --git a/ltk 04 -project starter/components/util_units.py b/ltk 04 -project starter/components/util_units.py new file mode 100644 index 0000000..34b4f5d --- /dev/null +++ b/ltk 04 -project starter/components/util_units.py @@ -0,0 +1,118 @@ + +# Here we assume all calcs are done in yds/in +# but we want to present unit agnostically. +# - all epi/wpi,ppi are in delivered in inches +# - all yds/ft/in/m/cm/mm are delivered in yds +# E.g. "3epcm" becomes "7.5epi", "1ft" becomes "0.333yds" + +import re + +# convert value in units to result in yards (or inch for epi/ppi/epcm) (use multiply) +convert_factors = {"yd": 1, "yds": 1, "ft": 0.333333, "in": 0.027777777, + "m": 1.09361, "cm": 0.0109361, "mm": 0.00109361, + # epi are in inches + "wpi": 1, "epi": 1, "ppi": 1, "wpcm": 2.54, "epcm": 2.54, "ppcm": 2.54} + +yard_based = ["yd", "m"] +unit_swaps = {"in":"cm", "cm":"in", + "ft":"cm", + "yd":"m", "m":"yd", + "wpi":"wpcm", "wpcm":"wpi", + "epi":"epcm", "epcm":"epi", + "ppi":"ppcm", "ppcm":"ppi"} +metric_units = ["m", "cm", "wpcm", "epcm","ppcm"] + +# 3epcm (7.5epi), 3.5-4 epcm (9-10 ppi), 1 epcm (2.5ppi) + +def parse_units(measure): + """ + 12yd, 3ft, 2m, 20cm, 200mm, 10in + 7epi, 7ppi, 10epcm + - just extract number and units + """ + # Pyodide + match = re.match(r"(^[0-9]*\.?[0-9]*)(.*)", str(measure)) + #p = re.compile(r"(^[0-9]*\.?[0-9]*)(.*)") + #found = p.findall(measure) + #if found: + # num, unit = found[0] + # Micropython + #match = re.match("(^[0-9]*\.?[0-9]*)(.*)", str(measure)) + if match: + num, unit = match.group(1), match.group(2) + # + if num and not num[-1].isalpha(): + number = float(num) + else: + number = -1 + return number, unit + + +def parse_to_US(measure): + """ + 12yd, 3ft, 2m, 20cm, 200mm, 10in + 7epi, 7ppi, 10epcm + - convert to yd or epi + """ + number, unit = parse_units(measure) + if unit in convert_factors.keys(): + val = number * convert_factors[unit] + if val == int(val): + val = int(val) + else: + #print("Sorry - did not understand units:",measure) + #val, unit = -1, None + val, unit = int(number), "yd" # more useful for this application + return val, unit + +def convert_sett(sett): + """ + if epi then return inches conversion factor + else cm factor + """ + if sett in ['epi', 'wpi', 'ppi']: + return convert_factors['in'] + else: + return convert_factors['cm'] + +def convert_imp(value, units, inches=False): + """ + Convert value from yards into units. + - if inches then from inches to units + Return a well presented float. 0,1 decimal place + """ + result = value / convert_factors[units] + if inches: + result *= convert_factors['in'] + remainder = (result - int(result)) + # small values get meaningful decimal points of accuracy + if remainder >= result/100: + return f"{result:.1f}{units}" + else: + return f"{result:.0f}{units}" + +def format_nice(value): + """ + use single decimal point + - remove '.0' for neatness + """ + vstr = f"{value:1.1f}" + if vstr[-2:] == ".0": + vstr = vstr[:-2] + return vstr + + + +# Testing +if __name__ == "__main__": + a = "22m" + p = parse_units(a) + print(f"Parse: '{a}' = {p} or nicely as '{format_nice(p[0])}'") + imp = parse_to_US(a) + print(f"in yds (imperial) = {imp[0]}") + m = convert_imp(imp[0], "cm") + print(f"and back to '{m}'") + print(f"or perhaps {imp[0] / convert_factors['ft']:1.2f} 'ft'") + + + \ No newline at end of file diff --git a/ltk 04 -project starter/index.html b/ltk 04 -project starter/index.html new file mode 100644 index 0000000..774a702 --- /dev/null +++ b/ltk 04 -project starter/index.html @@ -0,0 +1,101 @@ + + + + Newtitle + + + + + + + + + + + + + + + + + + + + +
+

Building:

+

Newtitle

+

Assembling bits and pieces...

+

Almost there...

+
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/ltk 04 -project starter/main.css b/ltk 04 -project starter/main.css new file mode 100644 index 0000000..0fef763 --- /dev/null +++ b/ltk 04 -project starter/main.css @@ -0,0 +1,56 @@ +.description { + font-size: 1.0rem; + font-style: light; + border: 2px dashed #e232ca; +} +.basetext { + font-size: 1.4rem; + padding: 3px; +} +.short { + width: 60px; + align-self: center; + padding: 3px; +} +#graphic { + border: 1px dashed #0d720d; +} + +/* from Dimension_component's css file */ +.param { + border: 3px dashed #1999b3; +} +.paramtitle { + font-family: "Inter", sans-serif; + font-size: 1.4rem; + font-style: normal; + margin: 0px 0px 0px 8px; +} +.measure { + font-family: "Inter", sans-serif; + font-size: 1rem; + height: 2.5rem; + width: 90px; + margin: 4px; + padding: 8px; + background-color: #fefdeb; + border-radius: 5px; + border: 0.5px solid black; +} +.short { + width: 60px; + background-color: #e2fde4; +} +.altlabel { + font-size: 0.9rem; +} +.noborder { + border: none; + background-color: #ffffff; +} + +/* ltk overrides */ +.ltk-input { + font-size: 1.4rem; + padding: 3px; +} \ No newline at end of file diff --git a/ltk 04 -project starter/main.py b/ltk 04 -project starter/main.py new file mode 100644 index 0000000..2942d41 --- /dev/null +++ b/ltk 04 -project starter/main.py @@ -0,0 +1,169 @@ +#imports +import ltk +import svg as SVG +import random +import dimension_component as DC + +from pyscript import config +MICROPYTHON = config["type"] == "mpy" # if we need to know if we're running pyodide or micropython + +if '__terminal__' in locals(): # resize terminal if started in index.html + __terminal__.resize(60, 10) + +# globals +g_widget = None # Hold ref to widget for nested components +# for SVG +solid_style = {"fill":"#AA0", "stroke":"#A00", "stroke-width":"1px"} + +def create_svg_box(size=5): + overall = 100 + box_size = 100 / size + highlight_svg = SVG.svg(width=overall, height=overall, + preserveAspectRatio="xMidYMid meet", + viewBox=f"0 0 {overall+2} {overall+2}") + # Transform the group to move its children + grp = SVG.g(id="highlight", transform='translate(0 0)', style=solid_style) + highlight_svg.appendChild(grp) + grp.appendChild(SVG.rect(x=0, y=0, width=box_size, height=box_size)) + # + return highlight_svg + +## Events +# - moved event to Reactive area for event control so no global handlers +# UNUSED +def button_click_handler_dperecated(event): + print("The non-reactive button has been clicked, the svg transformed") + print(f"- checkbox clicked {g_widget.params.count_check_ticks} times.") + # move the svg child + x,y = random.randrange(0, 100-20), random.randrange(0, 100-20) + ltk.find("#highlight").attr("transform", f"translate({x} {y})") + +### Reactive UI variables and behaviour +class Reactive_params(ltk.Model): + """ + """ + # These variables are all reactive + check_me = False + Drawin = 12 # Can use Drawin: int = 12 but python runtime does not check + Draw_label = "Subtract 2" + + def __init__(self): + """ + variables instanced here will not be reactive (only the classvars above) + but its a good place to put all the other variables your widget needs. + """ + super().__init__() + self.inhibit_update = False + self.count_check_ticks = 0 # as a useless example + + def changed(self, name, value): + """ + Called whenever a UI element is changed by user. + - name is always a string. + Do whatever changes are required in here + Also invoked for internal changes + - inhibit_update is used to prevent needless rippling + """ + #print("- Change requested:",name,value) + if not self.inhibit_update: + self.inhibit_update = True # inhibit rippling calls while we update + print("Changing:", name, value) + if name == "check_me": + # changing another reactive value does not ripple because of global inhibit var + if self.check_me: + self.Draw_label = "Add 2 extra" + self.Drawin -= 2 + else: + self.Draw_label = "Subtract 2" + self.Drawin += 2 + # reset ripple updates + self.inhibit_update = False + + def button_click_handler(self, event): + print("The non-reactive button has been clicked, the svg transformed") + print(f"- checkbox clicked {self.count_check_ticks} times.") + # move the svg child + x,y = random.randrange(0, 100-20), random.randrange(0, 100-20) + ltk.find("#highlight").attr("transform", f"translate({x} {y})") + +### UI +class UI_widget(object): + """ + Top level object that shows App. + """ + + def __init__(self): + self.params = Reactive_params() + self.explanation = ["Ltk project example:", + "- Everything from starter_03 and building a UI element from a separate component file.", + "- This means you can construct standalone UI components to include when needed.", + "- Making single file components that can be reused is critical for large projects.", + "", + " In the index.html - you can uncomment script lines to run:", + "- the base module that is imported 'util_units.y' which runs a test", + "- the single file component (with tests) 'dimension_component.py'", + "- or the main.py which uses the single file component twice", + "", + "(Use ctrl-shift while moving the mouse over ltk UI elements for descriptions.)" + ] + + def label_box(self): + """ + Can make parts of the UI like this + """ + return (ltk.HBox( + ltk.Label(self.params.Draw_label), + ltk.Checkbox(self.params.check_me).attr("id", "check_1") + ).css("border","1px dashed cyan") + ) + + def create(self): + """ + Return the UI structure + """ + # Use a single file component defined elsewhere + measure1 = DC.Component("Length", defaults=["250","yd","m"], inch_base=False).create() + measure2 = DC.Component("Width", defaults=["22","yd","m"], inch_base=False).create() + return ( + ltk.VBox( + ltk.HorizontalSplitPane( # or VerticalSplitPane if your UI is better that way + ltk.Div( # put your UI in here + # - can easily just remove these dummy panes when you no longer need the terminal + ltk.VBox( + ltk.Button("Click me!", self.params.button_click_handler).attr("id","my-button"), + self.label_box(), + ltk.HBox( + ltk.Label("Draw-in").addClass("basetext"), + ltk.Input(self.params.Drawin).attr("type","number").attr("id","drawin").addClass("short") + ), + measure1, + measure2 + ), + ltk.Div().attr("id", "graphic"), + # our description + ltk.Paragraph("
".join(self.explanation)).attr("id","explanation").addClass("description") + ).attr("id", "mydiv").css("border", "2px solid red"), + # Other side for the terminal + ltk.Div().attr("id", "forterm").css("border", "2px solid green"), + "Temp-split-pane" + ) + ) + ) + + +if __name__ == "__main__": + g_widget = UI_widget() + widget = g_widget.create() + widget.appendTo(ltk.window.document.body) # ui is now running + # + # move terminal to sliding pane + term = ltk.find("py-terminal")[0] + term_box = ltk.find("#forterm")[0] + term_box.appendChild(term) + # + print(f"The Terminal: ({"mpy" if MICROPYTHON else "pyodide"})") + # svg insert into Div + somesvg = create_svg_box() + ltk.find("#graphic").html(somesvg.outerHTML) + + \ No newline at end of file diff --git a/ltk 04 -project starter/pyscript_mpy.toml b/ltk 04 -project starter/pyscript_mpy.toml new file mode 100644 index 0000000..7323fb7 --- /dev/null +++ b/ltk 04 -project starter/pyscript_mpy.toml @@ -0,0 +1,15 @@ +name = "Newtitle" + +[files] +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/jquery.py" = "ltk/jquery.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/widgets.py" = "ltk/widgets.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/pubsub.py" = "ltk/pubsub.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/__init__.py" = "ltk/__init__.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/logger.py" = "ltk/logger.py" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css" + +#local filesystem +"./components/util_units.py" = "./util_units.py" +"./components/dimension_component.py" = "./dimension_component.py" +"./svg.py" = "./svg.py" \ No newline at end of file diff --git a/ltk 04 -project starter/pyscript_py.toml b/ltk 04 -project starter/pyscript_py.toml new file mode 100644 index 0000000..e3f34d8 --- /dev/null +++ b/ltk 04 -project starter/pyscript_py.toml @@ -0,0 +1,12 @@ +name = "Newtitle" + +packages = [ "pyscript-ltk==0.2.30"] + +[files] +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.js" = "ltk/ltk.js" +"https://raw.githubusercontent.com/pyscript/ltk/main/ltk/ltk.css" = "ltk/ltk.css" + +#local filesystem +"./components/util_units.py" = "./util_units.py" +"./components/dimension_component.py" = "./dimension_component.py" +"./svg.py" = "./svg.py" diff --git a/ltk 04 -project starter/svg.py b/ltk 04 -project starter/svg.py new file mode 100644 index 0000000..05c94ed --- /dev/null +++ b/ltk 04 -project starter/svg.py @@ -0,0 +1,179 @@ +""" +A rewrite of Brython's SVG module, to remove JavaScript / document related +interactions (so this can be used within a web worker, where document is not +conveniently available). + +Author: Nicholas H.Tollervey (ntollervey@anaconda.com) +Based on original work by: Romain Casati +Mods for class,todom from Neon22(https://github.com/Neon22) + +License: GPL v3 or higher. +""" +from pyscript import document + +def todom(node): + """ + Get back entity we can jam into the dom live. + - If your inserted svg is being ignored then use this as a wrapper before inserting + """ + svgnode = document.createElementNS('http://www.w3.org/2000/svg', node.tagName) + for a in node.attributes: + svgnode.setAttributeNS(None,a, node.attributes[a]) + return svgnode + + +class Node: + """ + Represents a node in the DOM. + """ + + def __init__(self, **kwargs): + self._node = kwargs + self.parent = kwargs.get("parent") + + @property + def outerHTML(self): + """ + Get a string representation of the element's outer HTML. + """ + return NotImplemented + + +class ElementNode(Node): + """ + An element defined by a tag, may have attributes and children. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.tagName = kwargs["tagName"] + self.attributes = kwargs.get("attributes", {}) + self.value = kwargs.get("value") + self.childNodes = [] + + def appendChild(self, child): + """ + Add a child node to the children of this node. Using DOM API naming + conventions. + """ + child.parent = self + self.childNodes.append(child) + + def setAttribute(self, key, value): + """ + Sets an attribute on the node. + """ + self.attributes[key] = value + + @property + def outerHTML(self): + """ + Get a string representation of the element's outer HTML. Using DOM API + naming conventions. + """ + result = "<" + self.tagName + for attr, val in self.attributes.items(): + result += " " + attr + '="' + str(val) + '"' + result += ">" + result += self.innerHTML + result += "" + return result + + @property + def innerHTML(self): + """ + Get a string representation of the element's inner HTML. Using DOM API + naming conventions. + """ + result = "" + for child in self.childNodes: + result += child.outerHTML + return result + + +class TextNode(Node): + """ + Textual content inside an ElementNode. + """ + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.nodeValue = kwargs.get("nodeValue") + + @property + def outerHTML(self): + """ + Get a string representation of the element's outer HTML. + """ + return self.nodeValue + + +_svg_ns = "http://www.w3.org/2000/svg" +_xlink_ns = "http://www.w3.org/1999/xlink" + + +def _tag_func(tag): + def func(*args, **kwargs): + node = ElementNode(tagName=tag) + # this is mandatory to display svg properly + if tag == "svg": + node.setAttribute("xmlns", _svg_ns) + for arg in args: + if isinstance(arg, (str, int, float)): + arg = TextNode(nodeValue=str(arg)) + node.appendChild(arg) + for key, value in kwargs.items(): + key = key.lower() + if key[0:2] == "on": + # Ignore event handlers within the SVG. This shouldn't happen. + pass + elif key == "style": + node.setAttribute( + "style", "; ".join(f"{k}: {v}" for k, v in value.items()) + ) + elif key == "_class": + node.setAttribute( + "class", str(value)) + elif value is not False: + node.setAttribute(key.replace("_", "-"), str(value)) + return node + + return func + + +a = _tag_func("a") +altGlyph = _tag_func("altGlyph") +altGlyphDef = _tag_func("altGlyphDef") +altGlyphItem = _tag_func("altGlyphItem") +animate = _tag_func("animate") +animateColor = _tag_func("animateColor") +animateMotion = _tag_func("animateMotion") +animateTransform = _tag_func("animateTransform") +circle = _tag_func("circle") +clipPath = _tag_func("clipPath") +color_profile = _tag_func("color_profile") +cursor = _tag_func("cursor") +defs = _tag_func("defs") +desc = _tag_func("desc") +ellipse = _tag_func("ellipse") +feBlend = _tag_func("feBlend") +foreignObject = _tag_func("foreignObject") +g = _tag_func("g") +image = _tag_func("image") +line = _tag_func("line") +linearGradient = _tag_func("linearGradient") +marker = _tag_func("marker") +mask = _tag_func("mask") +path = _tag_func("path") +pattern = _tag_func("pattern") +polygon = _tag_func("polygon") +polyline = _tag_func("polyline") +radialGradient = _tag_func("radialGradient") +rect = _tag_func("rect") +set = _tag_func("set") +stop = _tag_func("stop") +svg = _tag_func("svg") +text = _tag_func("text") +tref = _tag_func("tref") +tspan = _tag_func("tspan") +use = _tag_func("use") \ No newline at end of file