##// END OF EJS Templates
Address problems found in in-person review
Jonathan Frederic -
Show More
@@ -1,576 +1,588 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define(["widgets/js/manager",
4 define(["widgets/js/manager",
5 "underscore",
5 "underscore",
6 "backbone",
6 "backbone",
7 "jquery",
7 "jquery",
8 "base/js/namespace",
8 "base/js/namespace",
9 ], function(widgetmanager, _, Backbone, $, IPython){
9 ], function(widgetmanager, _, Backbone, $, IPython){
10
10
11 var WidgetModel = Backbone.Model.extend({
11 var WidgetModel = Backbone.Model.extend({
12 constructor: function (widget_manager, model_id, comm) {
12 constructor: function (widget_manager, model_id, comm) {
13 // Constructor
13 // Constructor
14 //
14 //
15 // Creates a WidgetModel instance.
15 // Creates a WidgetModel instance.
16 //
16 //
17 // Parameters
17 // Parameters
18 // ----------
18 // ----------
19 // widget_manager : WidgetManager instance
19 // widget_manager : WidgetManager instance
20 // model_id : string
20 // model_id : string
21 // An ID unique to this model.
21 // An ID unique to this model.
22 // comm : Comm instance (optional)
22 // comm : Comm instance (optional)
23 this.widget_manager = widget_manager;
23 this.widget_manager = widget_manager;
24 this._buffered_state_diff = {};
24 this._buffered_state_diff = {};
25 this.pending_msgs = 0;
25 this.pending_msgs = 0;
26 this.msg_buffer = null;
26 this.msg_buffer = null;
27 this.key_value_lock = null;
27 this.key_value_lock = null;
28 this.id = model_id;
28 this.id = model_id;
29 this.views = [];
29 this.views = [];
30
30
31 if (comm !== undefined) {
31 if (comm !== undefined) {
32 // Remember comm associated with the model.
32 // Remember comm associated with the model.
33 this.comm = comm;
33 this.comm = comm;
34 comm.model = this;
34 comm.model = this;
35
35
36 // Hook comm messages up to model.
36 // Hook comm messages up to model.
37 comm.on_close($.proxy(this._handle_comm_closed, this));
37 comm.on_close($.proxy(this._handle_comm_closed, this));
38 comm.on_msg($.proxy(this._handle_comm_msg, this));
38 comm.on_msg($.proxy(this._handle_comm_msg, this));
39 }
39 }
40 return Backbone.Model.apply(this);
40 return Backbone.Model.apply(this);
41 },
41 },
42
42
43 send: function (content, callbacks) {
43 send: function (content, callbacks) {
44 // Send a custom msg over the comm.
44 // Send a custom msg over the comm.
45 if (this.comm !== undefined) {
45 if (this.comm !== undefined) {
46 var data = {method: 'custom', content: content};
46 var data = {method: 'custom', content: content};
47 this.comm.send(data, callbacks);
47 this.comm.send(data, callbacks);
48 this.pending_msgs++;
48 this.pending_msgs++;
49 }
49 }
50 },
50 },
51
51
52 _handle_comm_closed: function (msg) {
52 _handle_comm_closed: function (msg) {
53 // Handle when a widget is closed.
53 // Handle when a widget is closed.
54 this.trigger('comm:close');
54 this.trigger('comm:close');
55 delete this.comm.model; // Delete ref so GC will collect widget model.
55 delete this.comm.model; // Delete ref so GC will collect widget model.
56 delete this.comm;
56 delete this.comm;
57 delete this.model_id; // Delete id from model so widget manager cleans up.
57 delete this.model_id; // Delete id from model so widget manager cleans up.
58 _.each(this.views, function(view, i) {
58 _.each(this.views, function(view, i) {
59 view.remove();
59 view.remove();
60 });
60 });
61 },
61 },
62
62
63 _handle_comm_msg: function (msg) {
63 _handle_comm_msg: function (msg) {
64 // Handle incoming comm msg.
64 // Handle incoming comm msg.
65 var method = msg.content.data.method;
65 var method = msg.content.data.method;
66 switch (method) {
66 switch (method) {
67 case 'update':
67 case 'update':
68 this.apply_update(msg.content.data.state);
68 this.apply_update(msg.content.data.state);
69 break;
69 break;
70 case 'custom':
70 case 'custom':
71 this.trigger('msg:custom', msg.content.data.content);
71 this.trigger('msg:custom', msg.content.data.content);
72 break;
72 break;
73 case 'display':
73 case 'display':
74 this.widget_manager.display_view(msg, this);
74 this.widget_manager.display_view(msg, this);
75 break;
75 break;
76 }
76 }
77 },
77 },
78
78
79 apply_update: function (state) {
79 apply_update: function (state) {
80 // Handle when a widget is updated via the python side.
80 // Handle when a widget is updated via the python side.
81 var that = this;
81 var that = this;
82 _.each(state, function(value, key) {
82 _.each(state, function(value, key) {
83 that.key_value_lock = [key, value];
83 that.key_value_lock = [key, value];
84 try {
84 try {
85 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
85 WidgetModel.__super__.set.apply(that, [key, that._unpack_models(value)]);
86 } finally {
86 } finally {
87 that.key_value_lock = null;
87 that.key_value_lock = null;
88 }
88 }
89 });
89 });
90 },
90 },
91
91
92 _handle_status: function (msg, callbacks) {
92 _handle_status: function (msg, callbacks) {
93 // Handle status msgs.
93 // Handle status msgs.
94
94
95 // execution_state : ('busy', 'idle', 'starting')
95 // execution_state : ('busy', 'idle', 'starting')
96 if (this.comm !== undefined) {
96 if (this.comm !== undefined) {
97 if (msg.content.execution_state ==='idle') {
97 if (msg.content.execution_state ==='idle') {
98 // Send buffer if this message caused another message to be
98 // Send buffer if this message caused another message to be
99 // throttled.
99 // throttled.
100 if (this.msg_buffer !== null &&
100 if (this.msg_buffer !== null &&
101 (this.get('msg_throttle') || 3) === this.pending_msgs) {
101 (this.get('msg_throttle') || 3) === this.pending_msgs) {
102 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
102 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
103 this.comm.send(data, callbacks);
103 this.comm.send(data, callbacks);
104 this.msg_buffer = null;
104 this.msg_buffer = null;
105 } else {
105 } else {
106 --this.pending_msgs;
106 --this.pending_msgs;
107 }
107 }
108 }
108 }
109 }
109 }
110 },
110 },
111
111
112 callbacks: function(view) {
112 callbacks: function(view) {
113 // Create msg callbacks for a comm msg.
113 // Create msg callbacks for a comm msg.
114 var callbacks = this.widget_manager.callbacks(view);
114 var callbacks = this.widget_manager.callbacks(view);
115
115
116 if (callbacks.iopub === undefined) {
116 if (callbacks.iopub === undefined) {
117 callbacks.iopub = {};
117 callbacks.iopub = {};
118 }
118 }
119
119
120 var that = this;
120 var that = this;
121 callbacks.iopub.status = function (msg) {
121 callbacks.iopub.status = function (msg) {
122 that._handle_status(msg, callbacks);
122 that._handle_status(msg, callbacks);
123 };
123 };
124 return callbacks;
124 return callbacks;
125 },
125 },
126
126
127 set: function(key, val, options) {
127 set: function(key, val, options) {
128 // Set a value.
128 // Set a value.
129 var return_value = WidgetModel.__super__.set.apply(this, arguments);
129 var return_value = WidgetModel.__super__.set.apply(this, arguments);
130
130
131 // Backbone only remembers the diff of the most recent set()
131 // Backbone only remembers the diff of the most recent set()
132 // operation. Calling set multiple times in a row results in a
132 // operation. Calling set multiple times in a row results in a
133 // loss of diff information. Here we keep our own running diff.
133 // loss of diff information. Here we keep our own running diff.
134 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
134 this._buffered_state_diff = $.extend(this._buffered_state_diff, this.changedAttributes() || {});
135 return return_value;
135 return return_value;
136 },
136 },
137
137
138 sync: function (method, model, options) {
138 sync: function (method, model, options) {
139 // Handle sync to the back-end. Called when a model.save() is called.
139 // Handle sync to the back-end. Called when a model.save() is called.
140
140
141 // Make sure a comm exists.
141 // Make sure a comm exists.
142 var error = options.error || function() {
142 var error = options.error || function() {
143 console.error('Backbone sync error:', arguments);
143 console.error('Backbone sync error:', arguments);
144 };
144 };
145 if (this.comm === undefined) {
145 if (this.comm === undefined) {
146 error();
146 error();
147 return false;
147 return false;
148 }
148 }
149
149
150 // Delete any key value pairs that the back-end already knows about.
150 // Delete any key value pairs that the back-end already knows about.
151 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
151 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
152 if (this.key_value_lock !== null) {
152 if (this.key_value_lock !== null) {
153 var key = this.key_value_lock[0];
153 var key = this.key_value_lock[0];
154 var value = this.key_value_lock[1];
154 var value = this.key_value_lock[1];
155 if (attrs[key] === value) {
155 if (attrs[key] === value) {
156 delete attrs[key];
156 delete attrs[key];
157 }
157 }
158 }
158 }
159
159
160 // Only sync if there are attributes to send to the back-end.
160 // Only sync if there are attributes to send to the back-end.
161 attrs = this._pack_models(attrs);
161 attrs = this._pack_models(attrs);
162 if (_.size(attrs) > 0) {
162 if (_.size(attrs) > 0) {
163
163
164 // If this message was sent via backbone itself, it will not
164 // If this message was sent via backbone itself, it will not
165 // have any callbacks. It's important that we create callbacks
165 // have any callbacks. It's important that we create callbacks
166 // so we can listen for status messages, etc...
166 // so we can listen for status messages, etc...
167 var callbacks = options.callbacks || this.callbacks();
167 var callbacks = options.callbacks || this.callbacks();
168
168
169 // Check throttle.
169 // Check throttle.
170 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
170 if (this.pending_msgs >= (this.get('msg_throttle') || 3)) {
171 // The throttle has been exceeded, buffer the current msg so
171 // The throttle has been exceeded, buffer the current msg so
172 // it can be sent once the kernel has finished processing
172 // it can be sent once the kernel has finished processing
173 // some of the existing messages.
173 // some of the existing messages.
174
174
175 // Combine updates if it is a 'patch' sync, otherwise replace updates
175 // Combine updates if it is a 'patch' sync, otherwise replace updates
176 switch (method) {
176 switch (method) {
177 case 'patch':
177 case 'patch':
178 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
178 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
179 break;
179 break;
180 case 'update':
180 case 'update':
181 case 'create':
181 case 'create':
182 this.msg_buffer = attrs;
182 this.msg_buffer = attrs;
183 break;
183 break;
184 default:
184 default:
185 error();
185 error();
186 return false;
186 return false;
187 }
187 }
188 this.msg_buffer_callbacks = callbacks;
188 this.msg_buffer_callbacks = callbacks;
189
189
190 } else {
190 } else {
191 // We haven't exceeded the throttle, send the message like
191 // We haven't exceeded the throttle, send the message like
192 // normal.
192 // normal.
193 var data = {method: 'backbone', sync_data: attrs};
193 var data = {method: 'backbone', sync_data: attrs};
194 this.comm.send(data, callbacks);
194 this.comm.send(data, callbacks);
195 this.pending_msgs++;
195 this.pending_msgs++;
196 }
196 }
197 }
197 }
198 // Since the comm is a one-way communication, assume the message
198 // Since the comm is a one-way communication, assume the message
199 // arrived. Don't call success since we don't have a model back from the server
199 // arrived. Don't call success since we don't have a model back from the server
200 // this means we miss out on the 'sync' event.
200 // this means we miss out on the 'sync' event.
201 this._buffered_state_diff = {};
201 this._buffered_state_diff = {};
202 },
202 },
203
203
204 save_changes: function(callbacks) {
204 save_changes: function(callbacks) {
205 // Push this model's state to the back-end
205 // Push this model's state to the back-end
206 //
206 //
207 // This invokes a Backbone.Sync.
207 // This invokes a Backbone.Sync.
208 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
208 this.save(this._buffered_state_diff, {patch: true, callbacks: callbacks});
209 },
209 },
210
210
211 _pack_models: function(value) {
211 _pack_models: function(value) {
212 // Replace models with model ids recursively.
212 // Replace models with model ids recursively.
213 var that = this;
213 var that = this;
214 var packed;
214 var packed;
215 if (value instanceof Backbone.Model) {
215 if (value instanceof Backbone.Model) {
216 return "IPY_MODEL_" + value.id;
216 return "IPY_MODEL_" + value.id;
217
217
218 } else if ($.isArray(value)) {
218 } else if ($.isArray(value)) {
219 packed = [];
219 packed = [];
220 _.each(value, function(sub_value, key) {
220 _.each(value, function(sub_value, key) {
221 packed.push(that._pack_models(sub_value));
221 packed.push(that._pack_models(sub_value));
222 });
222 });
223 return packed;
223 return packed;
224
224
225 } else if (value instanceof Object) {
225 } else if (value instanceof Object) {
226 packed = {};
226 packed = {};
227 _.each(value, function(sub_value, key) {
227 _.each(value, function(sub_value, key) {
228 packed[key] = that._pack_models(sub_value);
228 packed[key] = that._pack_models(sub_value);
229 });
229 });
230 return packed;
230 return packed;
231
231
232 } else {
232 } else {
233 return value;
233 return value;
234 }
234 }
235 },
235 },
236
236
237 _unpack_models: function(value) {
237 _unpack_models: function(value) {
238 // Replace model ids with models recursively.
238 // Replace model ids with models recursively.
239 var that = this;
239 var that = this;
240 var unpacked;
240 var unpacked;
241 if ($.isArray(value)) {
241 if ($.isArray(value)) {
242 unpacked = [];
242 unpacked = [];
243 _.each(value, function(sub_value, key) {
243 _.each(value, function(sub_value, key) {
244 unpacked.push(that._unpack_models(sub_value));
244 unpacked.push(that._unpack_models(sub_value));
245 });
245 });
246 return unpacked;
246 return unpacked;
247
247
248 } else if (value instanceof Object) {
248 } else if (value instanceof Object) {
249 unpacked = {};
249 unpacked = {};
250 _.each(value, function(sub_value, key) {
250 _.each(value, function(sub_value, key) {
251 unpacked[key] = that._unpack_models(sub_value);
251 unpacked[key] = that._unpack_models(sub_value);
252 });
252 });
253 return unpacked;
253 return unpacked;
254
254
255 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
255 } else if (typeof value === 'string' && value.slice(0,10) === "IPY_MODEL_") {
256 var model = this.widget_manager.get_model(value.slice(10, value.length));
256 var model = this.widget_manager.get_model(value.slice(10, value.length));
257 if (model) {
257 if (model) {
258 return model;
258 return model;
259 } else {
259 } else {
260 return value;
260 return value;
261 }
261 }
262 } else {
262 } else {
263 return value;
263 return value;
264 }
264 }
265 },
265 },
266
266
267 });
267 });
268 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
268 widgetmanager.WidgetManager.register_widget_model('WidgetModel', WidgetModel);
269
269
270
270
271 var WidgetView = Backbone.View.extend({
271 var WidgetView = Backbone.View.extend({
272 initialize: function(parameters) {
272 initialize: function(parameters) {
273 // Public constructor.
273 // Public constructor.
274 this.model.on('change',this.update,this);
274 this.model.on('change',this.update,this);
275 this.options = parameters.options;
275 this.options = parameters.options;
276 this.child_model_views = {};
276 this.child_model_views = {};
277 this.child_views = {};
277 this.child_views = {};
278 this.model.views.push(this);
278 this.model.views.push(this);
279 this.id = this.id || IPython.utils.uuid();
279 this.id = this.id || IPython.utils.uuid();
280 this.on('displayed', function() {
280 this.on('displayed', function() {
281 this.is_displayed = true;
281 this.is_displayed = true;
282 }, this);
282 }, this);
283 },
283 },
284
284
285 update: function(){
285 update: function(){
286 // Triggered on model change.
286 // Triggered on model change.
287 //
287 //
288 // Update view to be consistent with this.model
288 // Update view to be consistent with this.model
289 },
289 },
290
290
291 create_child_view: function(child_model, options) {
291 create_child_view: function(child_model, options) {
292 // Create and return a child view.
292 // Create and return a child view.
293 //
293 //
294 // -given a model and (optionally) a view name if the view name is
294 // -given a model and (optionally) a view name if the view name is
295 // not given, it defaults to the model's default view attribute.
295 // not given, it defaults to the model's default view attribute.
296
296
297 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
297 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
298 // it would be great to have the widget manager add the cell metadata
298 // it would be great to have the widget manager add the cell metadata
299 // to the subview without having to add it here.
299 // to the subview without having to add it here.
300 options = $.extend({ parent: this }, options || {});
300 options = $.extend({ parent: this }, options || {});
301 var child_view = this.model.widget_manager.create_view(child_model, options, this);
301 var child_view = this.model.widget_manager.create_view(child_model, options, this);
302
302
303 // Associate the view id with the model id.
303 // Associate the view id with the model id.
304 if (this.child_model_views[child_model.id] === undefined) {
304 if (this.child_model_views[child_model.id] === undefined) {
305 this.child_model_views[child_model.id] = [];
305 this.child_model_views[child_model.id] = [];
306 }
306 }
307 this.child_model_views[child_model.id].push(child_view.id);
307 this.child_model_views[child_model.id].push(child_view.id);
308
308
309 // Remember the view by id.
309 // Remember the view by id.
310 this.child_views[child_view.id] = child_view;
310 this.child_views[child_view.id] = child_view;
311 return child_view;
311 return child_view;
312 },
312 },
313
313
314 pop_child_view: function(child_model) {
314 pop_child_view: function(child_model) {
315 // Delete a child view that was previously created using create_child_view.
315 // Delete a child view that was previously created using create_child_view.
316 var view_ids = this.child_model_views[child_model.id];
316 var view_ids = this.child_model_views[child_model.id];
317 if (view_ids !== undefined) {
317 if (view_ids !== undefined) {
318
318
319 // Only delete the first view in the list.
319 // Only delete the first view in the list.
320 var view_id = view_ids[0];
320 var view_id = view_ids[0];
321 var view = this.child_views[view_id];
321 var view = this.child_views[view_id];
322 delete this.child_views[view_id];
322 delete this.child_views[view_id];
323 view_ids.splice(0,1);
323 view_ids.splice(0,1);
324 child_model.views.pop(view);
324 child_model.views.pop(view);
325
325
326 // Remove the view list specific to this model if it is empty.
326 // Remove the view list specific to this model if it is empty.
327 if (view_ids.length === 0) {
327 if (view_ids.length === 0) {
328 delete this.child_model_views[child_model.id];
328 delete this.child_model_views[child_model.id];
329 }
329 }
330 return view;
330 return view;
331 }
331 }
332 return null;
332 return null;
333 },
333 },
334
334
335 do_diff: function(old_list, new_list, removed_callback, added_callback) {
335 do_diff: function(old_list, new_list, removed_callback, added_callback) {
336 // Difference a changed list and call remove and add callbacks for
336 // Difference a changed list and call remove and add callbacks for
337 // each removed and added item in the new list.
337 // each removed and added item in the new list.
338 //
338 //
339 // Parameters
339 // Parameters
340 // ----------
340 // ----------
341 // old_list : array
341 // old_list : array
342 // new_list : array
342 // new_list : array
343 // removed_callback : Callback(item)
343 // removed_callback : Callback(item)
344 // Callback that is called for each item removed.
344 // Callback that is called for each item removed.
345 // added_callback : Callback(item)
345 // added_callback : Callback(item)
346 // Callback that is called for each item added.
346 // Callback that is called for each item added.
347
347
348 // Walk the lists until an unequal entry is found.
348 // Walk the lists until an unequal entry is found.
349 var i;
349 var i;
350 for (i = 0; i < new_list.length; i++) {
350 for (i = 0; i < new_list.length; i++) {
351 if (i >= old_list.length || new_list[i] !== old_list[i]) {
351 if (i >= old_list.length || new_list[i] !== old_list[i]) {
352 break;
352 break;
353 }
353 }
354 }
354 }
355
355
356 // Remove the non-matching items from the old list.
356 // Remove the non-matching items from the old list.
357 for (var j = i; j < old_list.length; j++) {
357 for (var j = i; j < old_list.length; j++) {
358 removed_callback(old_list[j]);
358 removed_callback(old_list[j]);
359 }
359 }
360
360
361 // Add the rest of the new list items.
361 // Add the rest of the new list items.
362 for (; i < new_list.length; i++) {
362 for (; i < new_list.length; i++) {
363 added_callback(new_list[i]);
363 added_callback(new_list[i]);
364 }
364 }
365 },
365 },
366
366
367 callbacks: function(){
367 callbacks: function(){
368 // Create msg callbacks for a comm msg.
368 // Create msg callbacks for a comm msg.
369 return this.model.callbacks(this);
369 return this.model.callbacks(this);
370 },
370 },
371
371
372 render: function(){
372 render: function(){
373 // Render the view.
373 // Render the view.
374 //
374 //
375 // By default, this is only called the first time the view is created
375 // By default, this is only called the first time the view is created
376 },
376 },
377
377
378 show: function(){
378 show: function(){
379 // Show the widget-area
379 // Show the widget-area
380 if (this.options && this.options.cell &&
380 if (this.options && this.options.cell &&
381 this.options.cell.widget_area !== undefined) {
381 this.options.cell.widget_area !== undefined) {
382 this.options.cell.widget_area.show();
382 this.options.cell.widget_area.show();
383 }
383 }
384 },
384 },
385
385
386 send: function (content) {
386 send: function (content) {
387 // Send a custom msg associated with this view.
387 // Send a custom msg associated with this view.
388 this.model.send(content, this.callbacks());
388 this.model.send(content, this.callbacks());
389 },
389 },
390
390
391 touch: function () {
391 touch: function () {
392 this.model.save_changes(this.callbacks());
392 this.model.save_changes(this.callbacks());
393 },
393 },
394
394
395 after_displayed: function (callback, context) {
395 after_displayed: function (callback, context) {
396 // Calls the callback right away is the view is already displayed
396 // Calls the callback right away is the view is already displayed
397 // otherwise, register the callback to the 'displayed' event.
397 // otherwise, register the callback to the 'displayed' event.
398 if (this.is_displayed) {
398 if (this.is_displayed) {
399 callback.apply(context);
399 callback.apply(context);
400 } else {
400 } else {
401 this.on('displayed', callback, context);
401 this.on('displayed', callback, context);
402 }
402 }
403 },
403 },
404 });
404 });
405
405
406
406
407 var DOMWidgetView = WidgetView.extend({
407 var DOMWidgetView = WidgetView.extend({
408 initialize: function (parameters) {
408 initialize: function (parameters) {
409 // Public constructor
409 // Public constructor
410 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
410 DOMWidgetView.__super__.initialize.apply(this, [parameters]);
411 this.on('displayed', this.show, this);
411 this.on('displayed', this.show, this);
412 this.model.on('change:visible', this.update_visible, this);
412 this.model.on('change:visible', this.update_visible, this);
413 this.model.on('change:_css', this.update_css, this);
413 this.model.on('change:_css', this.update_css, this);
414
414
415 this.model.on('change:_dom_classes', function(model, new_classes) {
415 this.model.on('change:_dom_classes', function(model, new_classes) {
416 var old_classes = model.previous('_dom_classes');
416 var old_classes = model.previous('_dom_classes');
417 this.update_classes(old_classes, new_classes);
417 this.update_classes(old_classes, new_classes);
418 }, this);
418 }, this);
419
419
420 this.model.on('change:color', function (model, value) {
420 this.model.on('change:color', function (model, value) {
421 this.update_attr('color', value); }, this);
421 this.update_attr('color', value); }, this);
422
422
423 this.model.on('change:background_color', function (model, value) {
423 this.model.on('change:background_color', function (model, value) {
424 this.update_attr('background', value); }, this);
424 this.update_attr('background', value); }, this);
425
425
426 this.model.on('change:width', function (model, value) {
426 this.model.on('change:width', function (model, value) {
427 this.update_attr('width', value); }, this);
427 this.update_attr('width', value); }, this);
428
428
429 this.model.on('change:height', function (model, value) {
429 this.model.on('change:height', function (model, value) {
430 this.update_attr('height', value); }, this);
430 this.update_attr('height', value); }, this);
431
431
432 this.model.on('change:border_color', function (model, value) {
432 this.model.on('change:border_color', function (model, value) {
433 this.update_attr('border-color', value); }, this);
433 this.update_attr('border-color', value); }, this);
434
434
435 this.model.on('change:border_width', function (model, value) {
435 this.model.on('change:border_width', function (model, value) {
436 this.update_attr('border-width', value); }, this);
436 this.update_attr('border-width', value); }, this);
437
437
438 this.model.on('change:border_style', function (model, value) {
438 this.model.on('change:border_style', function (model, value) {
439 this.update_attr('border-style', value); }, this);
439 this.update_attr('border-style', value); }, this);
440
440
441 this.model.on('change:font_style', function (model, value) {
441 this.model.on('change:font_style', function (model, value) {
442 this.update_attr('font-style', value); }, this);
442 this.update_attr('font-style', value); }, this);
443
443
444 this.model.on('change:font_weight', function (model, value) {
444 this.model.on('change:font_weight', function (model, value) {
445 this.update_attr('font-weight', value); }, this);
445 this.update_attr('font-weight', value); }, this);
446
446
447 this.model.on('change:font_size', function (model, value) {
447 this.model.on('change:font_size', function (model, value) {
448 this.update_attr('font-size', value); }, this);
448 this.update_attr('font-size', this._default_px(value)); }, this);
449
449
450 this.model.on('change:font_family', function (model, value) {
450 this.model.on('change:font_family', function (model, value) {
451 this.update_attr('font-family', value); }, this);
451 this.update_attr('font-family', value); }, this);
452
452
453 this.model.on('change:padding', function (model, value) {
453 this.model.on('change:padding', function (model, value) {
454 this.update_attr('padding', value); }, this);
454 this.update_attr('padding', value); }, this);
455
455
456 this.model.on('change:margin', function (model, value) {
456 this.model.on('change:margin', function (model, value) {
457 this.update_attr('margin', value); }, this);
457 this.update_attr('margin', this._default_px(value)); }, this);
458
459 this.model.on('change:border_radius', function (model, value) {
460 this.update_attr('border-radius', this._default_px(value)); }, this);
458
461
459 this.after_displayed(function() {
462 this.after_displayed(function() {
460 this.update_visible(this.model, this.model.get("visible"));
463 this.update_visible(this.model, this.model.get("visible"));
461 this.update_css(this.model, this.model.get("_css"));
464 this.update_css(this.model, this.model.get("_css"));
462
465
463 this.update_classes([], this.model.get('_dom_classes'));
466 this.update_classes([], this.model.get('_dom_classes'));
464 this.update_attr('color', this.model.get('color'));
467 this.update_attr('color', this.model.get('color'));
465 this.update_attr('background', this.model.get('background_color'));
468 this.update_attr('background', this.model.get('background_color'));
466 this.update_attr('width', this.model.get('width'));
469 this.update_attr('width', this.model.get('width'));
467 this.update_attr('height', this.model.get('height'));
470 this.update_attr('height', this.model.get('height'));
468 this.update_attr('border-color', this.model.get('border_color'));
471 this.update_attr('border-color', this.model.get('border_color'));
469 this.update_attr('border-width', this.model.get('border_width'));
472 this.update_attr('border-width', this.model.get('border_width'));
470 this.update_attr('border-style', this.model.get('border_style'));
473 this.update_attr('border-style', this.model.get('border_style'));
471 this.update_attr('font-style', this.model.get('font_style'));
474 this.update_attr('font-style', this.model.get('font_style'));
472 this.update_attr('font-weight', this.model.get('font_weight'));
475 this.update_attr('font-weight', this.model.get('font_weight'));
473 this.update_attr('font-size', this.model.get('font_size'));
476 this.update_attr('font-size', this.model.get('font_size'));
474 this.update_attr('font-family', this.model.get('font_family'));
477 this.update_attr('font-family', this.model.get('font_family'));
475 this.update_attr('padding', this.model.get('padding'));
478 this.update_attr('padding', this.model.get('padding'));
476 this.update_attr('margin', this.model.get('margin'));
479 this.update_attr('margin', this.model.get('margin'));
480 this.update_attr('border-radius', this.model.get('border_radius'));
477 }, this);
481 }, this);
478 },
482 },
479
483
484 _default_px: function(value) {
485 // Makes browser interpret a numerical string as a pixel value.
486 if (/^\d+\.?(\d+)?$/.test(value.trim())) {
487 return value.trim() + 'px';
488 }
489 return value;
490 },
491
480 update_attr: function(name, value) {
492 update_attr: function(name, value) {
481 // Set a css attr of the widget view.
493 // Set a css attr of the widget view.
482 this.$el.css(name, value);
494 this.$el.css(name, value);
483 },
495 },
484
496
485 update_visible: function(model, value) {
497 update_visible: function(model, value) {
486 // Update visibility
498 // Update visibility
487 this.$el.toggle(value);
499 this.$el.toggle(value);
488 },
500 },
489
501
490 update_css: function (model, css) {
502 update_css: function (model, css) {
491 // Update the css styling of this view.
503 // Update the css styling of this view.
492 var e = this.$el;
504 var e = this.$el;
493 if (css === undefined) {return;}
505 if (css === undefined) {return;}
494 for (var i = 0; i < css.length; i++) {
506 for (var i = 0; i < css.length; i++) {
495 // Apply the css traits to all elements that match the selector.
507 // Apply the css traits to all elements that match the selector.
496 var selector = css[i][0];
508 var selector = css[i][0];
497 var elements = this._get_selector_element(selector);
509 var elements = this._get_selector_element(selector);
498 if (elements.length > 0) {
510 if (elements.length > 0) {
499 var trait_key = css[i][1];
511 var trait_key = css[i][1];
500 var trait_value = css[i][2];
512 var trait_value = css[i][2];
501 elements.css(trait_key ,trait_value);
513 elements.css(trait_key ,trait_value);
502 }
514 }
503 }
515 }
504 },
516 },
505
517
506 update_classes: function (old_classes, new_classes, $el) {
518 update_classes: function (old_classes, new_classes, $el) {
507 // Update the DOM classes applied to an element, default to this.$el.
519 // Update the DOM classes applied to an element, default to this.$el.
508 if ($el===undefined) {
520 if ($el===undefined) {
509 $el = this.$el;
521 $el = this.$el;
510 }
522 }
511 this.do_diff(old_classes, new_classes, function(removed) {
523 this.do_diff(old_classes, new_classes, function(removed) {
512 $el.removeClass(removed);
524 $el.removeClass(removed);
513 }, function(added) {
525 }, function(added) {
514 $el.addClass(added);
526 $el.addClass(added);
515 });
527 });
516 },
528 },
517
529
518 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
530 update_mapped_classes: function(class_map, trait_name, previous_trait_value, $el) {
519 // Update the DOM classes applied to the widget based on a single
531 // Update the DOM classes applied to the widget based on a single
520 // trait's value.
532 // trait's value.
521 //
533 //
522 // Given a trait value classes map, this function automatically
534 // Given a trait value classes map, this function automatically
523 // handles applying the appropriate classes to the widget element
535 // handles applying the appropriate classes to the widget element
524 // and removing classes that are no longer valid.
536 // and removing classes that are no longer valid.
525 //
537 //
526 // Parameters
538 // Parameters
527 // ----------
539 // ----------
528 // class_map: dictionary
540 // class_map: dictionary
529 // Dictionary of trait values to class lists.
541 // Dictionary of trait values to class lists.
530 // Example:
542 // Example:
531 // {
543 // {
532 // success: ['alert', 'alert-success'],
544 // success: ['alert', 'alert-success'],
533 // info: ['alert', 'alert-info'],
545 // info: ['alert', 'alert-info'],
534 // warning: ['alert', 'alert-warning'],
546 // warning: ['alert', 'alert-warning'],
535 // danger: ['alert', 'alert-danger']
547 // danger: ['alert', 'alert-danger']
536 // };
548 // };
537 // trait_name: string
549 // trait_name: string
538 // Name of the trait to check the value of.
550 // Name of the trait to check the value of.
539 // previous_trait_value: optional string, default ''
551 // previous_trait_value: optional string, default ''
540 // Last trait value
552 // Last trait value
541 // $el: optional jQuery element handle, defaults to this.$el
553 // $el: optional jQuery element handle, defaults to this.$el
542 // Element that the classes are applied to.
554 // Element that the classes are applied to.
543 var key = previous_trait_value;
555 var key = previous_trait_value;
544 if (key === undefined) {
556 if (key === undefined) {
545 key = this.model.previous(trait_name);
557 key = this.model.previous(trait_name);
546 }
558 }
547 var old_classes = class_map[key] ? class_map[key] : [];
559 var old_classes = class_map[key] ? class_map[key] : [];
548 key = this.model.get(trait_name);
560 key = this.model.get(trait_name);
549 var new_classes = class_map[key] ? class_map[key] : [];
561 var new_classes = class_map[key] ? class_map[key] : [];
550
562
551 this.update_classes(old_classes, new_classes, $el || this.$el);
563 this.update_classes(old_classes, new_classes, $el || this.$el);
552 },
564 },
553
565
554 _get_selector_element: function (selector) {
566 _get_selector_element: function (selector) {
555 // Get the elements via the css selector.
567 // Get the elements via the css selector.
556 var elements;
568 var elements;
557 if (!selector) {
569 if (!selector) {
558 elements = this.$el;
570 elements = this.$el;
559 } else {
571 } else {
560 elements = this.$el.find(selector).addBack(selector);
572 elements = this.$el.find(selector).addBack(selector);
561 }
573 }
562 return elements;
574 return elements;
563 },
575 },
564 });
576 });
565
577
566 var widget = {
578 var widget = {
567 'WidgetModel': WidgetModel,
579 'WidgetModel': WidgetModel,
568 'WidgetView': WidgetView,
580 'WidgetView': WidgetView,
569 'DOMWidgetView': DOMWidgetView,
581 'DOMWidgetView': DOMWidgetView,
570 };
582 };
571
583
572 // For backwards compatability.
584 // For backwards compatability.
573 $.extend(IPython, widget);
585 $.extend(IPython, widget);
574
586
575 return widget;
587 return widget;
576 });
588 });
@@ -1,344 +1,344 b''
1 // Copyright (c) IPython Development Team.
1 // Copyright (c) IPython Development Team.
2 // Distributed under the terms of the Modified BSD License.
2 // Distributed under the terms of the Modified BSD License.
3
3
4 define([
4 define([
5 "widgets/js/widget",
5 "widgets/js/widget",
6 "jqueryui",
6 "jqueryui",
7 "bootstrap",
7 "bootstrap",
8 ], function(widget, $){
8 ], function(widget, $){
9
9
10 var BoxView = widget.DOMWidgetView.extend({
10 var BoxView = widget.DOMWidgetView.extend({
11 initialize: function(){
11 initialize: function(){
12 // Public constructor
12 // Public constructor
13 BoxView.__super__.initialize.apply(this, arguments);
13 BoxView.__super__.initialize.apply(this, arguments);
14 this.model.on('change:children', function(model, value) {
14 this.model.on('change:children', function(model, value) {
15 this.update_children(model.previous('children'), value);
15 this.update_children(model.previous('children'), value);
16 }, this);
16 }, this);
17 this.model.on('change:overflow_x', function(model, value) {
17 this.model.on('change:overflow_x', function(model, value) {
18 this.update_overflow_x();
18 this.update_overflow_x();
19 }, this);
19 }, this);
20 this.model.on('change:overflow_y', function(model, value) {
20 this.model.on('change:overflow_y', function(model, value) {
21 this.update_overflow_y();
21 this.update_overflow_y();
22 }, this);
22 }, this);
23 this.model.on('change:box_style', function(model, value) {
23 this.model.on('change:box_style', function(model, value) {
24 this.update_box_style();
24 this.update_box_style();
25 }, this);
25 }, this);
26 },
26 },
27
27
28 update_attr: function(name, value) {
28 update_attr: function(name, value) {
29 // Set a css attr of the widget view.
29 // Set a css attr of the widget view.
30 this.$box.css(name, value);
30 this.$box.css(name, value);
31 },
31 },
32
32
33 render: function(){
33 render: function(){
34 // Called when view is rendered.
34 // Called when view is rendered.
35 this.$box = this.$el;
35 this.$box = this.$el;
36 this.$box.addClass('widget-box');
36 this.$box.addClass('widget-box');
37 this.update_children([], this.model.get('children'));
37 this.update_children([], this.model.get('children'));
38 this.update_overflow_x();
38 this.update_overflow_x();
39 this.update_overflow_y();
39 this.update_overflow_y();
40 this.update_box_style('');
40 this.update_box_style('');
41 },
41 },
42
42
43 update_overflow_x: function() {
43 update_overflow_x: function() {
44 // Called when the x-axis overflow setting is changed.
44 // Called when the x-axis overflow setting is changed.
45 this.$box.css('overflow-x', this.model.get('overflow_x'));
45 this.$box.css('overflow-x', this.model.get('overflow_x'));
46 },
46 },
47
47
48 update_overflow_y: function() {
48 update_overflow_y: function() {
49 // Called when the y-axis overflow setting is changed.
49 // Called when the y-axis overflow setting is changed.
50 this.$box.css('overflow-y', this.model.get('overflow_y'));
50 this.$box.css('overflow-y', this.model.get('overflow_y'));
51 },
51 },
52
52
53 update_box_style: function(previous_trait_value) {
53 update_box_style: function(previous_trait_value) {
54 var class_map = {
54 var class_map = {
55 success: ['alert', 'alert-success'],
55 success: ['alert', 'alert-success'],
56 info: ['alert', 'alert-info'],
56 info: ['alert', 'alert-info'],
57 warning: ['alert', 'alert-warning'],
57 warning: ['alert', 'alert-warning'],
58 danger: ['alert', 'alert-danger']
58 danger: ['alert', 'alert-danger']
59 };
59 };
60 this.update_mapped_classes(class_map, 'box_style', previous_trait_value, this.$box);
60 this.update_mapped_classes(class_map, 'box_style', previous_trait_value, this.$box);
61 },
61 },
62
62
63 update_children: function(old_list, new_list) {
63 update_children: function(old_list, new_list) {
64 // Called when the children list changes.
64 // Called when the children list changes.
65 this.do_diff(old_list, new_list,
65 this.do_diff(old_list, new_list,
66 $.proxy(this.remove_child_model, this),
66 $.proxy(this.remove_child_model, this),
67 $.proxy(this.add_child_model, this));
67 $.proxy(this.add_child_model, this));
68 },
68 },
69
69
70 remove_child_model: function(model) {
70 remove_child_model: function(model) {
71 // Called when a model is removed from the children list.
71 // Called when a model is removed from the children list.
72 this.pop_child_view(model).remove();
72 this.pop_child_view(model).remove();
73 },
73 },
74
74
75 add_child_model: function(model) {
75 add_child_model: function(model) {
76 // Called when a model is added to the children list.
76 // Called when a model is added to the children list.
77 var view = this.create_child_view(model);
77 var view = this.create_child_view(model);
78 this.$box.append(view.$el);
78 this.$box.append(view.$el);
79
79
80 // Trigger the displayed event of the child view.
80 // Trigger the displayed event of the child view.
81 this.after_displayed(function() {
81 this.after_displayed(function() {
82 view.trigger('displayed');
82 view.trigger('displayed');
83 });
83 });
84 },
84 },
85 });
85 });
86
86
87
87
88 var FlexBoxView = BoxView.extend({
88 var FlexBoxView = BoxView.extend({
89 render: function(){
89 render: function(){
90 FlexBoxView.__super__.render.apply(this);
90 FlexBoxView.__super__.render.apply(this);
91 this.model.on('change:orientation', this.update_orientation, this);
91 this.model.on('change:orientation', this.update_orientation, this);
92 this.model.on('change:flex', this._flex_changed, this);
92 this.model.on('change:flex', this._flex_changed, this);
93 this.model.on('change:pack', this._pack_changed, this);
93 this.model.on('change:pack', this._pack_changed, this);
94 this.model.on('change:align', this._align_changed, this);
94 this.model.on('change:align', this._align_changed, this);
95 this._flex_changed();
95 this._flex_changed();
96 this._pack_changed();
96 this._pack_changed();
97 this._align_changed();
97 this._align_changed();
98 this.update_orientation();
98 this.update_orientation();
99 },
99 },
100
100
101 update_orientation: function(){
101 update_orientation: function(){
102 var orientation = this.model.get("orientation");
102 var orientation = this.model.get("orientation");
103 if (orientation == "vertical") {
103 if (orientation == "vertical") {
104 this.$box.removeClass("hbox").addClass("vbox");
104 this.$box.removeClass("hbox").addClass("vbox");
105 } else {
105 } else {
106 this.$box.removeClass("vbox").addClass("hbox");
106 this.$box.removeClass("vbox").addClass("hbox");
107 }
107 }
108 },
108 },
109
109
110 _flex_changed: function(){
110 _flex_changed: function(){
111 if (this.model.previous('flex')) {
111 if (this.model.previous('flex')) {
112 this.$box.removeClass('box-flex' + this.model.previous('flex'));
112 this.$box.removeClass('box-flex' + this.model.previous('flex'));
113 }
113 }
114 this.$box.addClass('box-flex' + this.model.get('flex'));
114 this.$box.addClass('box-flex' + this.model.get('flex'));
115 },
115 },
116
116
117 _pack_changed: function(){
117 _pack_changed: function(){
118 if (this.model.previous('pack')) {
118 if (this.model.previous('pack')) {
119 this.$box.removeClass(this.model.previous('pack'));
119 this.$box.removeClass(this.model.previous('pack'));
120 }
120 }
121 this.$box.addClass(this.model.get('pack'));
121 this.$box.addClass(this.model.get('pack'));
122 },
122 },
123
123
124 _align_changed: function(){
124 _align_changed: function(){
125 if (this.model.previous('align')) {
125 if (this.model.previous('align')) {
126 this.$box.removeClass('align-' + this.model.previous('align'));
126 this.$box.removeClass('align-' + this.model.previous('align'));
127 }
127 }
128 this.$box.addClass('align-' + this.model.get('align'));
128 this.$box.addClass('align-' + this.model.get('align'));
129 },
129 },
130 });
130 });
131
131
132 var PopupView = BoxView.extend({
132 var PopupView = BoxView.extend({
133
133
134 render: function(){
134 render: function(){
135 // Called when view is rendered.
135 // Called when view is rendered.
136 var that = this;
136 var that = this;
137
137
138 this.$el.on("remove", function(){
138 this.$el.on("remove", function(){
139 that.$backdrop.remove();
139 that.$backdrop.remove();
140 });
140 });
141 this.$backdrop = $('<div />')
141 this.$backdrop = $('<div />')
142 .appendTo($('#notebook-container'))
142 .appendTo($('#notebook-container'))
143 .addClass('modal-dialog')
143 .addClass('modal-dialog')
144 .css('position', 'absolute')
144 .css('position', 'absolute')
145 .css('left', '0px')
145 .css('left', '0px')
146 .css('top', '0px');
146 .css('top', '0px');
147 this.$window = $('<div />')
147 this.$window = $('<div />')
148 .appendTo(this.$backdrop)
148 .appendTo(this.$backdrop)
149 .addClass('modal-content widget-modal')
149 .addClass('modal-content widget-modal')
150 .mousedown(function(){
150 .mousedown(function(){
151 that.bring_to_front();
151 that.bring_to_front();
152 });
152 });
153
153
154 // Set the elements array since the this.$window element is not child
154 // Set the elements array since the this.$window element is not child
155 // of this.$el and the parent widget manager or other widgets may
155 // of this.$el and the parent widget manager or other widgets may
156 // need to know about all of the top-level widgets. The IPython
156 // need to know about all of the top-level widgets. The IPython
157 // widget manager uses this to register the elements with the
157 // widget manager uses this to register the elements with the
158 // keyboard manager.
158 // keyboard manager.
159 this.additional_elements = [this.$window];
159 this.additional_elements = [this.$window];
160
160
161 this.$title_bar = $('<div />')
161 this.$title_bar = $('<div />')
162 .addClass('popover-title')
162 .addClass('popover-title')
163 .appendTo(this.$window)
163 .appendTo(this.$window)
164 .mousedown(function(){
164 .mousedown(function(){
165 that.bring_to_front();
165 that.bring_to_front();
166 });
166 });
167 this.$close = $('<button />')
167 this.$close = $('<button />')
168 .addClass('close fa fa-remove')
168 .addClass('close fa fa-remove')
169 .css('margin-left', '5px')
169 .css('margin-left', '5px')
170 .appendTo(this.$title_bar)
170 .appendTo(this.$title_bar)
171 .click(function(){
171 .click(function(){
172 that.hide();
172 that.hide();
173 event.stopPropagation();
173 event.stopPropagation();
174 });
174 });
175 this.$minimize = $('<button />')
175 this.$minimize = $('<button />')
176 .addClass('close fa fa-arrow-down')
176 .addClass('close fa fa-arrow-down')
177 .appendTo(this.$title_bar)
177 .appendTo(this.$title_bar)
178 .click(function(){
178 .click(function(){
179 that.popped_out = !that.popped_out;
179 that.popped_out = !that.popped_out;
180 if (!that.popped_out) {
180 if (!that.popped_out) {
181 that.$minimize
181 that.$minimize
182 .removeClass('fa fa-arrow-down')
182 .removeClass('fa-arrow-down')
183 .addClass('fa fa-arrow-up');
183 .addClass('fa-arrow-up');
184
184
185 that.$window
185 that.$window
186 .draggable('destroy')
186 .draggable('destroy')
187 .resizable('destroy')
187 .resizable('destroy')
188 .removeClass('widget-modal modal-content')
188 .removeClass('widget-modal modal-content')
189 .addClass('docked-widget-modal')
189 .addClass('docked-widget-modal')
190 .detach()
190 .detach()
191 .insertBefore(that.$show_button);
191 .insertBefore(that.$show_button);
192 that.$show_button.hide();
192 that.$show_button.hide();
193 that.$close.hide();
193 that.$close.hide();
194 } else {
194 } else {
195 that.$minimize
195 that.$minimize
196 .addClass('fa fa-arrow-down')
196 .addClass('fa-arrow-down')
197 .removeClass('fa fa-arrow-up');
197 .removeClass('fa-arrow-up');
198
198
199 that.$window
199 that.$window
200 .removeClass('docked-widget-modal')
200 .removeClass('docked-widget-modal')
201 .addClass('widget-modal modal-content')
201 .addClass('widget-modal modal-content')
202 .detach()
202 .detach()
203 .appendTo(that.$backdrop)
203 .appendTo(that.$backdrop)
204 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
204 .draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'})
205 .resizable()
205 .resizable()
206 .children('.ui-resizable-handle').show();
206 .children('.ui-resizable-handle').show();
207 that.show();
207 that.show();
208 that.$show_button.show();
208 that.$show_button.show();
209 that.$close.show();
209 that.$close.show();
210 }
210 }
211 event.stopPropagation();
211 event.stopPropagation();
212 });
212 });
213 this.$title = $('<div />')
213 this.$title = $('<div />')
214 .addClass('widget-modal-title')
214 .addClass('widget-modal-title')
215 .html("&nbsp;")
215 .html("&nbsp;")
216 .appendTo(this.$title_bar);
216 .appendTo(this.$title_bar);
217 this.$box = $('<div />')
217 this.$box = $('<div />')
218 .addClass('modal-body')
218 .addClass('modal-body')
219 .addClass('widget-modal-body')
219 .addClass('widget-modal-body')
220 .addClass('widget-box')
220 .addClass('widget-box')
221 .addClass('vbox')
221 .addClass('vbox')
222 .appendTo(this.$window);
222 .appendTo(this.$window);
223
223
224 this.$show_button = $('<button />')
224 this.$show_button = $('<button />')
225 .html("&nbsp;")
225 .html("&nbsp;")
226 .addClass('btn btn-info widget-modal-show')
226 .addClass('btn btn-info widget-modal-show')
227 .appendTo(this.$el)
227 .appendTo(this.$el)
228 .click(function(){
228 .click(function(){
229 that.show();
229 that.show();
230 });
230 });
231
231
232 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
232 this.$window.draggable({handle: '.popover-title', snap: '#notebook, .modal', snapMode: 'both'});
233 this.$window.resizable();
233 this.$window.resizable();
234 this.$window.on('resize', function(){
234 this.$window.on('resize', function(){
235 that.$box.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
235 that.$box.outerHeight(that.$window.innerHeight() - that.$title_bar.outerHeight());
236 });
236 });
237
237
238 this._shown_once = false;
238 this._shown_once = false;
239 this.popped_out = true;
239 this.popped_out = true;
240
240
241 this.update_children([], this.model.get('children'));
241 this.update_children([], this.model.get('children'));
242 this.model.on('change:children', function(model, value) {
242 this.model.on('change:children', function(model, value) {
243 this.update_children(model.previous('children'), value);
243 this.update_children(model.previous('children'), value);
244 }, this);
244 }, this);
245 },
245 },
246
246
247 hide: function() {
247 hide: function() {
248 // Called when the modal hide button is clicked.
248 // Called when the modal hide button is clicked.
249 this.$window.hide();
249 this.$window.hide();
250 this.$show_button.removeClass('btn-info');
250 this.$show_button.removeClass('btn-info');
251 },
251 },
252
252
253 show: function() {
253 show: function() {
254 // Called when the modal show button is clicked.
254 // Called when the modal show button is clicked.
255 this.$show_button.addClass('btn-info');
255 this.$show_button.addClass('btn-info');
256 this.$window.show();
256 this.$window.show();
257 if (this.popped_out) {
257 if (this.popped_out) {
258 this.$window.css("positon", "absolute");
258 this.$window.css("positon", "absolute");
259 this.$window.css("top", "0px");
259 this.$window.css("top", "0px");
260 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
260 this.$window.css("left", Math.max(0, (($('body').outerWidth() - this.$window.outerWidth()) / 2) +
261 $(window).scrollLeft()) + "px");
261 $(window).scrollLeft()) + "px");
262 this.bring_to_front();
262 this.bring_to_front();
263 }
263 }
264 },
264 },
265
265
266 bring_to_front: function() {
266 bring_to_front: function() {
267 // Make the modal top-most, z-ordered about the other modals.
267 // Make the modal top-most, z-ordered about the other modals.
268 var $widget_modals = $(".widget-modal");
268 var $widget_modals = $(".widget-modal");
269 var max_zindex = 0;
269 var max_zindex = 0;
270 $widget_modals.each(function (index, el){
270 $widget_modals.each(function (index, el){
271 var zindex = parseInt($(el).css('z-index'));
271 var zindex = parseInt($(el).css('z-index'));
272 if (!isNaN(zindex)) {
272 if (!isNaN(zindex)) {
273 max_zindex = Math.max(max_zindex, zindex);
273 max_zindex = Math.max(max_zindex, zindex);
274 }
274 }
275 });
275 });
276
276
277 // Start z-index of widget modals at 2000
277 // Start z-index of widget modals at 2000
278 max_zindex = Math.max(max_zindex, 2000);
278 max_zindex = Math.max(max_zindex, 2000);
279
279
280 $widget_modals.each(function (index, el){
280 $widget_modals.each(function (index, el){
281 $el = $(el);
281 $el = $(el);
282 if (max_zindex == parseInt($el.css('z-index'))) {
282 if (max_zindex == parseInt($el.css('z-index'))) {
283 $el.css('z-index', max_zindex - 1);
283 $el.css('z-index', max_zindex - 1);
284 }
284 }
285 });
285 });
286 this.$window.css('z-index', max_zindex);
286 this.$window.css('z-index', max_zindex);
287 },
287 },
288
288
289 update: function(){
289 update: function(){
290 // Update the contents of this view
290 // Update the contents of this view
291 //
291 //
292 // Called when the model is changed. The model may have been
292 // Called when the model is changed. The model may have been
293 // changed by another view or by a state update from the back-end.
293 // changed by another view or by a state update from the back-end.
294 var description = this.model.get('description');
294 var description = this.model.get('description');
295 if (description.trim().length === 0) {
295 if (description.trim().length === 0) {
296 this.$title.html("&nbsp;"); // Preserve title height
296 this.$title.html("&nbsp;"); // Preserve title height
297 } else {
297 } else {
298 this.$title.text(description);
298 this.$title.text(description);
299 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
299 MathJax.Hub.Queue(["Typeset",MathJax.Hub,this.$title.get(0)]);
300 }
300 }
301
301
302 var button_text = this.model.get('button_text');
302 var button_text = this.model.get('button_text');
303 if (button_text.trim().length === 0) {
303 if (button_text.trim().length === 0) {
304 this.$show_button.html("&nbsp;"); // Preserve button height
304 this.$show_button.html("&nbsp;"); // Preserve button height
305 } else {
305 } else {
306 this.$show_button.text(button_text);
306 this.$show_button.text(button_text);
307 }
307 }
308
308
309 if (!this._shown_once) {
309 if (!this._shown_once) {
310 this._shown_once = true;
310 this._shown_once = true;
311 this.show();
311 this.show();
312 }
312 }
313
313
314 return PopupView.__super__.update.apply(this);
314 return PopupView.__super__.update.apply(this);
315 },
315 },
316
316
317 _get_selector_element: function(selector) {
317 _get_selector_element: function(selector) {
318 // Get an element view a 'special' jquery selector. (see widget.js)
318 // Get an element view a 'special' jquery selector. (see widget.js)
319 //
319 //
320 // Since the modal actually isn't within the $el in the DOM, we need to extend
320 // Since the modal actually isn't within the $el in the DOM, we need to extend
321 // the selector logic to allow the user to set css on the modal if need be.
321 // the selector logic to allow the user to set css on the modal if need be.
322 // The convention used is:
322 // The convention used is:
323 // "modal" - select the modal div
323 // "modal" - select the modal div
324 // "modal [selector]" - select element(s) within the modal div.
324 // "modal [selector]" - select element(s) within the modal div.
325 // "[selector]" - select elements within $el
325 // "[selector]" - select elements within $el
326 // "" - select the $el
326 // "" - select the $el
327 if (selector.substring(0, 5) == 'modal') {
327 if (selector.substring(0, 5) == 'modal') {
328 if (selector == 'modal') {
328 if (selector == 'modal') {
329 return this.$window;
329 return this.$window;
330 } else {
330 } else {
331 return this.$window.find(selector.substring(6));
331 return this.$window.find(selector.substring(6));
332 }
332 }
333 } else {
333 } else {
334 return PopupView.__super__._get_selector_element.apply(this, [selector]);
334 return PopupView.__super__._get_selector_element.apply(this, [selector]);
335 }
335 }
336 },
336 },
337 });
337 });
338
338
339 return {
339 return {
340 'BoxView': BoxView,
340 'BoxView': BoxView,
341 'PopupView': PopupView,
341 'PopupView': PopupView,
342 'FlexBoxView': FlexBoxView,
342 'FlexBoxView': FlexBoxView,
343 };
343 };
344 });
344 });
@@ -1,427 +1,439 b''
1 """Base Widget class. Allows user to create widgets in the back-end that render
1 """Base Widget class. Allows user to create widgets in the back-end that render
2 in the IPython notebook front-end.
2 in the IPython notebook front-end.
3 """
3 """
4 #-----------------------------------------------------------------------------
4 #-----------------------------------------------------------------------------
5 # Copyright (c) 2013, the IPython Development Team.
5 # Copyright (c) 2013, the IPython Development Team.
6 #
6 #
7 # Distributed under the terms of the Modified BSD License.
7 # Distributed under the terms of the Modified BSD License.
8 #
8 #
9 # The full license is in the file COPYING.txt, distributed with this software.
9 # The full license is in the file COPYING.txt, distributed with this software.
10 #-----------------------------------------------------------------------------
10 #-----------------------------------------------------------------------------
11
11
12 #-----------------------------------------------------------------------------
12 #-----------------------------------------------------------------------------
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from contextlib import contextmanager
15 from contextlib import contextmanager
16 import collections
16 import collections
17
17
18 from IPython.core.getipython import get_ipython
18 from IPython.core.getipython import get_ipython
19 from IPython.kernel.comm import Comm
19 from IPython.kernel.comm import Comm
20 from IPython.config import LoggingConfigurable
20 from IPython.config import LoggingConfigurable
21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, \
22 CaselessStrEnum, Tuple, CUnicode, Int, Set
22 CaselessStrEnum, Tuple, CUnicode, Int, Set
23 from IPython.utils.py3compat import string_types
23 from IPython.utils.py3compat import string_types
24
24
25 #-----------------------------------------------------------------------------
25 #-----------------------------------------------------------------------------
26 # Classes
26 # Classes
27 #-----------------------------------------------------------------------------
27 #-----------------------------------------------------------------------------
28 class CallbackDispatcher(LoggingConfigurable):
28 class CallbackDispatcher(LoggingConfigurable):
29 """A structure for registering and running callbacks"""
29 """A structure for registering and running callbacks"""
30 callbacks = List()
30 callbacks = List()
31
31
32 def __call__(self, *args, **kwargs):
32 def __call__(self, *args, **kwargs):
33 """Call all of the registered callbacks."""
33 """Call all of the registered callbacks."""
34 value = None
34 value = None
35 for callback in self.callbacks:
35 for callback in self.callbacks:
36 try:
36 try:
37 local_value = callback(*args, **kwargs)
37 local_value = callback(*args, **kwargs)
38 except Exception as e:
38 except Exception as e:
39 ip = get_ipython()
39 ip = get_ipython()
40 if ip is None:
40 if ip is None:
41 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
41 self.log.warn("Exception in callback %s: %s", callback, e, exc_info=True)
42 else:
42 else:
43 ip.showtraceback()
43 ip.showtraceback()
44 else:
44 else:
45 value = local_value if local_value is not None else value
45 value = local_value if local_value is not None else value
46 return value
46 return value
47
47
48 def register_callback(self, callback, remove=False):
48 def register_callback(self, callback, remove=False):
49 """(Un)Register a callback
49 """(Un)Register a callback
50
50
51 Parameters
51 Parameters
52 ----------
52 ----------
53 callback: method handle
53 callback: method handle
54 Method to be registered or unregistered.
54 Method to be registered or unregistered.
55 remove=False: bool
55 remove=False: bool
56 Whether to unregister the callback."""
56 Whether to unregister the callback."""
57
57
58 # (Un)Register the callback.
58 # (Un)Register the callback.
59 if remove and callback in self.callbacks:
59 if remove and callback in self.callbacks:
60 self.callbacks.remove(callback)
60 self.callbacks.remove(callback)
61 elif not remove and callback not in self.callbacks:
61 elif not remove and callback not in self.callbacks:
62 self.callbacks.append(callback)
62 self.callbacks.append(callback)
63
63
64 def _show_traceback(method):
64 def _show_traceback(method):
65 """decorator for showing tracebacks in IPython"""
65 """decorator for showing tracebacks in IPython"""
66 def m(self, *args, **kwargs):
66 def m(self, *args, **kwargs):
67 try:
67 try:
68 return(method(self, *args, **kwargs))
68 return(method(self, *args, **kwargs))
69 except Exception as e:
69 except Exception as e:
70 ip = get_ipython()
70 ip = get_ipython()
71 if ip is None:
71 if ip is None:
72 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
72 self.log.warn("Exception in widget method %s: %s", method, e, exc_info=True)
73 else:
73 else:
74 ip.showtraceback()
74 ip.showtraceback()
75 return m
75 return m
76
76
77 class Widget(LoggingConfigurable):
77 class Widget(LoggingConfigurable):
78 #-------------------------------------------------------------------------
78 #-------------------------------------------------------------------------
79 # Class attributes
79 # Class attributes
80 #-------------------------------------------------------------------------
80 #-------------------------------------------------------------------------
81 _widget_construction_callback = None
81 _widget_construction_callback = None
82 widgets = {}
82 widgets = {}
83
83
84 @staticmethod
84 @staticmethod
85 def on_widget_constructed(callback):
85 def on_widget_constructed(callback):
86 """Registers a callback to be called when a widget is constructed.
86 """Registers a callback to be called when a widget is constructed.
87
87
88 The callback must have the following signature:
88 The callback must have the following signature:
89 callback(widget)"""
89 callback(widget)"""
90 Widget._widget_construction_callback = callback
90 Widget._widget_construction_callback = callback
91
91
92 @staticmethod
92 @staticmethod
93 def _call_widget_constructed(widget):
93 def _call_widget_constructed(widget):
94 """Static method, called when a widget is constructed."""
94 """Static method, called when a widget is constructed."""
95 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
95 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
96 Widget._widget_construction_callback(widget)
96 Widget._widget_construction_callback(widget)
97
97
98 #-------------------------------------------------------------------------
98 #-------------------------------------------------------------------------
99 # Traits
99 # Traits
100 #-------------------------------------------------------------------------
100 #-------------------------------------------------------------------------
101 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
101 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
102 registered in the front-end to create and sync this widget with.""")
102 registered in the front-end to create and sync this widget with.""")
103 _view_name = Unicode('WidgetView', help="""Default view registered in the front-end
103 _view_name = Unicode('WidgetView', help="""Default view registered in the front-end
104 to use to represent the widget.""", sync=True)
104 to use to represent the widget.""", sync=True)
105 comm = Instance('IPython.kernel.comm.Comm')
105 comm = Instance('IPython.kernel.comm.Comm')
106
106
107 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
107 msg_throttle = Int(3, sync=True, help="""Maximum number of msgs the
108 front-end can send before receiving an idle msg from the back-end.""")
108 front-end can send before receiving an idle msg from the back-end.""")
109
109
110 keys = List()
110 keys = List()
111 def _keys_default(self):
111 def _keys_default(self):
112 return [name for name in self.traits(sync=True)]
112 return [name for name in self.traits(sync=True)]
113
113
114 _property_lock = Tuple((None, None))
114 _property_lock = Tuple((None, None))
115 _send_state_lock = Int(0)
115 _send_state_lock = Int(0)
116 _states_to_send = Set(allow_none=False)
116 _states_to_send = Set(allow_none=False)
117 _display_callbacks = Instance(CallbackDispatcher, ())
117 _display_callbacks = Instance(CallbackDispatcher, ())
118 _msg_callbacks = Instance(CallbackDispatcher, ())
118 _msg_callbacks = Instance(CallbackDispatcher, ())
119
119
120 #-------------------------------------------------------------------------
120 #-------------------------------------------------------------------------
121 # (Con/de)structor
121 # (Con/de)structor
122 #-------------------------------------------------------------------------
122 #-------------------------------------------------------------------------
123 def __init__(self, **kwargs):
123 def __init__(self, **kwargs):
124 """Public constructor"""
124 """Public constructor"""
125 self._model_id = kwargs.pop('model_id', None)
125 self._model_id = kwargs.pop('model_id', None)
126 super(Widget, self).__init__(**kwargs)
126 super(Widget, self).__init__(**kwargs)
127
127
128 self.on_trait_change(self._handle_property_changed, self.keys)
128 self.on_trait_change(self._handle_property_changed, self.keys)
129 Widget._call_widget_constructed(self)
129 Widget._call_widget_constructed(self)
130 self.open()
130 self.open()
131
131
132 def __del__(self):
132 def __del__(self):
133 """Object disposal"""
133 """Object disposal"""
134 self.close()
134 self.close()
135
135
136 #-------------------------------------------------------------------------
136 #-------------------------------------------------------------------------
137 # Properties
137 # Properties
138 #-------------------------------------------------------------------------
138 #-------------------------------------------------------------------------
139
139
140 def open(self):
140 def open(self):
141 """Open a comm to the frontend if one isn't already open."""
141 """Open a comm to the frontend if one isn't already open."""
142 if self.comm is None:
142 if self.comm is None:
143 if self._model_id is None:
143 if self._model_id is None:
144 self.comm = Comm(target_name=self._model_name)
144 self.comm = Comm(target_name=self._model_name)
145 self._model_id = self.model_id
145 self._model_id = self.model_id
146 else:
146 else:
147 self.comm = Comm(target_name=self._model_name, comm_id=self._model_id)
147 self.comm = Comm(target_name=self._model_name, comm_id=self._model_id)
148 self.comm.on_msg(self._handle_msg)
148 self.comm.on_msg(self._handle_msg)
149 Widget.widgets[self.model_id] = self
149 Widget.widgets[self.model_id] = self
150
150
151 # first update
151 # first update
152 self.send_state()
152 self.send_state()
153
153
154 @property
154 @property
155 def model_id(self):
155 def model_id(self):
156 """Gets the model id of this widget.
156 """Gets the model id of this widget.
157
157
158 If a Comm doesn't exist yet, a Comm will be created automagically."""
158 If a Comm doesn't exist yet, a Comm will be created automagically."""
159 return self.comm.comm_id
159 return self.comm.comm_id
160
160
161 #-------------------------------------------------------------------------
161 #-------------------------------------------------------------------------
162 # Methods
162 # Methods
163 #-------------------------------------------------------------------------
163 #-------------------------------------------------------------------------
164
164
165 def close(self):
165 def close(self):
166 """Close method.
166 """Close method.
167
167
168 Closes the underlying comm.
168 Closes the underlying comm.
169 When the comm is closed, all of the widget views are automatically
169 When the comm is closed, all of the widget views are automatically
170 removed from the front-end."""
170 removed from the front-end."""
171 if self.comm is not None:
171 if self.comm is not None:
172 Widget.widgets.pop(self.model_id, None)
172 Widget.widgets.pop(self.model_id, None)
173 self.comm.close()
173 self.comm.close()
174 self.comm = None
174 self.comm = None
175
175
176 def send_state(self, key=None):
176 def send_state(self, key=None):
177 """Sends the widget state, or a piece of it, to the front-end.
177 """Sends the widget state, or a piece of it, to the front-end.
178
178
179 Parameters
179 Parameters
180 ----------
180 ----------
181 key : unicode, or iterable (optional)
181 key : unicode, or iterable (optional)
182 A single property's name or iterable of property names to sync with the front-end.
182 A single property's name or iterable of property names to sync with the front-end.
183 """
183 """
184 self._send({
184 self._send({
185 "method" : "update",
185 "method" : "update",
186 "state" : self.get_state(key=key)
186 "state" : self.get_state(key=key)
187 })
187 })
188
188
189 def get_state(self, key=None):
189 def get_state(self, key=None):
190 """Gets the widget state, or a piece of it.
190 """Gets the widget state, or a piece of it.
191
191
192 Parameters
192 Parameters
193 ----------
193 ----------
194 key : unicode or iterable (optional)
194 key : unicode or iterable (optional)
195 A single property's name or iterable of property names to get.
195 A single property's name or iterable of property names to get.
196 """
196 """
197 if key is None:
197 if key is None:
198 keys = self.keys
198 keys = self.keys
199 elif isinstance(key, string_types):
199 elif isinstance(key, string_types):
200 keys = [key]
200 keys = [key]
201 elif isinstance(key, collections.Iterable):
201 elif isinstance(key, collections.Iterable):
202 keys = key
202 keys = key
203 else:
203 else:
204 raise ValueError("key must be a string, an iterable of keys, or None")
204 raise ValueError("key must be a string, an iterable of keys, or None")
205 state = {}
205 state = {}
206 for k in keys:
206 for k in keys:
207 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
207 f = self.trait_metadata(k, 'to_json', self._trait_to_json)
208 value = getattr(self, k)
208 value = getattr(self, k)
209 state[k] = f(value)
209 state[k] = f(value)
210 return state
210 return state
211
211
212 def send(self, content):
212 def send(self, content):
213 """Sends a custom msg to the widget model in the front-end.
213 """Sends a custom msg to the widget model in the front-end.
214
214
215 Parameters
215 Parameters
216 ----------
216 ----------
217 content : dict
217 content : dict
218 Content of the message to send.
218 Content of the message to send.
219 """
219 """
220 self._send({"method": "custom", "content": content})
220 self._send({"method": "custom", "content": content})
221
221
222 def on_msg(self, callback, remove=False):
222 def on_msg(self, callback, remove=False):
223 """(Un)Register a custom msg receive callback.
223 """(Un)Register a custom msg receive callback.
224
224
225 Parameters
225 Parameters
226 ----------
226 ----------
227 callback: callable
227 callback: callable
228 callback will be passed two arguments when a message arrives::
228 callback will be passed two arguments when a message arrives::
229
229
230 callback(widget, content)
230 callback(widget, content)
231
231
232 remove: bool
232 remove: bool
233 True if the callback should be unregistered."""
233 True if the callback should be unregistered."""
234 self._msg_callbacks.register_callback(callback, remove=remove)
234 self._msg_callbacks.register_callback(callback, remove=remove)
235
235
236 def on_displayed(self, callback, remove=False):
236 def on_displayed(self, callback, remove=False):
237 """(Un)Register a widget displayed callback.
237 """(Un)Register a widget displayed callback.
238
238
239 Parameters
239 Parameters
240 ----------
240 ----------
241 callback: method handler
241 callback: method handler
242 Must have a signature of::
242 Must have a signature of::
243
243
244 callback(widget, **kwargs)
244 callback(widget, **kwargs)
245
245
246 kwargs from display are passed through without modification.
246 kwargs from display are passed through without modification.
247 remove: bool
247 remove: bool
248 True if the callback should be unregistered."""
248 True if the callback should be unregistered."""
249 self._display_callbacks.register_callback(callback, remove=remove)
249 self._display_callbacks.register_callback(callback, remove=remove)
250
250
251 #-------------------------------------------------------------------------
251 #-------------------------------------------------------------------------
252 # Support methods
252 # Support methods
253 #-------------------------------------------------------------------------
253 #-------------------------------------------------------------------------
254 @contextmanager
254 @contextmanager
255 def _lock_property(self, key, value):
255 def _lock_property(self, key, value):
256 """Lock a property-value pair.
256 """Lock a property-value pair.
257
257
258 The value should be the JSON state of the property.
258 The value should be the JSON state of the property.
259
259
260 NOTE: This, in addition to the single lock for all state changes, is
260 NOTE: This, in addition to the single lock for all state changes, is
261 flawed. In the future we may want to look into buffering state changes
261 flawed. In the future we may want to look into buffering state changes
262 back to the front-end."""
262 back to the front-end."""
263 self._property_lock = (key, value)
263 self._property_lock = (key, value)
264 try:
264 try:
265 yield
265 yield
266 finally:
266 finally:
267 self._property_lock = (None, None)
267 self._property_lock = (None, None)
268
268
269 @contextmanager
269 @contextmanager
270 def hold_sync(self):
270 def hold_sync(self):
271 """Hold syncing any state until the context manager is released"""
271 """Hold syncing any state until the context manager is released"""
272 # We increment a value so that this can be nested. Syncing will happen when
272 # We increment a value so that this can be nested. Syncing will happen when
273 # all levels have been released.
273 # all levels have been released.
274 self._send_state_lock += 1
274 self._send_state_lock += 1
275 try:
275 try:
276 yield
276 yield
277 finally:
277 finally:
278 self._send_state_lock -=1
278 self._send_state_lock -=1
279 if self._send_state_lock == 0:
279 if self._send_state_lock == 0:
280 self.send_state(self._states_to_send)
280 self.send_state(self._states_to_send)
281 self._states_to_send.clear()
281 self._states_to_send.clear()
282
282
283 def _should_send_property(self, key, value):
283 def _should_send_property(self, key, value):
284 """Check the property lock (property_lock)"""
284 """Check the property lock (property_lock)"""
285 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
285 to_json = self.trait_metadata(key, 'to_json', self._trait_to_json)
286 if (key == self._property_lock[0]
286 if (key == self._property_lock[0]
287 and to_json(value) == self._property_lock[1]):
287 and to_json(value) == self._property_lock[1]):
288 return False
288 return False
289 elif self._send_state_lock > 0:
289 elif self._send_state_lock > 0:
290 self._states_to_send.add(key)
290 self._states_to_send.add(key)
291 return False
291 return False
292 else:
292 else:
293 return True
293 return True
294
294
295 # Event handlers
295 # Event handlers
296 @_show_traceback
296 @_show_traceback
297 def _handle_msg(self, msg):
297 def _handle_msg(self, msg):
298 """Called when a msg is received from the front-end"""
298 """Called when a msg is received from the front-end"""
299 data = msg['content']['data']
299 data = msg['content']['data']
300 method = data['method']
300 method = data['method']
301 if not method in ['backbone', 'custom']:
301 if not method in ['backbone', 'custom']:
302 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
302 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
303
303
304 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
304 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
305 if method == 'backbone' and 'sync_data' in data:
305 if method == 'backbone' and 'sync_data' in data:
306 sync_data = data['sync_data']
306 sync_data = data['sync_data']
307 self._handle_receive_state(sync_data) # handles all methods
307 self._handle_receive_state(sync_data) # handles all methods
308
308
309 # Handle a custom msg from the front-end
309 # Handle a custom msg from the front-end
310 elif method == 'custom':
310 elif method == 'custom':
311 if 'content' in data:
311 if 'content' in data:
312 self._handle_custom_msg(data['content'])
312 self._handle_custom_msg(data['content'])
313
313
314 def _handle_receive_state(self, sync_data):
314 def _handle_receive_state(self, sync_data):
315 """Called when a state is received from the front-end."""
315 """Called when a state is received from the front-end."""
316 for name in self.keys:
316 for name in self.keys:
317 if name in sync_data:
317 if name in sync_data:
318 json_value = sync_data[name]
318 json_value = sync_data[name]
319 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
319 from_json = self.trait_metadata(name, 'from_json', self._trait_from_json)
320 with self._lock_property(name, json_value):
320 with self._lock_property(name, json_value):
321 setattr(self, name, from_json(json_value))
321 setattr(self, name, from_json(json_value))
322
322
323 def _handle_custom_msg(self, content):
323 def _handle_custom_msg(self, content):
324 """Called when a custom msg is received."""
324 """Called when a custom msg is received."""
325 self._msg_callbacks(self, content)
325 self._msg_callbacks(self, content)
326
326
327 def _handle_property_changed(self, name, old, new):
327 def _handle_property_changed(self, name, old, new):
328 """Called when a property has been changed."""
328 """Called when a property has been changed."""
329 # Make sure this isn't information that the front-end just sent us.
329 # Make sure this isn't information that the front-end just sent us.
330 if self._should_send_property(name, new):
330 if self._should_send_property(name, new):
331 # Send new state to front-end
331 # Send new state to front-end
332 self.send_state(key=name)
332 self.send_state(key=name)
333
333
334 def _handle_displayed(self, **kwargs):
334 def _handle_displayed(self, **kwargs):
335 """Called when a view has been displayed for this widget instance"""
335 """Called when a view has been displayed for this widget instance"""
336 self._display_callbacks(self, **kwargs)
336 self._display_callbacks(self, **kwargs)
337
337
338 def _trait_to_json(self, x):
338 def _trait_to_json(self, x):
339 """Convert a trait value to json
339 """Convert a trait value to json
340
340
341 Traverse lists/tuples and dicts and serialize their values as well.
341 Traverse lists/tuples and dicts and serialize their values as well.
342 Replace any widgets with their model_id
342 Replace any widgets with their model_id
343 """
343 """
344 if isinstance(x, dict):
344 if isinstance(x, dict):
345 return {k: self._trait_to_json(v) for k, v in x.items()}
345 return {k: self._trait_to_json(v) for k, v in x.items()}
346 elif isinstance(x, (list, tuple)):
346 elif isinstance(x, (list, tuple)):
347 return [self._trait_to_json(v) for v in x]
347 return [self._trait_to_json(v) for v in x]
348 elif isinstance(x, Widget):
348 elif isinstance(x, Widget):
349 return "IPY_MODEL_" + x.model_id
349 return "IPY_MODEL_" + x.model_id
350 else:
350 else:
351 return x # Value must be JSON-able
351 return x # Value must be JSON-able
352
352
353 def _trait_from_json(self, x):
353 def _trait_from_json(self, x):
354 """Convert json values to objects
354 """Convert json values to objects
355
355
356 Replace any strings representing valid model id values to Widget references.
356 Replace any strings representing valid model id values to Widget references.
357 """
357 """
358 if isinstance(x, dict):
358 if isinstance(x, dict):
359 return {k: self._trait_from_json(v) for k, v in x.items()}
359 return {k: self._trait_from_json(v) for k, v in x.items()}
360 elif isinstance(x, (list, tuple)):
360 elif isinstance(x, (list, tuple)):
361 return [self._trait_from_json(v) for v in x]
361 return [self._trait_from_json(v) for v in x]
362 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
362 elif isinstance(x, string_types) and x.startswith('IPY_MODEL_') and x[10:] in Widget.widgets:
363 # we want to support having child widgets at any level in a hierarchy
363 # we want to support having child widgets at any level in a hierarchy
364 # trusting that a widget UUID will not appear out in the wild
364 # trusting that a widget UUID will not appear out in the wild
365 return Widget.widgets[x]
365 return Widget.widgets[x]
366 else:
366 else:
367 return x
367 return x
368
368
369 def _ipython_display_(self, **kwargs):
369 def _ipython_display_(self, **kwargs):
370 """Called when `IPython.display.display` is called on the widget."""
370 """Called when `IPython.display.display` is called on the widget."""
371 # Show view. By sending a display message, the comm is opened and the
371 # Show view. By sending a display message, the comm is opened and the
372 # initial state is sent.
372 # initial state is sent.
373 self._send({"method": "display"})
373 self._send({"method": "display"})
374 self._handle_displayed(**kwargs)
374 self._handle_displayed(**kwargs)
375
375
376 def _send(self, msg):
376 def _send(self, msg):
377 """Sends a message to the model in the front-end."""
377 """Sends a message to the model in the front-end."""
378 self.comm.send(msg)
378 self.comm.send(msg)
379
379
380
380
381 class DOMWidget(Widget):
381 class DOMWidget(Widget):
382 visible = Bool(True, help="Whether the widget is visible.", sync=True)
382 visible = Bool(True, help="Whether the widget is visible.", sync=True)
383 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
383 _css = Tuple(sync=True, help="CSS property list: (selector, key, value)")
384 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
384 _dom_classes = Tuple(sync=True, help="DOM classes applied to widget.$el.")
385
385
386 width = CUnicode(sync=True)
386 width = CUnicode(sync=True)
387 height = CUnicode(sync=True)
387 height = CUnicode(sync=True)
388 padding = CUnicode(sync=True)
388 padding = CUnicode(sync=True)
389 margin = CUnicode(sync=True)
389 margin = CUnicode(sync=True)
390
390
391 color = Unicode(sync=True)
391 color = Unicode(sync=True)
392 background_color = Unicode(sync=True)
392 background_color = Unicode(sync=True)
393 border_color = Unicode(sync=True)
393 border_color = Unicode(sync=True)
394
394
395 border_width = CUnicode(sync=True)
395 border_width = CUnicode(sync=True)
396 border_radius = CUnicode(sync=True)
396 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
397 border_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_border-style.asp
397 'none',
398 'none',
398 'hidden',
399 'hidden',
399 'dotted',
400 'dotted',
400 'dashed',
401 'dashed',
401 'solid',
402 'solid',
402 'double',
403 'double',
403 'groove',
404 'groove',
404 'ridge',
405 'ridge',
405 'inset',
406 'inset',
406 'outset',
407 'outset',
407 'initial',
408 'initial',
408 'inherit', ''],
409 'inherit', ''],
409 default_value='', sync=True)
410 default_value='', sync=True)
410
411
411 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
412 font_style = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_font-style.asp
412 'normal',
413 'normal',
413 'italic',
414 'italic',
414 'oblique',
415 'oblique',
415 'initial',
416 'initial',
416 'inherit', ''],
417 'inherit', ''],
417 default_value='', sync=True)
418 default_value='', sync=True)
418 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
419 font_weight = CaselessStrEnum(values=[ # http://www.w3schools.com/cssref/pr_font_weight.asp
419 'normal',
420 'normal',
420 'bold',
421 'bold',
421 'bolder',
422 'bolder',
422 'lighter',
423 'lighter',
423 'initial',
424 'initial',
424 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
425 'inherit', ''] + [str(100 * (i+1)) for i in range(9)],
425 default_value='', sync=True)
426 default_value='', sync=True)
426 font_size = CUnicode(sync=True)
427 font_size = CUnicode(sync=True)
427 font_family = Unicode(sync=True)
428 font_family = Unicode(sync=True)
429
430 def __init__(self, *pargs, **kwargs):
431 super(DOMWidget, self).__init__(*pargs, **kwargs)
432
433 def _validate_border(name, old, new):
434 if new is not None and new != '':
435 if name != 'border_width' and not self.border_width:
436 self.border_width = 1
437 if name != 'border_style' and self.border_style == '':
438 self.border_style = 'solid'
439 self.on_trait_change(_validate_border, ['border_width', 'border_style', 'border_color'])
@@ -1,178 +1,179 b''
1 """Float class.
1 """Float class.
2
2
3 Represents an unbounded float using a widget.
3 Represents an unbounded float using a widget.
4 """
4 """
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Copyright (c) 2013, the IPython Development Team.
6 # Copyright (c) 2013, the IPython Development Team.
7 #
7 #
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9 #
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 from .widget import DOMWidget
16 from .widget import DOMWidget
17 from IPython.utils.traitlets import Unicode, CFloat, Bool, CaselessStrEnum, Tuple
17 from IPython.utils.traitlets import Unicode, CFloat, Bool, CaselessStrEnum, Tuple
18 from IPython.utils.warn import DeprecatedClass
18 from IPython.utils.warn import DeprecatedClass
19
19
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21 # Classes
21 # Classes
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 class _Float(DOMWidget):
23 class _Float(DOMWidget):
24 value = CFloat(0.0, help="Float value", sync=True)
24 value = CFloat(0.0, help="Float value", sync=True)
25 disabled = Bool(False, help="Enable or disable user changes", sync=True)
25 disabled = Bool(False, help="Enable or disable user changes", sync=True)
26 description = Unicode(help="Description of the value this widget represents", sync=True)
26 description = Unicode(help="Description of the value this widget represents", sync=True)
27
27
28
28
29 class _BoundedFloat(_Float):
29 class _BoundedFloat(_Float):
30 max = CFloat(100.0, help="Max value", sync=True)
30 max = CFloat(100.0, help="Max value", sync=True)
31 min = CFloat(0.0, help="Min value", sync=True)
31 min = CFloat(0.0, help="Min value", sync=True)
32 step = CFloat(0.1, help="Minimum step that the value can take (ignored by some views)", sync=True)
32 step = CFloat(0.1, help="Minimum step that the value can take (ignored by some views)", sync=True)
33
33
34 def __init__(self, *pargs, **kwargs):
34 def __init__(self, *pargs, **kwargs):
35 """Constructor"""
35 """Constructor"""
36 DOMWidget.__init__(self, *pargs, **kwargs)
36 DOMWidget.__init__(self, *pargs, **kwargs)
37 self._validate('value', None, self.value)
37 self._validate('value', None, self.value)
38 self.on_trait_change(self._validate, ['value', 'min', 'max'])
38 self.on_trait_change(self._validate, ['value', 'min', 'max'])
39
39
40 def _validate(self, name, old, new):
40 def _validate(self, name, old, new):
41 """Validate value, max, min."""
41 """Validate value, max, min."""
42 if self.min > new or new > self.max:
42 if self.min > new or new > self.max:
43 self.value = min(max(new, self.min), self.max)
43 self.value = min(max(new, self.min), self.max)
44
44
45
45
46 class FloatText(_Float):
46 class FloatText(_Float):
47 _view_name = Unicode('FloatTextView', sync=True)
47 _view_name = Unicode('FloatTextView', sync=True)
48
48
49
49
50 class BoundedFloatText(_BoundedFloat):
50 class BoundedFloatText(_BoundedFloat):
51 _view_name = Unicode('FloatTextView', sync=True)
51 _view_name = Unicode('FloatTextView', sync=True)
52
52
53
53
54 class FloatSlider(_BoundedFloat):
54 class FloatSlider(_BoundedFloat):
55 _view_name = Unicode('FloatSliderView', sync=True)
55 _view_name = Unicode('FloatSliderView', sync=True)
56 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
56 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
57 default_value='horizontal',
57 default_value='horizontal',
58 help="Vertical or horizontal.", allow_none=False, sync=True)
58 help="Vertical or horizontal.", allow_none=False, sync=True)
59 _range = Bool(False, help="Display a range selector", sync=True)
59 _range = Bool(False, help="Display a range selector", sync=True)
60 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
60 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
61 slider_color = Unicode(sync=True)
61 slider_color = Unicode(sync=True)
62
62
63
63
64 class FloatProgress(_BoundedFloat):
64 class FloatProgress(_BoundedFloat):
65 _view_name = Unicode('ProgressView', sync=True)
65 _view_name = Unicode('ProgressView', sync=True)
66
66
67 bar_style = CaselessStrEnum(
67 bar_style = CaselessStrEnum(
68 values=['success', 'info', 'warning', 'danger', ''],
68 values=['success', 'info', 'warning', 'danger', ''],
69 default_value='', allow_none=True, sync=True, help="""Use a
69 default_value='', allow_none=True, sync=True, help="""Use a
70 predefined styling for the progess bar.""")
70 predefined styling for the progess bar.""")
71
71
72 class _FloatRange(_Float):
72 class _FloatRange(_Float):
73 value = Tuple(CFloat, CFloat, default_value=(0.0, 1.0), help="Tuple of (lower, upper) bounds", sync=True)
73 value = Tuple(CFloat, CFloat, default_value=(0.0, 1.0), help="Tuple of (lower, upper) bounds", sync=True)
74 lower = CFloat(0.0, help="Lower bound", sync=False)
74 lower = CFloat(0.0, help="Lower bound", sync=False)
75 upper = CFloat(1.0, help="Upper bound", sync=False)
75 upper = CFloat(1.0, help="Upper bound", sync=False)
76
76
77 def __init__(self, *pargs, **kwargs):
77 def __init__(self, *pargs, **kwargs):
78 value_given = 'value' in kwargs
78 value_given = 'value' in kwargs
79 lower_given = 'lower' in kwargs
79 lower_given = 'lower' in kwargs
80 upper_given = 'upper' in kwargs
80 upper_given = 'upper' in kwargs
81 if value_given and (lower_given or upper_given):
81 if value_given and (lower_given or upper_given):
82 raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
82 raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
83 if lower_given != upper_given:
83 if lower_given != upper_given:
84 raise ValueError("Must specify both 'lower' and 'upper' for range widget")
84 raise ValueError("Must specify both 'lower' and 'upper' for range widget")
85
85
86 DOMWidget.__init__(self, *pargs, **kwargs)
86 DOMWidget.__init__(self, *pargs, **kwargs)
87
87
88 # ensure the traits match, preferring whichever (if any) was given in kwargs
88 # ensure the traits match, preferring whichever (if any) was given in kwargs
89 if value_given:
89 if value_given:
90 self.lower, self.upper = self.value
90 self.lower, self.upper = self.value
91 else:
91 else:
92 self.value = (self.lower, self.upper)
92 self.value = (self.lower, self.upper)
93
93
94 self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
94 self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
95
95
96 def _validate(self, name, old, new):
96 def _validate(self, name, old, new):
97 if name == 'value':
97 if name == 'value':
98 self.lower, self.upper = min(new), max(new)
98 self.lower, self.upper = min(new), max(new)
99 elif name == 'lower':
99 elif name == 'lower':
100 self.value = (new, self.value[1])
100 self.value = (new, self.value[1])
101 elif name == 'upper':
101 elif name == 'upper':
102 self.value = (self.value[0], new)
102 self.value = (self.value[0], new)
103
103
104 class _BoundedFloatRange(_FloatRange):
104 class _BoundedFloatRange(_FloatRange):
105 step = CFloat(1.0, help="Minimum step that the value can take (ignored by some views)", sync=True)
105 step = CFloat(1.0, help="Minimum step that the value can take (ignored by some views)", sync=True)
106 max = CFloat(100.0, help="Max value", sync=True)
106 max = CFloat(100.0, help="Max value", sync=True)
107 min = CFloat(0.0, help="Min value", sync=True)
107 min = CFloat(0.0, help="Min value", sync=True)
108
108
109 def __init__(self, *pargs, **kwargs):
109 def __init__(self, *pargs, **kwargs):
110 any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
110 any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
111 _FloatRange.__init__(self, *pargs, **kwargs)
111 _FloatRange.__init__(self, *pargs, **kwargs)
112
112
113 # ensure a minimal amount of sanity
113 # ensure a minimal amount of sanity
114 if self.min > self.max:
114 if self.min > self.max:
115 raise ValueError("min must be <= max")
115 raise ValueError("min must be <= max")
116
116
117 if any_value_given:
117 if any_value_given:
118 # if a value was given, clamp it within (min, max)
118 # if a value was given, clamp it within (min, max)
119 self._validate("value", None, self.value)
119 self._validate("value", None, self.value)
120 else:
120 else:
121 # otherwise, set it to 25-75% to avoid the handles overlapping
121 # otherwise, set it to 25-75% to avoid the handles overlapping
122 self.value = (0.75*self.min + 0.25*self.max,
122 self.value = (0.75*self.min + 0.25*self.max,
123 0.25*self.min + 0.75*self.max)
123 0.25*self.min + 0.75*self.max)
124 # callback already set for 'value', 'lower', 'upper'
124 # callback already set for 'value', 'lower', 'upper'
125 self.on_trait_change(self._validate, ['min', 'max'])
125 self.on_trait_change(self._validate, ['min', 'max'])
126
126
127
127
128 def _validate(self, name, old, new):
128 def _validate(self, name, old, new):
129 if name == "min":
129 if name == "min":
130 if new > self.max:
130 if new > self.max:
131 raise ValueError("setting min > max")
131 raise ValueError("setting min > max")
132 self.min = new
132 self.min = new
133 elif name == "max":
133 elif name == "max":
134 if new < self.min:
134 if new < self.min:
135 raise ValueError("setting max < min")
135 raise ValueError("setting max < min")
136 self.max = new
136 self.max = new
137
137
138 low, high = self.value
138 low, high = self.value
139 if name == "value":
139 if name == "value":
140 low, high = min(new), max(new)
140 low, high = min(new), max(new)
141 elif name == "upper":
141 elif name == "upper":
142 if new < self.lower:
142 if new < self.lower:
143 raise ValueError("setting upper < lower")
143 raise ValueError("setting upper < lower")
144 high = new
144 high = new
145 elif name == "lower":
145 elif name == "lower":
146 if new > self.upper:
146 if new > self.upper:
147 raise ValueError("setting lower > upper")
147 raise ValueError("setting lower > upper")
148 low = new
148 low = new
149
149
150 low = max(self.min, min(low, self.max))
150 low = max(self.min, min(low, self.max))
151 high = min(self.max, max(high, self.min))
151 high = min(self.max, max(high, self.min))
152
152
153 # determine the order in which we should update the
153 # determine the order in which we should update the
154 # lower, upper traits to avoid a temporary inverted overlap
154 # lower, upper traits to avoid a temporary inverted overlap
155 lower_first = high < self.lower
155 lower_first = high < self.lower
156
156
157 self.value = (low, high)
157 self.value = (low, high)
158 if lower_first:
158 if lower_first:
159 self.lower = low
159 self.lower = low
160 self.upper = high
160 self.upper = high
161 else:
161 else:
162 self.upper = high
162 self.upper = high
163 self.lower = low
163 self.lower = low
164
164
165
165
166 class FloatRangeSlider(_BoundedFloatRange):
166 class FloatRangeSlider(_BoundedFloatRange):
167 _view_name = Unicode('FloatSliderView', sync=True)
167 _view_name = Unicode('FloatSliderView', sync=True)
168 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
168 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
169 default_value='horizontal', allow_none=False,
169 default_value='horizontal', allow_none=False,
170 help="Vertical or horizontal.", sync=True)
170 help="Vertical or horizontal.", sync=True)
171 _range = Bool(True, help="Display a range selector", sync=True)
171 _range = Bool(True, help="Display a range selector", sync=True)
172 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
172 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
173 slider_color = Unicode(sync=True)
173
174
174 # Remove in IPython 4.0
175 # Remove in IPython 4.0
175 FloatTextWidget = DeprecatedClass(FloatText, 'FloatTextWidget')
176 FloatTextWidget = DeprecatedClass(FloatText, 'FloatTextWidget')
176 BoundedFloatTextWidget = DeprecatedClass(BoundedFloatText, 'BoundedFloatTextWidget')
177 BoundedFloatTextWidget = DeprecatedClass(BoundedFloatText, 'BoundedFloatTextWidget')
177 FloatSliderWidget = DeprecatedClass(FloatSlider, 'FloatSliderWidget')
178 FloatSliderWidget = DeprecatedClass(FloatSlider, 'FloatSliderWidget')
178 FloatProgressWidget = DeprecatedClass(FloatProgress, 'FloatProgressWidget')
179 FloatProgressWidget = DeprecatedClass(FloatProgress, 'FloatProgressWidget')
@@ -1,182 +1,183 b''
1 """Int class.
1 """Int class.
2
2
3 Represents an unbounded int using a widget.
3 Represents an unbounded int using a widget.
4 """
4 """
5 #-----------------------------------------------------------------------------
5 #-----------------------------------------------------------------------------
6 # Copyright (c) 2013, the IPython Development Team.
6 # Copyright (c) 2013, the IPython Development Team.
7 #
7 #
8 # Distributed under the terms of the Modified BSD License.
8 # Distributed under the terms of the Modified BSD License.
9 #
9 #
10 # The full license is in the file COPYING.txt, distributed with this software.
10 # The full license is in the file COPYING.txt, distributed with this software.
11 #-----------------------------------------------------------------------------
11 #-----------------------------------------------------------------------------
12
12
13 #-----------------------------------------------------------------------------
13 #-----------------------------------------------------------------------------
14 # Imports
14 # Imports
15 #-----------------------------------------------------------------------------
15 #-----------------------------------------------------------------------------
16 from .widget import DOMWidget
16 from .widget import DOMWidget
17 from IPython.utils.traitlets import Unicode, CInt, Bool, CaselessStrEnum, Tuple
17 from IPython.utils.traitlets import Unicode, CInt, Bool, CaselessStrEnum, Tuple
18 from IPython.utils.warn import DeprecatedClass
18 from IPython.utils.warn import DeprecatedClass
19
19
20 #-----------------------------------------------------------------------------
20 #-----------------------------------------------------------------------------
21 # Classes
21 # Classes
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 class _Int(DOMWidget):
23 class _Int(DOMWidget):
24 """Base class used to create widgets that represent an int."""
24 """Base class used to create widgets that represent an int."""
25 value = CInt(0, help="Int value", sync=True)
25 value = CInt(0, help="Int value", sync=True)
26 disabled = Bool(False, help="Enable or disable user changes", sync=True)
26 disabled = Bool(False, help="Enable or disable user changes", sync=True)
27 description = Unicode(help="Description of the value this widget represents", sync=True)
27 description = Unicode(help="Description of the value this widget represents", sync=True)
28
28
29
29
30 class _BoundedInt(_Int):
30 class _BoundedInt(_Int):
31 """Base class used to create widgets that represent a int that is bounded
31 """Base class used to create widgets that represent a int that is bounded
32 by a minium and maximum."""
32 by a minium and maximum."""
33 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
33 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
34 max = CInt(100, help="Max value", sync=True)
34 max = CInt(100, help="Max value", sync=True)
35 min = CInt(0, help="Min value", sync=True)
35 min = CInt(0, help="Min value", sync=True)
36
36
37 def __init__(self, *pargs, **kwargs):
37 def __init__(self, *pargs, **kwargs):
38 """Constructor"""
38 """Constructor"""
39 DOMWidget.__init__(self, *pargs, **kwargs)
39 DOMWidget.__init__(self, *pargs, **kwargs)
40 self.on_trait_change(self._validate, ['value', 'min', 'max'])
40 self.on_trait_change(self._validate, ['value', 'min', 'max'])
41
41
42 def _validate(self, name, old, new):
42 def _validate(self, name, old, new):
43 """Validate value, max, min."""
43 """Validate value, max, min."""
44 if self.min > new or new > self.max:
44 if self.min > new or new > self.max:
45 self.value = min(max(new, self.min), self.max)
45 self.value = min(max(new, self.min), self.max)
46
46
47
47
48 class IntText(_Int):
48 class IntText(_Int):
49 """Textbox widget that represents a int."""
49 """Textbox widget that represents a int."""
50 _view_name = Unicode('IntTextView', sync=True)
50 _view_name = Unicode('IntTextView', sync=True)
51
51
52
52
53 class BoundedIntText(_BoundedInt):
53 class BoundedIntText(_BoundedInt):
54 """Textbox widget that represents a int bounded by a minimum and maximum value."""
54 """Textbox widget that represents a int bounded by a minimum and maximum value."""
55 _view_name = Unicode('IntTextView', sync=True)
55 _view_name = Unicode('IntTextView', sync=True)
56
56
57
57
58 class IntSlider(_BoundedInt):
58 class IntSlider(_BoundedInt):
59 """Slider widget that represents a int bounded by a minimum and maximum value."""
59 """Slider widget that represents a int bounded by a minimum and maximum value."""
60 _view_name = Unicode('IntSliderView', sync=True)
60 _view_name = Unicode('IntSliderView', sync=True)
61 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
61 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
62 default_value='horizontal', allow_none=False,
62 default_value='horizontal', allow_none=False,
63 help="Vertical or horizontal.", sync=True)
63 help="Vertical or horizontal.", sync=True)
64 _range = Bool(False, help="Display a range selector", sync=True)
64 _range = Bool(False, help="Display a range selector", sync=True)
65 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
65 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
66 slider_color = Unicode(sync=True)
66 slider_color = Unicode(sync=True)
67
67
68
68
69 class IntProgress(_BoundedInt):
69 class IntProgress(_BoundedInt):
70 """Progress bar that represents a int bounded by a minimum and maximum value."""
70 """Progress bar that represents a int bounded by a minimum and maximum value."""
71 _view_name = Unicode('ProgressView', sync=True)
71 _view_name = Unicode('ProgressView', sync=True)
72
72
73 bar_style = CaselessStrEnum(
73 bar_style = CaselessStrEnum(
74 values=['success', 'info', 'warning', 'danger', ''],
74 values=['success', 'info', 'warning', 'danger', ''],
75 default_value='', allow_none=True, sync=True, help="""Use a
75 default_value='', allow_none=True, sync=True, help="""Use a
76 predefined styling for the progess bar.""")
76 predefined styling for the progess bar.""")
77
77
78 class _IntRange(_Int):
78 class _IntRange(_Int):
79 value = Tuple(CInt, CInt, default_value=(0, 1), help="Tuple of (lower, upper) bounds", sync=True)
79 value = Tuple(CInt, CInt, default_value=(0, 1), help="Tuple of (lower, upper) bounds", sync=True)
80 lower = CInt(0, help="Lower bound", sync=False)
80 lower = CInt(0, help="Lower bound", sync=False)
81 upper = CInt(1, help="Upper bound", sync=False)
81 upper = CInt(1, help="Upper bound", sync=False)
82
82
83 def __init__(self, *pargs, **kwargs):
83 def __init__(self, *pargs, **kwargs):
84 value_given = 'value' in kwargs
84 value_given = 'value' in kwargs
85 lower_given = 'lower' in kwargs
85 lower_given = 'lower' in kwargs
86 upper_given = 'upper' in kwargs
86 upper_given = 'upper' in kwargs
87 if value_given and (lower_given or upper_given):
87 if value_given and (lower_given or upper_given):
88 raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
88 raise ValueError("Cannot specify both 'value' and 'lower'/'upper' for range widget")
89 if lower_given != upper_given:
89 if lower_given != upper_given:
90 raise ValueError("Must specify both 'lower' and 'upper' for range widget")
90 raise ValueError("Must specify both 'lower' and 'upper' for range widget")
91
91
92 DOMWidget.__init__(self, *pargs, **kwargs)
92 DOMWidget.__init__(self, *pargs, **kwargs)
93
93
94 # ensure the traits match, preferring whichever (if any) was given in kwargs
94 # ensure the traits match, preferring whichever (if any) was given in kwargs
95 if value_given:
95 if value_given:
96 self.lower, self.upper = self.value
96 self.lower, self.upper = self.value
97 else:
97 else:
98 self.value = (self.lower, self.upper)
98 self.value = (self.lower, self.upper)
99
99
100 self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
100 self.on_trait_change(self._validate, ['value', 'upper', 'lower'])
101
101
102 def _validate(self, name, old, new):
102 def _validate(self, name, old, new):
103 if name == 'value':
103 if name == 'value':
104 self.lower, self.upper = min(new), max(new)
104 self.lower, self.upper = min(new), max(new)
105 elif name == 'lower':
105 elif name == 'lower':
106 self.value = (new, self.value[1])
106 self.value = (new, self.value[1])
107 elif name == 'upper':
107 elif name == 'upper':
108 self.value = (self.value[0], new)
108 self.value = (self.value[0], new)
109
109
110 class _BoundedIntRange(_IntRange):
110 class _BoundedIntRange(_IntRange):
111 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
111 step = CInt(1, help="Minimum step that the value can take (ignored by some views)", sync=True)
112 max = CInt(100, help="Max value", sync=True)
112 max = CInt(100, help="Max value", sync=True)
113 min = CInt(0, help="Min value", sync=True)
113 min = CInt(0, help="Min value", sync=True)
114
114
115 def __init__(self, *pargs, **kwargs):
115 def __init__(self, *pargs, **kwargs):
116 any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
116 any_value_given = 'value' in kwargs or 'upper' in kwargs or 'lower' in kwargs
117 _IntRange.__init__(self, *pargs, **kwargs)
117 _IntRange.__init__(self, *pargs, **kwargs)
118
118
119 # ensure a minimal amount of sanity
119 # ensure a minimal amount of sanity
120 if self.min > self.max:
120 if self.min > self.max:
121 raise ValueError("min must be <= max")
121 raise ValueError("min must be <= max")
122
122
123 if any_value_given:
123 if any_value_given:
124 # if a value was given, clamp it within (min, max)
124 # if a value was given, clamp it within (min, max)
125 self._validate("value", None, self.value)
125 self._validate("value", None, self.value)
126 else:
126 else:
127 # otherwise, set it to 25-75% to avoid the handles overlapping
127 # otherwise, set it to 25-75% to avoid the handles overlapping
128 self.value = (0.75*self.min + 0.25*self.max,
128 self.value = (0.75*self.min + 0.25*self.max,
129 0.25*self.min + 0.75*self.max)
129 0.25*self.min + 0.75*self.max)
130 # callback already set for 'value', 'lower', 'upper'
130 # callback already set for 'value', 'lower', 'upper'
131 self.on_trait_change(self._validate, ['min', 'max'])
131 self.on_trait_change(self._validate, ['min', 'max'])
132
132
133 def _validate(self, name, old, new):
133 def _validate(self, name, old, new):
134 if name == "min":
134 if name == "min":
135 if new > self.max:
135 if new > self.max:
136 raise ValueError("setting min > max")
136 raise ValueError("setting min > max")
137 self.min = new
137 self.min = new
138 elif name == "max":
138 elif name == "max":
139 if new < self.min:
139 if new < self.min:
140 raise ValueError("setting max < min")
140 raise ValueError("setting max < min")
141 self.max = new
141 self.max = new
142
142
143 low, high = self.value
143 low, high = self.value
144 if name == "value":
144 if name == "value":
145 low, high = min(new), max(new)
145 low, high = min(new), max(new)
146 elif name == "upper":
146 elif name == "upper":
147 if new < self.lower:
147 if new < self.lower:
148 raise ValueError("setting upper < lower")
148 raise ValueError("setting upper < lower")
149 high = new
149 high = new
150 elif name == "lower":
150 elif name == "lower":
151 if new > self.upper:
151 if new > self.upper:
152 raise ValueError("setting lower > upper")
152 raise ValueError("setting lower > upper")
153 low = new
153 low = new
154
154
155 low = max(self.min, min(low, self.max))
155 low = max(self.min, min(low, self.max))
156 high = min(self.max, max(high, self.min))
156 high = min(self.max, max(high, self.min))
157
157
158 # determine the order in which we should update the
158 # determine the order in which we should update the
159 # lower, upper traits to avoid a temporary inverted overlap
159 # lower, upper traits to avoid a temporary inverted overlap
160 lower_first = high < self.lower
160 lower_first = high < self.lower
161
161
162 self.value = (low, high)
162 self.value = (low, high)
163 if lower_first:
163 if lower_first:
164 self.lower = low
164 self.lower = low
165 self.upper = high
165 self.upper = high
166 else:
166 else:
167 self.upper = high
167 self.upper = high
168 self.lower = low
168 self.lower = low
169
169
170 class IntRangeSlider(_BoundedIntRange):
170 class IntRangeSlider(_BoundedIntRange):
171 _view_name = Unicode('IntSliderView', sync=True)
171 _view_name = Unicode('IntSliderView', sync=True)
172 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
172 orientation = CaselessStrEnum(values=['horizontal', 'vertical'],
173 default_value='horizontal', allow_none=False,
173 default_value='horizontal', allow_none=False,
174 help="Vertical or horizontal.", sync=True)
174 help="Vertical or horizontal.", sync=True)
175 _range = Bool(True, help="Display a range selector", sync=True)
175 _range = Bool(True, help="Display a range selector", sync=True)
176 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
176 readout = Bool(True, help="Display the current value of the slider next to it.", sync=True)
177 slider_color = Unicode(sync=True)
177
178
178 # Remove in IPython 4.0
179 # Remove in IPython 4.0
179 IntTextWidget = DeprecatedClass(IntText, 'IntTextWidget')
180 IntTextWidget = DeprecatedClass(IntText, 'IntTextWidget')
180 BoundedIntTextWidget = DeprecatedClass(BoundedIntText, 'BoundedIntTextWidget')
181 BoundedIntTextWidget = DeprecatedClass(BoundedIntText, 'BoundedIntTextWidget')
181 IntSliderWidget = DeprecatedClass(IntSlider, 'IntSliderWidget')
182 IntSliderWidget = DeprecatedClass(IntSlider, 'IntSliderWidget')
182 IntProgressWidget = DeprecatedClass(IntProgress, 'IntProgressWidget')
183 IntProgressWidget = DeprecatedClass(IntProgress, 'IntProgressWidget')
General Comments 0
You need to be logged in to leave comments. Login now