wwwpy
Advanced tools
| import ast | ||
| import inspect | ||
| from collections.abc import Callable | ||
| class _AttrVisitor(ast.NodeVisitor): | ||
| def __init__(self): | ||
| self.attr = None | ||
| def visit_Lambda(self, node): | ||
| # Only support simple attribute access: lambda x: x.foo | ||
| if isinstance(node.body, ast.Attribute): | ||
| self.attr = node.body.attr | ||
| def visit_FunctionDef(self, node): | ||
| # Only support simple attribute access: def foo(x): return x.bar | ||
| if len(node.body) == 1 and isinstance(node.body[0], ast.Return): | ||
| ret = node.body[0].value | ||
| if isinstance(ret, ast.Attribute): | ||
| self.attr = ret.attr | ||
| def attr_name(block: Callable) -> str: | ||
| src = inspect.getsource(block).strip() | ||
| visitor = _AttrVisitor() | ||
| visitor.visit(ast.parse(src)) | ||
| if visitor.attr is None: | ||
| raise ValueError("Callable must be of the form: lambda x: x.foo or def foo(x): return x.bar") | ||
| return visitor.attr | ||
| def test_attr_name_with_lambda(): | ||
| result = attr_name(lambda x: x.foo) | ||
| assert result == "foo" | ||
| def test_attr_name_with_function(): | ||
| def foo(x): | ||
| return x.bar | ||
| result = attr_name(foo) | ||
| assert result == "bar" |
| from pathlib import Path | ||
| _parent = Path(__file__).parent | ||
| def _svg(filename: str) -> Path: return _parent / filename | ||
| # you can control-click the icon filename to open it (at least in PyCharm) | ||
| class AllIcons: | ||
| services_dark = _svg("jb/services_dark.svg") | ||
| todo_20x20_dark = _svg("jb/todo@20x20_dark.svg") | ||
| console_dark = _svg("jb/console_dark.svg") | ||
| pythonPackages_dark = _svg("jb/pythonPackages_dark.svg") | ||
| python_stroke = _svg("jb/python_stroke.svg") | ||
| toolWindowComponents_dark = _svg("jb/toolWindowComponents_dark.svg") | ||
| properties_dark = _svg("jb/properties_dark.svg") | ||
| project_20x20_dark = _svg("jb/project@20x20_dark.svg") | ||
| structure_20x20_dark = _svg("jb/structure@20x20_dark.svg") | ||
| events = _svg("events.svg") | ||
| toolWindowComponents_20x20_dark = _svg("jb/toolWindowComponents@20x20_dark.svg") | ||
| @classmethod | ||
| def all_icons(cls) -> tuple[Path, ...]: | ||
| return _all_icons | ||
| _all_icons = tuple( | ||
| value for name, value in vars(AllIcons).items() | ||
| if isinstance(value, Path) and not name.startswith('_') | ||
| ) |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> | ||
| <circle cx="12" cy="12" r="10" fill="none" stroke="#CED0D6" stroke-width="2"/> | ||
| <path d="M13 5 L8 13 H12 L11 19 L16 11 H12 L13 5 Z" fill="#CED0D6"/> | ||
| </svg> |
| <!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> | ||
| <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <path d="M4.12642 5.16655C4.32168 4.97129 4.63827 4.97129 4.83353 5.16655L7.16698 7.5L4.83353 9.83345C4.63827 10.0287 4.32168 10.0287 4.12642 9.83345C3.93116 9.63819 3.93116 9.32161 4.12642 9.12635L5.75277 7.5L4.12642 5.87365C3.93116 5.67839 3.93116 5.36181 4.12642 5.16655Z" | ||
| fill="#CED0D6"/> | ||
| <path d="M7.5 10C7.22386 10 7 10.2239 7 10.5C7 10.7761 7.22386 11 7.5 11H10.5C10.7761 11 11 10.7761 11 10.5C11 10.2239 10.7761 10 10.5 10L7.5 10Z" | ||
| fill="#CED0D6"/> | ||
| <path fill-rule="evenodd" clip-rule="evenodd" | ||
| d="M3 2C1.89543 2 1 2.89543 1 4V12C1 13.1046 1.89543 14 3 14H13C14.1046 14 15 13.1046 15 12V4C15 2.89543 14.1046 2 13 2H3ZM13 3H3C2.44772 3 2 3.44772 2 4V12C2 12.5523 2.44772 13 3 13H13C13.5523 13 14 12.5523 14 12V4C14 3.44772 13.5523 3 13 3Z" | ||
| fill="#CED0D6"/> | ||
| </svg> |
| <!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> | ||
| <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <path d="M10.5199 5.57617L10.7285 5.75H11H17C17.6904 5.75 18.25 6.30964 18.25 7V15.1667C18.25 16.0671 17.553 16.75 16.75 16.75H3.25C2.44705 16.75 1.75 16.0671 1.75 15.1667V4.83333C1.75 3.93294 2.44705 3.25 3.25 3.25H7.63795C7.69643 3.25 7.75307 3.2705 7.798 3.30794L10.5199 5.57617Z" | ||
| stroke="#CED0D6" stroke-width="1.5"/> | ||
| </svg> |
| <!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> | ||
| <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <path d="M2.5 3.5h7m-7 4h2m-2 4h6m3-8h2m-7 4h7m-3 4h3" stroke="#CED0D6" stroke-linecap="round"/> | ||
| <circle cx="10.5" cy="3.5" r="1" stroke="#CED0D6"/> | ||
| <circle cx="5.5" cy="7.5" r="1" stroke="#CED0D6"/> | ||
| <circle cx="9.5" cy="11.5" r="1" stroke="#CED0D6"/> | ||
| </svg> |
| <!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> | ||
| <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <path fill-rule="evenodd" clip-rule="evenodd" | ||
| d="M6.48983 1.13677C7.02636 1.04476 7.57004 0.999004 8.11466 1.00002L8.11854 1.00004C8.69493 1.00471 9.27001 1.05462 9.83831 1.14927C10.3846 1.20957 10.893 1.45404 11.277 1.8415C11.6592 2.22707 11.9477 2.79308 11.9477 3.34128V6.28964C11.9478 6.86298 11.7202 7.41385 11.3133 7.82436C10.9063 8.23491 10.3524 8.47274 9.76965 8.48707L9.75441 8.48726H6.59961C6.2432 8.49379 5.90273 8.63409 5.64798 8.87952C5.3946 9.12364 5.24517 9.45331 5.22975 9.8016V11.3C5.22975 11.631 4.95698 11.8993 4.6205 11.8993H3.51574C2.88773 11.8993 2.3666 11.6627 1.97683 11.2638C1.60371 10.882 1.37487 10.3779 1.24477 9.85376C0.919717 8.71373 0.918415 7.50798 1.24086 6.36742C1.35923 5.78344 1.68245 5.25856 2.1545 4.88482C2.63144 4.50721 3.22913 4.30857 3.84095 4.32413H4.4093V3.3253C4.4093 2.85137 4.46916 2.32352 4.8446 1.89321C5.21181 1.47233 5.7798 1.26027 6.48837 1.13702L6.48983 1.13677ZM8.11043 2.19861C7.63761 2.19784 7.16563 2.2376 6.69983 2.31743C6.07673 2.4259 5.85691 2.57335 5.76924 2.67382C5.68978 2.7649 5.62779 2.92025 5.62779 3.3253C5.62779 3.72444 5.62779 4.12358 5.62779 4.52273C5.62779 5.07501 5.18007 5.52273 4.62779 5.52273H3.83255C3.82654 5.52273 3.82054 5.52264 3.81454 5.52247C3.48954 5.51301 3.17179 5.618 2.91851 5.81854C2.63197 6.0454 2.51677 6.33318 2.42054 6.66843C2.15114 7.60694 2.15114 8.60048 2.42054 9.53899C2.51264 9.85982 2.61418 10.1865 2.85527 10.4332C3.01991 10.6017 3.22582 10.7007 3.51574 10.7007H4.01126V9.78974C4.01126 9.78248 4.01139 9.77523 4.01166 9.76797C4.036 9.1091 4.31654 8.48455 4.79538 8.02322C5.27422 7.56189 5.91482 7.29898 6.585 7.28873L6.59446 7.28859L9.74591 7.28866C10.0083 7.2805 10.2574 7.17265 10.4408 6.98761C10.6258 6.801 10.7293 6.55065 10.7292 6.29002V3.36692C10.704 3.10789 10.5896 2.86485 10.4046 2.67824C10.194 2.46572 9.94035 2.38246 9.65164 2.33386C9.14243 2.24813 8.62702 2.2029 8.11043 2.19861Z" | ||
| fill="white"/> | ||
| <path fill-rule="evenodd" clip-rule="evenodd" | ||
| d="M10.7292 4.57159C10.7292 4.24061 11.0019 3.97229 11.3384 3.97229H12.565C13.2089 3.97229 13.7126 4.2299 14.0744 4.6449C14.4106 5.0305 14.604 5.5269 14.7275 6.00692C15.0925 7.15942 15.0907 8.39428 14.7221 9.54595L14.7208 9.54984C14.5615 10.036 14.3678 10.5376 14.0436 10.9147C13.6839 11.3329 13.1965 11.5634 12.565 11.5634H11.5902V12.5622C11.5902 13.2231 11.2876 13.7249 10.8697 14.0771C10.473 14.4114 9.97593 14.6111 9.53055 14.7347C8.38602 15.0892 7.15819 15.0884 6.01405 14.7322C5.13141 14.4621 4.01123 13.8361 4.01123 12.5622V11.2438C4.01123 10.9128 4.284 10.6445 4.62048 10.6445C4.95695 10.6445 5.22972 10.9128 5.22972 11.2438V12.5622C5.22972 12.9817 5.58664 13.3465 6.37715 13.5881L6.3804 13.5891C7.2888 13.8722 8.26394 13.8722 9.17234 13.5891C9.49224 13.4894 9.81727 13.3857 10.0769 13.1669C10.2682 13.0057 10.3718 12.8204 10.3718 12.5622C10.3718 12.3041 10.3718 11.8157 10.3718 11.3647C10.3718 10.8125 10.8195 10.3648 11.3718 10.3648H12.565C12.8515 10.3648 12.9937 10.2797 13.1133 10.1406C13.2681 9.96054 13.4032 9.66363 13.5604 9.18415C13.8553 8.26157 13.8551 7.27218 13.5598 6.34969C13.4565 6.0269 13.3799 5.68945 13.1495 5.4252C13.0097 5.26486 12.8391 5.17089 12.565 5.17089H11.3384C11.0019 5.17089 10.7292 4.90257 10.7292 4.57159Z" | ||
| fill="white"/> | ||
| <path d="M7.6514 3.59699C7.80442 3.59511 7.95456 3.63803 8.08273 3.72029C8.2109 3.80255 8.31131 3.92043 8.37121 4.05897C8.4311 4.1975 8.44776 4.35042 8.41908 4.4983C8.3904 4.64617 8.31768 4.78232 8.21015 4.88943C8.10261 4.99655 7.96513 5.06979 7.81518 5.09986C7.66522 5.12993 7.50957 5.11546 7.36799 5.05829C7.22642 5.00113 7.10532 4.90385 7.02009 4.77881C6.93486 4.65378 6.88934 4.50664 6.88933 4.3561C6.88805 4.25687 6.90682 4.15837 6.94455 4.06632C6.98227 3.97427 7.0382 3.89051 7.1091 3.81989C7.17999 3.74928 7.26443 3.69321 7.35752 3.65495C7.45061 3.61668 7.5505 3.59698 7.6514 3.59699Z" | ||
| fill="white"/> | ||
| <path d="M8.32857 11.0484C8.53325 11.0484 8.72953 11.1284 8.87426 11.2707C9.01898 11.4131 9.10029 11.6062 9.10029 11.8075C9.10029 12.0088 9.01898 12.2019 8.87426 12.3443C8.72953 12.4866 8.53325 12.5666 8.32857 12.5666C8.1239 12.5666 7.92762 12.4866 7.78289 12.3443C7.63817 12.2019 7.55686 12.0088 7.55686 11.8075C7.55686 11.6062 7.63817 11.4131 7.78289 11.2707C7.92762 11.1284 8.1239 11.0484 8.32857 11.0484Z" | ||
| fill="white"/> | ||
| </svg> |
| <!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> | ||
| <svg width="13" height="13" viewBox="0 0 13 13" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <path fill-rule="evenodd" clip-rule="evenodd" | ||
| d="M6.5 0.375L12.625 3.77778L6.5 7.18056L0.375 3.77778L6.5 0.375ZM11.4 5.81944L12.625 6.5L6.5 9.90278L0.375 6.5L1.6 5.81944L6.5 8.54167L11.4 5.81944ZM11.4 8.54167L12.625 9.22222L6.5 12.625L0.375 9.22222L1.6 8.54167L6.5 11.2639L11.4 8.54167Z" | ||
| fill="#AFB1B3"/> | ||
| </svg> |
| <!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> | ||
| <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <path d="M15.2117 7.50028C15.3901 7.80954 15.3901 8.19046 15.2117 8.49972L12.0386 13.9997C11.86 14.3093 11.5298 14.5 11.1724 14.5L4.82756 14.5C4.47018 14.5 4.13997 14.3093 3.96138 13.9997L0.788301 8.49972C0.609882 8.19046 0.609882 7.80954 0.788301 7.50028L3.96138 2.00028C4.13997 1.69072 4.47018 1.5 4.82756 1.5L11.1724 1.5C11.5298 1.5 11.86 1.69072 12.0386 2.00028L15.2117 7.50028Z" | ||
| stroke="#CED0D6"/> | ||
| <path d="M10.5 8.43301L6.75 10.5981C6.41667 10.7905 6 10.55 6 10.1651L6 5.83493C6 5.45004 6.41667 5.20947 6.75 5.40192L10.5 7.56699C10.8333 7.75944 10.8333 8.24056 10.5 8.43301Z" | ||
| stroke="#CED0D6"/> | ||
| </svg> |
| <!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> | ||
| <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <path fill-rule="evenodd" clip-rule="evenodd" | ||
| d="M2.75 1C3.16421 1 3.5 1.33579 3.5 1.75V4H6V3C6 1.89543 6.89543 1 8 1H16C17.1046 1 18 1.89543 18 3V7C18 8.10457 17.1046 9 16 9H8C6.89543 9 6 8.10457 6 7V5.5H3.5V14H6V13C6 11.8954 6.89543 11 8 11H16C17.1046 11 18 11.8954 18 13V17C18 18.1046 17.1046 19 16 19H8C6.89543 19 6 18.1046 6 17V15.5H3.5V18.25C3.5 18.6642 3.16421 19 2.75 19C2.33579 19 2 18.6642 2 18.25V1.75C2 1.33579 2.33579 1 2.75 1ZM16 12.5H8C7.72386 12.5 7.5 12.7239 7.5 13V17C7.5 17.2761 7.72386 17.5 8 17.5H16C16.2761 17.5 16.5 17.2761 16.5 17V13C16.5 12.7239 16.2761 12.5 16 12.5ZM8 2.5H16C16.2761 2.5 16.5 2.72386 16.5 3V7C16.5 7.27614 16.2761 7.5 16 7.5H8C7.72386 7.5 7.5 7.27614 7.5 7V3C7.5 2.72386 7.72386 2.5 8 2.5Z" | ||
| fill="#CED0D6"/> | ||
| </svg> |
| <!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> | ||
| <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <circle cx="3.1" cy="4.69998" r="1.1" fill="#CED0D6"/> | ||
| <path d="M5.5 4.70001C5.5 4.2858 5.83579 3.95001 6.25 3.95001H17.25C17.6642 3.95001 18 4.2858 18 4.70001V4.70001C18 5.11423 17.6642 5.45001 17.25 5.45001H6.25C5.83579 5.45001 5.5 5.11423 5.5 4.70001V4.70001Z" | ||
| fill="#CED0D6"/> | ||
| <circle cx="3.1" cy="10" r="1.1" fill="#CED0D6"/> | ||
| <path d="M5.5 10C5.5 9.58579 5.83579 9.25 6.25 9.25H17.25C17.6642 9.25 18 9.58579 18 10V10C18 10.4142 17.6642 10.75 17.25 10.75H6.25C5.83579 10.75 5.5 10.4142 5.5 10V10Z" | ||
| fill="#CED0D6"/> | ||
| <circle cx="3.1" cy="15.3" r="1.1" fill="#CED0D6"/> | ||
| <path d="M5.5 15.3C5.5 14.8858 5.83579 14.55 6.25 14.55H17.25C17.6642 14.55 18 14.8858 18 15.3V15.3C18 15.7142 17.6642 16.05 17.25 16.05H6.25C5.83579 16.05 5.5 15.7142 5.5 15.3V15.3Z" | ||
| fill="#CED0D6"/> | ||
| </svg> |
| <!-- Copyright © 2000–2024 JetBrains s.r.o. --> | ||
| <svg width="13" height="13" viewBox="0 0 13 13" xmlns="http://www.w3.org/2000/svg"> | ||
| <path fill="#afb1b3" d="M6 6H1V1h5zm0 1H1v5h5zm6 0H7v5h5zM9.5.5l-3 3 3 3 3-3z"/> | ||
| </svg> |
| <!-- Copyright © 2000–2024 JetBrains s.r.o. --> | ||
| <svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"> | ||
| <path fill="none" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" | ||
| d="M10 18.15v-15c0-.69-.56-1.25-1.25-1.25H3.12c-.69 0-1.25.56-1.25 1.25V16.9c0 .69.56 1.25 1.25 1.25h13.75c.69 0 1.25-.56 1.25-1.25v-5.62c0-.69-.56-1.25-1.25-1.25H1.88"/> | ||
| <path fill="none" stroke="#CED0D6" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" | ||
| d="M18.96 4.58c.19.19.29.45.29.7s-.1.51-.29.7l-3.51 3.51a.984.984 0 0 1-1.4 0l-3.51-3.51a.984.984 0 0 1 0-1.4l3.51-3.51c.97-.97 1.69.28 4.91 3.51"/> | ||
| </svg> |
| from __future__ import annotations | ||
| import logging | ||
| from functools import cached_property | ||
| from wwwpy.common.collectionlib import ObservableList | ||
| logger = logging.getLogger(__name__) | ||
| _node_id = 0 | ||
| class NodeList(ObservableList): | ||
| def __init__(self, *args): | ||
| super().__init__(*args) | ||
| self._parent = None | ||
| def _item_added(self, item, index): | ||
| if isinstance(item, Node): | ||
| item._parent = self | ||
| else: | ||
| logger.warning(f'Expected Node, got {type(item)}') | ||
| def _item_removed(self, item, index): | ||
| if isinstance(item, Node): | ||
| item._parent = None | ||
| else: | ||
| logger.warning(f'Expected Node, got {type(item)}') | ||
| @property | ||
| def children(self) -> list[Node]: | ||
| return self | ||
| def selected_nodes(self) -> set[Node]: | ||
| uf = [n for n in self.children if n.selected] | ||
| s = set(uf) | ||
| for ch in self.children: | ||
| s.update(ch.selected_nodes()) | ||
| return s | ||
| def deselect_all(self): | ||
| for ch in self.children: | ||
| ch.selected = False | ||
| for ch in self.children: | ||
| ch.deselect_all() | ||
| def root(self) -> NodeList: | ||
| if self._parent is None: | ||
| return self | ||
| return self._parent.root() | ||
| class Node(NodeList): | ||
| def __init__(self): | ||
| super().__init__() | ||
| self._selected = False | ||
| @property | ||
| def selected(self) -> bool: | ||
| return self._selected | ||
| @selected.setter | ||
| def selected(self, value: bool): | ||
| if value == self._selected: | ||
| return | ||
| if value is True: | ||
| self.root().deselect_all() | ||
| self._selected = value | ||
| def _perform_click(self): | ||
| self.selected = True | ||
| def _perform_toggle(self): | ||
| raise NotImplementedError | ||
| def __repr__(self): | ||
| return f'Node(id={self.node_id})' | ||
| @cached_property | ||
| def node_id(self) -> int: | ||
| global _node_id | ||
| _node_id += 1 | ||
| return _node_id | ||
| def __eq__(self, other): | ||
| return self is other | ||
| def __hash__(self): | ||
| return id(self) | ||
| class Tree(NodeList): | ||
| pass |
| import wwwpy.remote.component as wpc | ||
| from wwwpy.remote import dict_to_js | ||
| from wwwpy.remote.designer.ui.tree.custom_tree import TreeElement, ItemPresentation, CustomTree | ||
| # Helper to build TreeElement hierarchy from JSON | ||
| def createTreeFromJSON(json_node, parent=None): | ||
| text = json_node.get('text', 'Unnamed') | ||
| icon = json_node.get('icon', None) | ||
| bg = json_node.get('backgroundColor', None) | ||
| pres = ItemPresentation(text, icon, bg) | ||
| element = None | ||
| def loader(): | ||
| return [createTreeFromJSON(child, element) for child in json_node.get('children', [])] | ||
| element = TreeElement(pres, loader, parent) | ||
| return element | ||
| # Demo component that showcases the CustomTree usage | ||
| class CustomTreeDemo(wpc.Component, tag_name='custom-tree-demo'): | ||
| fileTree: CustomTree = wpc.element() | ||
| orgTree: CustomTree = wpc.element() | ||
| def init_component(self): | ||
| # attach shadow DOM and template | ||
| self.element.attachShadow(dict_to_js({'mode': 'open'})) | ||
| self.element.shadowRoot.innerHTML = """ | ||
| <style> | ||
| :host { display: block; } | ||
| .tree-container { background: #161b22; border-radius: 8px; padding: 20px; box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); max-width: 600px; border: 1px solid #30363d; margin-bottom: 30px; } | ||
| .demo-title { color: #f0f6fc; margin-bottom: 10px; font-size: 1.2em; } | ||
| </style> | ||
| <div class="demo-section"> | ||
| <h2 class="demo-title">File System Tree Example</h2> | ||
| <div class="tree-container"> | ||
| <custom-tree data-name="fileTree"></custom-tree> | ||
| </div> | ||
| </div> | ||
| <div class="demo-section"> | ||
| <h2 class="demo-title">Organization Tree Example</h2> | ||
| <div class="tree-container"> | ||
| <custom-tree data-name="orgTree"></custom-tree> | ||
| </div> | ||
| </div> | ||
| """ | ||
| async def after_init_component(self): | ||
| # JSON for file system tree | ||
| fileSystemJSON = { | ||
| 'text': 'Project Root', 'icon': '📁', | ||
| 'children': [ | ||
| {'text': 'src', 'icon': '📁', 'children': [ | ||
| {'text': 'components', 'icon': '📁', 'children': [ | ||
| {'text': 'Button.js', 'icon': '📄'}, | ||
| {'text': 'Modal.js', 'icon': '📄'}, | ||
| {'text': 'Tree.js', 'icon': '📄', 'backgroundColor': '#3d2817'} | ||
| ]}, | ||
| {'text': 'utils', 'icon': '📁', 'backgroundColor': '#2d4a22', 'children': [ | ||
| {'text': 'helpers.js', 'icon': '📄'}, | ||
| {'text': 'constants.js', 'icon': '📄'} | ||
| ]}, | ||
| {'text': 'index.js', 'icon': '📄', 'backgroundColor': '#1f2937'} | ||
| ]}, | ||
| {'text': 'public', 'icon': '📁', 'children': [ | ||
| {'text': 'index.html', 'icon': '🌐'}, | ||
| {'text': 'favicon.ico', 'icon': '🖼️', 'backgroundColor': '#4c1d95'} | ||
| ]}, | ||
| {'text': 'package.json', 'icon': '📦'}, | ||
| {'text': 'README.md', 'icon': '📝'} | ||
| ] | ||
| } | ||
| # JSON for organization tree | ||
| organizationJSON = { | ||
| 'text': 'TechCorp Inc.', 'icon': '🏢', 'backgroundColor': '#1e293b', | ||
| 'children': [ | ||
| {'text': 'Engineering', 'icon': '⚙️', 'backgroundColor': '#1f2937', 'children': [ | ||
| {'text': 'Frontend Team', 'icon': '💻', 'children': [ | ||
| {'text': 'Alice Johnson - Lead', 'icon': '👤', 'backgroundColor': '#7c2d12'}, | ||
| {'text': 'Bob Smith - Developer', 'icon': '👤'}, | ||
| {'text': 'Carol Williams - Developer', 'icon': '👤'} | ||
| ]}, | ||
| {'text': 'Backend Team', 'icon': '🔧', 'children': [ | ||
| {'text': 'David Brown - Lead', 'icon': '👤', 'backgroundColor': '#7c2d12'}, | ||
| {'text': 'Eva Davis - Developer', 'icon': '👤'} | ||
| ]} | ||
| ]}, | ||
| {'text': 'Product', 'icon': '📊', 'backgroundColor': '#2d1b69', 'children': [ | ||
| {'text': 'Sarah Wilson - Manager', 'icon': '👤', 'backgroundColor': '#7c2d12'}, | ||
| {'text': 'Mike Chen - Designer', 'icon': '👤'} | ||
| ]}, | ||
| {'text': 'Sales', 'icon': '💼', 'backgroundColor': '#14532d', 'children': [ | ||
| {'text': 'Tom Anderson - Director', 'icon': '👤', 'backgroundColor': '#7c2d12'}, | ||
| {'text': 'Lisa Garcia - Rep', 'icon': '👤'} | ||
| ]} | ||
| ] | ||
| } | ||
| # build trees | ||
| fileRoot = createTreeFromJSON(fileSystemJSON) | ||
| orgRoot = createTreeFromJSON(organizationJSON) | ||
| # render | ||
| self.fileTree.setRoot(fileRoot) | ||
| self.orgTree.setRoot(orgRoot) | ||
| # add selection listeners | ||
| # def on_file_select(e): js.console.log('File selected:', e.detail.treeElement.presentation.getPresentableText()) | ||
| # self.fileTree.addEventListener('nodeSelect', create_proxy(on_file_select)) | ||
| # def on_org_select(e): js.console.log('Person/Department selected:', e.detail.treeElement.presentation.getPresentableText()) | ||
| # self.orgTree.addEventListener('nodeSelect', create_proxy(on_org_select)) |
| from __future__ import annotations | ||
| import js | ||
| from pyodide.ffi import create_proxy | ||
| import wwwpy.remote.component as wpc | ||
| from wwwpy.remote import dict_to_js | ||
| class ItemPresentation: | ||
| def __init__(self, text, icon=None, backgroundColor=None): | ||
| self.text = text if not icon else f"{icon} {text}" | ||
| self.icon = icon | ||
| self.backgroundColor = backgroundColor | ||
| # Tree node model with lazy child loading and rendering | ||
| class TreeElement: | ||
| def __init__(self, presentation: ItemPresentation, child_loader, parent=None): | ||
| self.presentation = presentation | ||
| self.child_loader = child_loader | ||
| self.parent = parent | ||
| self.nodeDiv = None | ||
| self.contentDiv = None | ||
| self.children = None | ||
| @property | ||
| def level(self): | ||
| return self.parent.level + 1 if self.parent else 0 | ||
| def getChildren(self): | ||
| if self.children is None: | ||
| self.children = self.child_loader() | ||
| return self.children | ||
| def hasChildren(self): | ||
| return len(self.getChildren()) > 0 | ||
| def getNodeFragment(self, default_template): | ||
| frag = default_template.content.cloneNode(True) | ||
| self.nodeDiv = frag.querySelector('[data-id="node"]') | ||
| self.contentDiv = frag.querySelector('[data-id="content"]') | ||
| return frag | ||
| def toggle(self): | ||
| childrenDiv = self.nodeDiv.querySelector('.tree-node-children') | ||
| toggleBtn = self.nodeDiv.querySelector('.tree-node-toggle') | ||
| expanded = childrenDiv.classList.contains('expanded') | ||
| if expanded: | ||
| childrenDiv.classList.replace('expanded', 'collapsed') | ||
| toggleBtn.classList.remove('expanded') | ||
| else: | ||
| childrenDiv.classList.replace('collapsed', 'expanded') | ||
| toggleBtn.classList.add('expanded') | ||
| def render_fragment(self, default_template: js.HTMLTemplateElement, tree_instance: CustomTree): | ||
| frag = self.getNodeFragment(default_template) | ||
| self.nodeDiv.dataset.level = self.level | ||
| hasKids = self.hasChildren() | ||
| btn = frag.querySelector('[data-id="toggle-btn"]') | ||
| btn.classList.toggle('leaf', not hasKids) | ||
| if hasKids: | ||
| childrenDiv = frag.querySelector('[data-id="children-container"]') | ||
| childrenDiv.className = 'tree-node-children collapsed' | ||
| def on_toggle(e): | ||
| e.stopPropagation() | ||
| if not childrenDiv.hasChildNodes(): | ||
| for c in self.getChildren(): | ||
| fr = c.render_fragment(default_template, tree_instance) | ||
| childrenDiv.appendChild(fr) | ||
| self.toggle() | ||
| btn.addEventListener('click', create_proxy(on_toggle)) | ||
| textSpan = frag.querySelector('[data-id="text"]') | ||
| textSpan.textContent = self.presentation.text | ||
| self.contentDiv.style.paddingLeft = f"{self.level * 20 + 8}px" | ||
| bg = self.presentation.backgroundColor | ||
| if bg: | ||
| self.contentDiv.style.backgroundColor = bg | ||
| def on_select(ev): | ||
| tree_instance.selectNode(self.contentDiv, self) | ||
| self.contentDiv.addEventListener('click', create_proxy(on_select)) | ||
| return frag | ||
| # Web component for rendering a tree | ||
| class CustomTree(wpc.Component, tag_name='custom-tree'): | ||
| _tree_container: js.HTMLDivElement = wpc.element() | ||
| _default_template: js.HTMLTemplateElement = wpc.element() | ||
| def init_component(self): | ||
| # attach shadow and template | ||
| self.element.attachShadow(dict_to_js({'mode': 'open'})) | ||
| self.element.shadowRoot.innerHTML = """ | ||
| <style id="tree-style"> | ||
| :host { display: block; } | ||
| .tree-node { margin: 0; user-select: none; } | ||
| .tree-node-content { display: flex; align-items: center; padding: 2px; font-size: 12px; width: 100%; box-sizing: border-box; border-radius: 4px; } | ||
| .tree-node-content.selected { background-color: #1f6feb !important; color: #ffffff; } | ||
| .tree-node-toggle { width: 16px; height: 16px; margin-right: 4px; border: none; background: none; font-size: 12px; display: flex; align-items: center; justify-content: center; color: #8b949e; } | ||
| .tree-node-toggle .toggle-icon { display: flex; align-items: center; justify-content: center; width: 16px; height: 16px; } | ||
| .tree-node-toggle.expanded .toggle-icon { transform: rotate(90deg); } | ||
| .tree-node-toggle.leaf { visibility: hidden; } | ||
| .tree-node-text { flex: 1; padding: 2px 4px; } | ||
| .tree-node-children { margin-left: 0; overflow: auto; } | ||
| .tree-node-children.collapsed { max-height: 0; } | ||
| .tree-node-children.expanded { max-height: 1000px; } | ||
| </style> | ||
| <template data-name="_default_template"> | ||
| <div class="tree-node" data-id="node" data-level=""> | ||
| <div class="tree-node-content" data-id="content"> | ||
| <button class="tree-node-toggle" data-id="toggle-btn"> | ||
| <span class="toggle-icon" data-id="toggle-icon"> | ||
| <!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. --> | ||
| <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||
| <path d="M6 11.5L9.5 8L6 4.5" stroke="#B4B8BF" stroke-linecap="round"/> | ||
| </svg> | ||
| </span> | ||
| </button> | ||
| <span class="tree-node-text" data-id="text"></span> | ||
| </div> | ||
| <div class="tree-node-children" data-id="children-container"></div> | ||
| </div> | ||
| </template> | ||
| <div data-name="_tree_container"></div> | ||
| """ | ||
| self.root = None | ||
| self.selectedNode = None | ||
| def setRoot(self, root: TreeElement): | ||
| self.root = root | ||
| self._tree_container.innerHTML = '' | ||
| if root: | ||
| self._tree_container.append(root.render_fragment(self._default_template, self, )) | ||
| def selectNode(self, contentDiv, treeElement): | ||
| if self.selectedNode: | ||
| self.selectedNode.classList.remove('selected') | ||
| contentDiv.classList.add('selected') | ||
| self.selectedNode = contentDiv | ||
| self.element.dispatchEvent(js.CustomEvent.new( | ||
| 'nodeSelect', | ||
| dict_to_js({'detail': {'treeElement': treeElement}}) | ||
| )) |
+6
-3
| Metadata-Version: 2.4 | ||
| Name: wwwpy | ||
| Version: 0.1.85 | ||
| Version: 0.1.86 | ||
| Summary: Build Powerful Web Applications: Simple, Scalable, and Fully Customizable | ||
@@ -232,6 +232,7 @@ Author-email: Simone Giacomelli <simone.giacomelli@gmail.com> | ||
| Requires-Dist: pytest; extra == "test" | ||
| Requires-Dist: pytest-asyncio; extra == "test" | ||
| Requires-Dist: playwright; extra == "test" | ||
| Requires-Dist: pytest-playwright; extra == "test" | ||
| Requires-Dist: pytest-xvirt; extra == "test" | ||
| Requires-Dist: libcst==1.4.0; extra == "test" | ||
| Requires-Dist: libcst==1.6.0; extra == "test" | ||
| Requires-Dist: rope==1.13.0; extra == "test" | ||
@@ -246,4 +247,6 @@ Provides-Extra: dev | ||
| Requires-Dist: build; extra == "pypi" | ||
| Provides-Extra: stubs | ||
| Requires-Dist: pyodide-stubs; extra == "stubs" | ||
| Provides-Extra: all | ||
| Requires-Dist: wwwpy[dev,pypi,test]; extra == "all" | ||
| Requires-Dist: wwwpy[dev,pypi,stubs,test]; extra == "all" | ||
| Dynamic: license-file | ||
@@ -250,0 +253,0 @@ |
+16
-6
| # https://packaging.python.org/en/latest/tutorials/packaging-projects/ | ||
| [project] | ||
| name = "wwwpy" | ||
| version = "0.1.85" | ||
| version = "0.1.86" | ||
@@ -47,12 +47,16 @@ # todo | ||
| [tool.setuptools.package-data] | ||
| "wwwpy" = ["**/*.json", "**/*.txt"] | ||
| "wwwpy" = ["**/*.json", "**/*.txt", "**/*.svg"] | ||
| [project.optional-dependencies] | ||
| # libcst==1.4.0 is the version available in pyodide 0.27.2 | ||
| test = ["tox", "pytest", "playwright", "pytest-playwright", "pytest-xvirt", "libcst==1.4.0", "rope==1.13.0"] | ||
| # libcst==1.6.0 is the version available in pyodide 0.28.0 | ||
| test = ["tox", "pytest", "pytest-asyncio", "playwright", "pytest-playwright", "pytest-xvirt", "libcst==1.6.0", "rope==1.13.0"] | ||
| dev = ["webtypy", "playwright", "setuptools", "pytest-asyncio"] # setuptools is needed from PyCharm | ||
| pypi = ["twine", "build"] | ||
| # stubs = ["pyodide-stubs @ file://./pyodide-stubs"] | ||
| #stubs = ["pyodide-stubs @ file:///${PWD}/pyodide-stubs"] | ||
| stubs = ["pyodide-stubs"] | ||
| # pip install -e ".[all]" or pip install -e ".[test,dev]" | ||
| all = ["wwwpy[test,dev,pypi]"] | ||
| #all = ["wwwpy[test,dev,pypi]"] | ||
| all = ["wwwpy[test,dev,pypi,stubs]"] | ||
@@ -68,2 +72,8 @@ | ||
| [project.scripts] | ||
| wwwpy = "wwwpy.server.__main__:main" | ||
| wwwpy = "wwwpy.server.__main__:main" | ||
| #[tool.mypy] | ||
| #mypy_path = ["stubs"] | ||
| [tool.uv.sources] | ||
| pyodide-stubs = { path = "pyodide-stubs" } |
| Metadata-Version: 2.4 | ||
| Name: wwwpy | ||
| Version: 0.1.85 | ||
| Version: 0.1.86 | ||
| Summary: Build Powerful Web Applications: Simple, Scalable, and Fully Customizable | ||
@@ -232,6 +232,7 @@ Author-email: Simone Giacomelli <simone.giacomelli@gmail.com> | ||
| Requires-Dist: pytest; extra == "test" | ||
| Requires-Dist: pytest-asyncio; extra == "test" | ||
| Requires-Dist: playwright; extra == "test" | ||
| Requires-Dist: pytest-playwright; extra == "test" | ||
| Requires-Dist: pytest-xvirt; extra == "test" | ||
| Requires-Dist: libcst==1.4.0; extra == "test" | ||
| Requires-Dist: libcst==1.6.0; extra == "test" | ||
| Requires-Dist: rope==1.13.0; extra == "test" | ||
@@ -246,4 +247,6 @@ Provides-Extra: dev | ||
| Requires-Dist: build; extra == "pypi" | ||
| Provides-Extra: stubs | ||
| Requires-Dist: pyodide-stubs; extra == "stubs" | ||
| Provides-Extra: all | ||
| Requires-Dist: wwwpy[dev,pypi,test]; extra == "all" | ||
| Requires-Dist: wwwpy[dev,pypi,stubs,test]; extra == "all" | ||
| Dynamic: license-file | ||
@@ -250,0 +253,0 @@ |
@@ -6,3 +6,3 @@ tornado==6.4.2 | ||
| [all] | ||
| wwwpy[dev,pypi,test] | ||
| wwwpy[dev,pypi,stubs,test] | ||
@@ -19,9 +19,13 @@ [dev] | ||
| [stubs] | ||
| pyodide-stubs | ||
| [test] | ||
| tox | ||
| pytest | ||
| pytest-asyncio | ||
| playwright | ||
| pytest-playwright | ||
| pytest-xvirt | ||
| libcst==1.4.0 | ||
| libcst==1.6.0 | ||
| rope==1.13.0 |
@@ -33,2 +33,3 @@ LICENSE | ||
| src/wwwpy/common/asynclib.py | ||
| src/wwwpy/common/attr_lib.py | ||
| src/wwwpy/common/collectionlib.py | ||
@@ -90,2 +91,16 @@ src/wwwpy/common/detect.py | ||
| src/wwwpy/common/designer/ui/svg.py | ||
| src/wwwpy/common/designer/ui/tree_node.py | ||
| src/wwwpy/common/designer/ui/icons/__init__.py | ||
| src/wwwpy/common/designer/ui/icons/all_icons.py | ||
| src/wwwpy/common/designer/ui/icons/events.svg | ||
| src/wwwpy/common/designer/ui/icons/jb/console_dark.svg | ||
| src/wwwpy/common/designer/ui/icons/jb/project@20x20_dark.svg | ||
| src/wwwpy/common/designer/ui/icons/jb/properties_dark.svg | ||
| src/wwwpy/common/designer/ui/icons/jb/pythonPackages_dark.svg | ||
| src/wwwpy/common/designer/ui/icons/jb/python_stroke.svg | ||
| src/wwwpy/common/designer/ui/icons/jb/services_dark.svg | ||
| src/wwwpy/common/designer/ui/icons/jb/structure@20x20_dark.svg | ||
| src/wwwpy/common/designer/ui/icons/jb/todo@20x20_dark.svg | ||
| src/wwwpy/common/designer/ui/icons/jb/toolWindowComponents@20x20_dark.svg | ||
| src/wwwpy/common/designer/ui/icons/jb/toolWindowComponents_dark.svg | ||
| src/wwwpy/common/filesystem/__init__.py | ||
@@ -203,7 +218,5 @@ src/wwwpy/common/filesystem/sync/__init__.py | ||
| src/wwwpy/remote/designer/ui/quickstart_ui.py | ||
| src/wwwpy/remote/designer/ui/searchable_combobox.py | ||
| src/wwwpy/remote/designer/ui/searchable_combobox2.py | ||
| src/wwwpy/remote/designer/ui/searchable_list_1.py | ||
| src/wwwpy/remote/designer/ui/svg_icon.py | ||
| src/wwwpy/remote/designer/ui/toolbox.py | ||
| src/wwwpy/remote/designer/ui/window_component.py | ||
@@ -214,2 +227,5 @@ src/wwwpy/remote/designer/ui/system_tools/__init__.py | ||
| src/wwwpy/remote/designer/ui/system_tools/system_versions.py | ||
| src/wwwpy/remote/designer/ui/tree/__init__.py | ||
| src/wwwpy/remote/designer/ui/tree/custom_tree.py | ||
| src/wwwpy/remote/designer/ui/tree/custom_tree_demo.py | ||
| src/wwwpy/server/__init__.py | ||
@@ -216,0 +232,0 @@ src/wwwpy/server/__main__.py |
@@ -1,3 +0,3 @@ | ||
| __version__ = "0.1.85" | ||
| git_hash_short = "de8c216" | ||
| git_hash = "de8c2161a0c53284014bbbc2bd5e92f7087227b7" | ||
| __version__ = "0.1.86" | ||
| git_hash_short = "811ea0e" | ||
| git_hash = "811ea0e91bcb2135ffe67865b690c52145cb824d" |
@@ -0,1 +1,2 @@ | ||
| import json | ||
| import textwrap | ||
@@ -14,4 +15,4 @@ from typing import List, Tuple | ||
| python: str, | ||
| jspi=False, | ||
| zip_route_path: str = '/wwwpy/bundle.zip', | ||
| packages: list[str] | None = None, | ||
| html: str = f'<!DOCTYPE html><h1>Loading...</h1><script>{bootstrap_javascript_placeholder}</script>', | ||
@@ -38,3 +39,3 @@ ) -> Tuple[HttpRoute, HttpRoute]: | ||
| javascript = get_javascript_for(bootstrap_python, jspi) | ||
| javascript = get_javascript_for(bootstrap_python, packages=packages) | ||
| html_replaced = html.replace(bootstrap_javascript_placeholder, javascript) | ||
@@ -45,7 +46,11 @@ bootstrap_route = HttpRoute('/', lambda request, resp: resp(HttpResponse.text_html(html_replaced))) | ||
| def get_javascript_for(python_code: str, jspi=False) -> str: | ||
| load = '' if not jspi else '{enableRunUntilComplete: true}' | ||
| def get_javascript_for(python_code: str, packages: list[str] = None) -> str: | ||
| # see https://pyodide.org/en/stable/usage/api/js-api.html#globalThis.loadPyodide | ||
| loadPyodide_options = { | ||
| 'convertNullToNone': True, | ||
| 'packages': packages or [], | ||
| } | ||
| return (_js_content | ||
| .replace('# python replace marker', python_code) | ||
| .replace('`# load option marker`', load)) | ||
| .replace('`# load option marker`', json.dumps(loadPyodide_options))) | ||
@@ -58,3 +63,3 @@ | ||
| let script = document.createElement('script'); | ||
| script.src = 'https://cdn.jsdelivr.net/pyodide/v0.27.6/full/pyodide.js'; | ||
| script.src = 'https://cdn.jsdelivr.net/pyodide/v0.28.1/full/pyodide.js'; | ||
| script.onload = async () => { | ||
@@ -61,0 +66,0 @@ let pyodide = await loadPyodide(`# load option marker`); |
| from __future__ import annotations | ||
| from typing import Callable, TypeVar, Any, Generic, Collection | ||
| from typing import Callable, TypeVar, Collection, Generic | ||
@@ -38,1 +38,83 @@ T = TypeVar('T') | ||
| return self._map.get(key) | ||
| from collections import UserList | ||
| class ObservableList(UserList[T], Generic[T]): | ||
| def _item_added(self, item, index): | ||
| pass | ||
| def _item_removed(self, item, index): | ||
| pass | ||
| def append(self, item): | ||
| super().append(item) | ||
| self._item_added(item, len(self.data) - 1) | ||
| def extend(self, items): | ||
| start = len(self.data) | ||
| super().extend(items) | ||
| for i, item in enumerate(items, start): | ||
| self._item_added(item, i) | ||
| def insert(self, index, item): | ||
| super().insert(index, item) | ||
| self._item_added(item, index) | ||
| def remove(self, item): | ||
| idx = self.data.index(item) | ||
| super().remove(item) | ||
| self._item_removed(item, idx) | ||
| def pop(self, index=-1): | ||
| idx = index if index >= 0 else len(self.data) + index | ||
| item = super().pop(index) | ||
| self._item_removed(item, idx) | ||
| return item | ||
| def clear(self): | ||
| for i, item in enumerate(self.data): | ||
| self._item_removed(item, i) | ||
| super().clear() | ||
| def __setitem__(self, index, value): | ||
| if isinstance(index, slice): | ||
| old = self.data[index] | ||
| super().__setitem__(index, value) | ||
| for i, item in enumerate(value, index.start or 0): | ||
| self._item_added(item, i) | ||
| for i, item in enumerate(old, index.start or 0): | ||
| self._item_removed(item, i) | ||
| else: | ||
| idx = index if index >= 0 else len(self.data) + index | ||
| old = self.data[index] | ||
| super().__setitem__(index, value) | ||
| self._item_removed(old, idx) | ||
| self._item_added(value, idx) | ||
| def __delitem__(self, index): | ||
| if isinstance(index, slice): | ||
| for i, item in enumerate(self.data[index], index.start or 0): | ||
| self._item_removed(item, i) | ||
| else: | ||
| idx = index if index >= 0 else len(self.data) + index | ||
| item = self.data[index] | ||
| self._item_removed(item, idx) | ||
| super().__delitem__(index) | ||
| def __iadd__(self, other): | ||
| self.extend(other) | ||
| return self | ||
| def __imul__(self, n): | ||
| original = list(self.data) | ||
| for _ in range(n - 1): | ||
| self.extend(original) | ||
| return self | ||
| def __add__(self, other): | ||
| new = type(self)(self.data + list(other)) | ||
| for i, item in enumerate(other, len(self.data)): | ||
| new._item_added(item, i) | ||
| return new |
@@ -1,1 +0,1 @@ | ||
| pypi_packages = ['libcst==1.4.0', 'rope==1.13.0'] | ||
| pypi_packages = ['libcst==1.6.0', 'rope==1.13.0'] |
@@ -15,3 +15,3 @@ from __future__ import annotations | ||
| from wwwpy.common.designer.element_library import ElementDefBase | ||
| from wwwpy.common.designer.html_edit import Position, html_add_indexed, html_remove_indexed | ||
| from wwwpy.common.designer.html_edit import Position, html_add_indexed, html_remove_indexed, HtmlAddResult | ||
| from wwwpy.common.designer.html_locator import NodePath, IndexPath, data_name | ||
@@ -107,2 +107,9 @@ | ||
| position: Position) -> AddResult | AddFailed: | ||
| logger.debug(f'Adding element `{edb.tag_name}` of type `{edb.python_type}` to class {class_name}' | ||
| f' at index path {index_path} position {position}') | ||
| class_info = code_info.class_info(source_code, class_name) | ||
| if class_info is None: | ||
| print(f'Class {class_name} not found inside source ```{source_code}```') | ||
| return AddFailed(Exception(f'Class not found `{class_name}`'), '', '') | ||
| source_code_orig = source_code | ||
@@ -117,6 +124,2 @@ try: | ||
| source_code = ensure_imports(source_code, imp_tuple) | ||
| class_info = code_info.class_info(source_code, class_name) | ||
| if class_info is None: | ||
| print(f'Class {class_name} not found inside source ```{source_code}```') | ||
| return None | ||
@@ -128,20 +131,14 @@ attr_name = class_info.next_attribute_name(edb.tag_name) | ||
| Attribute(attr_name, type_name, 'wpc.element()')) | ||
| changed_html = [] | ||
| html_add_res: HtmlAddResult | None = None | ||
| def manipulate_html(html): | ||
| add = html_add_indexed(html, named_html, index_path, position) | ||
| changed_html.append(add) | ||
| return add | ||
| nonlocal html_add_res | ||
| html_add_res = html_add_indexed(html, named_html, index_path, position) | ||
| return html_add_res.html | ||
| source2 = html_string_edit(source1, class_name, manipulate_html) | ||
| new_tree = html_parser.html_to_tree(source2) | ||
| if position == Position.afterbegin: | ||
| indexes = index_path + [0] | ||
| elif position == Position.beforeend: | ||
| indexes = index_path + [-1] | ||
| if html_add_res is None or not html_add_res.success: | ||
| result = AddFailed(Exception('Failed to add element'), '', '') | ||
| else: | ||
| displacement = 0 if position == Position.beforebegin else 1 | ||
| indexes = index_path[0:-1] + [index_path[-1] + displacement] | ||
| new_node_path = html_locator.tree_to_path(new_tree, indexes) | ||
| result = AddResult(source2, new_node_path, changed_html[0]) | ||
| result = AddResult(source2, html_add_res.new_node_path, html_add_res.html) | ||
| except Exception as e: | ||
@@ -148,0 +145,0 @@ import traceback |
@@ -20,5 +20,2 @@ from __future__ import annotations | ||
| class LocatorNode: | ||
| # locator: Locator | ||
| # children: list[LocatorNode] | ||
| # cst_node: CstNode | ||
| _cst_children: list[CstNode] | ||
@@ -44,11 +41,2 @@ | ||
| # class LocatorTree(UserList[LocatorNode]): | ||
| # parent: CompInfo | ||
| # cst_tree: CstTree | ||
| # | ||
| # def __init__(self, parent: CompInfo, cst: CstTree): | ||
| # nodes = (LocatorNode()) | ||
| # super().__init__() | ||
| # todo extend CompInfo such as it provides the means for CompStructure component to show | ||
@@ -64,5 +52,24 @@ # the tree structure of the component's HTML. | ||
| path: Path | ||
| cst_tree: CstTree | ||
| source_code: str | ||
| _html_found: bool = None | ||
| @cached_property | ||
| def html(self) -> str: | ||
| html = html_from_source(self.source_code, self.class_name) | ||
| self._html_found = html is not None | ||
| if html is None: | ||
| logger.debug(f'Cannot find html for {self.class_name} in {self.path}') | ||
| return '' | ||
| return html | ||
| @property | ||
| def html_found(self) -> bool: | ||
| x = self.html | ||
| return self._html_found | ||
| @cached_property | ||
| def cst_tree(self) -> CstTree: | ||
| return html_to_tree(self.html) | ||
| @cached_property | ||
| def locator_root(self) -> LocatorNode: | ||
@@ -87,3 +94,6 @@ """The CST tree of the component's HTML. | ||
| for path in sorted(folder.glob('*.py'), key=lambda p: p.stem): | ||
| yield from iter_comp_info(path, package + '.' + path.stem) | ||
| infos = iter_comp_info(path, package + '.' + path.stem) | ||
| for info in infos: | ||
| if info.html_found: | ||
| yield info | ||
@@ -96,14 +106,3 @@ | ||
| ci = code_info.info(source_code) | ||
| return (c for c in (_to_comp_info(source_code, path, cl, package) for cl in ci.classes) if c is not None) | ||
| def _to_comp_info(source_code: str, path: Path, cl: code_info.ClassInfo, package: str) -> CompInfo | None: | ||
| class_name = cl.name | ||
| html = html_from_source(source_code, class_name) | ||
| if html is None: | ||
| logger.warning(f'Cannot find html for {class_name} in {path}') | ||
| return None | ||
| cst_tree = html_to_tree(html) | ||
| return CompInfo(package, class_name, cl.tag_name, path, cst_tree) | ||
| return (c for c in (CompInfo(package, cl.name, cl.tag_name, path, source_code) for cl in ci.classes) if | ||
| c is not None) |
@@ -328,3 +328,4 @@ from typing import List | ||
| 'progress': lambda: f'<progress data-name="{name}" value="70" max="100">70%</progress>', | ||
| 'textarea': _def(placeHolder=True, inner='', add='rows="6" wrap="off" style="width: 100%"'), | ||
| 'textarea': _def(placeHolder=True, inner='', | ||
| add='rows="6" wrap="off" style="width: 100%; box-sizing: border-box"'), | ||
| 'select': _def(inner=''' | ||
@@ -331,0 +332,0 @@ <option value="option1">Option 1</option> |
@@ -154,2 +154,4 @@ from __future__ import annotations | ||
| value = node.attributes.get(attribute_def.name, None) | ||
| if value is not None: | ||
| value = escape_string(value) | ||
| attribute_editor = AttributeEditor(attribute_def, exists, value, | ||
@@ -190,2 +192,4 @@ self._attribute_set_value, self._attribute_remove) | ||
| def _attribute_set_value(self, attribute_editor: AttributeEditor, value: str | None): | ||
| if value is not None: | ||
| value = unescape_string(value) | ||
@@ -192,0 +196,0 @@ def _html_manipulate(html: str) -> str: |
| """This module contains the HTML string manipulator functions.""" | ||
| from __future__ import annotations | ||
| from dataclasses import dataclass | ||
| from enum import Enum | ||
@@ -28,6 +29,19 @@ from html import escape | ||
| def html_add_indexed(html: str, add: str, index_path: IndexPath, position: Position) -> str: | ||
| @dataclass | ||
| class HtmlAddResult: | ||
| html: str | ||
| new_node_path: NodePath | None | ||
| @property | ||
| def success(self) -> bool: | ||
| return self.new_node_path is not None | ||
| def html_add_indexed(html: str, add: str, index_path: IndexPath, position: Position) -> HtmlAddResult: | ||
| check_node_path(index_path) | ||
| """This function adds an HTML piece to the specified position in the HTML string.""" | ||
| def fail() -> HtmlAddResult: | ||
| return HtmlAddResult(html, None) | ||
| start, end = html_locator.locate_span_indexed(html, index_path) | ||
@@ -43,3 +57,3 @@ | ||
| if node is None or node.content_span is None: | ||
| return html | ||
| return fail() | ||
| index = node.content_span[0] | ||
@@ -50,3 +64,3 @@ elif position == Position.beforeend: | ||
| if node is None or node.content_span is None: | ||
| return html | ||
| return fail() | ||
| index = node.content_span[1] | ||
@@ -56,5 +70,18 @@ else: | ||
| return html[:index] + add + html[index:] | ||
| new_html = html[:index] + add + html[index:] | ||
| import wwwpy.common.designer.html_parser as html_parser | ||
| new_tree = html_parser.html_to_tree(new_html) | ||
| if position == Position.afterbegin: | ||
| indexes = index_path + [0] | ||
| elif position == Position.beforeend: | ||
| indexes = index_path + [-1] | ||
| else: | ||
| displacement = 0 if position == Position.beforebegin else 1 | ||
| indexes = index_path[0:-1] + [index_path[-1] + displacement] | ||
| new_node_path = html_locator.tree_to_path(new_tree, indexes) | ||
| return HtmlAddResult(new_html, new_node_path) | ||
| def html_edit(html: str, edit: str, node_path: NodePath) -> str: | ||
@@ -114,3 +141,2 @@ """This function edits the HTML string at the specified path.""" | ||
| value_present = cst_attr.value_span is not None | ||
@@ -117,0 +143,0 @@ x = cst_attr.name_span[0] |
| from __future__ import annotations | ||
| import json | ||
| import logging | ||
@@ -40,2 +39,3 @@ from dataclasses import dataclass | ||
| def path_to_index(path: NodePath) -> IndexPath: | ||
@@ -50,31 +50,7 @@ return [node.child_index for node in path] | ||
| def node_path_serialize(path: NodePath) -> str: | ||
| return json.dumps([node.__dict__ for node in path]) | ||
| def node_path_deserialize(serialized: str) -> NodePath: | ||
| node_dicts = json.loads(serialized) | ||
| return [Node(**node_dict) for node_dict in node_dicts] | ||
| def locate_node(html: str, path: NodePath) -> CstNode | None: | ||
| cst_tree = html_to_tree(html) | ||
| target_node = _locate_node_rec(cst_tree, path) | ||
| logger.debug(f'locate_node {path} -> {target_node} for html=```{html}```') | ||
| return target_node | ||
| index = path_to_index(path) | ||
| return locate_node_indexed(html, index) | ||
| def _locate_node_rec(nodes: CstTree, path: NodePath, depth: int = 0) -> CstNode | None: | ||
| if depth >= len(path): | ||
| return None | ||
| target_node = path[depth] | ||
| if target_node.child_index < 0 or target_node.child_index >= len(nodes): | ||
| return None | ||
| node = nodes[target_node.child_index] | ||
| if depth == len(path) - 1: | ||
| return node | ||
| return _locate_node_rec(node.children, path, depth + 1) | ||
| def locate_node_indexed(html: str, index_path: IndexPath) -> CstNode | None: | ||
@@ -89,3 +65,7 @@ check_node_path(index_path) | ||
| child_index = path[depth] | ||
| if child_index < 0: | ||
| child_index = len(nodes) + child_index | ||
| if child_index < 0 or child_index >= len(nodes): | ||
| logger.warning(f'Child index {child_index} out of bounds for nodes' | ||
| f' {len(nodes)} at depth {depth} in path {path}') | ||
| return None | ||
@@ -106,4 +86,4 @@ node = nodes[child_index] | ||
| """ | ||
| node = locate_node(html, path) | ||
| index = path_to_index(path) | ||
| node = locate_node_indexed(html, index) | ||
| return node.span if node else None | ||
@@ -110,0 +90,0 @@ |
@@ -408,3 +408,3 @@ """A parser for HTML and XHTML.""" | ||
| gtpos = rawdata.find('>', namematch.end()) | ||
| self.handle_endtag_extended(tagname, False) | ||
| self.handle_endtag_extended(tagname, False, None) | ||
| self.handle_endtag(tagname) | ||
@@ -419,3 +419,3 @@ return gtpos + 1 | ||
| self.handle_endtag_extended(elem, False) | ||
| self.handle_endtag_extended(elem, False, match) | ||
| self.handle_endtag(elem) | ||
@@ -429,3 +429,3 @@ self.clear_cdata_mode() | ||
| self.handle_starttag_extended(tag, attrs, attrs_extended, True) | ||
| self.handle_endtag_extended(tag, True) | ||
| self.handle_endtag_extended(tag, True, None) | ||
| self.handle_endtag(tag) | ||
@@ -442,3 +442,3 @@ | ||
| # CUSTOMIZED | ||
| def handle_endtag_extended(self, tag, autoclosing): | ||
| def handle_endtag_extended(self, tag, autoclosing, match): | ||
| pass | ||
@@ -445,0 +445,0 @@ |
@@ -72,5 +72,9 @@ from __future__ import annotations | ||
| def html_to_tree(html: str) -> CstTree: | ||
| cache = _html_tree_cache.get(html, None) | ||
| if cache is not None: | ||
| return cache | ||
| parser = _PositionalHTMLParser(html) | ||
| parse = parser.parse() | ||
| _complete_tree_data(html, parse) | ||
| _html_tree_cache[html] = parse | ||
| return parse | ||
@@ -122,5 +126,12 @@ | ||
| def handle_endtag_extended(self, tag, autoclosing): | ||
| def handle_endtag_extended(self, tag, autoclosing, match): | ||
| if autoclosing: | ||
| return | ||
| if self._unexpected_endtag(tag): | ||
| if match is not None: | ||
| group0 = match.group(0) | ||
| self.handle_data(group0) | ||
| else: | ||
| self.handle_data(f'</{tag}>') | ||
| return | ||
| if tag in self.void_tags: # a void tag with end tag | ||
@@ -168,2 +179,8 @@ return | ||
| def _unexpected_endtag(self, tag: str) -> bool: | ||
| if not self.stack: | ||
| return True | ||
| if self.stack[-1].tag_name != tag: | ||
| return True | ||
| return False | ||
@@ -180,1 +197,27 @@ def _complete_tree_data(html: str, tree: CstTree, parent: CstNode | None = None, level: int = 0): | ||
| _complete_tree_data(html, node.children, node, level + 1) | ||
| from collections import OrderedDict | ||
| class LRUCache(OrderedDict): | ||
| def __init__(self, capacity): | ||
| super().__init__() | ||
| self.capacity = capacity | ||
| def __getitem__(self, key): | ||
| value = super().__getitem__(key) | ||
| self.move_to_end(key) | ||
| return value | ||
| def __setitem__(self, key, value): | ||
| if key in self: | ||
| super().__setitem__(key, value) | ||
| self.move_to_end(key) | ||
| else: | ||
| if len(self) >= self.capacity: | ||
| self.popitem(last=False) | ||
| super().__setitem__(key, value) | ||
| _html_tree_cache = LRUCache(200) |
@@ -34,8 +34,8 @@ from __future__ import annotations | ||
| def get_all_paths_with_hashes(path: str | Path) -> set[tuple[str, str | None]]: | ||
| path = Path(path) | ||
| base_path = Path(path) | ||
| paths = set() | ||
| for path in path.rglob('*'): | ||
| relative_path = path.relative_to(path) | ||
| if path.is_file(): | ||
| file_hash = get_file_hash(path) | ||
| for entry in base_path.rglob('*'): | ||
| relative_path = entry.relative_to(base_path) | ||
| if entry.is_file(): | ||
| file_hash = get_file_hash(entry) | ||
| paths.add((str(relative_path), file_hash)) | ||
@@ -42,0 +42,0 @@ else: |
@@ -56,2 +56,6 @@ import dataclasses | ||
| continue | ||
| if e.event_type == 'created' and not e.is_directory: | ||
| # treat created files as modified because sometimes we do not get 'modified' events | ||
| e = dataclasses.replace(e, event_type='modified') | ||
| rel = e.relative_to(fs) | ||
@@ -58,0 +62,0 @@ if rel.src_path == '' or rel.src_path == '.': |
| from collections import defaultdict | ||
| from contextlib import contextmanager | ||
| from dataclasses import dataclass, field | ||
| from typing import Callable, List, Optional | ||
| from contextlib import contextmanager | ||
@@ -88,3 +88,9 @@ | ||
| def is_attr_monitored(name: str) -> bool: | ||
| return not name.startswith("_") | ||
| def new_setattr(self, name, value): | ||
| if not is_attr_monitored(name): | ||
| return original_setattr(self, name, value) | ||
| old_value = getattr(self, name, None) | ||
@@ -102,3 +108,3 @@ original_setattr(self, name, value) | ||
| m = Monitor() | ||
| instance.__instance_monitor_attr = m | ||
| setattr(instance, __instance_monitor_attr, m) | ||
| return m | ||
@@ -122,2 +128,3 @@ | ||
| # todo rename to group_notifications ? | ||
| @contextmanager | ||
@@ -124,0 +131,0 @@ def group_changes(instance): |
@@ -22,4 +22,4 @@ from __future__ import annotations | ||
| # _delete_empty_project(directory) | ||
| logger.info(f'Quickstart applying {quickstart_name} to {directory}') | ||
| shutil.copytree(source, directory, dirs_exist_ok=True) | ||
| logger.warning(f'Quickstart applied {quickstart_name} to {directory}') | ||
| # print_tree(directory, logger.warning) | ||
@@ -26,0 +26,0 @@ |
@@ -5,6 +5,6 @@ from js import document | ||
| async def main(): | ||
| from wwwpy.remote import shoelace | ||
| shoelace.setup_shoelace() | ||
| from wwwpy.remote import simple_dark_theme | ||
| simple_dark_theme.setup() | ||
| from . import component1 # for component registration | ||
| document.body.innerHTML = '<component-1></component-1>' |
@@ -132,3 +132,3 @@ from __future__ import annotations | ||
| _eventbus: EventBus = injector.field() | ||
| span1: js.HTMLElement = wpc.element() | ||
| _new_component: js.HTMLElement = wpc.element() | ||
@@ -154,3 +154,3 @@ def init_component(self): | ||
| <div> | ||
| <div data-name="span1">Create new Component</div> | ||
| <button data-name="_new_component">Create new Component</button> | ||
| <div data-name="_div"></div> | ||
@@ -182,3 +182,3 @@ </div> | ||
| async def span1__click(self, event): | ||
| async def _new_component__click(self, event): | ||
| logger.debug(f'{inspect.currentframe().f_code.co_name} event fired %s', event) | ||
@@ -185,0 +185,0 @@ if js.window.confirm('Add new component file?\nIt will be added to your "remote" folder.'): |
@@ -42,19 +42,8 @@ import dataclasses | ||
| assert isinstance(locator_event, LocatorEvent), f'Expected LocatorEvent, got {type(locator_event)}' | ||
| tool = self._tool | ||
| if self._is_recursive(locator_event): | ||
| logger.debug(f'Ignoring hover on {locator_event} because it is recursive') | ||
| tool.hide() | ||
| return | ||
| if not tool.element.isConnected: | ||
| js.document.body.appendChild(tool.element) | ||
| tool.set_reference_geometry2( | ||
| locator_event.main_element.getBoundingClientRect(), | ||
| locator_event.position() | ||
| ) | ||
| tool.show() | ||
| self._setup_tool(locator_event) | ||
| def on_submit(self, locator_event: LocatorEvent) -> bool: | ||
| if self._is_recursive(locator_event): | ||
| logger.debug(f'Ignoring submit on {locator_event} because it is recursive') | ||
| self._tool.hide() | ||
| return False | ||
@@ -65,6 +54,17 @@ self.add_element(locator_event.locator, locator_event.position(), self.element_def) | ||
| def _setup_tool(self, locator_event: LocatorEvent): | ||
| tool = self._tool | ||
| if not tool.element.isConnected: | ||
| js.document.body.appendChild(tool.element) | ||
| tool.set_reference_geometry2(locator_event.main_element.getBoundingClientRect(), locator_event.position()) | ||
| tool.show() | ||
| def _is_recursive(self, locator_event: LocatorEvent) -> bool: | ||
| rec = locator_event.locator.class_full_name == self.element_def.python_type | ||
| logger.debug( | ||
| f'Recursive: {rec} {locator_event.locator.class_full_name} is recursive against {self.element_def.python_type}') | ||
| if rec: | ||
| self._tool.hide() | ||
| logger.debug(f'Ignoring hover on {locator_event} because it is recursive') | ||
| else: | ||
| logger.debug( | ||
| f'Recursive: {rec} {locator_event.locator.class_full_name} is recursive against {self.element_def.python_type}') | ||
| return rec | ||
@@ -71,0 +71,0 @@ |
@@ -68,2 +68,3 @@ from __future__ import annotations | ||
| if event: | ||
| logger.debug(f'Intent.on_hover_js: {event}') | ||
| self.on_hover(event) | ||
@@ -75,2 +76,3 @@ | ||
| if event: | ||
| logger.debug(f'Intent.on_submit_js: {event}') | ||
| return self.on_submit(event) | ||
@@ -77,0 +79,0 @@ return False |
@@ -34,7 +34,7 @@ import logging | ||
| <div slot="header">Structure</div> | ||
| <wwwpy-comp-structure style="height: 250px; display: flex; overflow: scroll"></wwwpy-comp-structure> | ||
| <wwwpy-comp-structure style="height: 150px; display: flex; overflow: auto"></wwwpy-comp-structure> | ||
| </wwwpy-accordion-section> | ||
| <wwwpy-accordion-section expanded> | ||
| <div slot="header">Attributes/Events</div> | ||
| <wwwpy-property-editor data-name="_property_editor" style="height: 250px; display: flex; overflow: scroll"></wwwpy-property-editor> | ||
| <wwwpy-property-editor data-name="_property_editor" style="height: 250px; display: flex; overflow: auto"></wwwpy-property-editor> | ||
| </wwwpy-accordion-section> | ||
@@ -41,0 +41,0 @@ <wwwpy-accordion-section expanded> |
@@ -24,3 +24,3 @@ from __future__ import annotations | ||
| _active: bool | ||
| border: int = 5 | ||
| host_style: dict[str, str] | ||
@@ -38,10 +38,44 @@ @classmethod | ||
| self.element.shadowRoot.innerHTML = """ | ||
| <style data-name="_style"></style> | ||
| <style data-name="_style"> | ||
| :host { display: flex; border: 0px solid transparent ; --svg-primary-color: #2B2D30; } | ||
| :host(:hover) { --svg-primary-color: #3C3E41; } | ||
| svg { display: block } | ||
| </style> | ||
| <div data-name="_div"></div> | ||
| """ | ||
| self.active = False | ||
| self.host_style: dict[str, str] = dict() | ||
| self.host_style['border'] = '5px solid transparent' | ||
| def _set_style(self, source='na'): | ||
| color, hover_color = (_BLUE, '') if self.active else (_BGRD, _GRAY) | ||
| logger.warning(f'_set_style: {source}') | ||
| sheet = self._style.sheet | ||
| js.console.log('=' * 10, self._style, sheet) | ||
| if not sheet or not sheet.cssRules: | ||
| logger.warning('No CSS rules found in the style element.') | ||
| return | ||
| cssRules = list(self._style.sheet.cssRules) | ||
| host_normal = cssRules[0] | ||
| host_normal.style.setProperty('--svg-primary-color', color) | ||
| for key, value in self.host_style.items(): | ||
| host_normal.style.setProperty(key, value) | ||
| host_hover = cssRules[1] | ||
| host_hover.style.setProperty('--svg-primary-color', hover_color) | ||
| return | ||
| # language=html | ||
| hover_style = ":host(:hover) { --svg-primary-color: %s; }" % (hover_color,) if hover_color else '' | ||
| s = ('svg { display: block }\n' + | ||
| ':host { display: flex; border: %spx solid transparent }\n' % self.border + | ||
| ':host { --svg-primary-color: %s; }\n' % color + hover_style) | ||
| logger.debug(f'set_style: `{s}`') | ||
| self._style.innerHTML = s | ||
| def load_svg_str(self, svg: str): | ||
| self._div.innerHTML = add_rounded_background2(svg, 'var(--svg-primary-color)') | ||
| self._set_style() | ||
@@ -57,15 +91,8 @@ @property | ||
| self._active = value | ||
| root, hover = (_BLUE, '') if value else (_BGRD, _GRAY) | ||
| self._set_style(root, hover) | ||
| self._set_style('active') | ||
| def _set_style(self, color: str, hover_color: str): | ||
| # language=html | ||
| hover_style = ":host(:hover) { --svg-primary-color: %s; }" % (hover_color,) if hover_color else '' | ||
| s = ('svg { display: block }\n' + | ||
| ':host { border: %spx solid transparent }\n' % self.border + | ||
| ':host { --svg-primary-color: %s; }\n' % color + hover_style) | ||
| logger.debug(f'set_style: `{s}`') | ||
| self._style.innerHTML = s | ||
| def connectedCallback(self): | ||
| self._set_style('connectedCallback') | ||
| def _div__click(self, event): | ||
| self.active = not self.active |
@@ -13,2 +13,5 @@ import js | ||
| <style> | ||
| html { box-sizing: border-box; } | ||
| *, *:before, *:after { box-sizing: inherit; } | ||
| :root { | ||
@@ -15,0 +18,0 @@ color-scheme: dark; |
@@ -117,2 +117,4 @@ import inspect | ||
| from wwwpy.remote.component import Component | ||
| class HolderWidget(Widget): | ||
@@ -124,4 +126,4 @@ def __init__(self, html: str = ''): | ||
| def show(self, widget: Widget): | ||
| widget.holder = self | ||
| def show(self, item: Widget | Component): | ||
| item.holder = self | ||
| c = self._custom_holder() | ||
@@ -131,10 +133,9 @@ while c.hasChildNodes(): | ||
| self._remove(widget) | ||
| self.stack.append(widget) | ||
| from wwwpy.remote.component import Component | ||
| if isinstance(widget, Component): | ||
| c.append(widget.element) | ||
| self._remove(item) | ||
| self.stack.append(item) | ||
| if isinstance(item, Component): | ||
| c.append(item.element) | ||
| else: | ||
| widget.append_to(c) | ||
| self.on_show(widget) | ||
| item.append_to(c) | ||
| self.on_show(item) | ||
@@ -141,0 +142,0 @@ def close(self, widget: Widget): |
@@ -54,3 +54,4 @@ from __future__ import annotations | ||
| playwright = sync_playwright().start() | ||
| launch_args = ['--enable-features=WebAssemblyExperimentalJSPI'] | ||
| # launch_args = ['--enable-features=WebAssemblyExperimentalJSPI'] | ||
| launch_args = [] | ||
| browser = playwright.chromium.launch(headless=args.headless, args=launch_args) | ||
@@ -57,0 +58,0 @@ page = browser.new_page(has_touch=True) |
@@ -0,3 +1,3 @@ | ||
| import logging | ||
| import os | ||
| import logging | ||
| from pathlib import Path | ||
@@ -27,4 +27,5 @@ | ||
| from wwwpy.remote import micropip_install | ||
| await micropip_install('pytest==7.2.2') # didn't work with update to 8.1.1 | ||
| await micropip_install('pytest-asyncio') | ||
| # these two packages are loaded in loadPyodide, to get a performance boost | ||
| # await micropip_install('pytest') | ||
| # await micropip_install('pytest-asyncio') | ||
| await micropip_install('pytest-xvirt') | ||
@@ -31,0 +32,0 @@ import wwwpy.common.designer as des |
@@ -93,3 +93,4 @@ from __future__ import annotations | ||
| bootstrap_python = f'import remote_test_main; await remote_test_main.main({rootpath},{invocation_dir_json},{args_json})' | ||
| webserver.set_routes(*bootstrap_routes(resources, python=bootstrap_python, jspi=True), | ||
| packages = ['pytest', 'pytest-asyncio'] | ||
| webserver.set_routes(*bootstrap_routes(resources, python=bootstrap_python, packages=packages), | ||
| xvirt_notify_route, services.route) | ||
@@ -96,0 +97,0 @@ webserver.set_port(find_port()).start_listen() |
| from __future__ import annotations | ||
| from dataclasses import dataclass | ||
| from typing import List, Union | ||
| import js | ||
| from pyodide.ffi import create_proxy | ||
| import wwwpy.remote.component as wpc | ||
| from wwwpy.remote import dict_to_js | ||
| from wwwpy.remote.designer.global_interceptor import InterceptorEvent, GlobalInterceptor | ||
| @dataclass | ||
| class Actions: | ||
| set_input_value = True | ||
| hide_dropdown = True | ||
| dispatch_change_event = True | ||
| class Option: | ||
| parent: SearchableComboBox | ||
| def __init__(self, text: str = ''): | ||
| self.text = text | ||
| self.actions = Actions() | ||
| self.on_selected = lambda: None | ||
| def _on_click(self, event: js.MouseEvent): | ||
| js.console.log('_on_click', self.text) | ||
| if self.actions.set_input_value: | ||
| self.set_input_value() | ||
| if self.actions.hide_dropdown: | ||
| self.hide_dropdown() | ||
| if self.actions.dispatch_change_event: | ||
| self.dispatch_change_event() | ||
| self.on_selected() | ||
| def set_input_value(self): | ||
| self.parent.input.value = self.text | ||
| def hide_dropdown(self): | ||
| self.parent.hide_dropdown() | ||
| def dispatch_change_event(self): | ||
| self.parent._dispatch_change_event() | ||
| def create_element(self) -> js.HTMLElement: | ||
| element = self.new_element() | ||
| element.addEventListener('click', create_proxy(self._on_click)) | ||
| return element | ||
| def new_element(self) -> js.HTMLElement: | ||
| div: js.HTMLDivElement = js.document.createElement('div') | ||
| div.textContent = self.text | ||
| return div | ||
| class SearchableComboBox(wpc.Component, tag_name='wwwpy-searchable-combobox'): | ||
| input: js.HTMLInputElement = wpc.element() | ||
| search: js.HTMLInputElement = wpc.element() | ||
| dropdown: js.HTMLElement = wpc.element() | ||
| popup: js.HTMLElement = wpc.element() | ||
| def init_component(self): | ||
| self.element.attachShadow(dict_to_js({'mode': 'open'})) | ||
| # language=html | ||
| self.element.shadowRoot.innerHTML = """ | ||
| <style> | ||
| :host { | ||
| display: inline-block; | ||
| position: relative; | ||
| font-family: Arial, sans-serif; | ||
| } | ||
| input { | ||
| width: 90%; | ||
| padding: 5px; | ||
| background-color: #2a2a2a; | ||
| color: #e0e0e0; | ||
| border: 1px solid #444; | ||
| border-radius: 4px; | ||
| font-size: 14px; | ||
| } | ||
| input::placeholder { | ||
| color: #888; | ||
| } | ||
| .dropdown { | ||
| position: absolute; | ||
| width: 100%; | ||
| max-height: 200px; | ||
| resize: both; | ||
| overflow-y: auto; | ||
| border: 1px solid #444; | ||
| background-color: #333; | ||
| color: #e0e0e0; | ||
| border-radius: 0 0 4px 4px; | ||
| box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); | ||
| } | ||
| .popup { | ||
| position: absolute; | ||
| z-index: 1000; | ||
| display: none; | ||
| } | ||
| .dropdown * { | ||
| padding: 8px 12px; | ||
| cursor: pointer; | ||
| transition: background-color 0.2s ease; | ||
| } | ||
| .dropdown div:hover { | ||
| background-color: #444; | ||
| } | ||
| input:focus { | ||
| outline: none; | ||
| border-color: #666; | ||
| box-shadow: 0 0 0 2px rgba(100, 100, 100, 0.3); | ||
| } | ||
| </style> | ||
| <input type="text" placeholder="" data-name="input"> | ||
| <div data-name="popup" class="popup"> | ||
| <input type="search" placeholder="Search..." data-name="search"> | ||
| <div class="dropdown" data-name="dropdown"></div> | ||
| </div> | ||
| """ | ||
| self._interceptor = GlobalInterceptor(self._global_click) | ||
| self._options = [] | ||
| self._dirty = False | ||
| self.hide_dropdown() | ||
| def _global_click(self, event: InterceptorEvent): | ||
| click_inside = self.element.contains(event.target) | ||
| js.console.log(f'global click: contains={click_inside}', event.target) | ||
| if click_inside: | ||
| return | ||
| self.hide_dropdown() | ||
| def search__input(self, event): | ||
| self.filter_options() | ||
| def input__change(self, event): | ||
| if self._is_free_edit(): | ||
| self._dispatch_change_event() | ||
| def input__mousedown(self, event: js.MouseEvent): | ||
| if self._is_free_edit(): | ||
| return | ||
| event.preventDefault() | ||
| @property | ||
| def _search_visible(self): | ||
| return self.popup.style.display != 'none' | ||
| def input__click(self, event: js.MouseEvent): | ||
| if self._search_visible: | ||
| self.hide_dropdown() | ||
| else: | ||
| self.show_dropdown() | ||
| def show_dropdown(self): | ||
| if self._is_free_edit(): | ||
| return | ||
| if self._dirty: | ||
| self.filter_options() | ||
| self.popup.style.display = 'block' | ||
| self.search.focus() | ||
| self._interceptor.install() | ||
| def _is_free_edit(self): | ||
| return not self._options | ||
| def hide_dropdown(self): | ||
| self.popup.style.display = 'none' | ||
| self._interceptor.uninstall() | ||
| def set_options(self, options: List[Union[str, Option]]): | ||
| self._options = options | ||
| self._dirty = True | ||
| def filter_options(self, event=None): | ||
| self._dirty = False | ||
| filter_text = self.search.value.lower() | ||
| self.dropdown.innerHTML = '' | ||
| for option in self._options: | ||
| if isinstance(option, str): | ||
| option = Option(option) | ||
| option.parent = self | ||
| option: Option | ||
| if filter_text in option.text.lower(): | ||
| self.dropdown.appendChild(option.create_element()) | ||
| if not self.dropdown.hasChildNodes(): | ||
| div: js.HTMLDivElement = js.document.createElement('div') | ||
| div.textContent = 'No results' | ||
| div.style.textAlign = 'center' | ||
| div.style.fontStyle = 'italic' | ||
| div.style.fontWeight = 'bold' | ||
| div.style.pointerEvents = 'none' | ||
| self.dropdown.appendChild(div) | ||
| def select_option(self, option): | ||
| js.console.log(f'option: {option}') | ||
| self.input.value = option | ||
| self.hide_dropdown() | ||
| self._dispatch_change_event() | ||
| def _dispatch_change_event(self): | ||
| self.element.dispatchEvent(js.CustomEvent.new('wp-change', {'detail': self.input.value})) |
| from __future__ import annotations | ||
| import asyncio | ||
| import logging | ||
| from dataclasses import dataclass, field | ||
| from functools import partial | ||
| from typing import Optional, Tuple, List | ||
| import js | ||
| from js import document, console, Event, HTMLElement, window | ||
| from pyodide.ffi import create_proxy | ||
| import wwwpy.remote.component as wpc | ||
| from wwwpy.common import state, modlib | ||
| from wwwpy.common.designer import element_library | ||
| from wwwpy.common.designer.canvas_selection import CanvasSelection, CanvasSelectionChangeEvent | ||
| from wwwpy.common.designer.code_edit import add_element, AddResult, AddFailed | ||
| from wwwpy.common.designer.element_library import Help, ElementDef | ||
| from wwwpy.common.designer.html_edit import Position | ||
| from wwwpy.common.designer.html_locator import path_to_index | ||
| from wwwpy.common.designer.locator_lib import Locator | ||
| from wwwpy.common.injectorlib import inject | ||
| from wwwpy.remote import dict_to_js | ||
| from wwwpy.remote.designer.drop_zone import DropZone, DropZoneHover | ||
| from wwwpy.remote.designer.global_interceptor import GlobalInterceptor, InterceptorEvent | ||
| from wwwpy.remote.designer.helpers import _element_lbl, _help_button, info_link, _help_url | ||
| from wwwpy.remote.designer.ui.property_editor import PropertyEditor | ||
| from wwwpy.remote.designer.ui.window_component import WindowComponent | ||
| from wwwpy.remote.designer.ui.window_component import new_window, Geometry | ||
| from wwwpy.server.designer import rpc | ||
| from . import filesystem_tree | ||
| from .help_icon import HelpIcon # noqa | ||
| from .mailto_component import MailtoComponent | ||
| from .system_tools.logger_levels import LoggerLevelsComponent | ||
| from ..locator_js import locator_from | ||
| logger = logging.getLogger(__name__) | ||
| @dataclass | ||
| class ToolboxState: | ||
| geometry: Tuple[int, int, int, int] = field(default=(30, 200, 400, 330)) | ||
| toolbox_search: str = '' | ||
| selected_element_path: Optional[Locator] = None | ||
| @dataclass | ||
| class MenuMeta: | ||
| label: str | ||
| html: str | ||
| always_visible: bool = False | ||
| p_element: js.HTMLElement = None | ||
| help: Help = None | ||
| def menu(label, always_visible=False): | ||
| def wrapped(fn, label=label): | ||
| help = None | ||
| if isinstance(label, Help): | ||
| help = label | ||
| label = label.description | ||
| fn.label = label | ||
| fn.meta = MenuMeta(label, label, always_visible, help=help) | ||
| return fn | ||
| return wrapped | ||
| class ToolboxComponent(wpc.Component, tag_name='wwwpy-toolbox'): | ||
| _title: js.HTMLDivElement = wpc.element() | ||
| body: HTMLElement = wpc.element() | ||
| inputSearch: js.HTMLInputElement = wpc.element() | ||
| _window: WindowComponent = wpc.element() | ||
| property_editor: PropertyEditor = wpc.element() | ||
| _select_element_btn: js.HTMLElement = wpc.element() | ||
| _select_clear_btn: js.HTMLElement = wpc.element() | ||
| components_marker = '-components-' | ||
| _toolbox_state: ToolboxState = None | ||
| _canvas_selection: CanvasSelection = inject() | ||
| @property | ||
| def visible(self) -> bool: | ||
| return self._window.element.style.display != 'none' | ||
| @visible.setter | ||
| def visible(self, value: bool): | ||
| self._window.element.style.display = 'block' if value else 'none' | ||
| def init_component(self): | ||
| self.element.attachShadow(dict_to_js({'mode': 'open'})) | ||
| # language=html | ||
| self.element.shadowRoot.innerHTML = """ | ||
| <style> | ||
| .two-column-layout { | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| } | ||
| .two-column-layout p { | ||
| width: calc(50% - 10px); /* 50% width minus half of the gap */ | ||
| margin: 0 20px 10px 0; /* Right margin creates gap between columns */ | ||
| } | ||
| .two-column-layout p:nth-child(even) { | ||
| margin-right: 0; /* Removes right margin for every even child */ | ||
| } | ||
| </style> | ||
| <wwwpy-window data-name='_window'> | ||
| <div slot='title' data-name="_title" style='text-align: center'></div> | ||
| <div style="text-align: center; padding: 8px"> | ||
| <button data-name="_select_element_btn">Select element...</button> | ||
| <button data-name="_select_clear_btn">Clear selection</button> | ||
| <wwwpy-property-editor data-name="property_editor"></wwwpy-property-editor> | ||
| <p><input data-name='inputSearch' type='search' placeholder='type to filter...'></p> | ||
| <div data-name='body' class='two-column-layout'></div> | ||
| </div> | ||
| </wwwpy-window> | ||
| """ | ||
| import wwwpy | ||
| self._title.innerText = f'toolbox - {wwwpy.__banner__}' | ||
| def _csce(event: CanvasSelectionChangeEvent): | ||
| logger.debug(f'canvas selection changed: {event}') | ||
| self._toolbox_state.selected_element_path = event.new | ||
| self._restore_selected_element_path() | ||
| self._canvas_selection.on_change.add(_csce) | ||
| def connectedCallback(self): | ||
| self._manage_toolbox_state() | ||
| self._window.closable = False | ||
| attrs = [v for k, v in vars(self.__class__).items() if hasattr(v, 'label')] | ||
| self._all_items: List[MenuMeta] = [] | ||
| def add_p(menu_meta: MenuMeta, callback): # callback can be async | ||
| self._all_items.append(menu_meta) | ||
| p: js.HTMLElement = document.createElement('p') | ||
| p.style.color = 'white' | ||
| menu_meta.p_element = p | ||
| p.innerHTML = menu_meta.html | ||
| p.addEventListener('click', create_proxy(callback)) | ||
| def add_comp(element_def: ElementDef): | ||
| def _on_hover(drop_zone: DropZone | None): | ||
| console.log(f'pointed dropzone: {drop_zone}') | ||
| msg = (f'Insert new {element_def.tag_name}') | ||
| if drop_zone: | ||
| pos = 'before' if drop_zone.position == Position.beforebegin else 'after' | ||
| # msg += f' at {drop_zone.position.name} of {drop_zone.element.tagName}' | ||
| msg += f' {pos} {drop_zone.element.tagName}' | ||
| else: | ||
| msg += ' ... select a dropzone on the page.' | ||
| self.property_editor.message1div.innerHTML = msg | ||
| async def _start_drop_for_comp(event): | ||
| _on_hover(None) | ||
| res = await _drop_zone_start_selection_async(_on_hover, whole=False) | ||
| logger.debug(f'_start_drop_for_comp res={res}') | ||
| if res: | ||
| self.property_editor.set_state_selection_active() | ||
| await self._process_dropzone(res, element_def) | ||
| else: | ||
| await self._canceled() | ||
| element_html = f'<span style="cursor: pointer">{element_def.tag_name}</span> {_help_button(element_def)}' | ||
| add_p(MenuMeta(element_def.tag_name, element_html), _start_drop_for_comp) | ||
| for member in attrs: | ||
| if member.meta.label == self.components_marker: | ||
| [add_comp(ele_def) for ele_def in element_library.element_library().elements] | ||
| else: | ||
| help = member.meta.help | ||
| if help: | ||
| help_html = '' if not help.url else info_link(help.url) | ||
| element_html = f'{help.description} {help_html}' | ||
| add_p(MenuMeta(help.description, element_html), partial(member, self)) | ||
| else: | ||
| add_p(member.meta, partial(member, self)) | ||
| self._update_toolbox_elements() | ||
| async def _process_dropzone(self, drop_zone: DropZone, element_def: ElementDef): | ||
| el_path = locator_from(drop_zone.element) | ||
| if not el_path: | ||
| window.alert(f'No component found for dropzone!') | ||
| return | ||
| logger.debug(f'element_path={el_path}') | ||
| file = modlib._find_module_path(el_path.class_module) | ||
| old_source = file.read_text() | ||
| path_index = path_to_index(el_path.path) | ||
| add_result = add_element(old_source, el_path.class_name, element_def, path_index, drop_zone.position) | ||
| if isinstance(add_result, AddResult): | ||
| logger.debug(f'write_module_file len={len(add_result.source_code)} el_path={el_path}') | ||
| new_element_path = Locator(el_path.class_module, el_path.class_name, add_result.node_path, | ||
| el_path.origin) | ||
| self._toolbox_state.selected_element_path = new_element_path | ||
| write_res = await rpc.write_module_file(el_path.class_module, add_result.source_code) | ||
| logger.debug(f'write_module_file res={write_res}') | ||
| elif isinstance(add_result, AddFailed): | ||
| js.alert('Sorry, an error occurred while adding the component.') | ||
| _open_error_reporter_window( | ||
| 'Error report data:\n\n' + add_result.exception_report_b64, | ||
| title='Error report add_component - wwwpy' | ||
| ) | ||
| # pre1: js.HTMLElement = js.document.createElement('pre') | ||
| # pre1.innerText = self.path.read_text() | ||
| # win.element.append(pre1) | ||
| def _manage_toolbox_state(self): | ||
| self._toolbox_state = state._restore(ToolboxState) | ||
| self.inputSearch.value = self._toolbox_state.toolbox_search | ||
| self._window.set_geometry(Geometry(*self._toolbox_state.geometry)) | ||
| def on_toolbar_geometry_change(): | ||
| g = self._window.geometry() | ||
| acceptable_geometry = g.width > 100 and g.height > 100 and g.top > 0 and g.left > 0 | ||
| if acceptable_geometry: | ||
| self._toolbox_state.geometry = self._window.geometry() | ||
| self._window.geometry_change_listeners.append(on_toolbar_geometry_change) | ||
| self._restore_selected_element_path() | ||
| def inputSearch__input(self, e: Event): | ||
| self._toolbox_state.toolbox_search = self.inputSearch.value | ||
| self._update_toolbox_elements() | ||
| def _update_toolbox_elements(self): | ||
| def search_match(meta: MenuMeta): | ||
| search = self.inputSearch.value.lower() | ||
| lbl = meta.label.lower() | ||
| starts_with = search.startswith(' ') and lbl.startswith(search.lstrip()) | ||
| ends_with = search.endswith(' ') and lbl.endswith(search.rstrip()) | ||
| if starts_with or ends_with: | ||
| return True | ||
| return search in lbl | ||
| self.body.innerHTML = '' | ||
| for meta in self._all_items: | ||
| p = meta.p_element | ||
| if meta.always_visible or search_match(meta): | ||
| self.body.appendChild(p) | ||
| async def _select_clear_btn__click(self, e: Event): | ||
| self._toolbox_state.selected_element_path = None | ||
| self._restore_selected_element_path() | ||
| async def _select_element_btn__click(self, e: Event): | ||
| no_comp = 'Select an element on the page to be inspected and edited...' | ||
| self.property_editor.message1div.innerHTML = no_comp | ||
| def _on_hover(drop_zone: DropZone | None): | ||
| msg = no_comp | ||
| if drop_zone: | ||
| msg = 'Click to select ' + _element_lbl(drop_zone.element) | ||
| self.property_editor.message1div.innerHTML = msg | ||
| console.log(f'pointed dropzone: {drop_zone}') | ||
| res = await _drop_zone_start_selection_async(_on_hover, whole=True) | ||
| if res: | ||
| self.property_editor.set_state_selection_active() | ||
| self._toolbox_state.selected_element_path = locator_from(res.element) | ||
| else: | ||
| await self._canceled() | ||
| self._restore_selected_element_path() | ||
| # @menu(Help("Open errore reporter", '')) | ||
| # async def _open_error_reporter(self, e: Event): | ||
| # _open_error_reporter_window('test error report body') | ||
| @menu(Help('Create new component', _help_url('add_component'))) | ||
| async def _add_new_component(self, e: Event): | ||
| if js.window.confirm('Add new component file?\nIt will be added to your "remote" folder.'): | ||
| res = await rpc.add_new_component() | ||
| js.window.alert(res) | ||
| @menu(Help("Explore local filesystem", _help_url('remote_filesystem'))) | ||
| async def _browse_local_filesystem(self, e: Event): | ||
| from wwwpy.remote.designer.ui.dev_mode_component import DevModeComponent | ||
| filesystem_tree.show_explorer(DevModeComponent.instance.root_element()) | ||
| @menu(Help("Python console", '')) | ||
| async def _python_console(self, e: Event): | ||
| from wwwpy.remote.designer.ui.dev_mode_component import DevModeComponent | ||
| w1 = new_window('Python console') | ||
| from wwwpy.remote.designer.ui.python_console import PythonConsoleComponent | ||
| w1.element.append(PythonConsoleComponent().element) | ||
| DevModeComponent.instance.root_element().append(w1.element) | ||
| @menu(Help("Logger levels", '')) | ||
| async def _python_console(self, e: Event): | ||
| from wwwpy.remote.designer.ui.dev_mode_component import DevModeComponent | ||
| w1 = new_window('Logger levels') | ||
| w1.element.append(LoggerLevelsComponent().element) | ||
| DevModeComponent.instance.root_element().append(w1.element) | ||
| @menu(components_marker) | ||
| def _drop_zone_start(self, e: Event): | ||
| assert False, 'Just a placeholder' | ||
| # @menu('handle global click') | ||
| def _handle_global_click(self, e: Event): | ||
| # add input | ||
| def global_click(event: Event): | ||
| event.preventDefault() | ||
| event.stopImmediatePropagation() | ||
| event.stopPropagation() | ||
| console.log('global click', event.element) | ||
| if self._global_click: | ||
| document.removeEventListener('click', self._global_click, True) | ||
| self._global_click = None | ||
| self._global_click = create_proxy(global_click) | ||
| document.addEventListener('click', self._global_click, True) | ||
| def _restore_selected_element_path(self): | ||
| element_path = self._toolbox_state.selected_element_path | ||
| if element_path: | ||
| logger.debug(f'restoring selected element path: {element_path}') | ||
| if not element_path.valid(): | ||
| element_path = None | ||
| logger.warning(f'invalid element path, setting to None') | ||
| if element_path: | ||
| self._select_clear_btn.style.visibility = 'visible' | ||
| else: | ||
| self._select_clear_btn.style.visibility = 'hidden' | ||
| self.property_editor.selected_element_path = element_path | ||
| async def _canceled(self): | ||
| self.property_editor.message1div.innerHTML = 'Operation canceled' | ||
| await asyncio.sleep(2) | ||
| self.property_editor.message1div.innerHTML = '' | ||
| def is_inside_toolbar(element: HTMLElement | None): | ||
| if not element: | ||
| return False | ||
| orig = element | ||
| # loop until the root element and see if it is the toolbar | ||
| res = False | ||
| while element: | ||
| if element.tagName.lower() == ToolboxComponent.component_metadata.tag_name: | ||
| res = True | ||
| break | ||
| element = element.parentElement | ||
| return res | ||
| def _default_drop_zone_accept(drop_zone: DropZone): | ||
| element = drop_zone.element | ||
| name = element.tagName.lower() | ||
| from wwwpy.remote.designer.ui.dev_mode_component import DevModeComponent | ||
| root_elements = name == 'body' or name == 'html' | ||
| wwwpy_elements = name == DevModeComponent.component_metadata.tag_name or is_inside_toolbar(element) | ||
| accept = not (root_elements or wwwpy_elements) | ||
| return accept | ||
| async def _drop_zone_start_selection_async(on_hover: DropZoneHover, whole: bool) -> Optional[DropZone]: | ||
| from wwwpy.remote.designer.drop_zone import drop_zone_selector | ||
| event = asyncio.Event() | ||
| result = [] | ||
| def intercept_ended(ev: InterceptorEvent): | ||
| console.log('intercept_ended', ev.target) | ||
| ev.preventAndStop() | ||
| selected = drop_zone_selector.stop() | ||
| ev.uninstall() | ||
| if selected: | ||
| selected: DropZone | ||
| console.log( | ||
| f'\nselection accepted position {selected.position.name}' | ||
| f'\ntarget: ', selected.element, | ||
| '\nparent: ', selected.element.parentElement, | ||
| '\nevent: ', ev.event, | ||
| '\ncomposedPath: ', ev.event.composedPath(), | ||
| ) | ||
| result.append(selected) | ||
| else: | ||
| console.log('intercept_ended - canceled') | ||
| event.set() | ||
| GlobalInterceptor(intercept_ended, 'pointerdown').install() | ||
| click_inter = GlobalInterceptor(lambda ev: ev.preventAndStop(), 'click') | ||
| click_inter.install() | ||
| drop_zone_selector.start_selector(on_hover, _default_drop_zone_accept, whole=whole) | ||
| await event.wait() | ||
| console.log('drop_zone_selector event ended') | ||
| async def _click_inter_uninstall(): | ||
| await asyncio.sleep(0.5) | ||
| click_inter.uninstall() | ||
| asyncio.ensure_future(_click_inter_uninstall()) | ||
| if len(result) == 0: | ||
| return None | ||
| return result[0] | ||
| def _open_error_reporter_window(body: str, title='Error reporter - wwwpy'): | ||
| win = new_window(title, closable=True) | ||
| mailto_component = MailtoComponent() | ||
| mailto_component.subject = title | ||
| mailto_component.recipient = 'simone.giacomelli@gmail.com' | ||
| mailto_component.body = body | ||
| mailto_component.text_content = 'Click here to send an email with the error report' | ||
| mailto_component.element.style.display = 'block' | ||
| mailto_component.element.style.textAlign = 'center' | ||
| div: js.HTMLElement = js.document.createElement('div') | ||
| action = '<h3>Please, help us improve wwwpy by sending this error report</h3>' | ||
| div.insertAdjacentHTML('beforeend', action) | ||
| div.append(mailto_component.element) | ||
| div.style.margin = '15px' | ||
| win.element.append(div) | ||
| div.insertAdjacentHTML('beforeend', '<br>') | ||
| js.document.body.append(win.element) | ||
| from wwwpy.remote.designer.ui.dev_mode_component import DevModeComponent | ||
| DevModeComponent.instance.root_element().append(win.element) |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
968460
1.2%262
6.5%16021
-0.17%