├── 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 | 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 | [![Version](https://img.shields.io/pypi/v/ipypopout.svg)](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 | --------------------------------------------------------------------------------