Skip to content
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

add page support #427

Draft
wants to merge 17 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 25 additions & 1 deletion docs/spec.rst
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,24 @@ The ``spec`` fields of ``download`` commands:
* name: str, File name when downloading
* content: str, File content in base64 encoding.

open_page
^^^^^^^^^^^^^^^
Open new page

The ``spec`` fields of ``new_page`` commands:

* page_id: str, page id to be created
* new_window: bool, whether to open sub-page as new browser window or iframe

close_page
^^^^^^^^^^^^^^^
Close a page

The ``spec`` fields of ``close_page`` commands:

* page_id: str, page id to be closed


Event
------------

Expand Down Expand Up @@ -444,4 +462,10 @@ js_yield
^^^^^^^^^^^^^^^
submit data from js. It's a common event to submit data to backend.

The ``data`` of the event is the data need to submit
The ``data`` of the event is the data need to submit

page_close
^^^^^^^^^^^^^^^
Triggered when the user close the page

The ``data`` of the event is the page id that is closed
4 changes: 4 additions & 0 deletions pywebio/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ class SessionException(Exception):
"""Base class for PyWebIO session related exceptions"""


class PageClosedException(Exception):
"""The page has been closed abnormally"""


class SessionClosedException(SessionException):
"""The session has been closed abnormally"""

Expand Down
51 changes: 50 additions & 1 deletion pywebio/html/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -361,4 +361,53 @@ details[open]>summary {
color: #6c757d;
line-height: 14px;
vertical-align: text-top;
}
}

html.overflow-y-hidden {
overflow-y: hidden !important;
}

.pywebio-page-close-btn {
position: absolute;
right: 0;
top: 0;
z-index: 50;
}

.btn-close {
box-sizing: content-box;
width: 1em;
height: 1em;
padding: 0.25em;
margin: 0.75em;
color: #000;
background: transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;
border: 0;
border-radius: 0.25rem;
opacity: .5;
transition: opacity 0.2s ease-in-out;
}

.btn-close:hover {
color: #000;
text-decoration: none;
opacity: .95;
}

.pywebio-page {
width: 100%;
min-height: 100vh; /* `100%` will cause issue on mobile platform */
position: absolute;
top: 0;
bottom: 0;
box-shadow: 4px -4px 4px rgb(0 0 0 / 20%);
margin-left: -102%;
z-index: 200;
transition: margin-left 0.4s ease-in-out;
}
@media (max-width: 425px) {.pywebio-page{transition: margin-left 0.3s ease-in-out;}}

.pywebio-page.active {
margin-left: 0%;
background-color: white;
}
17 changes: 13 additions & 4 deletions pywebio/io_ctrl.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
from .utils import random_str
from .exceptions import PageClosedException

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -63,12 +64,11 @@ def safely_destruct(cls, obj):
pass

def __init__(self, spec, on_embed=None):
self.processed = False
self.processed = True # avoid `__del__` is invoked accidentally when exception occurs in `__init__`
self.on_embed = on_embed or (lambda d: d)
try:
self.spec = type(self).dump_dict(spec) # this may raise TypeError
except TypeError:
self.processed = True
type(self).safely_destruct(spec)
raise

Expand All @@ -84,7 +84,12 @@ def __init__(self, spec, on_embed=None):
# the Exception raised from there will be ignored by python interpreter,
# thus we can't end some session in some cases.
# See also: https://github.com/pywebio/PyWebIO/issues/243
get_current_session()
s = get_current_session()

# Try to make sure current page is active.
# Session.get_page_id will raise PageClosedException when the page is not activate
s.get_page_id()
self.processed = False

def enable_context_manager(self, container_selector=None, container_dom_id=None, custom_enter=None,
custom_exit=None):
Expand Down Expand Up @@ -214,7 +219,11 @@ def inner(*args, **kwargs):

def send_msg(cmd, spec=None, task_id=None):
msg = dict(command=cmd, spec=spec, task_id=task_id or get_current_task_id())
get_current_session().send_task_command(msg)
s = get_current_session()
page = s.get_page_id()
if page is not None:
msg['page'] = page
s.send_task_command(msg)


def single_input_kwargs(single_input_return):
Expand Down
81 changes: 80 additions & 1 deletion pywebio/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
| | `popup`:sup:`*†` | Show popup |
| +---------------------------+------------------------------------------------------------+
| | `close_popup` | Close the current popup window. |
| +---------------------------+------------------------------------------------------------+
| | `page` | Open a new page. |
+--------------------+---------------------------+------------------------------------------------------------+
| Layout and Style | `put_row`:sup:`*†` | Use row layout to output content |
| +---------------------------+------------------------------------------------------------+
Expand Down Expand Up @@ -218,6 +220,7 @@
from .io_ctrl import output_register_callback, send_msg, Output, safely_destruct_output_when_exp, OutputList, scope2dom
from .session import get_current_session, download
from .utils import random_str, iscoroutinefunction, check_dom_name_value
from .exceptions import PageClosedException

