├── MANIFEST.in
├── ipypopout
├── _version.py
├── __init__.py
├── popout_button.vue
└── popout_button.py
├── .gitignore
├── setup.cfg
├── release.sh
├── template
└── static
│ ├── popout.html
│ └── popout.js
├── .bumpversion.cfg
├── tests
├── unit
│ └── popout_test.py
└── ui
│ └── popout_test.py
├── release.md
├── LICENSE
├── example.ipynb
├── setup.py
├── .github
└── workflows
│ └── main.yml
└── README.md
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include LICENSE
2 |
--------------------------------------------------------------------------------
/ipypopout/_version.py:
--------------------------------------------------------------------------------
1 | __version__ = "2.0.1"
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ipypopout.egg-info
2 | build
3 | dist
4 |
--------------------------------------------------------------------------------
/ipypopout/__init__.py:
--------------------------------------------------------------------------------
1 | from ._version import __version__
2 | from ipypopout.popout_button import PopoutButton
3 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [bdist_wheel]
2 | universal=1
3 |
4 | [options.package_data]
5 | ipypopout = *.vue
6 |
7 | [flake8]
8 | max-line-length = 100
9 | per-file-ignores = __init__.py:F401
10 |
--------------------------------------------------------------------------------
/release.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | set -e -o pipefail
3 | # usage: ./release minor -n
4 | version=$(bump2version --dry-run --list $* | grep new_version | sed -r s,"^.*=",,)
5 | echo Version tag v$version
6 | bumpversion $* --verbose && git push upstream master v$version
7 |
--------------------------------------------------------------------------------
/template/static/popout.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
17 |
18 |
--------------------------------------------------------------------------------
/.bumpversion.cfg:
--------------------------------------------------------------------------------
1 | [bumpversion]
2 | current_version = 2.0.1
3 | commit = True
4 | tag = True
5 | parse = (?P\d+)(\.(?P\d+))(\.(?P\d+))((?P.)(?P\d+))?
6 | serialize =
7 | {major}.{minor}.{patch}{release}{build}
8 | {major}.{minor}.{patch}
9 |
10 | [bumpversion:part:release]
11 | optional_value = g
12 | first_value = g
13 | values =
14 | a
15 | b
16 | g
17 |
18 | [bumpversion:file:ipypopout/_version.py]
19 |
--------------------------------------------------------------------------------
/tests/unit/popout_test.py:
--------------------------------------------------------------------------------
1 | import ipypopout
2 | import ipywidgets as widgets
3 |
4 |
5 | def test_create_target():
6 | box = widgets.VBox()
7 | button = ipypopout.PopoutButton(target=box)
8 | assert button.target_model_id == box._model_id
9 | assert button.window_name == box._model_id
10 |
11 | box2 = widgets.VBox()
12 | button.target = box2
13 | assert button.target_model_id == box2._model_id
14 | assert button.window_name == box2._model_id
15 |
16 |
--------------------------------------------------------------------------------
/release.md:
--------------------------------------------------------------------------------
1 |
2 | # Fully automated
3 |
4 | $ ./release.sh patch
5 |
6 |
7 | ## Making an alpha release
8 |
9 |
10 | $ ./release.sh patch --new-version 1.0.0
11 |
12 |
13 | # semi automated
14 | To make a new release
15 | ```
16 | # update ipypopout/__init__.py
17 | $ git add -u && git commit -m 'Release v1.0.0' && git tag v1.0.0 && git push upstream master v1.0.0
18 | ```
19 |
20 |
21 | If a problem happens, and you want to keep the history clean
22 | ```
23 | # do fix
24 | $ git rebase -i HEAD~3
25 | $ git tag v1.0.0 -f && git push upstream master v1.0.0 -f
26 | ```
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Mario Buikhuizen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/example.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "39917761",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "import ipywidgets\n",
11 | "from ipypopout import PopoutButton "
12 | ]
13 | },
14 | {
15 | "cell_type": "code",
16 | "execution_count": null,
17 | "id": "f1dbb494",
18 | "metadata": {},
19 | "outputs": [],
20 | "source": [
21 | "main = ipywidgets.HBox()\n",
22 | "\n",
23 | "popout_button = PopoutButton(main)\n",
24 | "\n",
25 | "slider = ipywidgets.IntSlider()\n",
26 | "main.children = (slider, popout_button)\n",
27 | "\n",
28 | "main"
29 | ]
30 | },
31 | {
32 | "cell_type": "code",
33 | "execution_count": null,
34 | "id": "d95f96d8",
35 | "metadata": {},
36 | "outputs": [],
37 | "source": []
38 | }
39 | ],
40 | "metadata": {
41 | "kernelspec": {
42 | "display_name": "Python 3 (ipykernel)",
43 | "language": "python",
44 | "name": "python3"
45 | },
46 | "language_info": {
47 | "codemirror_mode": {
48 | "name": "ipython",
49 | "version": 3
50 | },
51 | "file_extension": ".py",
52 | "mimetype": "text/x-python",
53 | "name": "python",
54 | "nbconvert_exporter": "python",
55 | "pygments_lexer": "ipython3",
56 | "version": "3.10.0"
57 | }
58 | },
59 | "nbformat": 4,
60 | "nbformat_minor": 5
61 | }
62 |
--------------------------------------------------------------------------------
/tests/ui/popout_test.py:
--------------------------------------------------------------------------------
1 | import playwright.sync_api
2 | from IPython.display import display
3 | import ipypopout
4 | import queue
5 |
6 | def test_popout(
7 | ipywidgets_runner, page_session: playwright.sync_api.Page, context_session: playwright.sync_api.BrowserContext,assert_solara_snapshot
8 | ):
9 | def kernel_code():
10 | import ipyvuetify as v
11 | import ipypopout
12 |
13 | container = v.Container(
14 | children=[],
15 | )
16 |
17 | button = ipypopout.PopoutButton(
18 | target=container,
19 | )
20 | text = v.Html(tag="div", children=["Test ipypopout"])
21 | container.children = [button, text]
22 | display(container)
23 |
24 | ipywidgets_runner(kernel_code)
25 | with context_session.expect_page() as new_page_info:
26 | page_session.locator("_vue=v-btn[icon]").click()
27 | new_page = new_page_info.value
28 | new_page.locator("text=Test ipypopout").wait_for()
29 | # the button should not be on the page
30 | new_page.locator("_vue=v-btn").wait_for(state="detached")
31 | # if we do not go to a blank page, on solara, the server will not get the close beacon
32 | # and the kernel will not shut down
33 | new_page.goto("about:blank")
34 | # wait for the kernel to shut down, if we close the page to early
35 | # it seem again the server will not get the close beacon
36 | # and solara's test framework fails
37 | new_page.wait_for_timeout(1000)
38 | new_page.close()
39 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from setuptools import setup
4 | from setuptools.command.develop import develop
5 | from distutils import cmd
6 |
7 | template = ['template', 'static']
8 |
9 | share_voila_target = ['share', 'jupyter', 'voila', 'templates', 'ipypopout']
10 |
11 |
12 | class DevelopCmd(develop):
13 | def run(self):
14 | if '--user' in sys.prefix:
15 | raise NotImplemented('--user not supported')
16 |
17 | link_target = os.path.join(sys.prefix, *share_voila_target)
18 | print('linking', os.path.abspath(template[0]), '->', link_target)
19 | os.symlink(os.path.abspath(template[0]), os.path.abspath(link_target))
20 |
21 | super(DevelopCmd, self).run()
22 |
23 |
24 | class CleanDevelop(cmd.Command):
25 | user_options = []
26 |
27 | def finalize_options(self) -> None:
28 | pass
29 |
30 | def initialize_options(self) -> None:
31 | pass
32 |
33 | def run(self):
34 | os.unlink(os.path.join(sys.prefix, *share_voila_target))
35 |
36 |
37 | def get_data_files(target, src):
38 | files = [(os.path.join(target, os.path.relpath(dirpath, src)),
39 | [os.path.join(dirpath, name) for name in filenames])
40 | for (dirpath, _, filenames) in os.walk(src)]
41 | return files
42 |
43 |
44 | here = os.path.dirname(__file__)
45 | version_ns = {}
46 | with open(os.path.join(here, 'ipypopout', '_version.py')) as f:
47 | exec(f.read(), {}, version_ns)
48 |
49 |
50 | setup(
51 | name='ipypopout',
52 | version=version_ns['__version__'],
53 | author='Mario Buikhuizen',
54 | author_email='mariobuikhuizen@gmail.com',
55 | url='https://github.com/mariobuikhuizen/ipypopout',
56 | packages=['ipypopout'],
57 | install_requires=[
58 | 'ipywidgets>=7.7',
59 | 'ipyvuetify>=1.7.0,<2',
60 | ],
61 | extras_require={
62 | "test": [
63 | "solara[pytest]",
64 | ],
65 | "voila": [
66 | 'voila>=0.2.10,<0.5'
67 | ],
68 | "solara": [
69 | 'solara-server>=1.40.0'
70 | ]
71 | },
72 | data_files=get_data_files(os.path.join(*share_voila_target), os.path.join(template[0])),
73 | cmdclass={
74 | 'develop': DevelopCmd,
75 | 'clean_develop': CleanDevelop
76 | }
77 | )
78 |
--------------------------------------------------------------------------------
/template/static/popout.js:
--------------------------------------------------------------------------------
1 | function loadScript(url) {
2 | return new Promise((resolve, reject) => {
3 | const script = document.createElement('script');
4 | script.src = url;
5 | script.onload = resolve;
6 | script.onerror = reject;
7 | document.head.appendChild(script);
8 | });
9 | }
10 |
11 | function addStyle(href) {
12 | const link = document.createElement('link');
13 | link.href = href;
14 | link.type = "text/css";
15 | link.rel = "stylesheet";
16 | document.head.appendChild(link);
17 | }
18 |
19 | function requireAsync(urls) {
20 | return new Promise((resolve, reject) => {
21 | try {
22 | window.requirejs(urls, (...args) => {
23 | resolve(args);
24 | });
25 | } catch (e) {
26 | reject(e);
27 | }
28 | });
29 | }
30 |
31 | function getWidgetManager(voila, kernel) {
32 | function connect() {
33 | }
34 |
35 | return new voila.WidgetManager({
36 | saveState: { connect },
37 | sessionContext: {
38 | session: { kernel },
39 | kernelChanged: { connect },
40 | statusChanged: { connect },
41 | connectionStatusChanged: { connect },
42 | },
43 | },
44 | new voila.RenderMimeRegistry(),
45 | { saveState: false });
46 | }
47 |
48 | async function connectToJupyterKernel(kernelId, baseUrl, targetModelId) {
49 | await loadScript('https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js');
50 |
51 | const [voila] = await requireAsync([`${baseUrl}voila/static/voila.js`]);
52 | const kernel = await voila.connectKernel(baseUrl, kernelId);
53 |
54 | const widgetManager = getWidgetManager(voila, kernel);
55 |
56 | if (widgetManager._build_models) {
57 | await widgetManager._build_models();
58 | } else {
59 | /* Voila >= 0.3.4 */
60 | await widgetManager._loadFromKernel();
61 | }
62 |
63 | const model = await widgetManager._models[targetModelId]
64 | const container = document.getElementById('popout-widget-container')
65 | if (!model) {
66 | container.innerText = 'Model not found';
67 | return;
68 | }
69 | const view = await widgetManager.create_view(model)
70 | widgetManager.display_view(
71 | undefined,
72 | view,
73 | { el: container });
74 | }
75 |
76 | function init() {
77 | const urlParams = new URLSearchParams(window.location.search);
78 | const kernelId = urlParams.get('kernelid');
79 | const modelId = urlParams.get('modelid');
80 | const baseUrl = urlParams.get('baseurl');
81 | const isDark = urlParams.get('dark') === 'true';
82 |
83 | document.body.dataset.jpThemeLight = isDark ? 'false' : 'true';
84 | document.body.style.backgroundColor = isDark ? 'black' : 'white';
85 |
86 | addStyle(`${baseUrl}voila/static/index.css`);
87 | addStyle(isDark ? `${baseUrl}voila/static/theme-dark.css` : `${baseUrl}voila/static/theme-light.css`);
88 |
89 | connectToJupyterKernel(kernelId, baseUrl, modelId);
90 | }
91 |
--------------------------------------------------------------------------------
/ipypopout/popout_button.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 | mdi-application-export
11 |
12 |
13 |
14 |
101 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: Main
2 |
3 | on:
4 | push:
5 | pull_request:
6 | workflow_dispatch:
7 | schedule:
8 | # Run at 2:00 a.m. every weekday (Monday to Friday)
9 | - cron: "0 2 * * 1-5"
10 |
11 | jobs:
12 | build:
13 | runs-on: ubuntu-24.04
14 | steps:
15 | - uses: actions/checkout@v2
16 |
17 | - name: Install Python
18 | uses: actions/setup-python@v2
19 | with:
20 | python-version: "3.11"
21 |
22 | - name: Install dependencies
23 | run: |
24 | python -m pip install --upgrade pip
25 | pip install twine wheel jupyter-packaging "jupyterlab<4"
26 |
27 | - name: Build
28 | run: |
29 | python setup.py sdist bdist_wheel
30 |
31 | - name: Upload builds
32 | uses: actions/upload-artifact@v4
33 | with:
34 | name: ipypopout-dist-${{ github.run_number }}
35 | path: |
36 | ./dist
37 |
38 | test:
39 | needs: [build]
40 | runs-on: ubuntu-24.04
41 | strategy:
42 | fail-fast: false
43 | matrix:
44 | python-version: [3.8, 3.9, "3.10", "3.11"]
45 | steps:
46 | - uses: actions/checkout@v2
47 |
48 | - uses: actions/download-artifact@v4
49 | with:
50 | name: ipypopout-dist-${{ github.run_number }}
51 | # because we only upload 1 file, it's not put in a subdirectory
52 | path: dist
53 |
54 | - name: Install Python
55 | uses: actions/setup-python@v2
56 | with:
57 | python-version: ${{ matrix.python-version }}
58 |
59 | - name: Install
60 | run: pip install dist/*.whl
61 |
62 | - name: Import
63 | # do the import in a subdirectory, as after installation, files in de current directory are also imported
64 | run: |
65 | (mkdir test-install; cd test-install; python -c "from ipypopout import PopoutButton")
66 |
67 | ui-test:
68 | needs: [build]
69 | runs-on: ubuntu-24.04
70 | steps:
71 | - uses: actions/checkout@v2
72 |
73 | - uses: actions/download-artifact@v4
74 | with:
75 | # because we only upload 1 file, it's not put in a subdirectory
76 | path: dist
77 | name: ipypopout-dist-${{ github.run_number }}
78 |
79 | - name: Install Python
80 | uses: actions/setup-python@v2
81 | with:
82 | python-version: 3.8
83 |
84 | - name: Install ipypopout and test deps
85 | run: |
86 | wheel=(dist/*.whl)
87 | pip install ${wheel}[test] ${wheel}[voila,solara] "jupyter_server<2"
88 |
89 | - name: Install playwright browsers
90 | run: playwright install chromium
91 |
92 | - name: Run ui-tests
93 | run: pytest tests/ui/ --video=retain-on-failure --solara-update-snapshots-ci -s
94 |
95 | - name: Upload Test artifacts
96 | if: always()
97 | uses: actions/upload-artifact@v4
98 | with:
99 | name: ipypopout-test-results
100 | path: test-results
101 |
102 | release:
103 | if: startsWith(github.event.ref, 'refs/tags/v')
104 | needs: [test]
105 | runs-on: ubuntu-24.04
106 | steps:
107 | - uses: actions/download-artifact@v4
108 | with:
109 | # because we only upload 1 file, it's not put in a subdirectory
110 | path: dist
111 | name: ipypopout-dist-${{ github.run_number }}
112 |
113 | - name: Install Python
114 | uses: actions/setup-python@v2
115 | with:
116 | python-version: 3.8
117 |
118 | - name: Install dependencies
119 | run: |
120 | python -m pip install --upgrade pip
121 | pip install twine wheel
122 |
123 | - name: Publish the Python package
124 | env:
125 | TWINE_USERNAME: __token__
126 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }}
127 | run: twine upload --skip-existing dist/*
128 |
--------------------------------------------------------------------------------
/ipypopout/popout_button.py:
--------------------------------------------------------------------------------
1 | import os
2 | import re
3 | import IPython
4 | import traitlets
5 | import ipywidgets
6 | import ipyvuetify as v
7 | import sys
8 |
9 |
10 | DEFAULT_USE_BACKEND = os.environ.get("IPYPOPOUT_USE_BACKEND", "auto")
11 |
12 |
13 | def get_kernel_id():
14 | if "solara" in sys.modules:
15 | import solara
16 | if solara._using_solara_server():
17 | try:
18 | import solara.server.kernel_context
19 |
20 | context = solara.server.kernel_context.get_current_context()
21 | return context.id
22 | except RuntimeError:
23 | pass
24 | ipython = IPython.get_ipython()
25 | if not ipython or not hasattr(ipython, 'kernel'):
26 | return ''
27 | try:
28 | kernel = ipython.kernel
29 | regex = r'[\\/]kernel-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.json$'
30 | connection_file = kernel.config['IPKernelApp']['connection_file']
31 | return re.compile(regex).search(connection_file).group(1)
32 | except Exception:
33 | return ''
34 |
35 |
36 | class PopoutButton(v.VuetifyTemplate):
37 |
38 | template_file = (__file__, "popout_button.vue")
39 | kernel_id = traitlets.Unicode('').tag(sync=True)
40 | target_model_id = traitlets.Unicode().tag(sync=True)
41 | target = traitlets.Instance(ipywidgets.Widget, allow_none=True)
42 | echo_available = traitlets.Bool(False).tag(sync=True)
43 |
44 | use_backend = traitlets.Enum(values=["auto", "voila", "solara"], default_value=DEFAULT_USE_BACKEND).tag(sync=True)
45 |
46 | is_displayed = traitlets.Bool(False).tag(sync=True)
47 | open_window_on_display = traitlets.Bool(False).tag(sync=True)
48 | open_tab_on_display = traitlets.Bool(False).tag(sync=True)
49 |
50 | # If a window with the same name is available it will be reused, otherwise a new window is created.
51 | # See https://developer.mozilla.org/en-US/docs/Web/API/Window/open
52 | window_name = traitlets.Unicode('').tag(sync=True)
53 |
54 | # See: https://developer.mozilla.org/en-US/docs/Web/API/Window/open#window_features
55 | window_features = traitlets.Unicode('popup').tag(sync=True)
56 |
57 | def __init__(self, target=None, **kwargs):
58 | kwargs = kwargs.copy()
59 |
60 | if os.environ.get("JUPYTER_WIDGETS_ECHO") is None:
61 | ipywidgets.widgets.widget.JUPYTER_WIDGETS_ECHO = True
62 |
63 | self.echo_available = ipywidgets.widgets.widget.JUPYTER_WIDGETS_ECHO
64 | if target is not None:
65 | kwargs = {**kwargs, **{'target': target}}
66 | super(PopoutButton, self).__init__(**kwargs)
67 |
68 | @traitlets.observe('target')
69 | def _on_target_change(self, change):
70 | if change['new'] is not None:
71 | self.target_model_id = change['new']._model_id
72 | self.window_name = change['new']._model_id
73 |
74 | @traitlets.default("target_model_id")
75 | def _default_target_model_id(self):
76 | if self.target is not None:
77 | return self.target._model_id
78 | return ""
79 |
80 | @traitlets.default("window_name")
81 | def _default_window_name(self):
82 | return self.target_model_id or ""
83 |
84 | @traitlets.default("kernel_id")
85 | def _default_kernel_id(self):
86 | return get_kernel_id()
87 |
88 |
89 | def open_window(self):
90 | if self.is_displayed:
91 | self.send({
92 | 'method': 'open_window',
93 | })
94 | else:
95 | self.open_window_on_display = True
96 | display(v.Html(tag="div", children=[self], style_="display: none"))
97 |
98 | def open_tab(self):
99 | if self.is_displayed:
100 | self.send({
101 | 'method': 'open_tab',
102 | })
103 | else:
104 | self.open_tab_on_display = True
105 | display(v.Html(tag="div", children=[self], style_="display: none"))
106 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Ipypopout
2 |
3 | [](https://pypi.python.org/project/ipypopout)
4 |
5 | Use Ipypopout to display parts of your ipywidgets or solara app in separate browser windows.
6 | This is especially useful for those working with multiple screens.
7 |
8 | Works with:
9 |
10 | * Jupyter notebook
11 | * Jupyter lab
12 | * Voila (`version<0.5` only when running standalone)
13 | * [Solara](https://github.com/widgetti/solara/) (`version>=1.22`)
14 |
15 | In the Jupyter notebook and Jupyter lab environments, ipypopout will use either Solara or Voila to create the popout window. The exist
16 |
17 | ## Installation
18 |
19 | ### To use the Solara backend
20 |
21 | ```
22 | $ pip install "ipypopout[solara]"
23 | ```
24 |
25 | ### To use the Voila backend
26 |
27 | ```
28 | $ pip install "ipypopout[voila]"
29 | ```
30 |
31 | *Note: ipypopout is not compatible with Voila >= 0.5 standalone (e.g. running as `voila mynotebook.ipynb`). If you use Voila >=0.5 as a Jupyter server extension, such as when running Jupyter Lab, ipypopout can only use solara and therefore you need to `pip install ipypopout[solara]`.*
32 |
33 | ## Usage
34 |
35 | ### With ipywidgets
36 |
37 | ```python
38 | import ipywidgets as widgets
39 | from ipypopout import PopoutButton
40 |
41 | main = widgets.VBox()
42 |
43 | # see https://developer.mozilla.org/en-US/docs/Web/API/Window/open#window_features
44 | # for window_features
45 | popout_button = PopoutButton(main, window_features='popup,width=400,height=600')
46 |
47 | slider1 = widgets.IntSlider(value=1)
48 | slider2 = widgets.IntSlider(value=2)
49 | result = widgets.Label()
50 | def update_result(_ignore=None):
51 | result.value = value=f"Sum of {slider1.value} and {slider2.value} = {slider1.value + slider2.value}"
52 | update_result()
53 |
54 | main.children = (slider1, slider2, result, popout_button)
55 | slider1.observe(update_result, "value")
56 | slider2.observe(update_result, "value")
57 |
58 | display(main)
59 | ```
60 |
61 | https://github.com/widgetti/ipypopout/assets/1765949/61091b71-309c-472f-8814-184ea2012b82
62 |
63 |
64 | ### With Solara
65 |
66 | ```python
67 | import solara
68 | import plotly.express as px
69 | import numpy as np
70 | from ipypopout import PopoutButton
71 |
72 |
73 | freq = solara.reactive(1)
74 | damping = solara.reactive(0.5)
75 |
76 | t = np.arange(0, 100, 0.1)/10
77 |
78 | @solara.component
79 | def Page():
80 | target_model_id = solara.use_reactive("")
81 |
82 | y = np.sin(t * 2 * np.pi * freq.value) * np.exp(-t*damping.value)
83 |
84 | with solara.Column():
85 | with solara.Card("Controls") as control:
86 | solara.SliderFloat("Freq", value=freq, min=1, max=10)
87 | solara.SliderFloat("zeta", value=damping, min=0, max=2)
88 | if target_model_id.value:
89 | PopoutButton.element(target_model_id=target_model_id.value, window_features='popup,width=400,height=300')
90 | fig = px.line(x=t, y=y)
91 | solara.FigurePlotly(fig)
92 | # with solara we have to use use_effect + get_widget to get the widget id
93 | solara.use_effect(lambda: target_model_id.set(solara.get_widget(control)._model_id))
94 | display(Page())
95 | ```
96 |
97 | Because Solara creates elements instead of widgets, we have to use the `use_effect`/`get_widget` trick to feed the widget ID to the PopoutButton.
98 |
99 |
100 | https://github.com/widgetti/ipypopout/assets/1765949/430cae12-2527-404b-9861-610565ac1471
101 |
102 |
103 |
104 | ## API
105 |
106 | * PopoutButton
107 | * constructor arguments:
108 | * `target - ipywidgets.Widget | None`: The widget that will be shown in the popout window.
109 | * `target_model_id - str`: The widget id (defaults to `target._model_id`)
110 | * `window_name - str`: If a window with the same name is available it will be reused, otherwise a new window is created (defaults to `target_model_id`).
111 | See [https://developer.mozilla.org/en-US/docs/Web/API/Window/open](https://developer.mozilla.org/en-US/docs/Web/API/Window/open) for more details.
112 | * `window_features - str`: See: [https://developer.mozilla.org/en-US/docs/Web/API/Window/open#window_features](https://developer.mozilla.org/en-US/docs/Web/API/Window/open#window_features)
113 |
114 | ### Which backend to use
115 |
116 | Note that ipypopout will automatically detect if it can use Solara, and use it if available, otherwise it will use Voila. If you want to force the use of Voila, you can set the environment variable `IPYPOPOUT_USE_BACKEND=voila`, the other options are `auto` (the default) and `solara` (in case our auto detect fails).
117 |
--------------------------------------------------------------------------------