##// END OF EJS Templates
Fixed a bug that didn't allow callbacks to set a property...
Jonathan Frederic -
Show More
@@ -1,158 +1,154 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // StringWidget
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["notebook/js/widget"], function(widget_manager){
18 18 var StringWidgetModel = IPython.WidgetModel.extend({});
19 19 widget_manager.register_widget_model('StringWidgetModel', StringWidgetModel);
20 20
21 21 var LabelView = IPython.WidgetView.extend({
22 22
23 23 // Called when view is rendered.
24 24 render : function(){
25 25 this.$el = $('<div />');
26 26 this.update(); // Set defaults.
27 27 },
28 28
29 29 // Handles: Backend -> Frontend Sync
30 30 // Frontent -> Frontend Sync
31 31 update : function(){
32 32 this.$el.html(this.model.get('value'));
33 33 return IPython.WidgetView.prototype.update.call(this);
34 34 },
35 35
36 36 });
37 37
38 38 widget_manager.register_widget_view('LabelView', LabelView);
39 39
40 40 var TextAreaView = IPython.WidgetView.extend({
41 41
42 42 // Called when view is rendered.
43 43 render : function(){
44 44 this.$el
45 45 .addClass('widget-hbox')
46 46 .html('');
47 47 this.$label = $('<div />')
48 48 .appendTo(this.$el)
49 49 .addClass('widget-hlabel')
50 50 .hide();
51 51 this.$textbox = $('<textarea />')
52 52 .attr('rows', 5)
53 53 .addClass('widget-text')
54 54 .appendTo(this.$el);
55 55 this.$el_to_style = this.$textbox; // Set default element to style
56 56 this.update(); // Set defaults.
57 57 },
58 58
59 59 // Handles: Backend -> Frontend Sync
60 60 // Frontent -> Frontend Sync
61 61 update : function(){
62 62 if (!this.user_invoked_update) {
63 63 this.$textbox.val(this.model.get('value'));
64 64 }
65 65
66 66 var disabled = this.model.get('disabled');
67 67 this.$textbox.prop('disabled', disabled);
68 68
69 69 var description = this.model.get('description');
70 70 if (description.length == 0) {
71 71 this.$label.hide();
72 72 } else {
73 73 this.$label.html(description);
74 74 this.$label.show();
75 75 }
76 76 return IPython.WidgetView.prototype.update.call(this);
77 77 },
78 78
79 79 events: {"keyup textarea" : "handleChanging",
80 80 "paste textarea" : "handleChanging",
81 81 "cut textarea" : "handleChanging"},
82 82
83 83 // Handles and validates user input.
84 84 handleChanging: function(e) {
85 85 this.user_invoked_update = true;
86 86 this.model.set('value', e.target.value);
87 87 this.model.update_other_views(this);
88 88 this.user_invoked_update = false;
89 89 },
90 90 });
91 91
92 92 widget_manager.register_widget_view('TextAreaView', TextAreaView);
93 93
94 94 var TextBoxView = IPython.WidgetView.extend({
95 95
96 96 // Called when view is rendered.
97 97 render : function(){
98 98 this.$el
99 99 .addClass('widget-hbox-single')
100 100 .html('');
101 101 this.$label = $('<div />')
102 102 .addClass('widget-hlabel')
103 103 .appendTo(this.$el)
104 104 .hide();
105 105 this.$textbox = $('<input type="text" />')
106 106 .addClass('input')
107 107 .addClass('widget-text')
108 108 .appendTo(this.$el);
109 109 this.$el_to_style = this.$textbox; // Set default element to style
110 110 this.update(); // Set defaults.
111 111 },
112 112
113 113 // Handles: Backend -> Frontend Sync
114 114 // Frontent -> Frontend Sync
115 115 update : function(){
116 if (!this.user_invoked_update) {
116 if (this.$textbox.val() != this.model.get('value')) {
117 117 this.$textbox.val(this.model.get('value'));
118 118 }
119 119
120 120 var disabled = this.model.get('disabled');
121 121 this.$textbox.prop('disabled', disabled);
122 122
123 123 var description = this.model.get('description');
124 124 if (description.length == 0) {
125 125 this.$label.hide();
126 126 } else {
127 127 this.$label.html(description);
128 128 this.$label.show();
129 129 }
130 130 return IPython.WidgetView.prototype.update.call(this);
131 131 },
132 132
133 133 events: {"keyup input" : "handleChanging",
134 134 "paste input" : "handleChanging",
135 135 "cut input" : "handleChanging",
136 136 "keypress input" : "handleKeypress"},
137 137
138 138 // Handles and validates user input.
139 139 handleChanging: function(e) {
140 this.user_invoked_update = true;
141 140 this.model.set('value', e.target.value);
142 141 this.model.update_other_views(this);
143 this.user_invoked_update = false;
144 142 },
145 143
146 144 // Handles text submition
147 145 handleKeypress: function(e) {
148 146 if (e.keyCode == 13) { // Return key
149 this.user_invoked_update = true;
150 147 this.model.set('submits', this.model.get('submits') + 1);
151 148 this.model.update_other_views(this);
152 this.user_invoked_update = false;
153 149 }
154 150 },
155 151 });
156 152
157 153 widget_manager.register_widget_view('TextBoxView', TextBoxView);
158 154 });
@@ -1,415 +1,416 b''
1 1 """Base Widget class. Allows user to create widgets in the backend that render
2 2 in the IPython notebook frontend.
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 copy import copy
16 16 from glob import glob
17 17 import uuid
18 18 import sys
19 19 import os
20 20 import inspect
21 21 import types
22 22
23 23 import IPython
24 24 from IPython.kernel.comm import Comm
25 25 from IPython.config import LoggingConfigurable
26 26 from IPython.utils.traitlets import Unicode, Dict, List, Instance, Bool
27 27 from IPython.display import Javascript, display
28 28 from IPython.utils.py3compat import string_types
29 29
30 30
31 31 #-----------------------------------------------------------------------------
32 32 # Classes
33 33 #-----------------------------------------------------------------------------
34 34 class Widget(LoggingConfigurable):
35 35
36 36 # Shared declarations
37 37 _keys = []
38 38
39 39 # Public declarations
40 40 target_name = Unicode('widget', help="""Name of the backbone model
41 41 registered in the frontend to create and sync this widget with.""")
42 42 default_view_name = Unicode(help="""Default view registered in the frontend
43 43 to use to represent the widget.""")
44 44 parent = Instance('IPython.html.widgets.widget.Widget')
45 45 visible = Bool(True, help="Whether or not the widget is visible.")
46 46
47 47 def _parent_changed(self, name, old, new):
48 48 if self._displayed:
49 49 raise Exception('Parent cannot be set because widget has been displayed.')
50 50 elif new == self:
51 51 raise Exception('Parent cannot be set to self.')
52 52 else:
53 53
54 54 # Parent/child association
55 55 if new is not None and not self in new._children:
56 56 new._children.append(self)
57 57 if old is not None and self in old._children:
58 58 old._children.remove(self)
59 59
60 60 # Private/protected declarations
61 _property_lock = False
61 _property_lock = (None, None) # Last updated (key, value) from the front-end. Prevents echo.
62 62 _css = Dict() # Internal CSS property dict
63 63 _add_class = List() # Used to add a js class to a DOM element (call#, selector, class_name)
64 64 _remove_class = List() # Used to remove a js class from a DOM element (call#, selector, class_name)
65 65 _displayed = False
66 66 _comm = None
67 67
68 68
69 69 def __init__(self, **kwargs):
70 70 """Public constructor
71 71
72 72 Parameters
73 73 ----------
74 74 parent : Widget instance (optional)
75 75 Widget that this widget instance is child of. When the widget is
76 76 displayed in the frontend, it's corresponding view will be made
77 77 child of the parent's view if the parent's view exists already. If
78 78 the parent's view is displayed, it will automatically display this
79 79 widget's default view as it's child. The default view can be set
80 80 via the default_view_name property.
81 81 """
82 82 self._children = []
83 83 self._add_class = [0]
84 84 self._remove_class = [0]
85 85 self._display_callbacks = []
86 86 self._msg_callbacks = []
87 87 super(Widget, self).__init__(**kwargs)
88 88
89 89 # Register after init to allow default values to be specified
90 90 self.on_trait_change(self._handle_property_changed, self.keys)
91 91
92 92
93 93 def __del__(self):
94 94 """Object disposal"""
95 95 self.close()
96 96
97 97
98 98 def close(self):
99 99 """Close method. Closes the widget which closes the underlying comm.
100 100 When the comm is closed, all of the widget views are automatically
101 101 removed from the frontend."""
102 102 try:
103 103 self._comm.close()
104 104 del self._comm
105 105 except:
106 106 pass # Comm doesn't exist and/or is already closed.
107 107
108 108
109 109 # Properties
110 110 def _get_keys(self):
111 111 keys = ['visible', '_css', '_add_class', '_remove_class']
112 112 keys.extend(self._keys)
113 113 return keys
114 114 keys = property(_get_keys)
115 115
116 116
117 117 # Event handlers
118 118 def _handle_msg(self, msg):
119 119 """Called when a msg is recieved from the frontend"""
120 120 data = msg['content']['data']
121 121
122 122 # Handle backbone sync methods CREATE, PATCH, and UPDATE
123 123 if 'sync_method' in data and 'sync_data' in data:
124 124 sync_method = data['sync_method']
125 125 sync_data = data['sync_data']
126 126 self._handle_recieve_state(sync_data) # handles all methods
127 127
128 128 # Handle a custom msg from the front-end
129 129 if 'custom_content' in data:
130 130 self._handle_custom_msg(data['custom_content'])
131 131
132 132
133 133 def _handle_custom_msg(self, content):
134 134 """Called when a custom msg is recieved."""
135 135 for handler in self._msg_callbacks:
136 136 if callable(handler):
137 137 argspec = inspect.getargspec(handler)
138 138 nargs = len(argspec[0])
139 139
140 140 # Bound methods have an additional 'self' argument
141 141 if isinstance(handler, types.MethodType):
142 142 nargs -= 1
143 143
144 144 # Call the callback
145 145 if nargs == 1:
146 146 handler(content)
147 147 elif nargs == 2:
148 148 handler(self, content)
149 149 else:
150 150 raise TypeError('Widget msg callback must ' \
151 151 'accept 1 or 2 arguments, not %d.' % nargs)
152 152
153 153
154 154 def _handle_recieve_state(self, sync_data):
155 155 """Called when a state is recieved from the frontend."""
156 self._property_lock = True
157 try:
158
159 # Use _keys instead of keys - Don't get retrieve the css from the client side.
160 for name in self._keys:
161 if name in sync_data:
156 # Use _keys instead of keys - Don't get retrieve the css from the client side.
157 for name in self._keys:
158 if name in sync_data:
159 try:
160 self._property_lock = (name, sync_data[name])
162 161 setattr(self, name, sync_data[name])
163 finally:
164 self._property_lock = False
162 finally:
163 self._property_lock = (None, None)
165 164
166 165
167 166 def _handle_property_changed(self, name, old, new):
168 167 """Called when a proeprty has been changed."""
169 if not self._property_lock and self._comm is not None:
168 # Make sure this isn't information that the front-end just sent us.
169 if self._property_lock[0] != name and self._property_lock[1] != new \
170 and self._comm is not None:
170 171 # TODO: Validate properties.
171 172 # Send new state to frontend
172 173 self.send_state(key=name)
173 174
174 175
175 176 def _handle_close(self):
176 177 """Called when the comm is closed by the frontend."""
177 178 self._comm = None
178 179
179 180
180 181 def _handle_displayed(self, view_name):
181 182 """Called when a view has been displayed for this widget instance
182 183
183 184 Parameters
184 185 ----------
185 186 view_name: unicode
186 187 Name of the view that was displayed."""
187 188 for handler in self._display_callbacks:
188 189 if callable(handler):
189 190 argspec = inspect.getargspec(handler)
190 191 nargs = len(argspec[0])
191 192
192 193 # Bound methods have an additional 'self' argument
193 194 if isinstance(handler, types.MethodType):
194 195 nargs -= 1
195 196
196 197 # Call the callback
197 198 if nargs == 0:
198 199 handler()
199 200 elif nargs == 1:
200 201 handler(self)
201 202 elif nargs == 2:
202 203 handler(self, view_name)
203 204 else:
204 205 raise TypeError('Widget display callback must ' \
205 206 'accept 0-2 arguments, not %d.' % nargs)
206 207
207 208
208 209 # Public methods
209 210 def send_state(self, key=None):
210 211 """Sends the widget state, or a piece of it, to the frontend.
211 212
212 213 Parameters
213 214 ----------
214 215 key : unicode (optional)
215 216 A single property's name to sync with the frontend.
216 217 """
217 218 if self._comm is not None:
218 219 state = {}
219 220
220 221 # If a key is provided, just send the state of that key.
221 222 keys = []
222 223 if key is None:
223 224 keys.extend(self.keys)
224 225 else:
225 226 keys.append(key)
226 227 for key in self.keys:
227 228 try:
228 229 state[key] = getattr(self, key)
229 230 except Exception as e:
230 231 pass # Eat errors, nom nom nom
231 232 self._comm.send({"method": "update",
232 233 "state": state})
233 234
234 235
235 236 def get_css(self, key, selector=""):
236 237 """Get a CSS property of the widget. Note, this function does not
237 238 actually request the CSS from the front-end; Only properties that have
238 239 been set with set_css can be read.
239 240
240 241 Parameters
241 242 ----------
242 243 key: unicode
243 244 CSS key
244 245 selector: unicode (optional)
245 246 JQuery selector used when the CSS key/value was set.
246 247 """
247 248 if selector in self._css and key in self._css[selector]:
248 249 return self._css[selector][key]
249 250 else:
250 251 return None
251 252
252 253
253 254 def set_css(self, *args, **kwargs):
254 255 """Set one or more CSS properties of the widget (shared among all of the
255 256 views). This function has two signatures:
256 257 - set_css(css_dict, [selector=''])
257 258 - set_css(key, value, [selector=''])
258 259
259 260 Parameters
260 261 ----------
261 262 css_dict : dict
262 263 CSS key/value pairs to apply
263 264 key: unicode
264 265 CSS key
265 266 value
266 267 CSS value
267 268 selector: unicode (optional)
268 269 JQuery selector to use to apply the CSS key/value.
269 270 """
270 271 selector = kwargs.get('selector', '')
271 272
272 273 # Signature 1: set_css(css_dict, [selector=''])
273 274 if len(args) == 1:
274 275 if isinstance(args[0], dict):
275 276 for (key, value) in args[0].items():
276 277 self.set_css(key, value, selector=selector)
277 278 else:
278 279 raise Exception('css_dict must be a dict.')
279 280
280 281 # Signature 2: set_css(key, value, [selector=''])
281 282 elif len(args) == 2 or len(args) == 3:
282 283
283 284 # Selector can be a positional arg if it's the 3rd value
284 285 if len(args) == 3:
285 286 selector = args[2]
286 287 if selector not in self._css:
287 288 self._css[selector] = {}
288 289
289 290 # Only update the property if it has changed.
290 291 key = args[0]
291 292 value = args[1]
292 293 if not (key in self._css[selector] and value in self._css[selector][key]):
293 294 self._css[selector][key] = value
294 295 self.send_state('_css') # Send new state to client.
295 296 else:
296 297 raise Exception('set_css only accepts 1-3 arguments')
297 298
298 299
299 300 def add_class(self, class_name, selector=""):
300 301 """Add class[es] to a DOM element
301 302
302 303 Parameters
303 304 ----------
304 305 class_name: unicode
305 306 Class name(s) to add to the DOM element(s). Multiple class names
306 307 must be space separated.
307 308 selector: unicode (optional)
308 309 JQuery selector to select the DOM element(s) that the class(es) will
309 310 be added to.
310 311 """
311 312 self._add_class = [self._add_class[0] + 1, selector, class_name]
312 313 self.send_state(key='_add_class')
313 314
314 315
315 316 def remove_class(self, class_name, selector=""):
316 317 """Remove class[es] from a DOM element
317 318
318 319 Parameters
319 320 ----------
320 321 class_name: unicode
321 322 Class name(s) to remove from the DOM element(s). Multiple class
322 323 names must be space separated.
323 324 selector: unicode (optional)
324 325 JQuery selector to select the DOM element(s) that the class(es) will
325 326 be removed from.
326 327 """
327 328 self._remove_class = [self._remove_class[0] + 1, selector, class_name]
328 329 self.send_state(key='_remove_class')
329 330
330 331
331 332 def send(self, content):
332 333 """Sends a custom msg to the widget model in the front-end.
333 334
334 335 Parameters
335 336 ----------
336 337 content : dict
337 338 Content of the message to send.
338 339 """
339 340 if self._comm is not None:
340 341 self._comm.send({"method": "custom",
341 342 "custom_content": content})
342 343
343 344
344 345 def on_msg(self, callback, remove=False):
345 346 """Register a callback for when a custom msg is recieved from the front-end
346 347
347 348 Parameters
348 349 ----------
349 350 callback: method handler
350 351 Can have a signature of:
351 352 - callback(content)
352 353 - callback(sender, content)
353 354 remove: bool
354 355 True if the callback should be unregistered."""
355 356 if remove and callback in self._msg_callbacks:
356 357 self._msg_callbacks.remove(callback)
357 358 elif not remove and not callback in self._msg_callbacks:
358 359 self._msg_callbacks.append(callback)
359 360
360 361
361 362 def on_displayed(self, callback, remove=False):
362 363 """Register a callback to be called when the widget has been displayed
363 364
364 365 Parameters
365 366 ----------
366 367 callback: method handler
367 368 Can have a signature of:
368 369 - callback()
369 370 - callback(sender)
370 371 - callback(sender, view_name)
371 372 remove: bool
372 373 True if the callback should be unregistered."""
373 374 if remove and callback in self._display_callbacks:
374 375 self._display_callbacks.remove(callback)
375 376 elif not remove and not callback in self._display_callbacks:
376 377 self._display_callbacks.append(callback)
377 378
378 379
379 380 # Support methods
380 381 def _repr_widget_(self, view_name=None):
381 382 """Function that is called when `IPython.display.display` is called on
382 383 the widget.
383 384
384 385 Parameters
385 386 ----------
386 387 view_name: unicode (optional)
387 388 View to display in the frontend. Overrides default_view_name."""
388 389
389 390 if not view_name:
390 391 view_name = self.default_view_name
391 392
392 393 # Create a comm.
393 394 if self._comm is None:
394 395 self._comm = Comm(target_name=self.target_name)
395 396 self._comm.on_msg(self._handle_msg)
396 397 self._comm.on_close(self._handle_close)
397 398
398 399 # Make sure model is syncronized
399 400 self.send_state()
400 401
401 402 # Show view.
402 403 if self.parent is None or self.parent._comm is None:
403 404 self._comm.send({"method": "display", "view_name": view_name})
404 405 else:
405 406 self._comm.send({"method": "display",
406 407 "view_name": view_name,
407 408 "parent": self.parent._comm.comm_id})
408 409 self._displayed = True
409 410 self._handle_displayed(view_name)
410 411
411 412 # Now display children if any.
412 413 for child in self._children:
413 414 if child != self:
414 415 child._repr_widget_()
415 416 return None
General Comments 0
You need to be logged in to leave comments. Login now