Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
97bd320
Issue when validating the existance of an asset when the asset is a l…
Aug 19, 2020
3a37599
Merge pull request #1 from flexxui/master
ceprio Oct 12, 2020
8b32cd7
added eclipse settings to .gitignore
Oct 12, 2020
c285b94
Merge branch 'master' of https://github.com/ceprio/flexx
Oct 12, 2020
52b3578
not working: flexx not compatible with gevent
Oct 14, 2020
3046f84
Merge pull request #2 from flexxui/master
ceprio Dec 10, 2020
653ba82
Merge branch 'master' of https://github.com/ceprio/flexx into flaskse…
Dec 10, 2020
ee39244
Added flask server as backend
Dec 10, 2020
84d02d6
Merge branch 'master' of https://github.com/ceprio/flexx
Dec 10, 2020
4af18e4
Merge branch 'flaskserver'
Dec 10, 2020
bf619c4
shuffled things a bit for understandability
Dec 10, 2020
6019074
Added comments and correction of a Bug specific to Python 3.8
Dec 14, 2020
e0d3c6e
correction for python 3.8
Dec 15, 2020
b28bc12
Rearanged _markdown comments for doc-export rendering.
Dec 16, 2020
3d05160
Correction to reduce CPU usage on flask server thread.
Feb 9, 2021
c9ebc95
Added support to style PyWidget at instanciation (e.g. PyWidget(style…
May 26, 2021
0aa5454
Merge branch 'master' of https://github.com/ceprio/flexx
May 26, 2021
fbcef26
Improved comments on changes
Jun 2, 2021
4472c6e
Merge branch 'master' of https://github.com/flexxui/flexx
ceprio Jan 12, 2022
a614a7b
Added DynamicWidgetContainer class
Jan 13, 2022
5e7608c
Update flexx/ui/pywidgets/_dynamicwidgetcontainer.py
ceprio Jan 26, 2022
7e4b59a
Update flexx/ui/pywidgets/_dynamicwidgetcontainer.py
ceprio Jan 26, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion flexx/app/_flaskserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions flexx/event/_component.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions flexx/ui/pywidgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@

from .. import PyWidget
from ._filebrowser import FileBrowserWidget
from ._dynamicwidgetcontainer import DynamicWidgetContainer

81 changes: 81 additions & 0 deletions flexx/ui/pywidgets/_dynamicwidgetcontainer.py
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:
Copy link
Member

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.task is appended to .pages and that is exchanged for an actual page instance here. Does this trick have to do with thread-safety?

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this block be dendented? (It's now under the if options is None.)


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
80 changes: 80 additions & 0 deletions flexxamples/howtos/dynamic_container_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from flexx import flx
Copy link
Member

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 short docstring at the top of the examples to briefly explain the purpose of the examples?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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()
121 changes: 121 additions & 0 deletions flexxamples/howtos/dynamic_container_in_background.py
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
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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)