-
Notifications
You must be signed in to change notification settings - Fork 259
Added a DynamicWidgetContainer #718
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
97bd320
3a37599
8b32cd7
c285b94
52b3578
3046f84
653ba82
ee39244
84d02d6
4af18e4
bf619c4
6019074
e0d3c6e
b28bc12
3d05160
c9ebc95
0aa5454
fbcef26
4472c6e
a614a7b
5e7608c
7e4b59a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Comment on lines
+61
to
+65
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this block be dendented? (It's now under the |
||
|
|
||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| from flexx import flx | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you please add a short docstring at the top of the examples to briefly explain the purpose of the examples?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, will do. |
||
| 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() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are a few TODO's in this this PR's code. Do you plan to fix these in this PR, or do you think that's not needed?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This todo will need fixing to have PyWidget work like a Widget. I'm just not shure how to do it at this point. |
||
| 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) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please add a few lines of comments explaining the flow. As I understand it, first a
threading.taskis appended to.pagesand that is exchanged for an actual page instance here. Does this trick have to do with thread-safety?