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