diff --git a/flexx/app/_flaskserver.py b/flexx/app/_flaskserver.py index 6782ca8f..ded76c24 100644 --- a/flexx/app/_flaskserver.py +++ b/flexx/app/_flaskserver.py @@ -711,7 +711,7 @@ def write_command(self, cmd): bb = serializer.encode(cmd) try: self.write_message(bb, binary=True) - except flask.Exception: # Note: is there a more specific error we could use? + except WebSocketClosedError: self.close(1000, 'closed by client') def close(self, *args): diff --git a/flexx/event/_component.py b/flexx/event/_component.py index 90c8d945..d395273a 100644 --- a/flexx/event/_component.py +++ b/flexx/event/_component.py @@ -220,7 +220,7 @@ def __repr__(self): def _comp_init_property_values(self, property_values): """ Initialize property values, combining given kwargs (in order) and default values. - Property values are popped when consumed so that the remainer is used for + Property values are popped when consumed so that the remainer is used for other initialisations without mixup. """ values = [] @@ -237,7 +237,7 @@ def _comp_init_property_values(self, property_values): raise AttributeError('%s.%s is an attribute, not a property' % (self._id, name)) else: - # if the proxy instance does not exist, we want the attribute + # if the proxy instance does not exist, we want the attribute # to be passed through to the Widget instantiation. # No exception if the proxy does not exists. if self._has_proxy is True: diff --git a/flexx/ui/pywidgets/__init__.py b/flexx/ui/pywidgets/__init__.py index 863b9d09..ec8c14ae 100644 --- a/flexx/ui/pywidgets/__init__.py +++ b/flexx/ui/pywidgets/__init__.py @@ -4,3 +4,5 @@ from .. import PyWidget from ._filebrowser import FileBrowserWidget +from ._dynamicwidgetcontainer import DynamicWidgetContainer + diff --git a/flexx/ui/pywidgets/_dynamicwidgetcontainer.py b/flexx/ui/pywidgets/_dynamicwidgetcontainer.py new file mode 100644 index 00000000..8100a085 --- /dev/null +++ b/flexx/ui/pywidgets/_dynamicwidgetcontainer.py @@ -0,0 +1,81 @@ +from ... import event +from .._widget import PyWidget +import threading + + +class DynamicWidgetContainer(PyWidget): + """ Widget container to allow dynamic insertion and disposal of widgets. + """ + + DEFAULT_MIN_SIZE = 0, 0 + + def init(self, *init_args, **property_values): + # TODO: figure out if init_args is needed for something + super(DynamicWidgetContainer, self).init() # call to _component.py -> Component.init(self) + # the page + self.pages = [] # pages are one on top of another + + def _init_events(self): + pass # just don't use standard events + + def clean_pages(self): # remove empty pages from the top + while self.pages[-1] is None: + del self.pages[-1] + + @event.reaction("remove") + def __remove(self, *events): + if self.pages: + page = self.pages[events[0]['page_position']] + page.dyn_stop_event.set() + page.dyn_id = None + page.dispose() + page._jswidget.dispose() # <-- added + self.pages[events[0]['page_position']] = None + + @event.emitter + def remove(self, page_position): + return dict(page_position=page_position) + + @event.reaction("_emit_instantiate") + def __instantiate(self, *events): + with self: + with events[0]['widget_type'](events[0]['style']) as page: + page.parent = self + page.dyn_id = events[0]['page_position'] # TODO use a class attribute to allow non pyWidget + page.dyn_stop_event = threading.Event() + task = self.pages[page.dyn_id] # the location contains a task + self.pages[page.dyn_id] = page # record the instance + task.set() # set the task as done as the instantiation is done + self.clean_pages() # only clean after instanciation so it does not delete future location + + @event.emitter + def _emit_instantiate(self, widget_type, page_position, options): # can't put default arguments + return dict(widget_type=widget_type, page_position=page_position, style=options['style']) + + def instantiate(self, widget_type, options=None): + """ Send an instantiate command and return the widget instance id. + This function is thread safe. """ + if options is None: + options = dict({'style':"width: 100%; height: 100%;"}) + + async_task = threading.Event() + pos = len(self.pages) + self.pages.append(async_task) # this is the new location for this instance + while self.pages[pos] is not async_task: + pos += 1 # in case some other thread added to the list + + def out_of_thread_call(): + nonlocal pos + self._emit_instantiate(widget_type, pos, options) + + event.loop.call_soon(out_of_thread_call) + return pos + + def get_instance(self, page_position): + """ returns None if not yet instanciated """ + ret = self.pages[page_position] + if isinstance(ret, threading.Event): + ret.wait() # wait until event would be .set() + return self.pages[page_position] + else: + return ret \ No newline at end of file diff --git a/flexxamples/howtos/dynamic_container_app.py b/flexxamples/howtos/dynamic_container_app.py new file mode 100644 index 00000000..deb54b9b --- /dev/null +++ b/flexxamples/howtos/dynamic_container_app.py @@ -0,0 +1,80 @@ +from flexx import flx +from flexx import event + +import threading +import asyncio + + +class PyWidget1(flx.PyWidget): + frame = None + + def init(self, additional_style="width: 100%; height: 100%;"): + with flx.VFix(flex=1, style=additional_style) as self.frame: + with flx.VFix(flex=1) as self.page: + self.custom = flx.VFix(flex=1, style="width: 100%; height: 100%; border: 5px solid green;") + with flx.HFix(flex=1): + self.but = flx.Button(text="Replace by PyWidget2") + self.but_close = flx.Button(text="Close") + self.input = flx.LineEdit(text="input") + + def dispose(self): + self.frame.dispose() + self.frame = None + super().dispose() + + @flx.reaction("but.pointer_click") + def delete_function(self, *events): + self.parent.remove(self.dyn_id) + self.parent.instantiate(PyWidget2) + + @flx.reaction("but_close.pointer_click") + def close_function(self, *events): + self.parent.remove(self.dyn_id) + + +class PyWidget2(flx.PyWidget): + frame = None + + def init(self, additional_style="width: 100%; height: 100%;"): + with flx.VFix(flex=1, style=additional_style) as self.frame: + with flx.VFix(flex=1) as self.page: + self.custom = flx.VFix(flex=1, style="width: 100%; height: 100%; border: 5px solid blue;") + self.but = flx.Button(text="Swap back to a PyWidget1") + + def dispose(self): + self.frame.dispose() + self.frame = None + super().dispose() + + @flx.reaction("but.pointer_click") + def delete_function(self, *events): + self.parent.remove(self.dyn_id) + self.parent.instantiate(PyWidget1) + + +class Example(flx.PyWidget): + + # The CSS is not used by flex in PyWiget but it should be applied to the top div: TODO + CSS = """ + .flx-DynamicWidgetContainer { + white-space: nowrap; + padding: 0.2em 0.4em; + border-radius: 3px; + color: #333; + } + """ + + def init(self): + with flx.VFix(flex=1) as self.frame_layout: + self.dynamic = flx.DynamicWidgetContainer( + style="width: 100%; height: 100%; border: 5px solid black;", flex=1 + ) + self.but = flx.Button(text="Instanciate a PyWidget1 in the dynamic container") + + @flx.reaction("but.pointer_click") + def click(self, *events): + self.dynamic.instantiate(PyWidget1) + + +m = flx.launch(Example) +flx.run() \ No newline at end of file diff --git a/flexxamples/howtos/dynamic_container_in_background.py b/flexxamples/howtos/dynamic_container_in_background.py new file mode 100644 index 00000000..aee4a257 --- /dev/null +++ b/flexxamples/howtos/dynamic_container_in_background.py @@ -0,0 +1,121 @@ +# Following line may be needed for step by step debugging into threads +# from gevent import monkey; monkey.patch_all() # do it before modules like requests gets imported + +from flexx import flx +from flexx import event + +import threading +import asyncio + + +class PyWidget1(flx.PyWidget): + frame = None + + def init(self, additional_style="width: 100%; height: 100%;"): + with flx.VFix(flex=1, style=additional_style) as self.frame: + with flx.VFix(flex=1) as self.page: + self.custom = flx.VFix(flex=1, style="width: 100%; height: 100%; border: 5px solid green;") + with flx.HFix(flex=1): + self.but = flx.Button(text="Replace by PyWidget2") + self.but_close = flx.Button(text="Close") + self.input = flx.LineEdit(text="input") + + def dispose(self): + self.frame.dispose() + self.frame = None + super().dispose() + + @flx.reaction("but.pointer_click") + def delete_function(self, *events): + self.parent.remove(self.dyn_id) + self.parent.instantiate(PyWidget2) + + @flx.reaction("but_close.pointer_click") + def close_function(self, *events): + self.parent.remove(self.dyn_id) + + +class PyWidget2(flx.PyWidget): + frame = None + + def init(self, additional_style="width: 100%; height: 100%;"): + with flx.VFix(flex=1, style=additional_style) as self.frame: + with flx.VFix(flex=1) as self.page: + self.custom = flx.VFix(flex=1, style="width: 100%; height: 100%; border: 5px solid blue;") + self.but = flx.Button(text="Swap back to a PyWidget1") + + def dispose(self): + self.frame.dispose() + self.frame = None + super().dispose() + + @flx.reaction("but.pointer_click") + def delete_function(self, *events): + self.parent.remove(self.dyn_id) + self.parent.instantiate(PyWidget1) + + +class Example(flx.PyWidget): + + # The CSS is not used by flex in PyWiget but it should be applied to the top div: TODO + CSS = """ + .flx-DynamicWidgetContainer { + white-space: nowrap; + padding: 0.2em 0.4em; + border-radius: 3px; + color: #333; + } + """ + + def init(self): + with flx.VFix(flex=1) as self.frame_layout: + self.dynamic = flx.DynamicWidgetContainer( + style="width: 100%; height: 100%; border: 5px solid black;", flex=1 + ) + self.but = flx.Button(text="Instanciate a PyWidget1 in the dynamic container") + + @flx.reaction("but.pointer_click") + def click(self, *events): + self.dynamic.instantiate(PyWidget1) + + +flexx_app = threading.Event() +flexx_thread = None + + +def start_flexx_app(): + """ + Starts the flexx thread that manages the flexx asyncio worker loop. + """ + + flexx_loop = asyncio.new_event_loop() # assign the loop to the manager so it can be accessed later. + + def flexx_run(loop): + """ + Function to start a thread containing the main loop of flexx. + """ + global flexx_app + asyncio.set_event_loop(loop) + + event = flexx_app # flexx_app was initialized with an Event() + flexx_app = flx.launch(Example, loop=loop) + event.set() + flx.run() + + global flexx_thread + flexx_thread = threading.Thread(target=flexx_run, args=(flexx_loop,)) + flexx_thread.daemon = True + flexx_thread.start() + + +start_flexx_app() +app = flexx_app +if isinstance(app, threading.Event): # check if app was instanciated + app.wait() # wait for instanciation +# At this point flexx_app contains the Example application +pos = flexx_app.dynamic.instantiate(PyWidget1) +instance = flexx_app.dynamic.get_instance(pos) +instance.but.set_text("it worked") +# instance.dyn_stop_event.wait() # This waits for the instance to be removed +flexx_thread.join() # Wait for the flexx event loop to terminate. +print(instance.input.text)