##// END OF EJS Templates
Merge pull request #6665 from jdfreder/travis_please_run_on_this...
Min RK -
r18273:e1cad3d0 merge
parent child Browse files
Show More
@@ -1,201 +1,190 b''
1 1 // Copyright (c) IPython Development Team.
2 2 // Distributed under the terms of the Modified BSD License.
3 3
4 4 define([
5 5 "underscore",
6 6 "backbone",
7 7 "jquery",
8 8 "base/js/namespace"
9 9 ], function (_, Backbone, $, IPython) {
10 10
11 11 //--------------------------------------------------------------------
12 12 // WidgetManager class
13 13 //--------------------------------------------------------------------
14 14 var WidgetManager = function (comm_manager, notebook) {
15 15 // Public constructor
16 16 WidgetManager._managers.push(this);
17 17
18 18 // Attach a comm manager to the
19 19 this.keyboard_manager = notebook.keyboard_manager;
20 20 this.notebook = notebook;
21 21 this.comm_manager = comm_manager;
22 22 this._models = {}; /* Dictionary of model ids and model instances */
23 23
24 // Register already-registered widget model types with the comm manager.
25 var that = this;
26 _.each(WidgetManager._model_types, function(model_type, model_name) {
27 that.comm_manager.register_target(model_name, $.proxy(that._handle_comm_open, that));
28 });
24 // Register with the comm manager.
25 this.comm_manager.register_target('ipython.widget', $.proxy(this._handle_comm_open, this));
29 26 };
30 27
31 28 //--------------------------------------------------------------------
32 29 // Class level
33 30 //--------------------------------------------------------------------
34 31 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
35 32 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
36 33 WidgetManager._managers = []; /* List of widget managers */
37 34
38 35 WidgetManager.register_widget_model = function (model_name, model_type) {
39 36 // Registers a widget model by name.
40 37 WidgetManager._model_types[model_name] = model_type;
41
42 // Register the widget with the comm manager. Make sure to pass this object's context
43 // in so `this` works in the call back.
44 _.each(WidgetManager._managers, function(instance, i) {
45 if (instance.comm_manager !== null) {
46 instance.comm_manager.register_target(model_name, $.proxy(instance._handle_comm_open, instance));
47 }
48 });
49 38 };
50 39
51 40 WidgetManager.register_widget_view = function (view_name, view_type) {
52 41 // Registers a widget view by name.
53 42 WidgetManager._view_types[view_name] = view_type;
54 43 };
55 44
56 45 //--------------------------------------------------------------------
57 46 // Instance level
58 47 //--------------------------------------------------------------------
59 48 WidgetManager.prototype.display_view = function(msg, model) {
60 49 // Displays a view for a particular model.
61 50 var cell = this.get_msg_cell(msg.parent_header.msg_id);
62 51 if (cell === null) {
63 52 console.log("Could not determine where the display" +
64 53 " message was from. Widget will not be displayed");
65 54 } else {
66 55 var view = this.create_view(model, {cell: cell});
67 56 if (view === null) {
68 57 console.error("View creation failed", model);
69 58 }
70 59 this._handle_display_view(view);
71 60 if (cell.widget_subarea) {
72 61 cell.widget_subarea.append(view.$el);
73 62 }
74 63 view.trigger('displayed');
75 64 }
76 65 };
77 66
78 67 WidgetManager.prototype._handle_display_view = function (view) {
79 68 // Have the IPython keyboard manager disable its event
80 69 // handling so the widget can capture keyboard input.
81 70 // Note, this is only done on the outer most widgets.
82 71 if (this.keyboard_manager) {
83 72 this.keyboard_manager.register_events(view.$el);
84 73
85 74 if (view.additional_elements) {
86 75 for (var i = 0; i < view.additional_elements.length; i++) {
87 76 this.keyboard_manager.register_events(view.additional_elements[i]);
88 77 }
89 78 }
90 79 }
91 80 };
92 81
93 82 WidgetManager.prototype.create_view = function(model, options, view) {
94 83 // Creates a view for a particular model.
95 84 var view_name = model.get('_view_name');
96 85 var ViewType = WidgetManager._view_types[view_name];
97 86 if (ViewType) {
98 87
99 88 // If a view is passed into the method, use that view's cell as
100 89 // the cell for the view that is created.
101 90 options = options || {};
102 91 if (view !== undefined) {
103 92 options.cell = view.options.cell;
104 93 }
105 94
106 95 // Create and render the view...
107 96 var parameters = {model: model, options: options};
108 97 view = new ViewType(parameters);
109 98 view.render();
110 99 model.on('destroy', view.remove, view);
111 100 return view;
112 101 }
113 102 return null;
114 103 };
115 104
116 105 WidgetManager.prototype.get_msg_cell = function (msg_id) {
117 106 var cell = null;
118 107 // First, check to see if the msg was triggered by cell execution.
119 108 if (this.notebook) {
120 109 cell = this.notebook.get_msg_cell(msg_id);
121 110 }
122 111 if (cell !== null) {
123 112 return cell;
124 113 }
125 114 // Second, check to see if a get_cell callback was defined
126 115 // for the message. get_cell callbacks are registered for
127 116 // widget messages, so this block is actually checking to see if the
128 117 // message was triggered by a widget.
129 118 var kernel = this.comm_manager.kernel;
130 119 if (kernel) {
131 120 var callbacks = kernel.get_callbacks_for_msg(msg_id);
132 121 if (callbacks && callbacks.iopub &&
133 122 callbacks.iopub.get_cell !== undefined) {
134 123 return callbacks.iopub.get_cell();
135 124 }
136 125 }
137 126
138 127 // Not triggered by a cell or widget (no get_cell callback
139 128 // exists).
140 129 return null;
141 130 };
142 131
143 132 WidgetManager.prototype.callbacks = function (view) {
144 133 // callback handlers specific a view
145 134 var callbacks = {};
146 135 if (view && view.options.cell) {
147 136
148 137 // Try to get output handlers
149 138 var cell = view.options.cell;
150 139 var handle_output = null;
151 140 var handle_clear_output = null;
152 141 if (cell.output_area) {
153 142 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
154 143 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
155 144 }
156 145
157 146 // Create callback dict using what is known
158 147 var that = this;
159 148 callbacks = {
160 149 iopub : {
161 150 output : handle_output,
162 151 clear_output : handle_clear_output,
163 152
164 153 // Special function only registered by widget messages.
165 154 // Allows us to get the cell for a message so we know
166 155 // where to add widgets if the code requires it.
167 156 get_cell : function () {
168 157 return cell;
169 158 },
170 159 },
171 160 };
172 161 }
173 162 return callbacks;
174 163 };
175 164
176 165 WidgetManager.prototype.get_model = function (model_id) {
177 166 // Look-up a model instance by its id.
178 167 var model = this._models[model_id];
179 168 if (model !== undefined && model.id == model_id) {
180 169 return model;
181 170 }
182 171 return null;
183 172 };
184 173
185 174 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
186 175 // Handle when a comm is opened.
187 176 var that = this;
188 177 var model_id = comm.comm_id;
189 var widget_type_name = msg.content.target_name;
178 var widget_type_name = msg.content.data.model_name;
190 179 var widget_model = new WidgetManager._model_types[widget_type_name](this, model_id, comm);
191 180 widget_model.on('comm:close', function () {
192 181 delete that._models[model_id];
193 182 });
194 183 this._models[model_id] = widget_model;
195 184 };
196 185
197 186 // Backwards compatability.
198 187 IPython.WidgetManager = WidgetManager;
199 188
200 189 return {'WidgetManager': WidgetManager};
201 190 });
@@ -1,446 +1,447 b''
1 1 """Base Widget class. Allows user to create widgets in the back-end that render
2 2 in the IPython notebook front-end.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15 from contextlib import contextmanager
16 16 import collections
17 17
18 18 from IPython.core.getipython import get_ipython
19 19 from IPython.kernel.comm import Comm
20 20 from IPython.config import LoggingConfigurable
21 21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
22 22 CaselessStrEnum, Tuple, CUnicode, Int, Set
23 23 from IPython.utils.py3compat import string_types
24 24
25 25 #-----------------------------------------------------------------------------
26 26 # Classes
27 27 #-----------------------------------------------------------------------------
28 28 class CallbackDispatcher(LoggingConfigurable):
29 29 """A structure for registering and running callbacks"""
30 30 callbacks = List()
31 31
32 32 def __call__(self, *args, **kwargs):
33 33 """Call all of the registered callbacks."""
34 34 value = None
35 35 for callback in self.callbacks:
36 36 try:
37 37 local_value = callback(*args, **kwargs)
38 38 except Exception as e:
39 39 ip = get_ipython()
40 40 if ip is None:
41 41 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
42 42 else:
43 43 ip.showtraceback()
44 44 else:
45 45 value = local_value if local_value is not None else value
46 46 return value
47 47
48 48 def register_callback(self, callback, remove=False):
49 49 """(Un)Register a callback
50 50
51 51 Parameters
52 52 ----------
53 53 callback: method handle
54 54 Method to be registered or unregistered.
55 55 remove=False: bool
56 56 Whether to unregister the callback."""
57 57
58 58 # (Un)Register the callback.
59 59 if remove and callback in self.callbacks:
60 60 self.callbacks.remove(callback)
61 61 elif not remove and callback not in self.callbacks:
62 62 self.callbacks.append(callback)
63 63
64 64 def _show_traceback(method):
65 65 """decorator for showing tracebacks in IPython"""
66 66 def m(self, *args, **kwargs):
67 67 try:
68 68 return(method(self, *args, **kwargs))
69 69 except Exception as e:
70 70 ip = get_ipython()
71 71 if ip is None:
72 72 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
73 73 else:
74 74 ip.showtraceback()
75 75 return m
76 76
77 77 class Widget(LoggingConfigurable):
78 78 #-------------------------------------------------------------------------
79 79 # Class attributes
80 80 #-------------------------------------------------------------------------
81 81 _widget_construction_callback = None
82 82 widgets = {}
83 83
84 84 @staticmethod
85 85 def on_widget_constructed(callback):
86 86 """Registers a callback to be called when a widget is constructed.
87 87
88 88 The callback must have the following signature:
89 89 callback(widget)"""
90 90 Widget._widget_construction_callback = callback
91 91
92 92 @staticmethod
93 93 def _call_widget_constructed(widget):
94 94 """Static method, called when a widget is constructed."""
95 95 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
96 96 Widget._widget_construction_callback(widget)
97 97
98 98 #-------------------------------------------------------------------------
99 99 # Traits
100 100 #-------------------------------------------------------------------------
101 101 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
102 102 registered in the front-end to create and sync this widget with.""")
103 103 _view_name = Unicode(None, allow_none=True, help="""Default view registered in the front-end
104 104 to use to represent the widget.""", sync=True)
105 105 comm = Instance('IPython.kernel.comm.Comm')
106 106
107 107 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
108 108 front-end can send before receiving an idle msg from the back-end.""")
109 109
110 110 keys = List()
111 111 def _keys_default(self):
112 112 return [name for name in self.traits(sync=True)]
113 113
114 114 _property_lock = Tuple((None, None))
115 115 _send_state_lock = Int(0)
116 116 _states_to_send = Set(allow_none=False)
117 117 _display_callbacks = Instance(CallbackDispatcher, ())
118 118 _msg_callbacks = Instance(CallbackDispatcher, ())
119 119
120 120 #-------------------------------------------------------------------------
121 121 # (Con/de)structor
122 122 #-------------------------------------------------------------------------
123 123 def __init__(self, **kwargs):
124 124 """Public constructor"""
125 125 self._model_id = kwargs.pop('model_id', None)
126 126 super(Widget, self).__init__(**kwargs)
127 127
128 128 Widget._call_widget_constructed(self)
129 129 self.open()
130 130
131 131 def __del__(self):
132 132 """Object disposal"""
133 133 self.close()
134 134
135 135 #-------------------------------------------------------------------------
136 136 # Properties
137 137 #-------------------------------------------------------------------------
138 138
139 139 def open(self):
140 140 """Open a comm to the frontend if one isn't already open."""
141 141 if self.comm is None:
142 if self._model_id is None:
143 self.comm = Comm(target_name=self._model_name)
144 self._model_id = self.model_id
145 else:
146 self.comm = Comm(target_name=self._model_name, comm_id=self._model_id)
142 args = dict(target_name='ipython.widget', data={ 'model_name': self._model_name })
143 if self._model_id is not None:
144 args['comm_id'] = self._model_id
145 self.comm = Comm(**args)
146 self._model_id = self.model_id
147
147 148 self.comm.on_msg(self._handle_msg)
148 149 Widget.widgets[self.model_id] = self
149 150
150 151 # first update
151 152 self.send_state()
152 153
153 154 @property
154 155 def model_id(self):
155 156 """Gets the model id of this widget.
156 157
157 158 If a Comm doesn't exist yet, a Comm will be created automagically."""
158 159 return self.comm.comm_id
159 160
160 161 #-------------------------------------------------------------------------
161 162 # Methods
162 163 #-------------------------------------------------------------------------
163 164
164 165 def close(self):
165 166 """Close method.
166 167
167 168 Closes the underlying comm.
168 169 When the comm is closed, all of the widget views are automatically
169 170 removed from the front-end."""
170 171 if self.comm is not None:
171 172 Widget.widgets.pop(self.model_id, None)
172 173 self.comm.close()
173 174 self.comm = None
174 175
175 176 def send_state(self, key=None):
176 177 """Sends the widget state, or a piece of it, to the front-end.
177 178
178 179 Parameters
179 180 ----------
180 181 key : unicode, or iterable (optional)
181 182 A single property's name or iterable of property names to sync with the front-end.
182 183 """
183 184 self._send({
184 185 "method" : "update",
185 186 "state" : self.get_state(key=key)
186 187 })
187 188
188 189 def get_state(self, key=None):
189 190 """Gets the widget state, or a piece of it.
190 191
191 192 Parameters
192 193 ----------
193 194 key : unicode or iterable (optional)
194 195 A single property's name or iterable of property names to get.
195 196 """
196 197 if key is None:
197 198 keys = self.keys
198 199 elif isinstance(key, string_types):
199 200 keys = [key]
200 201 elif isinstance(key, collections.Iterable):
201 202 keys = key
202 203 else:
203 204 raise ValueError("key must be a string, an iterable of keys, or None")
204 205 state = {}
205 206 for k in keys:
206 207 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
207 208 value = getattr(self, k)
208 209 state[k] = f(value)
209 210 return state
210 211
211 212 def set_state(self, sync_data):
212 213 """Called when a state is received from the front-end."""
213 214 for name in self.keys:
214 215 if name in sync_data:
215 216 json_value = sync_data[name]
216 217 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
217 218 with self._lock_property(name, json_value):
218 219 setattr(self, name, from_json(json_value))
219 220
220 221 def send(self, content):
221 222 """Sends a custom msg to the widget model in the front-end.
222 223
223 224 Parameters
224 225 ----------
225 226 content : dict
226 227 Content of the message to send.
227 228 """
228 229 self._send({"method": "custom", "content": content})
229 230
230 231 def on_msg(self, callback, remove=False):
231 232 """(Un)Register a custom msg receive callback.
232 233
233 234 Parameters
234 235 ----------
235 236 callback: callable
236 237 callback will be passed two arguments when a message arrives::
237 238
238 239 callback(widget, content)
239 240
240 241 remove: bool
241 242 True if the callback should be unregistered."""
242 243 self._msg_callbacks.register_callback(callback, remove=remove)
243 244
244 245 def on_displayed(self, callback, remove=False):
245 246 """(Un)Register a widget displayed callback.
246 247
247 248 Parameters
248 249 ----------
249 250 callback: method handler
250 251 Must have a signature of::
251 252
252 253 callback(widget, **kwargs)
253 254
254 255 kwargs from display are passed through without modification.
255 256 remove: bool
256 257 True if the callback should be unregistered."""
257 258 self._display_callbacks.register_callback(callback, remove=remove)
258 259
259 260 #-------------------------------------------------------------------------
260 261 # Support methods
261 262 #-------------------------------------------------------------------------
262 263 @contextmanager
263 264 def _lock_property(self, key, value):
264 265 """Lock a property-value pair.
265 266
266 267 The value should be the JSON state of the property.
267 268
268 269 NOTE: This, in addition to the single lock for all state changes, is
269 270 flawed. In the future we may want to look into buffering state changes
270 271 back to the front-end."""
271 272 self._property_lock = (key, value)
272 273 try:
273 274 yield
274 275 finally:
275 276 self._property_lock = (None, None)
276 277
277 278 @contextmanager
278 279 def hold_sync(self):
279 280 """Hold syncing any state until the context manager is released"""
280 281 # We increment a value so that this can be nested. Syncing will happen when
281 282 # all levels have been released.
282 283 self._send_state_lock += 1
283 284 try:
284 285 yield
285 286 finally:
286 287 self._send_state_lock -=1
287 288 if self._send_state_lock == 0:
288 289 self.send_state(self._states_to_send)
289 290 self._states_to_send.clear()
290 291
291 292 def _should_send_property(self, key, value):
292 293 """Check the property lock (property_lock)"""
293 294 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
294 295 if (key == self._property_lock[0]
295 296 and to_json(value) == self._property_lock[1]):
296 297 return False
297 298 elif self._send_state_lock > 0:
298 299 self._states_to_send.add(key)
299 300 return False
300 301 else:
301 302 return True
302 303
303 304 # Event handlers
304 305 @_show_traceback
305 306 def _handle_msg(self, msg):
306 307 """Called when a msg is received from the front-end"""
307 308 data = msg['content']['data']
308 309 method = data['method']
309 310 if not method in ['backbone', 'custom']:
310 311 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
311 312
312 313 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
313 314 if method == 'backbone' and 'sync_data' in data:
314 315 sync_data = data['sync_data']
315 316 self.set_state(sync_data) # handles all methods
316 317
317 318 # Handle a custom msg from the front-end
318 319 elif method == 'custom':
319 320 if 'content' in data:
320 321 self._handle_custom_msg(data['content'])
321 322
322 323 def _handle_custom_msg(self, content):
323 324 """Called when a custom msg is received."""
324 325 self._msg_callbacks(self, content)
325 326
326 327 def _notify_trait(self, name, old_value, new_value):
327 328 """Called when a property has been changed."""
328 329 # Trigger default traitlet callback machinery. This allows any user
329 330 # registered validation to be processed prior to allowing the widget
330 331 # machinery to handle the state.
331 332 LoggingConfigurable._notify_trait(self, name, old_value, new_value)
332 333
333 334 # Send the state after the user registered callbacks for trait changes
334 335 # have all fired (allows for user to validate values).
335 336 if self.comm is not None and name in self.keys:
336 337 # Make sure this isn't information that the front-end just sent us.
337 338 if self._should_send_property(name, new_value):
338 339 # Send new state to front-end
339 340 self.send_state(key=name)
340 341
341 342 def _handle_displayed(self, **kwargs):
342 343 """Called when a view has been displayed for this widget instance"""
343 344 self._display_callbacks(self, **kwargs)
344 345
345 346 def _trait_to_json(self, x):
346 347 """Convert a trait value to json
347 348
348 349 Traverse lists/tuples and dicts and serialize their values as well.
349 350 Replace any widgets with their model_id
350 351 """
351 352 if isinstance(x, dict):
352 353 return {k: self._trait_to_json(v) for k, v in x.items()}
353 354 elif isinstance(x, (list, tuple)):
354 355 return [self._trait_to_json(v) for v in x]
355 356 elif isinstance(x, Widget):
356 357 return "IPY_MODEL_" + x.model_id
357 358 else:
358 359 return x # Value must be JSON-able
359 360
360 361 def _trait_from_json(self, x):
361 362 """Convert json values to objects
362 363
363 364 Replace any strings representing valid model id values to Widget references.
364 365 """
365 366 if isinstance(x, dict):
366 367 return {k: self._trait_from_json(v) for k, v in x.items()}
367 368 elif isinstance(x, (list, tuple)):
368 369 return [self._trait_from_json(v) for v in x]
369 370 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
370 371 # we want to support having child widgets at any level in a hierarchy
371 372 # trusting that a widget UUID will not appear out in the wild
372 373 return Widget.widgets[x[10:]]
373 374 else:
374 375 return x
375 376
376 377 def _ipython_display_(self, **kwargs):
377 378 """Called when `IPython.display.display` is called on the widget."""
378 379 # Show view.
379 380 if self._view_name is not None:
380 381 self._send({"method": "display"})
381 382 self._handle_displayed(**kwargs)
382 383
383 384 def _send(self, msg):
384 385 """Sends a message to the model in the front-end."""
385 386 self.comm.send(msg)
386 387
387 388
388 389 class DOMWidget(Widget):
389 390 visible = Bool(True, help="Whether the widget is visible.", sync=True)
390 391 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
391 392 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
392 393
393 394 width = CUnicode(sync=True)
394 395 height = CUnicode(sync=True)
395 396 padding = CUnicode(sync=True)
396 397 margin = CUnicode(sync=True)
397 398
398 399 color = Unicode(sync=True)
399 400 background_color = Unicode(sync=True)
400 401 border_color = Unicode(sync=True)
401 402
402 403 border_width = CUnicode(sync=True)
403 404 border_radius = CUnicode(sync=True)
404 405 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
405 406 'none',
406 407 'hidden',
407 408 'dotted',
408 409 'dashed',
409 410 'solid',
410 411 'double',
411 412 'groove',
412 413 'ridge',
413 414 'inset',
414 415 'outset',
415 416 'initial',
416 417 'inherit', ''],
417 418 default_value='', sync=True)
418 419
419 420 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
420 421 'normal',
421 422 'italic',
422 423 'oblique',
423 424 'initial',
424 425 'inherit', ''],
425 426 default_value='', sync=True)
426 427 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
427 428 'normal',
428 429 'bold',
429 430 'bolder',
430 431 'lighter',
431 432 'initial',
432 433 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
433 434 default_value='', sync=True)
434 435 font_size = CUnicode(sync=True)
435 436 font_family = Unicode(sync=True)
436 437
437 438 def __init__(self, *pargs, **kwargs):
438 439 super(DOMWidget, self).__init__(*pargs, **kwargs)
439 440
440 441 def _validate_border(name, old, new):
441 442 if new is not None and new != '':
442 443 if name != 'border_width' and not self.border_width:
443 444 self.border_width = 1
444 445 if name != 'border_style' and self.border_style == '':
445 446 self.border_style = 'solid'
446 447 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
General Comments 0
You need to be logged in to leave comments. Login now