diff --git a/bokeh/models/widgets/icons.py b/bokeh/models/widgets/icons.py index 2fccbff3b20..14248d9a901 100644 --- a/bokeh/models/widgets/icons.py +++ b/bokeh/models/widgets/icons.py @@ -3,7 +3,7 @@ """ from __future__ import absolute_import -from ...properties import Bool, Float, Enum +from ...properties import Bool, Float, Enum, Int from ...enums import NamedIcon from ..widget import Widget @@ -35,3 +35,8 @@ class Icon(AbstractIcon): Indicates a spinning (animated) icon. This value is ignored for icons that do not support spinning. """) + + spin_updates = Int(0, help=""" + This is a dummy field for generated a second callback if the spin state has + been updated. + """) diff --git a/bokehjs/src/coffee/widget/icon.coffee b/bokehjs/src/coffee/widget/icon.coffee index e257a407478..6e1e3a1f623 100644 --- a/bokehjs/src/coffee/widget/icon.coffee +++ b/bokehjs/src/coffee/widget/icon.coffee @@ -4,14 +4,19 @@ HasParent = require "../common/has_parent" class IconView extends ContinuumView tagName: "i" + events: + "change input": "change_input" initialize: (options) -> super(options) + @prev_spin_state = @mget("spin") @render() @listenTo(@model, 'change', @render) render: () -> + # Reset modified html properties @$el.empty() + @$el.removeClass() @$el.addClass("bk-fa") @$el.addClass("bk-fa-" + @mget("name")) @@ -25,8 +30,29 @@ class IconView extends ContinuumView if @mget("spin") @$el.addClass("bk-fa-spin") + if @prev_spin_state != @mget("spin") + # This is a hack to add an additional request/response cycle when updating + # the icon's spin attribute. Suppose we want to load a large amount of data + # or perform a large computation, but want to notify the user that this is + # happening by adding a spin animation to this icon. Since the bokeh-server + # only provides a single transaction for the initial action, we can't first + # update the spin animation and then load the data. Instead, we only update + # the spin animation, send back that the spin_updates counter has incremented, + # and then have the downstream script listen to changes in spin_updates to + # perform its larger actions. + # + # Try for example: bokeh-server --script examples/app/spinning_icon/spin_app.py + @prev_spin_state = @mget("spin") + @change_input() + return @ + change_input: () -> + # Increment counter of number of changes of spin + @mset('spin_updates', @mget('spin_updates') + 1) + @model.save() + @mget('callback')?.execute(@model) + class Icon extends HasParent type: "Icon" default_view: IconView @@ -37,8 +63,9 @@ class Icon extends HasParent size: null flip: null spin: false + spin_updates: 0 } module.exports = Model: Icon - View: IconView \ No newline at end of file + View: IconView diff --git a/examples/app/spinning_icon/spin_app.py b/examples/app/spinning_icon/spin_app.py new file mode 100644 index 00000000000..ac5df875b9d --- /dev/null +++ b/examples/app/spinning_icon/spin_app.py @@ -0,0 +1,67 @@ +""" +Demonstrate a simple app that in response to a button click, +updates an icon to spin and then does additional updates. +""" + +from __future__ import print_function + +import time + +from bokeh.models import Plot +from bokeh.models.widgets import VBox, Icon, Button +from bokeh.plotting import figure, curdoc +from bokeh.properties import Instance +from bokeh.server.app import bokeh_app +from bokeh.server.utils.plugins import object_page +import numpy as np + +class SpinApp(VBox): + extra_generated_classes = [["SpinApp", "SpinApp", "VBox"]] + jsmodel = "VBox" + + icon = Instance(Icon) + button = Instance(Button) + plot = Instance(Plot) + + @classmethod + def create(cls): + obj = cls() + obj.icon = Icon(name="refresh") + obj.button = Button(label="Load", type="primary", icon=obj.icon) + obj.plot = figure(title="random data") + obj.set_children() + return obj + + def set_children(self): + self.children = [self.button, self.plot] + + def setup_events(self): + if self.icon: + self.icon.on_change('spin_updates', self, 'on_spin_change') + if self.button: + self.button.on_change('clicks', self, 'on_button_click') + + def on_button_click(self, obj, attrname, old, new): + self.icon.spin = True + + def on_spin_change(self, obj, attrname, old, new): + """On html spin update""" + print("SpinApp: Received spin update", attrname, old, new, self.icon.spin) + if self.icon.spin: + time.sleep(5) + self.load_plot() + self.icon.spin = False + self.set_children() + curdoc().add(self) + + def load_plot(self): + p = figure(title="random data") + data_length = 100 + p.circle(np.arange(data_length), np.random.rand(data_length), size=5) + self.plot = p + +# The following code adds a "/bokeh/spin/" url to the bokeh-server. +@bokeh_app.route("/bokeh/spin/") +@object_page("spin") +def make_spin_app(): + return SpinApp.create()