try:
from PIL.Image import Image as PILImage
Expand All @@ -231,7 +234,7 @@
'put_table', 'put_buttons', 'put_image', 'put_file', 'PopupSize', 'popup', 'put_button',
'close_popup', 'put_widget', 'put_collapse', 'put_link', 'put_scrollable', 'style', 'put_column',
'put_row', 'put_grid', 'span', 'put_processbar', 'set_processbar', 'put_loading',
'output', 'toast', 'get_scope', 'put_info', 'put_error', 'put_warning', 'put_success']
'output', 'toast', 'get_scope', 'put_info', 'put_error', 'put_warning', 'put_success', 'page']


# popup size
Expand Down Expand Up @@ -1809,3 +1812,79 @@ async def coro_wrapper(*args, **kwargs):
return coro_wrapper
else:
return wrapper


def page(new_window=False, silent_quit=False):
"""
Open a page. Can be used as context manager and decorator.

:param bool silent_quit: whether to quit silently when the page is closed accidentally by app user

:Usage:

::

with page() as scope_name:
input()
put_xxx()

@page()
def content():
input()
put_xxx()
"""
p = page_(silent_quit=silent_quit, new_window=new_window)
return p


class page_:
page_id: str
new_window: bool
silent_quit: bool = False

def __init__(self, silent_quit, new_window):
self.silent_quit = silent_quit
self.new_window = new_window
self.page_id = random_str(10)

def new_page(self):
return page_(self.silent_quit, new_window=self.new_window)

def __enter__(self):
send_msg('open_page', dict(page_id=self.page_id, new_window=self.new_window))
get_current_session().push_page(self.page_id)

def __exit__(self, exc_type, exc_val, exc_tb):
"""
If this method returns True, it means that the context manager can handle the exception,
so that the with statement terminates the propagation of the exception
"""
get_current_session().pop_page()
if isinstance(exc_val, PageClosedException): # page is close by app user
if self.silent_quit:
# suppress PageClosedException Exception
return True
else:
send_msg('close_page', dict(page_id=self.page_id))

return False # propagate Exception

def __call__(self, func):
"""decorator implement"""

@wraps(func)
def wrapper(*args, **kwargs):
# can't use `with self:`, it will use same object in
# different calls to same decorated func
with self.new_page():
return func(*args, **kwargs)

@wraps(func)
async def coro_wrapper(*args, **kwargs):
with self.new_page():
return await func(*args, **kwargs)

if iscoroutinefunction(func):
return coro_wrapper
else:
return wrapper
4 changes: 3 additions & 1 deletion pywebio/pin.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,9 @@
from pywebio.output import OutputPosition, Output
from pywebio.output import _get_output_spec
from .io_ctrl import send_msg, single_input_kwargs, output_register_callback
from .session import next_client_event, chose_impl
from .session import next_client_event, chose_impl, get_current_session
from .utils import check_dom_name_value
from .exceptions import PageClosedException

_pin_name_chars = set(string.ascii_letters + string.digits + '_-')

Expand Down Expand Up @@ -227,6 +228,7 @@ def put_actions(name, *, label='', buttons=None, help_text=None,
@chose_impl
def get_client_val():
res = yield next_client_event()

assert res['event'] == 'js_yield', "Internal Error, please report this bug on " \
"https://github.com/wang0618/PyWebIO/issues"
return res['data']
Expand Down
12 changes: 8 additions & 4 deletions pywebio/session/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ def show():
from .base import Session
from .coroutinebased import CoroutineBasedSession
from .threadbased import ThreadBasedSession, ScriptModeSession
from ..exceptions import SessionNotFoundException, SessionException
from ..exceptions import SessionNotFoundException, SessionException, PageClosedException
from ..utils import iscoroutinefunction, isgeneratorfunction, run_as_function, to_coroutine, ObjectDictProxy, \
ReadOnlyObjectDict

Expand Down Expand Up @@ -287,13 +287,15 @@ def inner(*args, **kwargs):

@chose_impl
def next_client_event():
res = yield get_current_session().next_client_event()
return res
session_ = get_current_session()
event = yield session_.next_client_event()
Session.client_event_pre_check(session_, event)
return event


@chose_impl
def hold():
"""Keep the session alive until the browser page is closed by user.
"""Hold and wait the browser page is closed by user.

.. attention::

Expand All @@ -315,6 +317,8 @@ def hold():
yield next_client_event()
except SessionException:
return
except PageClosedException:
return


def download(name, content):
Expand Down
Loading