diff --git a/examples/reference/widgets/MenuButton.ipynb b/examples/reference/widgets/MenuButton.ipynb new file mode 100644 index 0000000000..ef700f4d9e --- /dev/null +++ b/examples/reference/widgets/MenuButton.ipynb @@ -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 +} diff --git a/panel/widgets/__init__.py b/panel/widgets/__init__.py index 74744c9587..0069b60d6d 100644 --- a/panel/widgets/__init__.py +++ b/panel/widgets/__init__.py @@ -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, diff --git a/panel/widgets/button.py b/panel/widgets/button.py index b7f1aa76c3..ae56a2d4bb 100644 --- a/panel/widgets/button.py +++ b/panel/widgets/button.py @@ -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 @@ -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 @@ -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): """ @@ -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): @@ -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)