Sfoglia il codice sorgente

Merge branch 'update_bug' into main

Falko Schindler 3 anni fa
parent
commit
daa69d964d

+ 39 - 17
nicegui/binding.py

@@ -2,6 +2,8 @@
 import asyncio
 from collections import defaultdict
 from justpy.htmlcomponents import HTMLBaseComponent
+from typing import Any, Callable, List, Optional, Set, Tuple
+from .task_logger import create_task
 
 bindings = defaultdict(list)
 bindable_properties = dict()
@@ -9,53 +11,73 @@ active_links = []
 
 async def loop():
     while True:
-        visited = set()
-        invalidated_elements = []
+        visited: Set[Tuple[int, str]] = set()
+        visited_views: Set[HTMLBaseComponent] = set()
         for link in active_links:
             (source_obj, source_name, target_obj, target_name, transform) = link
             value = transform(getattr(source_obj, source_name))
             if getattr(target_obj, target_name) != value:
                 setattr(target_obj, target_name, value)
-                propagate(target_obj, target_name, visited)
-                if hasattr(target_obj, 'view') and isinstance(target_obj.view, HTMLBaseComponent):
-                    invalidated_elements.append(target_obj)
-        for element in invalidated_elements:
-            await element.view.update()
+                propagate(target_obj, target_name, visited, visited_views)
+        update_views(visited_views)
         await asyncio.sleep(0.1)
 
-def propagate(source_obj, source_name, visited=None):
+async def update_views_async(views: List[HTMLBaseComponent]):
+    for view in views:
+        await view.update()
+
+def update_views(views: List[HTMLBaseComponent]):
+    if asyncio._get_running_loop() is None:
+        return  # NOTE: no need to update view if event loop is not running, yet
+    create_task(update_views_async(views))
+
+def propagate(source_obj,
+              source_name,
+              visited: Set[Tuple[int, str]] = None,
+              visited_views: Set[HTMLBaseComponent] = None) -> List[HTMLBaseComponent]:
     if visited is None:
         visited = set()
+    if visited_views is None:
+        visited_views = set()
     visited.add((id(source_obj), source_name))
+    if isinstance(source_obj, HTMLBaseComponent):
+        visited_views.add(source_obj)
     for _, target_obj, target_name, transform in bindings[(id(source_obj), source_name)]:
         if (id(target_obj), target_name) in visited:
             continue
         target_value = transform(getattr(source_obj, source_name))
         if getattr(target_obj, target_name) != target_value:
             setattr(target_obj, target_name, target_value)
-            propagate(target_obj, target_name, visited)
+            propagate(target_obj, target_name, visited, visited_views)
+    return visited_views
 
-def bind_to(self_obj, self_name, other_obj, other_name, forward):
+def bind_to(self_obj: Any, self_name: str, other_obj: Any, other_name: str, forward: Callable):
     bindings[(id(self_obj), self_name)].append((self_obj, other_obj, other_name, forward))
     if (id(self_obj), self_name) not in bindable_properties:
         active_links.append((self_obj, self_name, other_obj, other_name, forward))
-    propagate(self_obj, self_name)
+    update_views(propagate(self_obj, self_name))
 
-def bind_from(self_obj, self_name, other_obj, other_name, backward):
+def bind_from(self_obj: Any, self_name: str, other_obj: Any, other_name: str, backward: Callable):
     bindings[(id(other_obj), other_name)].append((other_obj, self_obj, self_name, backward))
     if (id(other_obj), other_name) not in bindable_properties:
         active_links.append((other_obj, other_name, self_obj, self_name, backward))
-    propagate(other_obj, other_name)
+    update_views(propagate(other_obj, other_name))
 
 class BindableProperty:
 
-    def __set_name__(self, _, name):
+    def __init__(self, on_change: Optional[Callable] = None):
+        self.on_change = on_change
+
+    def __set_name__(self, _, name: str):
         self.name = name
 
