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 MenuButton widget #1533

Merged
merged 2 commits into from
Aug 19, 2020
Merged
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
178 changes: 178 additions & 0 deletions examples/reference/widgets/MenuButton.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import panel as pn\n",
"pn.extension()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``MenuButton`` widget allows specifying a list of menu items to select from triggering events when the button is clicked. Unlike other widgets, it does not have a ``value`` parameter. Instead it has a ``clicked`` parameter that can be watched to trigger events and which reports the last clicked menu item.\n",
"\n",
"For more information about listening to widget events and laying out widgets refer to the [widgets user guide](../../user_guide/Widgets.ipynb). Alternatively you can learn how to build GUIs by declaring parameters independently of any specific widgets in the [param user guide](../../user_guide/Param.ipynb). To express interactivity entirely using Javascript without the need for a Python server take a look at the [links user guide](../../user_guide/Param.ipynb).\n",
"\n",
"#### Parameters:\n",
"\n",
"For layout and styling related parameters see the [customization user guide](../../user_guide/Customization.ipynb).\n",
"\n",
"##### Core\n",
"\n",
"* **``clicked``** (str): The last clicked menu item\n",
"* **``items``** (list(tuple or str or None): Menu items in the dropdown. Allows strings, tuples of the form (title, value) or Nones to separate groups of items.\n",
"* **``split``** (boolean): Whether to add separate dropdown area to button.\n",
"\n",
"##### Display\n",
"\n",
"* **``button_type``** (str): A button theme; should be one of ``'default'`` (white), ``'primary'`` (blue), ``'success'`` (green), ``'info'`` (yellow), or ``'danger'`` (red)\n",
"* **``disabled``** (boolean): Whether the widget is editable\n",
"* **``name``** (str): The title of the widget\n",
"\n",
"___"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The `MenuButton` is defined by the name of the button and a list of items corresponding to the menu items. By separating items by None we can group them into sections:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"menu_items = [('Option A', 'a'), ('Option B', 'b'), ('Option C', 'c'), None, ('Help', 'help')]\n",
"\n",
"menu_button = pn.widgets.MenuButton(name='Dropdown', items=menu_items, button_type='primary')\n",
"\n",
"pn.Column(menu_button, height=200)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``clicked`` parameter will report the last menu item that was clicked:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"menu_button.clicked"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The ``on_click`` method can trigger function when button is clicked:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"text = pn.widgets.TextInput(value='Ready')\n",
"\n",
"def b(event):\n",
" text.value = f'Clicked menu item: \"{event.new}\"'\n",
" \n",
"menu_button.on_click(b)\n",
"\n",
"pn.Row(menu_button, text, height=300)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The color of the button can be set by selecting one of the available button types:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.Column(*(pn.widgets.MenuButton(name=p, button_type=p, items=menu_items) for p in pn.widgets.MenuButton.param.button_type.objects))"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Additionally we can move the dropdown indicator into a separate area using the split option:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.Row(pn.widgets.MenuButton(name='Split Menu', split=True, items=menu_items), height=200)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### Controls\n",
"\n",
"The `MenuButton` widget exposes a number of options which can be changed from both Python and Javascript. Try out the effect of these parameters interactively:"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"pn.Row(menu_button.controls, menu_button)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.5"
},
"widgets": {
"application/vnd.jupyter.widget-state+json": {
"state": {},
"version_major": 2,
"version_minor": 0
}
}
},
"nbformat": 4,
"nbformat_minor": 4
}
2 changes: 1 addition & 1 deletion panel/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from .ace import Ace # noqa
from .base import Widget, CompositeWidget # noqa
from .button import Button, Toggle # noqa
from .button import Button, MenuButton, Toggle # noqa
from .file_selector import FileSelector # noqa
from .input import ( # noqa
ColorPicker,
Expand Down
93 changes: 66 additions & 27 deletions panel/widgets/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@

import param

from bokeh.models import Button as _BkButton, Toggle as _BkToggle
from bokeh.models import (
Button as _BkButton, Toggle as _BkToggle, Dropdown as _BkDropdown
)

from .base import Widget

Expand All @@ -23,38 +25,18 @@ class _ButtonBase(Widget):
__abstract = True


class Button(_ButtonBase):
class _ClickButton(_ButtonBase):

clicks = param.Integer(default=0)

_rename = {'clicks': None, 'name': 'label'}
__abstract = True

_widget_type = _BkButton
_event = 'button_click'

def _get_model(self, doc, root=None, parent=None, comm=None):
model = super(Button, self)._get_model(doc, root, parent, comm)
model = super(_ClickButton, self)._get_model(doc, root, parent, comm)
ref = (root or model).ref['id']
model.on_click(partial(self._server_click, doc, ref))
return model

def _server_click(self, doc, ref, event):
self._events.update({"clicks": 1})
if not self._processing:
self._processing = True
if doc.session_context:
doc.add_timeout_callback(partial(self._change_event, doc), self._debounce)
else:
self._change_event(doc)

def _process_property_change(self, msg):
msg = super(Button, self)._process_property_change(msg)
if 'clicks' in msg:
msg['clicks'] = self.clicks + 1
return msg

def on_click(self, callback):
self.param.watch(callback, 'clicks')

def js_on_click(self, args={}, code=""):
"""
Allows defining a JS callback to be triggered when the button
Expand All @@ -73,7 +55,7 @@ def js_on_click(self, args={}, code=""):
The Callback which can be used to disable the callback.
"""
from ..links import Callback
return Callback(self, code={'event:button_click': code}, args=args)
return Callback(self, code={'event:'+self._event: code}, args=args)

def jscallback(self, args={}, **callbacks):
"""
Expand All @@ -98,11 +80,37 @@ def jscallback(self, args={}, **callbacks):
from ..links import Callback
for k, v in list(callbacks.items()):
if k == 'clicks':
k = 'event:button_click'
k = 'event:'+self._event
callbacks[k] = self._rename.get(v, v)
return Callback(self, code=callbacks, args=args)


class Button(_ClickButton):

clicks = param.Integer(default=0)

_rename = {'clicks': None, 'name': 'label'}

_widget_type = _BkButton

def _server_click(self, doc, ref, event):
self._events.update({"clicks": self.clicks+1})
if not self._processing:
self._processing = True
if doc.session_context:
doc.add_timeout_callback(partial(self._change_event, doc), self._debounce)
else:
self._change_event(doc)

def _process_property_change(self, msg):
msg = super(Button, self)._process_property_change(msg)
if 'clicks' in msg:
msg['clicks'] = self.clicks + 1
return msg

def on_click(self, callback):
self.param.watch(callback, 'clicks', onlychanged=False)


class Toggle(_ButtonBase):

Expand All @@ -118,3 +126,34 @@ class Toggle(_ButtonBase):
def _get_embed_state(self, root, values=None, max_opts=3):
return (self, self._models[root.ref['id']][0], [False, True],
lambda x: x.active, 'active', 'cb_obj.active')


class MenuButton(_ClickButton):

clicked = param.String(default=None, doc="""
Last menu item that was clicked.""")

items = param.List(default=[], doc="""
Menu items in the dropdown. Allows strings, tuples of the form
(title, value) or Nones to separate groups of items.""")

split = param.Boolean(default=False, doc="""
Whether to add separate dropdown area to button.""")

_widget_type = _BkDropdown

_rename = {'name': 'label', 'items': 'menu', 'clicked': None}

_event = 'menu_item_click'

def on_click(self, callback):
self.param.watch(callback, 'clicked', onlychanged=False)

def _server_click(self, doc, ref, event):
self._events.update({"clicked": event.item})
if not self._processing:
self._processing = True
if doc.session_context:
doc.add_timeout_callback(partial(self._change_event, doc), self._debounce)
else:
self._change_event(doc)