-    def __get__(self, owner, _=None):
+    def __get__(self, owner: Any, _=None):
         return getattr(owner, '_' + self.name)
 
-    def __set__(self, owner, value):
+    def __set__(self, owner: Any, value: Any):
+        value_changed = getattr(owner, '_' + self.name, value) != value
         setattr(owner, '_' + self.name, value)
         bindable_properties[(id(owner), self.name)] = owner
-        propagate(owner, self.name)
+        update_views(propagate(owner, self.name))
+        if value_changed and self.on_change is not None:
+            self.on_change(owner, value)

+ 2 - 10
nicegui/elements/element.py

@@ -3,7 +3,8 @@ from ..binding import bind_from, bind_to, BindableProperty
 from ..globals import view_stack, page_stack
 
 class Element:
-    visible = BindableProperty()
+    visible = BindableProperty(
+        on_change=lambda sender, visible: (sender.view.remove_class if visible else sender.view.set_class)('hidden'))
 
     def __init__(self,
                  view: jp.HTMLBaseComponent,
@@ -16,15 +17,6 @@ class Element:
 
         self.visible = True
 
-    @property
-    def visible(self):
-        return self.visible_
-
-    @visible.setter
-    def visible(self, visible: bool):
-        self.visible_ = visible
-        (self.view.remove_class if self.visible_ else self.view.set_class)('hidden')
-
     def bind_visibility_to(self, target_object, target_name, forward=lambda x: x):
         bind_to(self, 'visible', target_object, target_name, forward=forward)
         return self

+ 3 - 3
nicegui/elements/interactive_image.py

@@ -1,6 +1,6 @@
 from __future__ import annotations
 from justpy import WebPage
-from typing import Any, Callable, Dict, Optional
+from typing import Any, Callable, Dict, List, Optional
 import traceback
 from ..events import MouseEventArguments, handle_event
 from .custom_view import CustomView
@@ -10,7 +10,7 @@ CustomView.use(__file__)
 
 class InteractiveImageView(CustomView):
 
-    def __init__(self, source: str, on_mouse: Callable, events: list[str], cross: bool):
+    def __init__(self, source: str, on_mouse: Callable, events: List[str], cross: bool):
         super().__init__('interactive_image', source=source, events=events, cross=cross, svg_content='')
         self.allowed_events = ['onMouse', 'onConnect']
         self.initialize(onMouse=on_mouse, onConnect=self.on_connect)
@@ -26,7 +26,7 @@ class InteractiveImageView(CustomView):
 
 class InteractiveImage(Element):
 
-    def __init__(self, source: str, *, on_mouse: Optional[Callable] = None, events: list[str] = ['click'], cross: bool = False):
+    def __init__(self, source: str, *, on_mouse: Optional[Callable] = None, events: List[str] = ['click'], cross: bool = False):
         """Interactive Image
 
         Create an image with an SVG overlay that handles mouse events and yields image coordinates.

+ 4 - 3
nicegui/elements/value_element.py

@@ -6,7 +6,10 @@ from ..binding import bind_from, bind_to, BindableProperty
 from .element import Element
 
 class ValueElement(Element):
-    value = BindableProperty()
+    value = BindableProperty(
+        on_change=lambda sender, value: handle_event(sender.change_handler,
+                                                     ValueChangeEventArguments(sender=sender, value=value),
+                                                     update=sender.parent_view))
 
     def __init__(self,
                  view: jp.HTMLBaseComponent,
@@ -25,8 +28,6 @@ class ValueElement(Element):
 
     def handle_change(self, msg):
         self.value = msg['value']
-        arguments = ValueChangeEventArguments(sender=self, value=self.value)
-        handle_event(self.change_handler, arguments, update=self.parent_view)
 
     def bind_value_to(self, target_object, target_name, *, forward=lambda x: x):
         bind_to(self, 'value', target_object, target_name, forward=forward)