##// END OF EJS Templates
Fixed context errors and a couple of typos to get the tests working again
Jonathan Frederic -
Show More
@@ -1,189 +1,189 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // WidgetModel, WidgetView, and WidgetManager
9 // WidgetModel, WidgetView, and WidgetManager
10 //============================================================================
10 //============================================================================
11 /**
11 /**
12 * Base Widget classes
12 * Base Widget classes
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 * @submodule widget
15 * @submodule widget
16 */
16 */
17
17
18 (function () {
18 (function () {
19 "use strict";
19 "use strict";
20
20
21 // Use require.js 'define' method so that require.js is intelligent enough to
21 // Use require.js 'define' method so that require.js is intelligent enough to
22 // syncronously load everything within this file when it is being 'required'
22 // syncronously load everything within this file when it is being 'required'
23 // elsewhere.
23 // elsewhere.
24 define(["underscore",
24 define(["underscore",
25 "backbone",
25 "backbone",
26 ], function (Underscore, Backbone) {
26 ], function (Underscore, Backbone) {
27
27
28 //--------------------------------------------------------------------
28 //--------------------------------------------------------------------
29 // WidgetManager class
29 // WidgetManager class
30 //--------------------------------------------------------------------
30 //--------------------------------------------------------------------
31 var WidgetManager = function (comm_manager) {
31 var WidgetManager = function (comm_manager) {
32 // Public constructor
32 // Public constructor
33 WidgetManager._managers.push(this);
33 WidgetManager._managers.push(this);
34
34
35 // Attach a comm manager to the
35 // Attach a comm manager to the
36 this.comm_manager = comm_manager;
36 this.comm_manager = comm_manager;
37 this._models = {}; /* Dictionary of model ids and model instances */
37 this._models = {}; /* Dictionary of model ids and model instances */
38
38
39 // Register already-registered widget model types with the comm manager.
39 // Register already-registered widget model types with the comm manager.
40 var that = this;
40 var that = this;
41 _.each(WidgetManager._model_types, function(value, key) {
41 _.each(WidgetManager._model_types, function(model_type, model_name) {
42 that.comm_manager.register_target(value, $.proxy(that._handle_comm_open, that));
42 that.comm_manager.register_target(model_name, $.proxy(that._handle_comm_open, that));
43 });
43 });
44 };
44 };
45
45
46 //--------------------------------------------------------------------
46 //--------------------------------------------------------------------
47 // Class level
47 // Class level
48 //--------------------------------------------------------------------
48 //--------------------------------------------------------------------
49 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
49 WidgetManager._model_types = {}; /* Dictionary of model type names (target_name) and model types. */
50 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
50 WidgetManager._view_types = {}; /* Dictionary of view names and view types. */
51 WidgetManager._managers = []; /* List of widget managers */
51 WidgetManager._managers = []; /* List of widget managers */
52
52
53 WidgetManager.register_widget_model = function (model_name, model_type) {
53 WidgetManager.register_widget_model = function (model_name, model_type) {
54 // Registers a widget model by name.
54 // Registers a widget model by name.
55 WidgetManager._model_types[model_name] = model_type;
55 WidgetManager._model_types[model_name] = model_type;
56
56
57 // Register the widget with the comm manager. Make sure to pass this object's context
57 // Register the widget with the comm manager. Make sure to pass this object's context
58 // in so `this` works in the call back.
58 // in so `this` works in the call back.
59 _.each(WidgetManager._managers, function(instance, i) {
59 _.each(WidgetManager._managers, function(instance, i) {
60 if (instance.comm_manager !== null) {
60 if (instance.comm_manager !== null) {
61 instance.comm_manager.register_target(model_name, $.proxy(instance._handle_comm_open, instance));
61 instance.comm_manager.register_target(model_name, $.proxy(instance._handle_comm_open, instance));
62 }
62 }
63 });
63 });
64 };
64 };
65
65
66 WidgetManager.register_widget_view = function (view_name, view_type) {
66 WidgetManager.register_widget_view = function (view_name, view_type) {
67 // Registers a widget view by name.
67 // Registers a widget view by name.
68 WidgetManager._view_types[view_name] = view_type;
68 WidgetManager._view_types[view_name] = view_type;
69 };
69 };
70
70
71 //--------------------------------------------------------------------
71 //--------------------------------------------------------------------
72 // Instance level
72 // Instance level
73 //--------------------------------------------------------------------
73 //--------------------------------------------------------------------
74 WidgetManager.prototype.display_view = function(msg, model) {
74 WidgetManager.prototype.display_view = function(msg, model) {
75 var cell = this.get_msg_cell(msg.parent_header.msg_id);
75 var cell = this.get_msg_cell(msg.parent_header.msg_id);
76 if (cell === null) {
76 if (cell === null) {
77 console.log("Could not determine where the display" +
77 console.log("Could not determine where the display" +
78 " message was from. Widget will not be displayed");
78 " message was from. Widget will not be displayed");
79 } else {
79 } else {
80 var view = this.create_view(model, {cell: cell});
80 var view = this.create_view(model, {cell: cell});
81 if (view === null) {
81 if (view === null) {
82 console.error("View creation failed", model);
82 console.error("View creation failed", model);
83 }
83 }
84 if (cell.widget_subarea !== undefined
84 if (cell.widget_subarea !== undefined
85 && cell.widget_subarea !== null) {
85 && cell.widget_subarea !== null) {
86
86
87 cell.widget_area.show();
87 cell.widget_area.show();
88 cell.widget_subarea.append(view.$el);
88 cell.widget_subarea.append(view.$el);
89 }
89 }
90 }
90 }
91 },
91 },
92
92
93 WidgetManager.prototype.create_view = function(model, options) {
93 WidgetManager.prototype.create_view = function(model, options) {
94 var view_name = model.get('view_name');
94 var view_name = model.get('view_name');
95 var ViewType = WidgetManager._view_types[view_name];
95 var ViewType = WidgetManager._view_types[view_name];
96 if (ViewType !== undefined && ViewType !== null) {
96 if (ViewType !== undefined && ViewType !== null) {
97 var parameters = {model: model, options: options};
97 var parameters = {model: model, options: options};
98 var view = new ViewType(parameters);
98 var view = new ViewType(parameters);
99 view.render();
99 view.render();
100 IPython.keyboard_manager.register_events(view.$el);
100 IPython.keyboard_manager.register_events(view.$el);
101 model.views.push(view);
101 model.views.push(view);
102 model.on('destroy', view.remove, view);
102 model.on('destroy', view.remove, view);
103 return view;
103 return view;
104 }
104 }
105 return null;
105 return null;
106 },
106 },
107
107
108 WidgetManager.prototype.get_msg_cell = function (msg_id) {
108 WidgetManager.prototype.get_msg_cell = function (msg_id) {
109 var cell = null;
109 var cell = null;
110 // First, check to see if the msg was triggered by cell execution.
110 // First, check to see if the msg was triggered by cell execution.
111 if (IPython.notebook !== undefined && IPython.notebook !== null) {
111 if (IPython.notebook !== undefined && IPython.notebook !== null) {
112 cell = IPython.notebook.get_msg_cell(msg_id);
112 cell = IPython.notebook.get_msg_cell(msg_id);
113 }
113 }
114 if (cell !== null) {
114 if (cell !== null) {
115 return cell
115 return cell
116 }
116 }
117 // Second, check to see if a get_cell callback was defined
117 // Second, check to see if a get_cell callback was defined
118 // for the message. get_cell callbacks are registered for
118 // for the message. get_cell callbacks are registered for
119 // widget messages, so this block is actually checking to see if the
119 // widget messages, so this block is actually checking to see if the
120 // message was triggered by a widget.
120 // message was triggered by a widget.
121 var kernel = this.comm_manager.kernel;
121 var kernel = this.comm_manager.kernel;
122 if (kernel !== undefined && kernel !== null) {
122 if (kernel !== undefined && kernel !== null) {
123 var callbacks = kernel.get_callbacks_for_msg(msg_id);
123 var callbacks = kernel.get_callbacks_for_msg(msg_id);
124 if (callbacks !== undefined &&
124 if (callbacks !== undefined &&
125 callbacks.iopub !== undefined &&
125 callbacks.iopub !== undefined &&
126 callbacks.iopub.get_cell !== undefined) {
126 callbacks.iopub.get_cell !== undefined) {
127
127
128 return callbacks.iopub.get_cell();
128 return callbacks.iopub.get_cell();
129 }
129 }
130 }
130 }
131
131
132 // Not triggered by a cell or widget (no get_cell callback
132 // Not triggered by a cell or widget (no get_cell callback
133 // exists).
133 // exists).
134 return null;
134 return null;
135 };
135 };
136
136
137 WidgetManager.prototype.callbacks = function (view) {
137 WidgetManager.prototype.callbacks = function (view) {
138 // callback handlers specific a view
138 // callback handlers specific a view
139 var callbacks = {};
139 var callbacks = {};
140 var cell = view.options.cell;
140 var cell = view.options.cell;
141 if (cell !== null) {
141 if (cell !== null) {
142 // Try to get output handlers
142 // Try to get output handlers
143 var handle_output = null;
143 var handle_output = null;
144 var handle_clear_output = null;
144 var handle_clear_output = null;
145 if (cell.output_area !== undefined && cell.output_area !== null) {
145 if (cell.output_area !== undefined && cell.output_area !== null) {
146 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
146 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
147 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
147 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
148 }
148 }
149
149
150 // Create callback dict using what is known
150 // Create callback dict using what is known
151 var that = this;
151 var that = this;
152 callbacks = {
152 callbacks = {
153 iopub : {
153 iopub : {
154 output : handle_output,
154 output : handle_output,
155 clear_output : handle_clear_output,
155 clear_output : handle_clear_output,
156
156
157 // Special function only registered by widget messages.
157 // Special function only registered by widget messages.
158 // Allows us to get the cell for a message so we know
158 // Allows us to get the cell for a message so we know
159 // where to add widgets if the code requires it.
159 // where to add widgets if the code requires it.
160 get_cell : function () {
160 get_cell : function () {
161 return cell;
161 return cell;
162 },
162 },
163 },
163 },
164 };
164 };
165 }
165 }
166 return callbacks;
166 return callbacks;
167 };
167 };
168
168
169 WidgetManager.prototype.get_model = function (model_id) {
169 WidgetManager.prototype.get_model = function (model_id) {
170 // Look-up a model instance by its id.
170 // Look-up a model instance by its id.
171 var model = this._models[model_id];
171 var model = this._models[model_id];
172 if (model !== undefined && model.id == model_id) {
172 if (model !== undefined && model.id == model_id) {
173 return model;
173 return model;
174 }
174 }
175 return null;
175 return null;
176 };
176 };
177
177
178 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
178 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
179 // Handle when a comm is opened.
179 // Handle when a comm is opened.
180 var model_id = comm.comm_id;
180 var model_id = comm.comm_id;
181 var widget_type_name = msg.content.target_name;
181 var widget_type_name = msg.content.target_name;
182 var widget_model = new WidgetManager._model_types[widget_type_name](this, model_id, comm);
182 var widget_model = new WidgetManager._model_types[widget_type_name](this, model_id, comm);
183 this._models[model_id] = widget_model;
183 this._models[model_id] = widget_model;
184 };
184 };
185
185
186 IPython.WidgetManager = WidgetManager;
186 IPython.WidgetManager = WidgetManager;
187 return IPython.WidgetManager;
187 return IPython.WidgetManager;
188 });
188 });
189 }());
189 }());
@@ -1,407 +1,411 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // Base Widget Model and View classes
9 // Base Widget Model and View classes
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["notebook/js/widgetmanager",
17 define(["notebook/js/widgetmanager",
18 "underscore",
18 "underscore",
19 "backbone"],
19 "backbone"],
20 function(WidgetManager, Underscore, Backbone){
20 function(WidgetManager, Underscore, Backbone){
21
21
22 var WidgetModel = Backbone.Model.extend({
22 var WidgetModel = Backbone.Model.extend({
23 constructor: function (widget_manager, model_id, comm) {
23 constructor: function (widget_manager, model_id, comm) {
24 // Construcctor
24 // Construcctor
25 //
25 //
26 // Creates a WidgetModel instance.
26 // Creates a WidgetModel instance.
27 //
27 //
28 // Parameters
28 // Parameters
29 // ----------
29 // ----------
30 // widget_manager : WidgetManager instance
30 // widget_manager : WidgetManager instance
31 // model_id : string
31 // model_id : string
32 // An ID unique to this model.
32 // An ID unique to this model.
33 // comm : Comm instance (optional)
33 // comm : Comm instance (optional)
34 this.widget_manager = widget_manager;
34 this.widget_manager = widget_manager;
35 this.pending_msgs = 0;
35 this.pending_msgs = 0;
36 this.msg_throttle = 2;
36 this.msg_throttle = 2;
37 this.msg_buffer = null;
37 this.msg_buffer = null;
38 this.key_value_lock = null;
38 this.key_value_lock = null;
39 this.id = model_id;
39 this.id = model_id;
40 this.views = [];
40 this.views = [];
41
41
42 if (comm !== undefined) {
42 if (comm !== undefined) {
43 // Remember comm associated with the model.
43 // Remember comm associated with the model.
44 this.comm = comm;
44 this.comm = comm;
45 comm.model = this;
45 comm.model = this;
46
46
47 // Hook comm messages up to model.
47 // Hook comm messages up to model.
48 comm.on_close($.proxy(this._handle_comm_closed, this));
48 comm.on_close($.proxy(this._handle_comm_closed, this));
49 comm.on_msg($.proxy(this._handle_comm_msg, this));
49 comm.on_msg($.proxy(this._handle_comm_msg, this));
50 }
50 }
51 return Backbone.Model.apply(this);
51 return Backbone.Model.apply(this);
52 },
52 },
53
53
54 send: function (content, callbacks) {
54 send: function (content, callbacks) {
55 // Send a custom msg over the comm.
55 // Send a custom msg over the comm.
56 if (this.comm !== undefined) {
56 if (this.comm !== undefined) {
57 var data = {method: 'custom', content: content};
57 var data = {method: 'custom', content: content};
58 this.comm.send(data, callbacks);
58 this.comm.send(data, callbacks);
59 }
59 }
60 },
60 },
61
61
62 _handle_comm_closed: function (msg) {
62 _handle_comm_closed: function (msg) {
63 // Handle when a widget is closed.
63 // Handle when a widget is closed.
64 this.trigger('comm:close');
64 this.trigger('comm:close');
65 delete this.comm.model; // Delete ref so GC will collect widget model.
65 delete this.comm.model; // Delete ref so GC will collect widget model.
66 delete this.comm;
66 delete this.comm;
67 delete this.model_id; // Delete id from model so widget manager cleans up.
67 delete this.model_id; // Delete id from model so widget manager cleans up.
68 // TODO: Handle deletion, like this.destroy(), and delete views, etc.
68 // TODO: Handle deletion, like this.destroy(), and delete views, etc.
69 },
69 },
70
70
71 _handle_comm_msg: function (msg) {
71 _handle_comm_msg: function (msg) {
72 // Handle incoming comm msg.
72 // Handle incoming comm msg.
73 var method = msg.content.data.method;
73 var method = msg.content.data.method;
74 switch (method) {
74 switch (method) {
75 case 'update':
75 case 'update':
76 this.apply_update(msg.content.data.state);
76 this.apply_update(msg.content.data.state);
77 break;
77 break;
78 case 'custom':
78 case 'custom':
79 this.trigger('msg:custom', msg.content.data.content);
79 this.trigger('msg:custom', msg.content.data.content);
80 break;
80 break;
81 case 'display':
81 case 'display':
82 this.widget_manager.display_view(msg, this);
82 this.widget_manager.display_view(msg, this);
83 break;
83 break;
84 }
84 }
85 },
85 },
86
86
87 apply_update: function (state) {
87 apply_update: function (state) {
88 // Handle when a widget is updated via the python side.
88 // Handle when a widget is updated via the python side.
89 var that = this;
89 _.each(state, function(value, key) {
90 _.each(state, function(value, key) {
90 this.key_value_lock = [key, value];
91 that.key_value_lock = [key, value];
91 try {
92 try {
92 this.set(key, this._unpack_models(value));
93 that.set(key, that._unpack_models(value));
93 } finally {
94 } finally {
94 this.key_value_lock = null;
95 that.key_value_lock = null;
95 }
96 }
96 });
97 });
97 },
98 },
98
99
99 _handle_status: function (msg, callbacks) {
100 _handle_status: function (msg, callbacks) {
100 // Handle status msgs.
101 // Handle status msgs.
101
102
102 // execution_state : ('busy', 'idle', 'starting')
103 // execution_state : ('busy', 'idle', 'starting')
103 if (this.comm !== undefined) {
104 if (this.comm !== undefined) {
104 if (msg.content.execution_state ==='idle') {
105 if (msg.content.execution_state ==='idle') {
105 // Send buffer if this message caused another message to be
106 // Send buffer if this message caused another message to be
106 // throttled.
107 // throttled.
107 if (this.msg_buffer !== null &&
108 if (this.msg_buffer !== null &&
108 this.msg_throttle === this.pending_msgs) {
109 this.msg_throttle === this.pending_msgs) {
109 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
110 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
110 this.comm.send(data, callbacks);
111 this.comm.send(data, callbacks);
111 this.msg_buffer = null;
112 this.msg_buffer = null;
112 } else {
113 } else {
113 --this.pending_msgs;
114 --this.pending_msgs;
114 }
115 }
115 }
116 }
116 }
117 }
117 },
118 },
118
119
119 callbacks: function(view) {
120 callbacks: function(view) {
120 // Create msg callbacks for a comm msg.
121 // Create msg callbacks for a comm msg.
121 var callbacks = this.widget_manager.callbacks(view);
122 var callbacks = this.widget_manager.callbacks(view);
122
123
123 if (callbacks.iopub === undefined) {
124 if (callbacks.iopub === undefined) {
124 callbacks.iopub = {};
125 callbacks.iopub = {};
125 }
126 }
126
127
127 var that = this;
128 var that = this;
128 callbacks.iopub.status = function (msg) {
129 callbacks.iopub.status = function (msg) {
129 that._handle_status(msg, callbacks);
130 that._handle_status(msg, callbacks);
130 }
131 }
131 return callbacks;
132 return callbacks;
132 },
133 },
133
134
134 sync: function (method, model, options) {
135 sync: function (method, model, options) {
135 // Handle sync to the back-end. Called when a model.save() is called.
136 // Handle sync to the back-end. Called when a model.save() is called.
136
137
137 // Make sure a comm exists.
138 // Make sure a comm exists.
138 var error = options.error || function() {
139 var error = options.error || function() {
139 console.error('Backbone sync error:', arguments);
140 console.error('Backbone sync error:', arguments);
140 }
141 }
141 if (this.comm === undefined) {
142 if (this.comm === undefined) {
142 error();
143 error();
143 return false;
144 return false;
144 }
145 }
145
146
146 // Delete any key value pairs that the back-end already knows about.
147 // Delete any key value pairs that the back-end already knows about.
147 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
148 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
148 if (this.key_value_lock !== null) {
149 if (this.key_value_lock !== null) {
149 var key = this.key_value_lock[0];
150 var key = this.key_value_lock[0];
150 var value = this.key_value_lock[1];
151 var value = this.key_value_lock[1];
151 if (attrs[key] === value) {
152 if (attrs[key] === value) {
152 delete attrs[key];
153 delete attrs[key];
153 }
154 }
154 }
155 }
155
156
156 // Only sync if there are attributes to send to the back-end.
157 // Only sync if there are attributes to send to the back-end.
157 if (attr.length > 0) {
158 if (_.size(attrs) > 0) {
158 var callbacks = options.callbacks || {};
159 var callbacks = options.callbacks || {};
159 if (this.pending_msgs >= this.msg_throttle) {
160 if (this.pending_msgs >= this.msg_throttle) {
160 // The throttle has been exceeded, buffer the current msg so
161 // The throttle has been exceeded, buffer the current msg so
161 // it can be sent once the kernel has finished processing
162 // it can be sent once the kernel has finished processing
162 // some of the existing messages.
163 // some of the existing messages.
163
164
164 // Combine updates if it is a 'patch' sync, otherwise replace updates
165 // Combine updates if it is a 'patch' sync, otherwise replace updates
165 switch (method) {
166 switch (method) {
166 case 'patch':
167 case 'patch':
167 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
168 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
168 break;
169 break;
169 case 'update':
170 case 'update':
170 case 'create':
171 case 'create':
171 this.msg_buffer = attrs;
172 this.msg_buffer = attrs;
172 break;
173 break;
173 default:
174 default:
174 error();
175 error();
175 return false;
176 return false;
176 }
177 }
177 this.msg_buffer_callbacks = callbacks;
178 this.msg_buffer_callbacks = callbacks;
178
179
179 } else {
180 } else {
180 // We haven't exceeded the throttle, send the message like
181 // We haven't exceeded the throttle, send the message like
181 // normal. If this is a patch operation, just send the
182 // normal. If this is a patch operation, just send the
182 // changes.
183 // changes.
183 var data = {method: 'backbone', sync_data: attrs};
184 var data = {method: 'backbone', sync_data: attrs};
184 this.comm.send(data, callbacks);
185 this.comm.send(data, callbacks);
185 this.pending_msgs++;
186 this.pending_msgs++;
186 }
187 }
187 }
188 }
188 // Since the comm is a one-way communication, assume the message
189 // Since the comm is a one-way communication, assume the message
189 // arrived. Don't call success since we don't have a model back from the server
190 // arrived. Don't call success since we don't have a model back from the server
190 // this means we miss out on the 'sync' event.
191 // this means we miss out on the 'sync' event.
191 },
192 },
192
193
193 save_changes: function(callbacks) {
194 save_changes: function(callbacks) {
194 // Push this model's state to the back-end
195 // Push this model's state to the back-end
195 //
196 //
196 // This invokes a Backbone.Sync.
197 // This invokes a Backbone.Sync.
197 this.save(this.changedAttributes(), {patch: true, callbacks: callbacks});
198 this.save(this.changedAttributes(), {patch: true, callbacks: callbacks});
198 },
199 },
199
200
200 _pack_models: function(value) {
201 _pack_models: function(value) {
201 // Replace models with model ids recursively.
202 // Replace models with model ids recursively.
202 if (value instanceof Backbone.Model) {
203 if (value instanceof Backbone.Model) {
203 return value.id;
204 return value.id;
204 } else if (value instanceof Object) {
205 } else if (value instanceof Object) {
205 var packed = {};
206 var packed = {};
207 var that = this;
206 _.each(value, function(sub_value, key) {
208 _.each(value, function(sub_value, key) {
207 packed[key] = this._pack_models(sub_value);
209 packed[key] = that._pack_models(sub_value);
208 });
210 });
209 return packed;
211 return packed;
210 } else {
212 } else {
211 return value;
213 return value;
212 }
214 }
213 },
215 },
214
216
215 _unpack_models: function(value) {
217 _unpack_models: function(value) {
216 // Replace model ids with models recursively.
218 // Replace model ids with models recursively.
217 if (value instanceof Object) {
219 if (value instanceof Object) {
218 var unpacked = {};
220 var unpacked = {};
221 var that = this;
219 _.each(value, function(sub_value, key) {
222 _.each(value, function(sub_value, key) {
220 unpacked[key] = this._unpack_models(sub_value);
223 unpacked[key] = that._unpack_models(sub_value);
221 });
224 });
222 return unpacked;
225 return unpacked;
223 } else {
226 } else {
224 var model = this.widget_manager.get_model(value);
227 var model = this.widget_manager.get_model(value);
225 if (model !== null) {
228 if (model !== null) {
226 return model;
229 return model;
227 } else {
230 } else {
228 return value;
231 return value;
229 }
232 }
230 }
233 }
231 },
234 },
232
235
233 });
236 });
234 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
237 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
235
238
236
239
237 var WidgetView = Backbone.View.extend({
240 var WidgetView = Backbone.View.extend({
238 initialize: function(parameters) {
241 initialize: function(parameters) {
239 // Public constructor.
242 // Public constructor.
240 this.model.on('change',this.update,this);
243 this.model.on('change',this.update,this);
241 this.options = parameters.options;
244 this.options = parameters.options;
242 this.child_views = [];
245 this.child_views = [];
243 this.model.views.push(this);
246 this.model.views.push(this);
244 },
247 },
245
248
246 update: function(){
249 update: function(){
247 // Triggered on model change.
250 // Triggered on model change.
248 //
251 //
249 // Update view to be consistent with this.model
252 // Update view to be consistent with this.model
250 },
253 },
251
254
252 create_child_view: function(child_model, options) {
255 create_child_view: function(child_model, options) {
253 // Create and return a child view.
256 // Create and return a child view.
254 //
257 //
255 // -given a model and (optionally) a view name if the view name is
258 // -given a model and (optionally) a view name if the view name is
256 // not given, it defaults to the model's default view attribute.
259 // not given, it defaults to the model's default view attribute.
257
260
258 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
261 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
259 // it would be great to have the widget manager add the cell metadata
262 // it would be great to have the widget manager add the cell metadata
260 // to the subview without having to add it here.
263 // to the subview without having to add it here.
261 var child_view = this.model.widget_manager.create_view(child_model, options || {});
264 var child_view = this.model.widget_manager.create_view(child_model, options || {});
262 this.child_views[child_model.id] = child_view;
265 this.child_views[child_model.id] = child_view;
263 return child_view;
266 return child_view;
264 },
267 },
265
268
266 delete_child_view: function(child_model, options) {
269 delete_child_view: function(child_model, options) {
267 // Delete a child view that was previously created using create_child_view.
270 // Delete a child view that was previously created using create_child_view.
268 var view = this.child_views[child_model.id];
271 var view = this.child_views[child_model.id];
269 if (view !== undefined) {
272 if (view !== undefined) {
270 delete this.child_views[child_model.id];
273 delete this.child_views[child_model.id];
271 view.remove();
274 view.remove();
272 }
275 }
273 },
276 },
274
277
275 do_diff: function(old_list, new_list, removed_callback, added_callback) {
278 do_diff: function(old_list, new_list, removed_callback, added_callback) {
276 // Difference a changed list and call remove and add callbacks for
279 // Difference a changed list and call remove and add callbacks for
277 // each removed and added item in the new list.
280 // each removed and added item in the new list.
278 //
281 //
279 // Parameters
282 // Parameters
280 // ----------
283 // ----------
281 // old_list : array
284 // old_list : array
282 // new_list : array
285 // new_list : array
283 // removed_callback : Callback(item)
286 // removed_callback : Callback(item)
284 // Callback that is called for each item removed.
287 // Callback that is called for each item removed.
285 // added_callback : Callback(item)
288 // added_callback : Callback(item)
286 // Callback that is called for each item added.
289 // Callback that is called for each item added.
287
290
288
291
289 // removed items
292 // removed items
290 _.each(_.difference(old_list, new_list), function(item, index, list) {
293 _.each(_.difference(old_list, new_list), function(item, index, list) {
291 removed_callback(item);
294 removed_callback(item);
292 }, this);
295 }, this);
293
296
294 // added items
297 // added items
295 _.each(_.difference(new_list, old_list), function(item, index, list) {
298 _.each(_.difference(new_list, old_list), function(item, index, list) {
296 added_callback(item);
299 added_callback(item);
297 }, this);
300 }, this);
298 },
301 },
299
302
300 callbacks: function(){
303 callbacks: function(){
301 // Create msg callbacks for a comm msg.
304 // Create msg callbacks for a comm msg.
302 return this.model.callbacks(this);
305 return this.model.callbacks(this);
303 },
306 },
304
307
305 render: function(){
308 render: function(){
306 // Render the view.
309 // Render the view.
307 //
310 //
308 // By default, this is only called the first time the view is created
311 // By default, this is only called the first time the view is created
309 },
312 },
310
313
311 send: function (content) {
314 send: function (content) {
312 // Send a custom msg associated with this view.
315 // Send a custom msg associated with this view.
313 this.model.send(content, this.callbacks());
316 this.model.send(content, this.callbacks());
314 },
317 },
315
318
316 touch: function () {
319 touch: function () {
317 this.model.save_changes(this.callbacks());
320 this.model.save_changes(this.callbacks());
318 },
321 },
319 });
322 });
320
323
321
324
322 var DOMWidgetView = WidgetView.extend({
325 var DOMWidgetView = WidgetView.extend({
323 initialize: function (options) {
326 initialize: function (options) {
324 // Public constructor
327 // Public constructor
325
328
326 // In the future we may want to make changes more granular
329 // In the future we may want to make changes more granular
327 // (e.g., trigger on visible:change).
330 // (e.g., trigger on visible:change).
328 this.model.on('change', this.update, this);
331 this.model.on('change', this.update, this);
329 this.model.on('msg:custom', this.on_msg, this);
332 this.model.on('msg:custom', this.on_msg, this);
330 DOMWidgetView.__super__.initialize.apply(this, arguments);
333 DOMWidgetView.__super__.initialize.apply(this, arguments);
331 },
334 },
332
335
333 on_msg: function(msg) {
336 on_msg: function(msg) {
334 // Handle DOM specific msgs.
337 // Handle DOM specific msgs.
335 switch(msg.msg_type) {
338 switch(msg.msg_type) {
336 case 'add_class':
339 case 'add_class':
337 this.add_class(msg.selector, msg.class_list);
340 this.add_class(msg.selector, msg.class_list);
338 break;
341 break;
339 case 'remove_class':
342 case 'remove_class':
340 this.remove_class(msg.selector, msg.class_list);
343 this.remove_class(msg.selector, msg.class_list);
341 break;
344 break;
342 }
345 }
343 },
346 },
344
347
345 add_class: function (selector, class_list) {
348 add_class: function (selector, class_list) {
346 // Add a DOM class to an element.
349 // Add a DOM class to an element.
347 this._get_selector_element(selector).addClass(class_list);
350 this._get_selector_element(selector).addClass(class_list);
348 },
351 },
349
352
350 remove_class: function (selector, class_list) {
353 remove_class: function (selector, class_list) {
351 // Remove a DOM class from an element.
354 // Remove a DOM class from an element.
352 this._get_selector_element(selector).removeClass(class_list);
355 this._get_selector_element(selector).removeClass(class_list);
353 },
356 },
354
357
355 update: function () {
358 update: function () {
356 // Update the contents of this view
359 // Update the contents of this view
357 //
360 //
358 // Called when the model is changed. The model may have been
361 // Called when the model is changed. The model may have been
359 // changed by another view or by a state update from the back-end.
362 // changed by another view or by a state update from the back-end.
360 // The very first update seems to happen before the element is
363 // The very first update seems to happen before the element is
361 // finished rendering so we use setTimeout to give the element time
364 // finished rendering so we use setTimeout to give the element time
362 // to render
365 // to render
363 var e = this.$el;
366 var e = this.$el;
364 var visible = this.model.get('visible');
367 var visible = this.model.get('visible');
365 setTimeout(function() {e.toggle(visible)},0);
368 setTimeout(function() {e.toggle(visible)},0);
366
369
367 var css = this.model.get('_css');
370 var css = this.model.get('_css');
368 if (css === undefined) {return;}
371 if (css === undefined) {return;}
372 var that = this;
369 _.each(css, function(css_traits, selector){
373 _.each(css, function(css_traits, selector){
370 // Apply the css traits to all elements that match the selector.
374 // Apply the css traits to all elements that match the selector.
371 var elements = this._get_selector_element(selector);
375 var elements = that._get_selector_element(selector);
372 if (elements.length > 0) {
376 if (elements.length > 0) {
373 _.each(css_traits, function(css_value, css_key){
377 _.each(css_traits, function(css_value, css_key){
374 elements.css(css_key, css_value);
378 elements.css(css_key, css_value);
375 });
379 });
376 }
380 }
377 });
381 });
378
382
379 },
383 },
380
384
381 _get_selector_element: function (selector) {
385 _get_selector_element: function (selector) {
382 // Get the elements via the css selector.
386 // Get the elements via the css selector.
383
387
384 // If the selector is blank, apply the style to the $el_to_style
388 // If the selector is blank, apply the style to the $el_to_style
385 // element. If the $el_to_style element is not defined, use apply
389 // element. If the $el_to_style element is not defined, use apply
386 // the style to the view's element.
390 // the style to the view's element.
387 var elements;
391 var elements;
388 if (selector === undefined || selector === null || selector === '') {
392 if (selector === undefined || selector === null || selector === '') {
389 if (this.$el_to_style === undefined) {
393 if (this.$el_to_style === undefined) {
390 elements = this.$el;
394 elements = this.$el;
391 } else {
395 } else {
392 elements = this.$el_to_style;
396 elements = this.$el_to_style;
393 }
397 }
394 } else {
398 } else {
395 elements = this.$el.find(selector);
399 elements = this.$el.find(selector);
396 }
400 }
397 return elements;
401 return elements;
398 },
402 },
399 });
403 });
400
404
401 IPython.WidgetModel = WidgetModel;
405 IPython.WidgetModel = WidgetModel;
402 IPython.WidgetView = WidgetView;
406 IPython.WidgetView = WidgetView;
403 IPython.DOMWidgetView = DOMWidgetView;
407 IPython.DOMWidgetView = DOMWidgetView;
404
408
405 // Pass through WidgetManager namespace.
409 // Pass through WidgetManager namespace.
406 return WidgetManager;
410 return WidgetManager;
407 });
411 });
@@ -1,267 +1,268 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // FloatWidget
9 // FloatWidget
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["notebook/js/widgets/widget"], function(WidgetManager){
17 define(["notebook/js/widgets/widget"], function(WidgetManager){
18
18
19 var FloatSliderView = IPython.DOMWidgetView.extend({
19 var FloatSliderView = IPython.DOMWidgetView.extend({
20 render : function(){
20 render : function(){
21 // Called when view is rendered.
21 // Called when view is rendered.
22 this.$el
22 this.$el
23 .addClass('widget-hbox-single');
23 .addClass('widget-hbox-single');
24 this.$label = $('<div />')
24 this.$label = $('<div />')
25 .appendTo(this.$el)
25 .appendTo(this.$el)
26 .addClass('widget-hlabel')
26 .addClass('widget-hlabel')
27 .hide();
27 .hide();
28 this.$slider = $('<div />')
28 this.$slider = $('<div />')
29 .slider({})
29 .slider({})
30 .addClass('slider');
30 .addClass('slider');
31
31
32 // Put the slider in a container
32 // Put the slider in a container
33 this.$slider_container = $('<div />')
33 this.$slider_container = $('<div />')
34 .addClass('widget-hslider')
34 .addClass('widget-hslider')
35 .append(this.$slider);
35 .append(this.$slider);
36 this.$el_to_style = this.$slider_container; // Set default element to style
36 this.$el_to_style = this.$slider_container; // Set default element to style
37 this.$el.append(this.$slider_container);
37 this.$el.append(this.$slider_container);
38
38
39 // Set defaults.
39 // Set defaults.
40 this.update();
40 this.update();
41 },
41 },
42
42
43 update : function(options){
43 update : function(options){
44 // Update the contents of this view
44 // Update the contents of this view
45 //
45 //
46 // Called when the model is changed. The model may have been
46 // Called when the model is changed. The model may have been
47 // changed by another view or by a state update from the back-end.
47 // changed by another view or by a state update from the back-end.
48
48
49 if (options === undefined || options.updated_view != this) {
49 if (options === undefined || options.updated_view != this) {
50 // JQuery slider option keys. These keys happen to have a
50 // JQuery slider option keys. These keys happen to have a
51 // one-to-one mapping with the corrosponding keys of the model.
51 // one-to-one mapping with the corrosponding keys of the model.
52 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
52 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
53 var that = this;
53 _.each(jquery_slider_keys, function(key, i) {
54 _.each(jquery_slider_keys, function(key, i) {
54 var model_value = this.model.get(key);
55 var model_value = that.model.get(key);
55 if (model_value !== undefined) {
56 if (model_value !== undefined) {
56 this.$slider.slider("option", key, model_value);
57 that.$slider.slider("option", key, model_value);
57 }
58 }
58 });
59 });
59
60
60 // WORKAROUND FOR JQUERY SLIDER BUG.
61 // WORKAROUND FOR JQUERY SLIDER BUG.
61 // The horizontal position of the slider handle
62 // The horizontal position of the slider handle
62 // depends on the value of the slider at the time
63 // depends on the value of the slider at the time
63 // of orientation change. Before applying the new
64 // of orientation change. Before applying the new
64 // workaround, we set the value to the minimum to
65 // workaround, we set the value to the minimum to
65 // make sure that the horizontal placement of the
66 // make sure that the horizontal placement of the
66 // handle in the vertical slider is always
67 // handle in the vertical slider is always
67 // consistent.
68 // consistent.
68 var orientation = this.model.get('orientation');
69 var orientation = this.model.get('orientation');
69 var value = this.model.get('min');
70 var value = this.model.get('min');
70 this.$slider.slider('option', 'value', value);
71 this.$slider.slider('option', 'value', value);
71 this.$slider.slider('option', 'orientation', orientation);
72 this.$slider.slider('option', 'orientation', orientation);
72 value = this.model.get('value');
73 value = this.model.get('value');
73 this.$slider.slider('option', 'value', value);
74 this.$slider.slider('option', 'value', value);
74
75
75 // Use the right CSS classes for vertical & horizontal sliders
76 // Use the right CSS classes for vertical & horizontal sliders
76 if (orientation=='vertical') {
77 if (orientation=='vertical') {
77 this.$slider_container
78 this.$slider_container
78 .removeClass('widget-hslider')
79 .removeClass('widget-hslider')
79 .addClass('widget-vslider');
80 .addClass('widget-vslider');
80 this.$el
81 this.$el
81 .removeClass('widget-hbox-single')
82 .removeClass('widget-hbox-single')
82 .addClass('widget-vbox-single');
83 .addClass('widget-vbox-single');
83 this.$label
84 this.$label
84 .removeClass('widget-hlabel')
85 .removeClass('widget-hlabel')
85 .addClass('widget-vlabel');
86 .addClass('widget-vlabel');
86
87
87 } else {
88 } else {
88 this.$slider_container
89 this.$slider_container
89 .removeClass('widget-vslider')
90 .removeClass('widget-vslider')
90 .addClass('widget-hslider');
91 .addClass('widget-hslider');
91 this.$el
92 this.$el
92 .removeClass('widget-vbox-single')
93 .removeClass('widget-vbox-single')
93 .addClass('widget-hbox-single');
94 .addClass('widget-hbox-single');
94 this.$label
95 this.$label
95 .removeClass('widget-vlabel')
96 .removeClass('widget-vlabel')
96 .addClass('widget-hlabel');
97 .addClass('widget-hlabel');
97 }
98 }
98
99
99 var description = this.model.get('description');
100 var description = this.model.get('description');
100 if (description.length === 0) {
101 if (description.length === 0) {
101 this.$label.hide();
102 this.$label.hide();
102 } else {
103 } else {
103 this.$label.text(description);
104 this.$label.text(description);
104 this.$label.show();
105 this.$label.show();
105 }
106 }
106 }
107 }
107 return FloatSliderView.__super__.update.apply(this);
108 return FloatSliderView.__super__.update.apply(this);
108 },
109 },
109
110
110 events: {
111 events: {
111 // Dictionary of events and their handlers.
112 // Dictionary of events and their handlers.
112 "slide" : "handleSliderChange"
113 "slide" : "handleSliderChange"
113 },
114 },
114
115
115 handleSliderChange: function(e, ui) {
116 handleSliderChange: function(e, ui) {
116 // Handle when the slider value is changed.
117 // Handle when the slider value is changed.
117
118
118 // Calling model.set will trigger all of the other views of the
119 // Calling model.set will trigger all of the other views of the
119 // model to update.
120 // model to update.
120 this.model.set('value', ui.value, {updated_view: this});
121 this.model.set('value', ui.value, {updated_view: this});
121 this.touch();
122 this.touch();
122 },
123 },
123 });
124 });
124 WidgetManager.register_widget_view('FloatSliderView', FloatSliderView);
125 WidgetManager.register_widget_view('FloatSliderView', FloatSliderView);
125
126
126
127
127 var FloatTextView = IPython.DOMWidgetView.extend({
128 var FloatTextView = IPython.DOMWidgetView.extend({
128 render : function(){
129 render : function(){
129 // Called when view is rendered.
130 // Called when view is rendered.
130 this.$el
131 this.$el
131 .addClass('widget-hbox-single');
132 .addClass('widget-hbox-single');
132 this.$label = $('<div />')
133 this.$label = $('<div />')
133 .appendTo(this.$el)
134 .appendTo(this.$el)
134 .addClass('widget-hlabel')
135 .addClass('widget-hlabel')
135 .hide();
136 .hide();
136 this.$textbox = $('<input type="text" />')
137 this.$textbox = $('<input type="text" />')
137 .addClass('input')
138 .addClass('input')
138 .addClass('widget-numeric-text')
139 .addClass('widget-numeric-text')
139 .appendTo(this.$el);
140 .appendTo(this.$el);
140 this.$el_to_style = this.$textbox; // Set default element to style
141 this.$el_to_style = this.$textbox; // Set default element to style
141 this.update(); // Set defaults.
142 this.update(); // Set defaults.
142 },
143 },
143
144
144 update : function(options){
145 update : function(options){
145 // Update the contents of this view
146 // Update the contents of this view
146 //
147 //
147 // Called when the model is changed. The model may have been
148 // Called when the model is changed. The model may have been
148 // changed by another view or by a state update from the back-end.
149 // changed by another view or by a state update from the back-end.
149 if (options === undefined || options.updated_view != this) {
150 if (options === undefined || options.updated_view != this) {
150 var value = this.model.get('value');
151 var value = this.model.get('value');
151 if (parseFloat(this.$textbox.val()) != value) {
152 if (parseFloat(this.$textbox.val()) != value) {
152 this.$textbox.val(value);
153 this.$textbox.val(value);
153 }
154 }
154
155
155 if (this.model.get('disabled')) {
156 if (this.model.get('disabled')) {
156 this.$textbox.attr('disabled','disabled');
157 this.$textbox.attr('disabled','disabled');
157 } else {
158 } else {
158 this.$textbox.removeAttr('disabled');
159 this.$textbox.removeAttr('disabled');
159 }
160 }
160
161
161 var description = this.model.get('description');
162 var description = this.model.get('description');
162 if (description.length === 0) {
163 if (description.length === 0) {
163 this.$label.hide();
164 this.$label.hide();
164 } else {
165 } else {
165 this.$label.text(description);
166 this.$label.text(description);
166 this.$label.show();
167 this.$label.show();
167 }
168 }
168 }
169 }
169 return FloatTextView.__super__.update.apply(this);
170 return FloatTextView.__super__.update.apply(this);
170 },
171 },
171
172
172 events: {
173 events: {
173 // Dictionary of events and their handlers.
174 // Dictionary of events and their handlers.
174
175
175 "keyup input" : "handleChanging",
176 "keyup input" : "handleChanging",
176 "paste input" : "handleChanging",
177 "paste input" : "handleChanging",
177 "cut input" : "handleChanging",
178 "cut input" : "handleChanging",
178
179
179 // Fires only when control is validated or looses focus.
180 // Fires only when control is validated or looses focus.
180 "change input" : "handleChanged"
181 "change input" : "handleChanged"
181 },
182 },
182
183
183 handleChanging: function(e) {
184 handleChanging: function(e) {
184 // Handles and validates user input.
185 // Handles and validates user input.
185
186
186 // Try to parse value as a float.
187 // Try to parse value as a float.
187 var numericalValue = 0.0;
188 var numericalValue = 0.0;
188 if (e.target.value !== '') {
189 if (e.target.value !== '') {
189 numericalValue = parseFloat(e.target.value);
190 numericalValue = parseFloat(e.target.value);
190 }
191 }
191
192
192 // If parse failed, reset value to value stored in model.
193 // If parse failed, reset value to value stored in model.
193 if (isNaN(numericalValue)) {
194 if (isNaN(numericalValue)) {
194 e.target.value = this.model.get('value');
195 e.target.value = this.model.get('value');
195 } else if (!isNaN(numericalValue)) {
196 } else if (!isNaN(numericalValue)) {
196 if (this.model.get('max') !== undefined) {
197 if (this.model.get('max') !== undefined) {
197 numericalValue = Math.min(this.model.get('max'), numericalValue);
198 numericalValue = Math.min(this.model.get('max'), numericalValue);
198 }
199 }
199 if (this.model.get('min') !== undefined) {
200 if (this.model.get('min') !== undefined) {
200 numericalValue = Math.max(this.model.get('min'), numericalValue);
201 numericalValue = Math.max(this.model.get('min'), numericalValue);
201 }
202 }
202
203
203 // Apply the value if it has changed.
204 // Apply the value if it has changed.
204 if (numericalValue != this.model.get('value')) {
205 if (numericalValue != this.model.get('value')) {
205
206
206 // Calling model.set will trigger all of the other views of the
207 // Calling model.set will trigger all of the other views of the
207 // model to update.
208 // model to update.
208 this.model.set('value', numericalValue, {updated_view: this});
209 this.model.set('value', numericalValue, {updated_view: this});
209 this.touch();
210 this.touch();
210 }
211 }
211 }
212 }
212 },
213 },
213
214
214 handleChanged: function(e) {
215 handleChanged: function(e) {
215 // Applies validated input.
216 // Applies validated input.
216 if (this.model.get('value') != e.target.value) {
217 if (this.model.get('value') != e.target.value) {
217 e.target.value = this.model.get('value');
218 e.target.value = this.model.get('value');
218 }
219 }
219 }
220 }
220 });
221 });
221 WidgetManager.register_widget_view('FloatTextView', FloatTextView);
222 WidgetManager.register_widget_view('FloatTextView', FloatTextView);
222
223
223
224
224 var ProgressView = IPython.DOMWidgetView.extend({
225 var ProgressView = IPython.DOMWidgetView.extend({
225 render : function(){
226 render : function(){
226 // Called when view is rendered.
227 // Called when view is rendered.
227 this.$el
228 this.$el
228 .addClass('widget-hbox-single');
229 .addClass('widget-hbox-single');
229 this.$label = $('<div />')
230 this.$label = $('<div />')
230 .appendTo(this.$el)
231 .appendTo(this.$el)
231 .addClass('widget-hlabel')
232 .addClass('widget-hlabel')
232 .hide();
233 .hide();
233 this.$progress = $('<div />')
234 this.$progress = $('<div />')
234 .addClass('progress')
235 .addClass('progress')
235 .addClass('widget-progress')
236 .addClass('widget-progress')
236 .appendTo(this.$el);
237 .appendTo(this.$el);
237 this.$el_to_style = this.$progress; // Set default element to style
238 this.$el_to_style = this.$progress; // Set default element to style
238 this.$bar = $('<div />')
239 this.$bar = $('<div />')
239 .addClass('bar')
240 .addClass('bar')
240 .css('width', '50%')
241 .css('width', '50%')
241 .appendTo(this.$progress);
242 .appendTo(this.$progress);
242 this.update(); // Set defaults.
243 this.update(); // Set defaults.
243 },
244 },
244
245
245 update : function(){
246 update : function(){
246 // Update the contents of this view
247 // Update the contents of this view
247 //
248 //
248 // Called when the model is changed. The model may have been
249 // Called when the model is changed. The model may have been
249 // changed by another view or by a state update from the back-end.
250 // changed by another view or by a state update from the back-end.
250 var value = this.model.get('value');
251 var value = this.model.get('value');
251 var max = this.model.get('max');
252 var max = this.model.get('max');
252 var min = this.model.get('min');
253 var min = this.model.get('min');
253 var percent = 100.0 * (value - min) / (max - min);
254 var percent = 100.0 * (value - min) / (max - min);
254 this.$bar.css('width', percent + '%');
255 this.$bar.css('width', percent + '%');
255
256
256 var description = this.model.get('description');
257 var description = this.model.get('description');
257 if (description.length === 0) {
258 if (description.length === 0) {
258 this.$label.hide();
259 this.$label.hide();
259 } else {
260 } else {
260 this.$label.text(description);
261 this.$label.text(description);
261 this.$label.show();
262 this.$label.show();
262 }
263 }
263 return ProgressView.__super__.update.apply(this);
264 return ProgressView.__super__.update.apply(this);
264 },
265 },
265 });
266 });
266 WidgetManager.register_widget_view('ProgressView', ProgressView);
267 WidgetManager.register_widget_view('ProgressView', ProgressView);
267 });
268 });
@@ -1,220 +1,221 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // IntWidget
9 // IntWidget
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["notebook/js/widgets/widget"], function(WidgetManager){
17 define(["notebook/js/widgets/widget"], function(WidgetManager){
18
18
19 var IntSliderView = IPython.DOMWidgetView.extend({
19 var IntSliderView = IPython.DOMWidgetView.extend({
20 render : function(){
20 render : function(){
21 // Called when view is rendered.
21 // Called when view is rendered.
22 this.$el
22 this.$el
23 .addClass('widget-hbox-single');
23 .addClass('widget-hbox-single');
24 this.$label = $('<div />')
24 this.$label = $('<div />')
25 .appendTo(this.$el)
25 .appendTo(this.$el)
26 .addClass('widget-hlabel')
26 .addClass('widget-hlabel')
27 .hide();
27 .hide();
28 this.$slider = $('<div />')
28 this.$slider = $('<div />')
29 .slider({})
29 .slider({})
30 .addClass('slider');
30 .addClass('slider');
31
31
32 // Put the slider in a container
32 // Put the slider in a container
33 this.$slider_container = $('<div />')
33 this.$slider_container = $('<div />')
34 .addClass('widget-hslider')
34 .addClass('widget-hslider')
35 .append(this.$slider);
35 .append(this.$slider);
36 this.$el_to_style = this.$slider_container; // Set default element to style
36 this.$el_to_style = this.$slider_container; // Set default element to style
37 this.$el.append(this.$slider_container);
37 this.$el.append(this.$slider_container);
38
38
39 // Set defaults.
39 // Set defaults.
40 this.update();
40 this.update();
41 },
41 },
42
42
43 update : function(options){
43 update : function(options){
44 // Update the contents of this view
44 // Update the contents of this view
45 //
45 //
46 // Called when the model is changed. The model may have been
46 // Called when the model is changed. The model may have been
47 // changed by another view or by a state update from the back-end.
47 // changed by another view or by a state update from the back-end.
48 if (options === undefined || options.updated_view != this) {
48 if (options === undefined || options.updated_view != this) {
49 // JQuery slider option keys. These keys happen to have a
49 // JQuery slider option keys. These keys happen to have a
50 // one-to-one mapping with the corrosponding keys of the model.
50 // one-to-one mapping with the corrosponding keys of the model.
51 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
51 var jquery_slider_keys = ['step', 'max', 'min', 'disabled'];
52 var that = this;
52 _.each(jquery_slider_keys, function(key, i) {
53 _.each(jquery_slider_keys, function(key, i) {
53 var model_value = this.model.get(key);
54 var model_value = that.model.get(key);
54 if (model_value !== undefined) {
55 if (model_value !== undefined) {
55 this.$slider.slider("option", key, model_value);
56 that.$slider.slider("option", key, model_value);
56 }
57 }
57 });
58 });
58
59
59 // WORKAROUND FOR JQUERY SLIDER BUG.
60 // WORKAROUND FOR JQUERY SLIDER BUG.
60 // The horizontal position of the slider handle
61 // The horizontal position of the slider handle
61 // depends on the value of the slider at the time
62 // depends on the value of the slider at the time
62 // of orientation change. Before applying the new
63 // of orientation change. Before applying the new
63 // workaround, we set the value to the minimum to
64 // workaround, we set the value to the minimum to
64 // make sure that the horizontal placement of the
65 // make sure that the horizontal placement of the
65 // handle in the vertical slider is always
66 // handle in the vertical slider is always
66 // consistent.
67 // consistent.
67 var orientation = this.model.get('orientation');
68 var orientation = this.model.get('orientation');
68 var value = this.model.get('min');
69 var value = this.model.get('min');
69 this.$slider.slider('option', 'value', value);
70 this.$slider.slider('option', 'value', value);
70 this.$slider.slider('option', 'orientation', orientation);
71 this.$slider.slider('option', 'orientation', orientation);
71 value = this.model.get('value');
72 value = this.model.get('value');
72 this.$slider.slider('option', 'value', value);
73 this.$slider.slider('option', 'value', value);
73
74
74 // Use the right CSS classes for vertical & horizontal sliders
75 // Use the right CSS classes for vertical & horizontal sliders
75 if (orientation=='vertical') {
76 if (orientation=='vertical') {
76 this.$slider_container
77 this.$slider_container
77 .removeClass('widget-hslider')
78 .removeClass('widget-hslider')
78 .addClass('widget-vslider');
79 .addClass('widget-vslider');
79 this.$el
80 this.$el
80 .removeClass('widget-hbox-single')
81 .removeClass('widget-hbox-single')
81 .addClass('widget-vbox-single');
82 .addClass('widget-vbox-single');
82 this.$label
83 this.$label
83 .removeClass('widget-hlabel')
84 .removeClass('widget-hlabel')
84 .addClass('widget-vlabel');
85 .addClass('widget-vlabel');
85
86
86 } else {
87 } else {
87 this.$slider_container
88 this.$slider_container
88 .removeClass('widget-vslider')
89 .removeClass('widget-vslider')
89 .addClass('widget-hslider');
90 .addClass('widget-hslider');
90 this.$el
91 this.$el
91 .removeClass('widget-vbox-single')
92 .removeClass('widget-vbox-single')
92 .addClass('widget-hbox-single');
93 .addClass('widget-hbox-single');
93 this.$label
94 this.$label
94 .removeClass('widget-vlabel')
95 .removeClass('widget-vlabel')
95 .addClass('widget-hlabel');
96 .addClass('widget-hlabel');
96 }
97 }
97
98
98 var description = this.model.get('description');
99 var description = this.model.get('description');
99 if (description.length === 0) {
100 if (description.length === 0) {
100 this.$label.hide();
101 this.$label.hide();
101 } else {
102 } else {
102 this.$label.text(description);
103 this.$label.text(description);
103 this.$label.show();
104 this.$label.show();
104 }
105 }
105 }
106 }
106 return IntSliderView.__super__.update.apply(this);
107 return IntSliderView.__super__.update.apply(this);
107 },
108 },
108
109
109 events: {
110 events: {
110 // Dictionary of events and their handlers.
111 // Dictionary of events and their handlers.
111 "slide" : "handleSliderChange"
112 "slide" : "handleSliderChange"
112 },
113 },
113
114
114 handleSliderChange: function(e, ui) {
115 handleSliderChange: function(e, ui) {
115 // Called when the slider value is changed.
116 // Called when the slider value is changed.
116
117
117 // Calling model.set will trigger all of the other views of the
118 // Calling model.set will trigger all of the other views of the
118 // model to update.
119 // model to update.
119 this.model.set('value', ~~ui.value, {updated_view: this}); // Double bit-wise not to truncate decimel
120 this.model.set('value', ~~ui.value, {updated_view: this}); // Double bit-wise not to truncate decimel
120 this.touch();
121 this.touch();
121 },
122 },
122 });
123 });
123 WidgetManager.register_widget_view('IntSliderView', IntSliderView);
124 WidgetManager.register_widget_view('IntSliderView', IntSliderView);
124
125
125
126
126 var IntTextView = IPython.DOMWidgetView.extend({
127 var IntTextView = IPython.DOMWidgetView.extend({
127 render : function(){
128 render : function(){
128 // Called when view is rendered.
129 // Called when view is rendered.
129 this.$el
130 this.$el
130 .addClass('widget-hbox-single');
131 .addClass('widget-hbox-single');
131 this.$label = $('<div />')
132 this.$label = $('<div />')
132 .appendTo(this.$el)
133 .appendTo(this.$el)
133 .addClass('widget-hlabel')
134 .addClass('widget-hlabel')
134 .hide();
135 .hide();
135 this.$textbox = $('<input type="text" />')
136 this.$textbox = $('<input type="text" />')
136 .addClass('input')
137 .addClass('input')
137 .addClass('widget-numeric-text')
138 .addClass('widget-numeric-text')
138 .appendTo(this.$el);
139 .appendTo(this.$el);
139 this.$el_to_style = this.$textbox; // Set default element to style
140 this.$el_to_style = this.$textbox; // Set default element to style
140 this.update(); // Set defaults.
141 this.update(); // Set defaults.
141 },
142 },
142
143
143 update : function(options){
144 update : function(options){
144 // Update the contents of this view
145 // Update the contents of this view
145 //
146 //
146 // Called when the model is changed. The model may have been
147 // Called when the model is changed. The model may have been
147 // changed by another view or by a state update from the back-end.
148 // changed by another view or by a state update from the back-end.
148 if (options === undefined || options.updated_view != this) {
149 if (options === undefined || options.updated_view != this) {
149 var value = this.model.get('value');
150 var value = this.model.get('value');
150 if (parseInt(this.$textbox.val()) != value) {
151 if (parseInt(this.$textbox.val()) != value) {
151 this.$textbox.val(value);
152 this.$textbox.val(value);
152 }
153 }
153
154
154 if (this.model.get('disabled')) {
155 if (this.model.get('disabled')) {
155 this.$textbox.attr('disabled','disabled');
156 this.$textbox.attr('disabled','disabled');
156 } else {
157 } else {
157 this.$textbox.removeAttr('disabled');
158 this.$textbox.removeAttr('disabled');
158 }
159 }
159
160
160 var description = this.model.get('description');
161 var description = this.model.get('description');
161 if (description.length === 0) {
162 if (description.length === 0) {
162 this.$label.hide();
163 this.$label.hide();
163 } else {
164 } else {
164 this.$label.text(description);
165 this.$label.text(description);
165 this.$label.show();
166 this.$label.show();
166 }
167 }
167 }
168 }
168 return IntTextView.__super__.update.apply(this);
169 return IntTextView.__super__.update.apply(this);
169 },
170 },
170
171
171 events: {
172 events: {
172 // Dictionary of events and their handlers.
173 // Dictionary of events and their handlers.
173 "keyup input" : "handleChanging",
174 "keyup input" : "handleChanging",
174 "paste input" : "handleChanging",
175 "paste input" : "handleChanging",
175 "cut input" : "handleChanging",
176 "cut input" : "handleChanging",
176
177
177 // Fires only when control is validated or looses focus.
178 // Fires only when control is validated or looses focus.
178 "change input" : "handleChanged"
179 "change input" : "handleChanged"
179 },
180 },
180
181
181 handleChanging: function(e) {
182 handleChanging: function(e) {
182 // Handles and validates user input.
183 // Handles and validates user input.
183
184
184 // Try to parse value as a float.
185 // Try to parse value as a float.
185 var numericalValue = 0;
186 var numericalValue = 0;
186 if (e.target.value !== '') {
187 if (e.target.value !== '') {
187 numericalValue = parseInt(e.target.value);
188 numericalValue = parseInt(e.target.value);
188 }
189 }
189
190
190 // If parse failed, reset value to value stored in model.
191 // If parse failed, reset value to value stored in model.
191 if (isNaN(numericalValue)) {
192 if (isNaN(numericalValue)) {
192 e.target.value = this.model.get('value');
193 e.target.value = this.model.get('value');
193 } else if (!isNaN(numericalValue)) {
194 } else if (!isNaN(numericalValue)) {
194 if (this.model.get('max') !== undefined) {
195 if (this.model.get('max') !== undefined) {
195 numericalValue = Math.min(this.model.get('max'), numericalValue);
196 numericalValue = Math.min(this.model.get('max'), numericalValue);
196 }
197 }
197 if (this.model.get('min') !== undefined) {
198 if (this.model.get('min') !== undefined) {
198 numericalValue = Math.max(this.model.get('min'), numericalValue);
199 numericalValue = Math.max(this.model.get('min'), numericalValue);
199 }
200 }
200
201
201 // Apply the value if it has changed.
202 // Apply the value if it has changed.
202 if (numericalValue != this.model.get('value')) {
203 if (numericalValue != this.model.get('value')) {
203
204
204 // Calling model.set will trigger all of the other views of the
205 // Calling model.set will trigger all of the other views of the
205 // model to update.
206 // model to update.
206 this.model.set('value', numericalValue, {updated_view: this});
207 this.model.set('value', numericalValue, {updated_view: this});
207 this.touch();
208 this.touch();
208 }
209 }
209 }
210 }
210 },
211 },
211
212
212 handleChanged: function(e) {
213 handleChanged: function(e) {
213 // Applies validated input.
214 // Applies validated input.
214 if (this.model.get('value') != e.target.value) {
215 if (this.model.get('value') != e.target.value) {
215 e.target.value = this.model.get('value');
216 e.target.value = this.model.get('value');
216 }
217 }
217 }
218 }
218 });
219 });
219 WidgetManager.register_widget_view('IntTextView', IntTextView);
220 WidgetManager.register_widget_view('IntTextView', IntTextView);
220 });
221 });
@@ -1,372 +1,376 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // SelectionWidget
9 // SelectionWidget
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["notebook/js/widgets/widget"], function(WidgetManager){
17 define(["notebook/js/widgets/widget"], function(WidgetManager){
18
18
19 var DropdownView = IPython.DOMWidgetView.extend({
19 var DropdownView = IPython.DOMWidgetView.extend({
20 render : function(){
20 render : function(){
21 // Called when view is rendered.
21 // Called when view is rendered.
22 this.$el
22 this.$el
23 .addClass('widget-hbox-single');
23 .addClass('widget-hbox-single');
24 this.$label = $('<div />')
24 this.$label = $('<div />')
25 .appendTo(this.$el)
25 .appendTo(this.$el)
26 .addClass('widget-hlabel')
26 .addClass('widget-hlabel')
27 .hide();
27 .hide();
28 this.$buttongroup = $('<div />')
28 this.$buttongroup = $('<div />')
29 .addClass('widget_item')
29 .addClass('widget_item')
30 .addClass('btn-group')
30 .addClass('btn-group')
31 .appendTo(this.$el);
31 .appendTo(this.$el);
32 this.$el_to_style = this.$buttongroup; // Set default element to style
32 this.$el_to_style = this.$buttongroup; // Set default element to style
33 this.$droplabel = $('<button />')
33 this.$droplabel = $('<button />')
34 .addClass('btn')
34 .addClass('btn')
35 .addClass('widget-combo-btn')
35 .addClass('widget-combo-btn')
36 .text(' ')
36 .text(' ')
37 .appendTo(this.$buttongroup);
37 .appendTo(this.$buttongroup);
38 this.$dropbutton = $('<button />')
38 this.$dropbutton = $('<button />')
39 .addClass('btn')
39 .addClass('btn')
40 .addClass('dropdown-toggle')
40 .addClass('dropdown-toggle')
41 .addClass('widget-combo-carrot-btn')
41 .addClass('widget-combo-carrot-btn')
42 .attr('data-toggle', 'dropdown')
42 .attr('data-toggle', 'dropdown')
43 .append($('<span />').addClass("caret"))
43 .append($('<span />').addClass("caret"))
44 .appendTo(this.$buttongroup);
44 .appendTo(this.$buttongroup);
45 this.$droplist = $('<ul />')
45 this.$droplist = $('<ul />')
46 .addClass('dropdown-menu')
46 .addClass('dropdown-menu')
47 .appendTo(this.$buttongroup);
47 .appendTo(this.$buttongroup);
48
48
49 // Set defaults.
49 // Set defaults.
50 this.update();
50 this.update();
51 },
51 },
52
52
53 update : function(options){
53 update : function(options){
54 // Update the contents of this view
54 // Update the contents of this view
55 //
55 //
56 // Called when the model is changed. The model may have been
56 // Called when the model is changed. The model may have been
57 // changed by another view or by a state update from the back-end.
57 // changed by another view or by a state update from the back-end.
58
58
59 if (options === undefined || options.updated_view != this) {
59 if (options === undefined || options.updated_view != this) {
60 var selected_item_text = this.model.get('value');
60 var selected_item_text = this.model.get('value');
61 if (selected_item_text.length === 0) {
61 if (selected_item_text.length === 0) {
62 this.$droplabel.text(' ');
62 this.$droplabel.text(' ');
63 } else {
63 } else {
64 this.$droplabel.text(selected_item_text);
64 this.$droplabel.text(selected_item_text);
65 }
65 }
66
66
67 var items = this.model.get('values');
67 var items = this.model.get('values');
68 var $replace_droplist = $('<ul />')
68 var $replace_droplist = $('<ul />')
69 .addClass('dropdown-menu');
69 .addClass('dropdown-menu');
70 var that = this;
70 _.each(items, function(item, i) {
71 _.each(items, function(item, i) {
71 var item_button = $('<a href="#"/>')
72 var item_button = $('<a href="#"/>')
72 .text(item)
73 .text(item)
73 .on('click', $.proxy(this.handle_click, this));
74 .on('click', $.proxy(that.handle_click, that));
74 $replace_droplist.append($('<li />').append(item_button));
75 $replace_droplist.append($('<li />').append(item_button));
75 });
76 });
76
77
77 this.$droplist.replaceWith($replace_droplist);
78 this.$droplist.replaceWith($replace_droplist);
78 this.$droplist.remove();
79 this.$droplist.remove();
79 this.$droplist = $replace_droplist;
80 this.$droplist = $replace_droplist;
80
81
81 if (this.model.get('disabled')) {
82 if (this.model.get('disabled')) {
82 this.$buttongroup.attr('disabled','disabled');
83 this.$buttongroup.attr('disabled','disabled');
83 this.$droplabel.attr('disabled','disabled');
84 this.$droplabel.attr('disabled','disabled');
84 this.$dropbutton.attr('disabled','disabled');
85 this.$dropbutton.attr('disabled','disabled');
85 this.$droplist.attr('disabled','disabled');
86 this.$droplist.attr('disabled','disabled');
86 } else {
87 } else {
87 this.$buttongroup.removeAttr('disabled');
88 this.$buttongroup.removeAttr('disabled');
88 this.$droplabel.removeAttr('disabled');
89 this.$droplabel.removeAttr('disabled');
89 this.$dropbutton.removeAttr('disabled');
90 this.$dropbutton.removeAttr('disabled');
90 this.$droplist.removeAttr('disabled');
91 this.$droplist.removeAttr('disabled');
91 }
92 }
92
93
93 var description = this.model.get('description');
94 var description = this.model.get('description');
94 if (description.length === 0) {
95 if (description.length === 0) {
95 this.$label.hide();
96 this.$label.hide();
96 } else {
97 } else {
97 this.$label.text(description);
98 this.$label.text(description);
98 this.$label.show();
99 this.$label.show();
99 }
100 }
100 }
101 }
101 return DropdownView.__super__.update.apply(this);
102 return DropdownView.__super__.update.apply(this);
102 },
103 },
103
104
104 handle_click: function (e) {
105 handle_click: function (e) {
105 // Handle when a value is clicked.
106 // Handle when a value is clicked.
106
107
107 // Calling model.set will trigger all of the other views of the
108 // Calling model.set will trigger all of the other views of the
108 // model to update.
109 // model to update.
109 this.model.set('value', $(e.target).text(), {updated_view: this});
110 this.model.set('value', $(e.target).text(), {updated_view: this});
110 this.touch();
111 this.touch();
111 },
112 },
112
113
113 });
114 });
114 WidgetManager.register_widget_view('DropdownView', DropdownView);
115 WidgetManager.register_widget_view('DropdownView', DropdownView);
115
116
116
117
117 var RadioButtonsView = IPython.DOMWidgetView.extend({
118 var RadioButtonsView = IPython.DOMWidgetView.extend({
118 render : function(){
119 render : function(){
119 // Called when view is rendered.
120 // Called when view is rendered.
120 this.$el
121 this.$el
121 .addClass('widget-hbox');
122 .addClass('widget-hbox');
122 this.$label = $('<div />')
123 this.$label = $('<div />')
123 .appendTo(this.$el)
124 .appendTo(this.$el)
124 .addClass('widget-hlabel')
125 .addClass('widget-hlabel')
125 .hide();
126 .hide();
126 this.$container = $('<div />')
127 this.$container = $('<div />')
127 .appendTo(this.$el)
128 .appendTo(this.$el)
128 .addClass('widget-container')
129 .addClass('widget-container')
129 .addClass('vbox');
130 .addClass('vbox');
130 this.$el_to_style = this.$container; // Set default element to style
131 this.$el_to_style = this.$container; // Set default element to style
131 this.update();
132 this.update();
132 },
133 },
133
134
134 update : function(options){
135 update : function(options){
135 // Update the contents of this view
136 // Update the contents of this view
136 //
137 //
137 // Called when the model is changed. The model may have been
138 // Called when the model is changed. The model may have been
138 // changed by another view or by a state update from the back-end.
139 // changed by another view or by a state update from the back-end.
139 if (options === undefined || options.updated_view != this) {
140 if (options === undefined || options.updated_view != this) {
140 // Add missing items to the DOM.
141 // Add missing items to the DOM.
141 var items = this.model.get('values');
142 var items = this.model.get('values');
142 var disabled = this.model.get('disabled');
143 var disabled = this.model.get('disabled');
144 var that = this;
143 _.each(items, function(item, index) {
145 _.each(items, function(item, index) {
144 var item_query = ' :input[value="' + item + '"]';
146 var item_query = ' :input[value="' + item + '"]';
145 if (this.$el.find(item_query).length === 0) {
147 if (that.$el.find(item_query).length === 0) {
146 var $label = $('<label />')
148 var $label = $('<label />')
147 .addClass('radio')
149 .addClass('radio')
148 .text(item)
150 .text(item)
149 .appendTo(this.$container);
151 .appendTo(that.$container);
150
152
151 $('<input />')
153 $('<input />')
152 .attr('type', 'radio')
154 .attr('type', 'radio')
153 .addClass(this.model)
155 .addClass(that.model)
154 .val(item)
156 .val(item)
155 .prependTo($label)
157 .prependTo($label)
156 .on('click', $.proxy(this.handle_click, this));
158 .on('click', $.proxy(that.handle_click, that));
157 }
159 }
158
160
159 var $item_element = this.$container.find(item_query);
161 var $item_element = this.$container.find(item_query);
160 if (this.model.get('value') == item) {
162 if (this.model.get('value') == item) {
161 $item_element.prop('checked', true);
163 $item_element.prop('checked', true);
162 } else {
164 } else {
163 $item_element.prop('checked', false);
165 $item_element.prop('checked', false);
164 }
166 }
165 $item_element.prop('disabled', disabled);
167 $item_element.prop('disabled', disabled);
166 });
168 });
167
169
168 // Remove items that no longer exist.
170 // Remove items that no longer exist.
169 this.$container.find('input').each(function(i, obj) {
171 this.$container.find('input').each(function(i, obj) {
170 var value = $(obj).val();
172 var value = $(obj).val();
171 var found = false;
173 var found = false;
172 _.each(items, function(item, index) {
174 _.each(items, function(item, index) {
173 if (item == value) {
175 if (item == value) {
174 found = true;
176 found = true;
175 return false;
177 return false;
176 }
178 }
177 });
179 });
178
180
179 if (!found) {
181 if (!found) {
180 $(obj).parent().remove();
182 $(obj).parent().remove();
181 }
183 }
182 });
184 });
183
185
184 var description = this.model.get('description');
186 var description = this.model.get('description');
185 if (description.length === 0) {
187 if (description.length === 0) {
186 this.$label.hide();
188 this.$label.hide();
187 } else {
189 } else {
188 this.$label.text(description);
190 this.$label.text(description);
189 this.$label.show();
191 this.$label.show();
190 }
192 }
191 }
193 }
192 return RadioButtonsView.__super__.update.apply(this);
194 return RadioButtonsView.__super__.update.apply(this);
193 },
195 },
194
196
195 handle_click: function (e) {
197 handle_click: function (e) {
196 // Handle when a value is clicked.
198 // Handle when a value is clicked.
197
199
198 // Calling model.set will trigger all of the other views of the
200 // Calling model.set will trigger all of the other views of the
199 // model to update.
201 // model to update.
200 this.model.set('value', $(e.target).val(), {updated_view: this});
202 this.model.set('value', $(e.target).val(), {updated_view: this});
201 this.touch();
203 this.touch();
202 },
204 },
203 });
205 });
204 WidgetManager.register_widget_view('RadioButtonsView', RadioButtonsView);
206 WidgetManager.register_widget_view('RadioButtonsView', RadioButtonsView);
205
207
206
208
207 var ToggleButtonsView = IPython.DOMWidgetView.extend({
209 var ToggleButtonsView = IPython.DOMWidgetView.extend({
208 render : function(){
210 render : function(){
209 // Called when view is rendered.
211 // Called when view is rendered.
210 this.$el
212 this.$el
211 .addClass('widget-hbox-single');
213 .addClass('widget-hbox-single');
212 this.$label = $('<div />')
214 this.$label = $('<div />')
213 .appendTo(this.$el)
215 .appendTo(this.$el)
214 .addClass('widget-hlabel')
216 .addClass('widget-hlabel')
215 .hide();
217 .hide();
216 this.$buttongroup = $('<div />')
218 this.$buttongroup = $('<div />')
217 .addClass('btn-group')
219 .addClass('btn-group')
218 .attr('data-toggle', 'buttons-radio')
220 .attr('data-toggle', 'buttons-radio')
219 .appendTo(this.$el);
221 .appendTo(this.$el);
220 this.$el_to_style = this.$buttongroup; // Set default element to style
222 this.$el_to_style = this.$buttongroup; // Set default element to style
221 this.update();
223 this.update();
222 },
224 },
223
225
224 update : function(options){
226 update : function(options){
225 // Update the contents of this view
227 // Update the contents of this view
226 //
228 //
227 // Called when the model is changed. The model may have been
229 // Called when the model is changed. The model may have been
228 // changed by another view or by a state update from the back-end.
230 // changed by another view or by a state update from the back-end.
229 if (options === undefined || options.updated_view != this) {
231 if (options === undefined || options.updated_view != this) {
230 // Add missing items to the DOM.
232 // Add missing items to the DOM.
231 var items = this.model.get('values');
233 var items = this.model.get('values');
232 var disabled = this.model.get('disabled');
234 var disabled = this.model.get('disabled');
235 var that = this;
233 _.each(items, function(item, index) {
236 _.each(items, function(item, index) {
234 var item_query = ' :contains("' + item + '")';
237 var item_query = ' :contains("' + item + '")';
235 if (this.$buttongroup.find(item_query).length === 0) {
238 if (that.$buttongroup.find(item_query).length === 0) {
236 $('<button />')
239 $('<button />')
237 .attr('type', 'button')
240 .attr('type', 'button')
238 .addClass('btn')
241 .addClass('btn')
239 .text(item)
242 .text(item)
240 .appendTo(this.$buttongroup)
243 .appendTo(that.$buttongroup)
241 .on('click', $.proxy(this.handle_click, this));
244 .on('click', $.proxy(that.handle_click, that));
242 }
245 }
243
246
244 var $item_element = this.$buttongroup.find(item_query);
247 var $item_element = this.$buttongroup.find(item_query);
245 if (this.model.get('value') == item) {
248 if (this.model.get('value') == item) {
246 $item_element.addClass('active');
249 $item_element.addClass('active');
247 } else {
250 } else {
248 $item_element.removeClass('active');
251 $item_element.removeClass('active');
249 }
252 }
250 $item_element.prop('disabled', disabled);
253 $item_element.prop('disabled', disabled);
251 });
254 });
252
255
253 // Remove items that no longer exist.
256 // Remove items that no longer exist.
254 this.$buttongroup.find('button').each(function(i, obj) {
257 this.$buttongroup.find('button').each(function(i, obj) {
255 var value = $(obj).text();
258 var value = $(obj).text();
256 var found = false;
259 var found = false;
257 _.each(items, function(item, index) {
260 _.each(items, function(item, index) {
258 if (item == value) {
261 if (item == value) {
259 found = true;
262 found = true;
260 return false;
263 return false;
261 }
264 }
262 });
265 });
263
266
264 if (!found) {
267 if (!found) {
265 $(obj).remove();
268 $(obj).remove();
266 }
269 }
267 });
270 });
268
271
269 var description = this.model.get('description');
272 var description = this.model.get('description');
270 if (description.length === 0) {
273 if (description.length === 0) {
271 this.$label.hide();
274 this.$label.hide();
272 } else {
275 } else {
273 this.$label.text(description);
276 this.$label.text(description);
274 this.$label.show();
277 this.$label.show();
275 }
278 }
276 }
279 }
277 return ToggleButtonsView.__super__.update.apply(this);
280 return ToggleButtonsView.__super__.update.apply(this);
278 },
281 },
279
282
280 handle_click: function (e) {
283 handle_click: function (e) {
281 // Handle when a value is clicked.
284 // Handle when a value is clicked.
282
285
283 // Calling model.set will trigger all of the other views of the
286 // Calling model.set will trigger all of the other views of the
284 // model to update.
287 // model to update.
285 this.model.set('value', $(e.target).text(), {updated_view: this});
288 this.model.set('value', $(e.target).text(), {updated_view: this});
286 this.touch();
289 this.touch();
287 },
290 },
288 });
291 });
289 WidgetManager.register_widget_view('ToggleButtonsView', ToggleButtonsView);
292 WidgetManager.register_widget_view('ToggleButtonsView', ToggleButtonsView);
290
293
291
294
292 var ListBoxView = IPython.DOMWidgetView.extend({
295 var ListBoxView = IPython.DOMWidgetView.extend({
293 render : function(){
296 render : function(){
294 // Called when view is rendered.
297 // Called when view is rendered.
295 this.$el
298 this.$el
296 .addClass('widget-hbox');
299 .addClass('widget-hbox');
297 this.$label = $('<div />')
300 this.$label = $('<div />')
298 .appendTo(this.$el)
301 .appendTo(this.$el)
299 .addClass('widget-hlabel')
302 .addClass('widget-hlabel')
300 .hide();
303 .hide();
301 this.$listbox = $('<select />')
304 this.$listbox = $('<select />')
302 .addClass('widget-listbox')
305 .addClass('widget-listbox')
303 .attr('size', 6)
306 .attr('size', 6)
304 .appendTo(this.$el);
307 .appendTo(this.$el);
305 this.$el_to_style = this.$listbox; // Set default element to style
308 this.$el_to_style = this.$listbox; // Set default element to style
306 this.update();
309 this.update();
307 },
310 },
308
311
309 update : function(options){
312 update : function(options){
310 // Update the contents of this view
313 // Update the contents of this view
311 //
314 //
312 // Called when the model is changed. The model may have been
315 // Called when the model is changed. The model may have been
313 // changed by another view or by a state update from the back-end.
316 // changed by another view or by a state update from the back-end.
314 if (options === undefined || options.updated_view != this) {
317 if (options === undefined || options.updated_view != this) {
315 // Add missing items to the DOM.
318 // Add missing items to the DOM.
316 var items = this.model.get('values');
319 var items = this.model.get('values');
320 var that = this;
317 _.each(items, function(item, index) {
321 _.each(items, function(item, index) {
318 var item_query = ' :contains("' + item + '")';
322 var item_query = ' :contains("' + item + '")';
319 if (this.$listbox.find(item_query).length === 0) {
323 if (that.$listbox.find(item_query).length === 0) {
320 $('<option />')
324 $('<option />')
321 .text(item)
325 .text(item)
322 .attr('value', item)
326 .attr('value', item)
323 .appendTo(this.$listbox)
327 .appendTo(that.$listbox)
324 .on('click', $.proxy(this.handle_click, this));
328 .on('click', $.proxy(that.handle_click, that));
325 }
329 }
326 });
330 });
327
331
328 // Select the correct element
332 // Select the correct element
329 this.$listbox.val(this.model.get('value'));
333 this.$listbox.val(this.model.get('value'));
330
334
331 // Disable listbox if needed
335 // Disable listbox if needed
332 var disabled = this.model.get('disabled');
336 var disabled = this.model.get('disabled');
333 this.$listbox.prop('disabled', disabled);
337 this.$listbox.prop('disabled', disabled);
334
338
335 // Remove items that no longer exist.
339 // Remove items that no longer exist.
336 this.$listbox.find('option').each(function(i, obj) {
340 this.$listbox.find('option').each(function(i, obj) {
337 var value = $(obj).text();
341 var value = $(obj).text();
338 var found = false;
342 var found = false;
339 _.each(items, function(item, index) {
343 _.each(items, function(item, index) {
340 if (item == value) {
344 if (item == value) {
341 found = true;
345 found = true;
342 return false;
346 return false;
343 }
347 }
344 });
348 });
345
349
346 if (!found) {
350 if (!found) {
347 $(obj).remove();
351 $(obj).remove();
348 }
352 }
349 });
353 });
350
354
351 var description = this.model.get('description');
355 var description = this.model.get('description');
352 if (description.length === 0) {
356 if (description.length === 0) {
353 this.$label.hide();
357 this.$label.hide();
354 } else {
358 } else {
355 this.$label.text(description);
359 this.$label.text(description);
356 this.$label.show();
360 this.$label.show();
357 }
361 }
358 }
362 }
359 return ListBoxView.__super__.update.apply(this);
363 return ListBoxView.__super__.update.apply(this);
360 },
364 },
361
365
362 handle_click: function (e) {
366 handle_click: function (e) {
363 // Handle when a value is clicked.
367 // Handle when a value is clicked.
364
368
365 // Calling model.set will trigger all of the other views of the
369 // Calling model.set will trigger all of the other views of the
366 // model to update.
370 // model to update.
367 this.model.set('value', $(e.target).text(), {updated_view: this});
371 this.model.set('value', $(e.target).text(), {updated_view: this});
368 this.touch();
372 this.touch();
369 },
373 },
370 });
374 });
371 WidgetManager.register_widget_view('ListBoxView', ListBoxView);
375 WidgetManager.register_widget_view('ListBoxView', ListBoxView);
372 });
376 });
@@ -1,242 +1,244 b''
1 //----------------------------------------------------------------------------
1 //----------------------------------------------------------------------------
2 // Copyright (C) 2013 The IPython Development Team
2 // Copyright (C) 2013 The IPython Development Team
3 //
3 //
4 // Distributed under the terms of the BSD License. The full license is in
4 // Distributed under the terms of the BSD License. The full license is in
5 // the file COPYING, distributed as part of this software.
5 // the file COPYING, distributed as part of this software.
6 //----------------------------------------------------------------------------
6 //----------------------------------------------------------------------------
7
7
8 //============================================================================
8 //============================================================================
9 // SelectionContainerWidget
9 // SelectionContainerWidget
10 //============================================================================
10 //============================================================================
11
11
12 /**
12 /**
13 * @module IPython
13 * @module IPython
14 * @namespace IPython
14 * @namespace IPython
15 **/
15 **/
16
16
17 define(["notebook/js/widgets/widget"], function(WidgetManager){
17 define(["notebook/js/widgets/widget"], function(WidgetManager){
18
18
19 var AccordionView = IPython.DOMWidgetView.extend({
19 var AccordionView = IPython.DOMWidgetView.extend({
20 render: function(){
20 render: function(){
21 // Called when view is rendered.
21 // Called when view is rendered.
22 var guid = 'accordion' + IPython.utils.uuid();
22 var guid = 'accordion' + IPython.utils.uuid();
23 this.$el
23 this.$el
24 .attr('id', guid)
24 .attr('id', guid)
25 .addClass('accordion');
25 .addClass('accordion');
26 this.containers = [];
26 this.containers = [];
27 this.model_containers = {};
27 this.model_containers = {};
28 this.update_children([], this.model.get('_children'));
28 this.update_children([], this.model.get('_children'));
29 this.model.on('change:_children', function(model, value, options) {
29 this.model.on('change:_children', function(model, value, options) {
30 this.update_children(model.previous('_children'), value);
30 this.update_children(model.previous('_children'), value);
31 }, this);
31 }, this);
32 },
32 },
33
33
34 update: function(options) {
34 update: function(options) {
35 // Update the contents of this view
35 // Update the contents of this view
36 //
36 //
37 // Called when the model is changed. The model may have been
37 // Called when the model is changed. The model may have been
38 // changed by another view or by a state update from the back-end.
38 // changed by another view or by a state update from the back-end.
39 if (options === undefined || options.updated_view != this) {
39 if (options === undefined || options.updated_view != this) {
40 // Set tab titles
40 // Set tab titles
41 var titles = this.model.get('_titles');
41 var titles = this.model.get('_titles');
42 var that = this;
42 _.each(titles, function(title, page_index) {
43 _.each(titles, function(title, page_index) {
43 var accordian = this.containers[page_index];
44 var accordian = that.containers[page_index];
44 if (accordian !== undefined) {
45 if (accordian !== undefined) {
45 accordian
46 accordian
46 .find('.accordion-heading')
47 .find('.accordion-heading')
47 .find('.accordion-toggle')
48 .find('.accordion-toggle')
48 .text(title);
49 .text(title);
49 }
50 }
50 });
51 });
51
52
52 // Set selected page
53 // Set selected page
53 var selected_index = this.model.get("selected_index");
54 var selected_index = this.model.get("selected_index");
54 if (0 <= selected_index && selected_index < this.containers.length) {
55 if (0 <= selected_index && selected_index < this.containers.length) {
55 _.each(this.containers, function(container, index) {
56 _.each(this.containers, function(container, index) {
56 if (index==selected_index) {
57 if (index==selected_index) {
57 container.find('.accordion-body').collapse('show');
58 container.find('.accordion-body').collapse('show');
58 } else {
59 } else {
59 container.find('.accordion-body').collapse('hide');
60 container.find('.accordion-body').collapse('hide');
60 }
61 }
61 });
62 });
62 }
63 }
63 }
64 }
64 return AccordionView.__super__.update.apply(this);
65 return AccordionView.__super__.update.apply(this);
65 },
66 },
66
67
67 update_children: function(old_list, new_list) {
68 update_children: function(old_list, new_list) {
68 // Called when the children list is modified.
69 // Called when the children list is modified.
69 this.do_diff(old_list,
70 this.do_diff(old_list,
70 new_list,
71 new_list,
71 $.proxy(this.remove_child_model, this),
72 $.proxy(this.remove_child_model, this),
72 $.proxy(this.add_child_model, this));
73 $.proxy(this.add_child_model, this));
73 },
74 },
74
75
75 remove_child_model: function(model) {
76 remove_child_model: function(model) {
76 // Called when a child is removed from children list.
77 // Called when a child is removed from children list.
77 var accordion_group = this.model_containers[model.id];
78 var accordion_group = this.model_containers[model.id];
78 this.containers.splice(accordion_group.container_index, 1);
79 this.containers.splice(accordion_group.container_index, 1);
79 delete this.model_containers[model.id];
80 delete this.model_containers[model.id];
80 accordion_group.remove();
81 accordion_group.remove();
81 this.delete_child_view(model);
82 this.delete_child_view(model);
82 },
83 },
83
84
84 add_child_model: function(model) {
85 add_child_model: function(model) {
85 // Called when a child is added to children list.
86 // Called when a child is added to children list.
86 var view = this.create_child_view(model);
87 var view = this.create_child_view(model);
87 var index = this.containers.length;
88 var index = this.containers.length;
88 var uuid = IPython.utils.uuid();
89 var uuid = IPython.utils.uuid();
89 var accordion_group = $('<div />')
90 var accordion_group = $('<div />')
90 .addClass('accordion-group')
91 .addClass('accordion-group')
91 .appendTo(this.$el);
92 .appendTo(this.$el);
92 var accordion_heading = $('<div />')
93 var accordion_heading = $('<div />')
93 .addClass('accordion-heading')
94 .addClass('accordion-heading')
94 .appendTo(accordion_group);
95 .appendTo(accordion_group);
95 var that = this;
96 var that = this;
96 var accordion_toggle = $('<a />')
97 var accordion_toggle = $('<a />')
97 .addClass('accordion-toggle')
98 .addClass('accordion-toggle')
98 .attr('data-toggle', 'collapse')
99 .attr('data-toggle', 'collapse')
99 .attr('data-parent', '#' + this.$el.attr('id'))
100 .attr('data-parent', '#' + this.$el.attr('id'))
100 .attr('href', '#' + uuid)
101 .attr('href', '#' + uuid)
101 .click(function(evt){
102 .click(function(evt){
102
103
103 // Calling model.set will trigger all of the other views of the
104 // Calling model.set will trigger all of the other views of the
104 // model to update.
105 // model to update.
105 that.model.set("selected_index", index, {updated_view: this});
106 that.model.set("selected_index", index, {updated_view: this});
106 that.touch();
107 that.touch();
107 })
108 })
108 .text('Page ' + index)
109 .text('Page ' + index)
109 .appendTo(accordion_heading);
110 .appendTo(accordion_heading);
110 var accordion_body = $('<div />', {id: uuid})
111 var accordion_body = $('<div />', {id: uuid})
111 .addClass('accordion-body collapse')
112 .addClass('accordion-body collapse')
112 .appendTo(accordion_group);
113 .appendTo(accordion_group);
113 var accordion_inner = $('<div />')
114 var accordion_inner = $('<div />')
114 .addClass('accordion-inner')
115 .addClass('accordion-inner')
115 .appendTo(accordion_body);
116 .appendTo(accordion_body);
116 var container_index = this.containers.push(accordion_group) - 1;
117 var container_index = this.containers.push(accordion_group) - 1;
117 accordion_group.container_index = container_index;
118 accordion_group.container_index = container_index;
118 this.model_containers[model.id] = accordion_group;
119 this.model_containers[model.id] = accordion_group;
119 accordion_inner.append(view.$el);
120 accordion_inner.append(view.$el);
120
121
121 this.update();
122 this.update();
122
123
123 // Stupid workaround to close the bootstrap accordion tabs which
124 // Stupid workaround to close the bootstrap accordion tabs which
124 // open by default even though they don't have the `in` class
125 // open by default even though they don't have the `in` class
125 // attached to them. For some reason a delay is required.
126 // attached to them. For some reason a delay is required.
126 // TODO: Better fix.
127 // TODO: Better fix.
127 setTimeout(function(){ that.update(); }, 500);
128 setTimeout(function(){ that.update(); }, 500);
128 },
129 },
129 });
130 });
130 WidgetManager.register_widget_view('AccordionView', AccordionView);
131 WidgetManager.register_widget_view('AccordionView', AccordionView);
131
132
132
133
133 var TabView = IPython.DOMWidgetView.extend({
134 var TabView = IPython.DOMWidgetView.extend({
134 initialize: function() {
135 initialize: function() {
135 // Public constructor.
136 // Public constructor.
136 this.containers = [];
137 this.containers = [];
137 TabView.__super__.initialize.apply(this, arguments);
138 TabView.__super__.initialize.apply(this, arguments);
138 },
139 },
139
140
140 render: function(){
141 render: function(){
141 // Called when view is rendered.
142 // Called when view is rendered.
142 var uuid = 'tabs'+IPython.utils.uuid();
143 var uuid = 'tabs'+IPython.utils.uuid();
143 var that = this;
144 var that = this;
144 this.$tabs = $('<div />', {id: uuid})
145 this.$tabs = $('<div />', {id: uuid})
145 .addClass('nav')
146 .addClass('nav')
146 .addClass('nav-tabs')
147 .addClass('nav-tabs')
147 .appendTo(this.$el);
148 .appendTo(this.$el);
148 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
149 this.$tab_contents = $('<div />', {id: uuid + 'Content'})
149 .addClass('tab-content')
150 .addClass('tab-content')
150 .appendTo(this.$el);
151 .appendTo(this.$el);
151 this.containers = [];
152 this.containers = [];
152 this.update_children([], this.model.get('_children'));
153 this.update_children([], this.model.get('_children'));
153 this.model.on('change:_children', function(model, value, options) {
154 this.model.on('change:_children', function(model, value, options) {
154 this.update_children(model.previous('_children'), value);
155 this.update_children(model.previous('_children'), value);
155 }, this);
156 }, this);
156 },
157 },
157
158
158 update_children: function(old_list, new_list) {
159 update_children: function(old_list, new_list) {
159 // Called when the children list is modified.
160 // Called when the children list is modified.
160 this.do_diff(old_list,
161 this.do_diff(old_list,
161 new_list,
162 new_list,
162 $.proxy(this.remove_child_model, this),
163 $.proxy(this.remove_child_model, this),
163 $.proxy(this.add_child_model, this));
164 $.proxy(this.add_child_model, this));
164 },
165 },
165
166
166 remove_child_model: function(model) {
167 remove_child_model: function(model) {
167 // Called when a child is removed from children list.
168 // Called when a child is removed from children list.
168 var view = this.child_views[model.id];
169 var view = this.child_views[model.id];
169 this.containers.splice(view.parent_tab.tab_text_index, 1);
170 this.containers.splice(view.parent_tab.tab_text_index, 1);
170 view.parent_tab.remove();
171 view.parent_tab.remove();
171 view.parent_container.remove();
172 view.parent_container.remove();
172 view.remove();
173 view.remove();
173 this.delete_child_view(model);
174 this.delete_child_view(model);
174 },
175 },
175
176
176 add_child_model: function(model) {
177 add_child_model: function(model) {
177 // Called when a child is added to children list.
178 // Called when a child is added to children list.
178 var view = this.create_child_view(model);
179 var view = this.create_child_view(model);
179 var index = this.containers.length;
180 var index = this.containers.length;
180 var uuid = IPython.utils.uuid();
181 var uuid = IPython.utils.uuid();
181
182
182 var that = this;
183 var that = this;
183 var tab = $('<li />')
184 var tab = $('<li />')
184 .css('list-style-type', 'none')
185 .css('list-style-type', 'none')
185 .appendTo(this.$tabs);
186 .appendTo(this.$tabs);
186 view.parent_tab = tab;
187 view.parent_tab = tab;
187
188
188 var tab_text = $('<a />')
189 var tab_text = $('<a />')
189 .attr('href', '#' + uuid)
190 .attr('href', '#' + uuid)
190 .attr('data-toggle', 'tab')
191 .attr('data-toggle', 'tab')
191 .text('Page ' + index)
192 .text('Page ' + index)
192 .appendTo(tab)
193 .appendTo(tab)
193 .click(function (e) {
194 .click(function (e) {
194
195
195 // Calling model.set will trigger all of the other views of the
196 // Calling model.set will trigger all of the other views of the
196 // model to update.
197 // model to update.
197 that.model.set("selected_index", index, {updated_view: this});
198 that.model.set("selected_index", index, {updated_view: this});
198 that.touch();
199 that.touch();
199 that.select_page(index);
200 that.select_page(index);
200 });
201 });
201 tab.tab_text_index = this.containers.push(tab_text) - 1;
202 tab.tab_text_index = this.containers.push(tab_text) - 1;
202
203
203 var contents_div = $('<div />', {id: uuid})
204 var contents_div = $('<div />', {id: uuid})
204 .addClass('tab-pane')
205 .addClass('tab-pane')
205 .addClass('fade')
206 .addClass('fade')
206 .append(view.$el)
207 .append(view.$el)
207 .appendTo(this.$tab_contents);
208 .appendTo(this.$tab_contents);
208 view.parent_container = contents_div;
209 view.parent_container = contents_div;
209 },
210 },
210
211
211 update: function(options) {
212 update: function(options) {
212 // Update the contents of this view
213 // Update the contents of this view
213 //
214 //
214 // Called when the model is changed. The model may have been
215 // Called when the model is changed. The model may have been
215 // changed by another view or by a state update from the back-end.
216 // changed by another view or by a state update from the back-end.
216 if (options === undefined || options.updated_view != this) {
217 if (options === undefined || options.updated_view != this) {
217 // Set tab titles
218 // Set tab titles
218 var titles = this.model.get('_titles');
219 var titles = this.model.get('_titles');
220 var that = this;
219 _.each(titles, function(title, page_index) {
221 _.each(titles, function(title, page_index) {
220 var tab_text = this.containers[page_index];
222 var tab_text = that.containers[page_index];
221 if (tab_text !== undefined) {
223 if (tab_text !== undefined) {
222 tab_text.text(title);
224 tab_text.text(title);
223 }
225 }
224 });
226 });
225
227
226 var selected_index = this.model.get('selected_index');
228 var selected_index = this.model.get('selected_index');
227 if (0 <= selected_index && selected_index < this.containers.length) {
229 if (0 <= selected_index && selected_index < this.containers.length) {
228 this.select_page(selected_index);
230 this.select_page(selected_index);
229 }
231 }
230 }
232 }
231 return TabView.__super__.update.apply(this);
233 return TabView.__super__.update.apply(this);
232 },
234 },
233
235
234 select_page: function(index) {
236 select_page: function(index) {
235 // Select a page.
237 // Select a page.
236 this.$tabs.find('li')
238 this.$tabs.find('li')
237 .removeClass('active');
239 .removeClass('active');
238 this.containers[index].tab('show');
240 this.containers[index].tab('show');
239 },
241 },
240 });
242 });
241 WidgetManager.register_widget_view('TabView', TabView);
243 WidgetManager.register_widget_view('TabView', TabView);
242 });
244 });
@@ -1,478 +1,483 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 inspect
16 import inspect
17 import types
17 import types
18
18
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 from IPython.utils.py3compat import string_types
22 from IPython.utils.py3compat import string_types
23
23
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 # Classes
25 # Classes
26 #-----------------------------------------------------------------------------
26 #-----------------------------------------------------------------------------
27 class CallbackDispatcher(LoggingConfigurable):
27 class CallbackDispatcher(LoggingConfigurable):
28 acceptable_nargs = List([], help="""List of integers.
28 acceptable_nargs = List([], help="""List of integers.
29 The number of arguments in the callbacks registered must match one of
29 The number of arguments in the callbacks registered must match one of
30 the integers in this list. If this list is empty or None, it will be
30 the integers in this list. If this list is empty or None, it will be
31 ignored.""")
31 ignored.""")
32
32
33 def __init__(self, *pargs, **kwargs):
33 def __init__(self, *pargs, **kwargs):
34 """Constructor"""
34 """Constructor"""
35 LoggingConfigurable.__init__(self, *pargs, **kwargs)
35 LoggingConfigurable.__init__(self, *pargs, **kwargs)
36 self.callbacks = {}
36 self.callbacks = {}
37
37
38 def __call__(self, *pargs, **kwargs):
38 def __call__(self, *pargs, **kwargs):
39 """Call all of the registered callbacks that have the same number of
39 """Call all of the registered callbacks that have the same number of
40 positional arguments."""
40 positional arguments."""
41 nargs = len(pargs)
41 nargs = len(pargs)
42 self._validate_nargs(nargs)
42 self._validate_nargs(nargs)
43 if nargs in self.callbacks:
43 if nargs in self.callbacks:
44 for callback in self.callbacks[nargs]:
44 for callback in self.callbacks[nargs]:
45 callback(*pargs, **kwargs)
45 callback(*pargs, **kwargs)
46
46
47 def register_callback(self, callback, remove=False):
47 def register_callback(self, callback, remove=False):
48 """(Un)Register a callback
48 """(Un)Register a callback
49
49
50 Parameters
50 Parameters
51 ----------
51 ----------
52 callback: method handle
52 callback: method handle
53 Method to be registered or unregisted.
53 Method to be registered or unregisted.
54 remove=False: bool
54 remove=False: bool
55 Whether or not to unregister the callback."""
55 Whether or not to unregister the callback."""
56
56
57 # Validate the number of arguments that the callback accepts.
57 # Validate the number of arguments that the callback accepts.
58 nargs = self._get_nargs(callback)
58 nargs = self._get_nargs(callback)
59 self._validate_nargs(nargs)
59 self._validate_nargs(nargs)
60
60
61 # Get/create the appropriate list of callbacks.
61 # Get/create the appropriate list of callbacks.
62 if nargs not in self.callbacks:
62 if nargs not in self.callbacks:
63 self.callbacks[nargs] = []
63 self.callbacks[nargs] = []
64 callback_list = self.callbacks[nargs]
64 callback_list = self.callbacks[nargs]
65
65
66 # (Un)Register the callback.
66 # (Un)Register the callback.
67 if remove and callback in callback_list:
67 if remove and callback in callback_list:
68 callback_list.remove(callback)
68 callback_list.remove(callback)
69 elif not remove and callback not in callback_list:
69 elif not remove and callback not in callback_list:
70 callback_list.append(callback)
70 callback_list.append(callback)
71
71
72 def _validate_nargs(self, nargs):
72 def _validate_nargs(self, nargs):
73 if self.acceptable_nargs is not None and \
73 if self.acceptable_nargs is not None and \
74 len(self.acceptable_nargs) > 0 and \
74 len(self.acceptable_nargs) > 0 and \
75 nargs not in self.acceptable_nargs:
75 nargs not in self.acceptable_nargs:
76
76
77 raise TypeError('Invalid number of positional arguments. See acceptable_nargs list.')
77 raise TypeError('Invalid number of positional arguments. See acceptable_nargs list.')
78
78
79 def _get_nargs(self, callback):
79 def _get_nargs(self, callback):
80 """Gets the number of arguments in a callback"""
80 """Gets the number of arguments in a callback"""
81 if callable(callback):
81 if callable(callback):
82 argspec = inspect.getargspec(callback)
82 argspec = inspect.getargspec(callback)
83 nargs = len(argspec[1]) # Only count vargs!
83 if argspec[0] is None:
84 nargs = 0
85 elif argspec[3] is None:
86 nargs = len(argspec[0]) # Only count vargs!
87 else:
88 nargs = len(argspec[0]) - len(argspec[3]) # Subtract number of defaults.
84
89
85 # Bound methods have an additional 'self' argument
90 # Bound methods have an additional 'self' argument
86 if isinstance(callback, types.MethodType):
91 if isinstance(callback, types.MethodType):
87 nargs -= 1
92 nargs -= 1
88 return nargs
93 return nargs
89 else:
94 else:
90 raise TypeError('Callback must be callable.')
95 raise TypeError('Callback must be callable.')
91
96
92
97
93 class Widget(LoggingConfigurable):
98 class Widget(LoggingConfigurable):
94 #-------------------------------------------------------------------------
99 #-------------------------------------------------------------------------
95 # Class attributes
100 # Class attributes
96 #-------------------------------------------------------------------------
101 #-------------------------------------------------------------------------
97 widget_construction_callback = None
102 widget_construction_callback = None
98 widgets = {}
103 widgets = {}
99
104
100 def on_widget_constructed(callback):
105 def on_widget_constructed(callback):
101 """Registers a callback to be called when a widget is constructed.
106 """Registers a callback to be called when a widget is constructed.
102
107
103 The callback must have the following signature:
108 The callback must have the following signature:
104 callback(widget)"""
109 callback(widget)"""
105 Widget.widget_construction_callback = callback
110 Widget.widget_construction_callback = callback
106
111
107 def _call_widget_constructed(widget):
112 def _call_widget_constructed(widget):
108 """Class method, called when a widget is constructed."""
113 """Class method, called when a widget is constructed."""
109 if Widget.widget_construction_callback is not None and callable(Widget.widget_construction_callback):
114 if Widget.widget_construction_callback is not None and callable(Widget.widget_construction_callback):
110 Widget.widget_construction_callback(widget)
115 Widget.widget_construction_callback(widget)
111
116
112 #-------------------------------------------------------------------------
117 #-------------------------------------------------------------------------
113 # Traits
118 # Traits
114 #-------------------------------------------------------------------------
119 #-------------------------------------------------------------------------
115 model_name = Unicode('WidgetModel', help="""Name of the backbone model
120 model_name = Unicode('WidgetModel', help="""Name of the backbone model
116 registered in the front-end to create and sync this widget with.""")
121 registered in the front-end to create and sync this widget with.""")
117 view_name = Unicode(help="""Default view registered in the front-end
122 view_name = Unicode(help="""Default view registered in the front-end
118 to use to represent the widget.""", sync=True)
123 to use to represent the widget.""", sync=True)
119 _comm = Instance('IPython.kernel.comm.Comm')
124 _comm = Instance('IPython.kernel.comm.Comm')
120
125
121 #-------------------------------------------------------------------------
126 #-------------------------------------------------------------------------
122 # (Con/de)structor
127 # (Con/de)structor
123 #-------------------------------------------------------------------------
128 #-------------------------------------------------------------------------
124 def __init__(self, **kwargs):
129 def __init__(self, **kwargs):
125 """Public constructor"""
130 """Public constructor"""
126 self.closed = False
131 self.closed = False
127
132
128 self._property_lock = (None, None)
133 self._property_lock = (None, None)
129 self._keys = None
134 self._keys = None
130
135
131 self._display_callbacks = CallbackDispatcher(acceptable_nargs=[0])
136 self._display_callbacks = CallbackDispatcher(acceptable_nargs=[0])
132 self._msg_callbacks = CallbackDispatcher(acceptable_nargs=[1, 2])
137 self._msg_callbacks = CallbackDispatcher(acceptable_nargs=[1, 2])
133
138
134 super(Widget, self).__init__(**kwargs)
139 super(Widget, self).__init__(**kwargs)
135
140
136 self.on_trait_change(self._handle_property_changed, self.keys)
141 self.on_trait_change(self._handle_property_changed, self.keys)
137 Widget._call_widget_constructed(self)
142 Widget._call_widget_constructed(self)
138
143
139 def __del__(self):
144 def __del__(self):
140 """Object disposal"""
145 """Object disposal"""
141 self.close()
146 self.close()
142
147
143 #-------------------------------------------------------------------------
148 #-------------------------------------------------------------------------
144 # Properties
149 # Properties
145 #-------------------------------------------------------------------------
150 #-------------------------------------------------------------------------
146 @property
151 @property
147 def keys(self):
152 def keys(self):
148 """Gets a list of the traitlets that should be synced with the front-end."""
153 """Gets a list of the traitlets that should be synced with the front-end."""
149 if self._keys is None:
154 if self._keys is None:
150 self._keys = []
155 self._keys = []
151 for trait_name in self.trait_names():
156 for trait_name in self.trait_names():
152 if self.trait_metadata(trait_name, 'sync'):
157 if self.trait_metadata(trait_name, 'sync'):
153 self._keys.append(trait_name)
158 self._keys.append(trait_name)
154 return self._keys
159 return self._keys
155
160
156 @property
161 @property
157 def comm(self):
162 def comm(self):
158 """Gets the Comm associated with this widget.
163 """Gets the Comm associated with this widget.
159
164
160 If a Comm doesn't exist yet, a Comm will be created automagically."""
165 If a Comm doesn't exist yet, a Comm will be created automagically."""
161 if self._comm is None:
166 if self._comm is None:
162 # Create a comm.
167 # Create a comm.
163 self._comm = Comm(target_name=self.model_name)
168 self._comm = Comm(target_name=self.model_name)
164 self._comm.on_msg(self._handle_msg)
169 self._comm.on_msg(self._handle_msg)
165 self._comm.on_close(self._close)
170 self._comm.on_close(self._close)
166 Widget.widgets[self.model_id] = self
171 Widget.widgets[self.model_id] = self
167
172
168 # first update
173 # first update
169 self.send_state()
174 self.send_state()
170 return self._comm
175 return self._comm
171
176
172 @property
177 @property
173 def model_id(self):
178 def model_id(self):
174 """Gets the model id of this widget.
179 """Gets the model id of this widget.
175
180
176 If a Comm doesn't exist yet, a Comm will be created automagically."""
181 If a Comm doesn't exist yet, a Comm will be created automagically."""
177 return self.comm.comm_id
182 return self.comm.comm_id
178
183
179 #-------------------------------------------------------------------------
184 #-------------------------------------------------------------------------
180 # Methods
185 # Methods
181 #-------------------------------------------------------------------------
186 #-------------------------------------------------------------------------
182 def close(self):
187 def close(self):
183 """Close method.
188 """Close method.
184
189
185 Closes the widget which closes the underlying comm.
190 Closes the widget which closes the underlying comm.
186 When the comm is closed, all of the widget views are automatically
191 When the comm is closed, all of the widget views are automatically
187 removed from the front-end."""
192 removed from the front-end."""
188 if not self.closed:
193 if not self.closed:
189 self._comm.close()
194 self._comm.close()
190 self._close()
195 self._close()
191
196
192 def send_state(self, key=None):
197 def send_state(self, key=None):
193 """Sends the widget state, or a piece of it, to the front-end.
198 """Sends the widget state, or a piece of it, to the front-end.
194
199
195 Parameters
200 Parameters
196 ----------
201 ----------
197 key : unicode (optional)
202 key : unicode (optional)
198 A single property's name to sync with the front-end.
203 A single property's name to sync with the front-end.
199 """
204 """
200 self._send({
205 self._send({
201 "method" : "update",
206 "method" : "update",
202 "state" : self.get_state()
207 "state" : self.get_state()
203 })
208 })
204
209
205 def get_state(self, key=None):
210 def get_state(self, key=None):
206 """Gets the widget state, or a piece of it.
211 """Gets the widget state, or a piece of it.
207
212
208 Parameters
213 Parameters
209 ----------
214 ----------
210 key : unicode (optional)
215 key : unicode (optional)
211 A single property's name to get.
216 A single property's name to get.
212 """
217 """
213 keys = self.keys if key is None else [key]
218 keys = self.keys if key is None else [key]
214 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
219 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
215
220
216 def send(self, content):
221 def send(self, content):
217 """Sends a custom msg to the widget model in the front-end.
222 """Sends a custom msg to the widget model in the front-end.
218
223
219 Parameters
224 Parameters
220 ----------
225 ----------
221 content : dict
226 content : dict
222 Content of the message to send.
227 Content of the message to send.
223 """
228 """
224 self._send({"method": "custom", "content": content})
229 self._send({"method": "custom", "content": content})
225
230
226 def on_msg(self, callback, remove=False):
231 def on_msg(self, callback, remove=False):
227 """(Un)Register a custom msg recieve callback.
232 """(Un)Register a custom msg recieve callback.
228
233
229 Parameters
234 Parameters
230 ----------
235 ----------
231 callback: method handler
236 callback: method handler
232 Can have a signature of:
237 Can have a signature of:
233 - callback(content) Signature 1
238 - callback(content) Signature 1
234 - callback(sender, content) Signature 2
239 - callback(sender, content) Signature 2
235 remove: bool
240 remove: bool
236 True if the callback should be unregistered."""
241 True if the callback should be unregistered."""
237 self._msg_callbacks.register_callback(callback, remove=remove)
242 self._msg_callbacks.register_callback(callback, remove=remove)
238
243
239 def on_displayed(self, callback, remove=False):
244 def on_displayed(self, callback, remove=False):
240 """(Un)Register a widget displayed callback.
245 """(Un)Register a widget displayed callback.
241
246
242 Parameters
247 Parameters
243 ----------
248 ----------
244 callback: method handler
249 callback: method handler
245 Can have a signature of:
250 Can have a signature of:
246 - callback(sender, **kwargs)
251 - callback(sender, **kwargs)
247 kwargs from display call passed through without modification.
252 kwargs from display call passed through without modification.
248 remove: bool
253 remove: bool
249 True if the callback should be unregistered."""
254 True if the callback should be unregistered."""
250 self._display_callbacks.register_callback(callback, remove=remove)
255 self._display_callbacks.register_callback(callback, remove=remove)
251
256
252 #-------------------------------------------------------------------------
257 #-------------------------------------------------------------------------
253 # Support methods
258 # Support methods
254 #-------------------------------------------------------------------------
259 #-------------------------------------------------------------------------
255 @contextmanager
260 @contextmanager
256 def _lock_property(self, key, value):
261 def _lock_property(self, key, value):
257 """Lock a property-value pair.
262 """Lock a property-value pair.
258
263
259 NOTE: This, in addition to the single lock for all state changes, is
264 NOTE: This, in addition to the single lock for all state changes, is
260 flawed. In the future we may want to look into buffering state changes
265 flawed. In the future we may want to look into buffering state changes
261 back to the front-end."""
266 back to the front-end."""
262 self._property_lock = (key, value)
267 self._property_lock = (key, value)
263 try:
268 try:
264 yield
269 yield
265 finally:
270 finally:
266 self._property_lock = (None, None)
271 self._property_lock = (None, None)
267
272
268 def _should_send_property(self, key, value):
273 def _should_send_property(self, key, value):
269 """Check the property lock (property_lock)"""
274 """Check the property lock (property_lock)"""
270 return key != self._property_lock[0] or \
275 return key != self._property_lock[0] or \
271 value != self._property_lock[1]
276 value != self._property_lock[1]
272
277
273 def _close(self):
278 def _close(self):
274 """Unsafe close"""
279 """Unsafe close"""
275 del Widget.widgets[self.model_id]
280 del Widget.widgets[self.model_id]
276 self._comm = None
281 self._comm = None
277 self.closed = True
282 self.closed = True
278
283
279 # Event handlers
284 # Event handlers
280 def _handle_msg(self, msg):
285 def _handle_msg(self, msg):
281 """Called when a msg is received from the front-end"""
286 """Called when a msg is received from the front-end"""
282 data = msg['content']['data']
287 data = msg['content']['data']
283 method = data['method']
288 method = data['method']
284 if not method in ['backbone', 'custom']:
289 if not method in ['backbone', 'custom']:
285 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
290 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
286
291
287 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
292 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
288 if method == 'backbone' and 'sync_data' in data:
293 if method == 'backbone' and 'sync_data' in data:
289 sync_data = data['sync_data']
294 sync_data = data['sync_data']
290 self._handle_receive_state(sync_data) # handles all methods
295 self._handle_receive_state(sync_data) # handles all methods
291
296
292 # Handle a custom msg from the front-end
297 # Handle a custom msg from the front-end
293 elif method == 'custom':
298 elif method == 'custom':
294 if 'content' in data:
299 if 'content' in data:
295 self._handle_custom_msg(data['content'])
300 self._handle_custom_msg(data['content'])
296
301
297 def _handle_receive_state(self, sync_data):
302 def _handle_receive_state(self, sync_data):
298 """Called when a state is received from the front-end."""
303 """Called when a state is received from the front-end."""
299 for name in self.keys:
304 for name in self.keys:
300 if name in sync_data:
305 if name in sync_data:
301 value = self._unpack_widgets(sync_data[name])
306 value = self._unpack_widgets(sync_data[name])
302 with self._lock_property(name, value):
307 with self._lock_property(name, value):
303 setattr(self, name, value)
308 setattr(self, name, value)
304
309
305 def _handle_custom_msg(self, content):
310 def _handle_custom_msg(self, content):
306 """Called when a custom msg is received."""
311 """Called when a custom msg is received."""
307 self._msg_callbacks(content) # Signature 1
312 self._msg_callbacks(content) # Signature 1
308 self._msg_callbacks(self, content) # Signature 2
313 self._msg_callbacks(self, content) # Signature 2
309
314
310 def _handle_property_changed(self, name, old, new):
315 def _handle_property_changed(self, name, old, new):
311 """Called when a property has been changed."""
316 """Called when a property has been changed."""
312 # Make sure this isn't information that the front-end just sent us.
317 # Make sure this isn't information that the front-end just sent us.
313 if self._should_send_property(name, new):
318 if self._should_send_property(name, new):
314 # Send new state to front-end
319 # Send new state to front-end
315 self.send_state(key=name)
320 self.send_state(key=name)
316
321
317 def _handle_displayed(self, **kwargs):
322 def _handle_displayed(self, **kwargs):
318 """Called when a view has been displayed for this widget instance"""
323 """Called when a view has been displayed for this widget instance"""
319 self._display_callbacks(**kwargs)
324 self._display_callbacks(**kwargs)
320
325
321 def _pack_widgets(self, x):
326 def _pack_widgets(self, x):
322 """Recursively converts all widget instances to model id strings.
327 """Recursively converts all widget instances to model id strings.
323
328
324 Children widgets will be stored and transmitted to the front-end by
329 Children widgets will be stored and transmitted to the front-end by
325 their model ids. Return value must be JSON-able."""
330 their model ids. Return value must be JSON-able."""
326 if isinstance(x, dict):
331 if isinstance(x, dict):
327 return {k: self._pack_widgets(v) for k, v in x.items()}
332 return {k: self._pack_widgets(v) for k, v in x.items()}
328 elif isinstance(x, list):
333 elif isinstance(x, list):
329 return [self._pack_widgets(v) for v in x]
334 return [self._pack_widgets(v) for v in x]
330 elif isinstance(x, Widget):
335 elif isinstance(x, Widget):
331 return x.model_id
336 return x.model_id
332 else:
337 else:
333 return x # Value must be JSON-able
338 return x # Value must be JSON-able
334
339
335 def _unpack_widgets(self, x):
340 def _unpack_widgets(self, x):
336 """Recursively converts all model id strings to widget instances.
341 """Recursively converts all model id strings to widget instances.
337
342
338 Children widgets will be stored and transmitted to the front-end by
343 Children widgets will be stored and transmitted to the front-end by
339 their model ids."""
344 their model ids."""
340 if isinstance(x, dict):
345 if isinstance(x, dict):
341 return {k: self._unpack_widgets(v) for k, v in x.items()}
346 return {k: self._unpack_widgets(v) for k, v in x.items()}
342 elif isinstance(x, list):
347 elif isinstance(x, list):
343 return [self._unpack_widgets(v) for v in x]
348 return [self._unpack_widgets(v) for v in x]
344 elif isinstance(x, string_types):
349 elif isinstance(x, string_types):
345 return x if x not in Widget.widgets else Widget.widgets[x]
350 return x if x not in Widget.widgets else Widget.widgets[x]
346 else:
351 else:
347 return x
352 return x
348
353
349 def _ipython_display_(self, **kwargs):
354 def _ipython_display_(self, **kwargs):
350 """Called when `IPython.display.display` is called on the widget."""
355 """Called when `IPython.display.display` is called on the widget."""
351 # Show view. By sending a display message, the comm is opened and the
356 # Show view. By sending a display message, the comm is opened and the
352 # initial state is sent.
357 # initial state is sent.
353 self._send({"method": "display"})
358 self._send({"method": "display"})
354 self._handle_displayed(**kwargs)
359 self._handle_displayed(**kwargs)
355
360
356 def _send(self, msg):
361 def _send(self, msg):
357 """Sends a message to the model in the front-end."""
362 """Sends a message to the model in the front-end."""
358 self.comm.send(msg)
363 self.comm.send(msg)
359
364
360
365
361 class DOMWidget(Widget):
366 class DOMWidget(Widget):
362 visible = Bool(True, help="Whether or not the widget is visible.", sync=True)
367 visible = Bool(True, help="Whether or not the widget is visible.", sync=True)
363 _css = Dict(sync=True) # Internal CSS property dict
368 _css = Dict(sync=True) # Internal CSS property dict
364
369
365 def get_css(self, key, selector=""):
370 def get_css(self, key, selector=""):
366 """Get a CSS property of the widget.
371 """Get a CSS property of the widget.
367
372
368 Note: This function does not actually request the CSS from the
373 Note: This function does not actually request the CSS from the
369 front-end; Only properties that have been set with set_css can be read.
374 front-end; Only properties that have been set with set_css can be read.
370
375
371 Parameters
376 Parameters
372 ----------
377 ----------
373 key: unicode
378 key: unicode
374 CSS key
379 CSS key
375 selector: unicode (optional)
380 selector: unicode (optional)
376 JQuery selector used when the CSS key/value was set.
381 JQuery selector used when the CSS key/value was set.
377 """
382 """
378 if selector in self._css and key in self._css[selector]:
383 if selector in self._css and key in self._css[selector]:
379 return self._css[selector][key]
384 return self._css[selector][key]
380 else:
385 else:
381 return None
386 return None
382
387
383 def set_css(self, *args, **kwargs):
388 def set_css(self, *args, **kwargs):
384 """Set one or more CSS properties of the widget.
389 """Set one or more CSS properties of the widget.
385
390
386 This function has two signatures:
391 This function has two signatures:
387 - set_css(css_dict, selector='')
392 - set_css(css_dict, selector='')
388 - set_css(key, value, selector='')
393 - set_css(key, value, selector='')
389
394
390 Parameters
395 Parameters
391 ----------
396 ----------
392 css_dict : dict
397 css_dict : dict
393 CSS key/value pairs to apply
398 CSS key/value pairs to apply
394 key: unicode
399 key: unicode
395 CSS key
400 CSS key
396 value
401 value
397 CSS value
402 CSS value
398 selector: unicode (optional)
403 selector: unicode (optional)
399 JQuery selector to use to apply the CSS key/value. If no selector
404 JQuery selector to use to apply the CSS key/value. If no selector
400 is provided, an empty selector is used. An empty selector makes the
405 is provided, an empty selector is used. An empty selector makes the
401 front-end try to apply the css to a default element. The default
406 front-end try to apply the css to a default element. The default
402 element is an attribute unique to each view, which is a DOM element
407 element is an attribute unique to each view, which is a DOM element
403 of the view that should be styled with common CSS (see
408 of the view that should be styled with common CSS (see
404 `$el_to_style` in the Javascript code).
409 `$el_to_style` in the Javascript code).
405 """
410 """
406 selector = kwargs.get('selector', '')
411 selector = kwargs.get('selector', '')
407 if not selector in self._css:
412 if not selector in self._css:
408 self._css[selector] = {}
413 self._css[selector] = {}
409
414
410 # Signature 1: set_css(css_dict, selector='')
415 # Signature 1: set_css(css_dict, selector='')
411 if len(args) == 1:
416 if len(args) == 1:
412 if isinstance(args[0], dict):
417 if isinstance(args[0], dict):
413 for (key, value) in args[0].items():
418 for (key, value) in args[0].items():
414 if not (key in self._css[selector] and value == self._css[selector][key]):
419 if not (key in self._css[selector] and value == self._css[selector][key]):
415 self._css[selector][key] = value
420 self._css[selector][key] = value
416 self.send_state('_css')
421 self.send_state('_css')
417 else:
422 else:
418 raise Exception('css_dict must be a dict.')
423 raise Exception('css_dict must be a dict.')
419
424
420 # Signature 2: set_css(key, value, selector='')
425 # Signature 2: set_css(key, value, selector='')
421 elif len(args) == 2 or len(args) == 3:
426 elif len(args) == 2 or len(args) == 3:
422
427
423 # Selector can be a positional arg if it's the 3rd value
428 # Selector can be a positional arg if it's the 3rd value
424 if len(args) == 3:
429 if len(args) == 3:
425 selector = args[2]
430 selector = args[2]
426 if selector not in self._css:
431 if selector not in self._css:
427 self._css[selector] = {}
432 self._css[selector] = {}
428
433
429 # Only update the property if it has changed.
434 # Only update the property if it has changed.
430 key = args[0]
435 key = args[0]
431 value = args[1]
436 value = args[1]
432 if not (key in self._css[selector] and value == self._css[selector][key]):
437 if not (key in self._css[selector] and value == self._css[selector][key]):
433 self._css[selector][key] = value
438 self._css[selector][key] = value
434 self.send_state('_css') # Send new state to client.
439 self.send_state('_css') # Send new state to client.
435 else:
440 else:
436 raise Exception('set_css only accepts 1-3 arguments')
441 raise Exception('set_css only accepts 1-3 arguments')
437
442
438 def add_class(self, class_names, selector=""):
443 def add_class(self, class_names, selector=""):
439 """Add class[es] to a DOM element.
444 """Add class[es] to a DOM element.
440
445
441 Parameters
446 Parameters
442 ----------
447 ----------
443 class_names: unicode or list
448 class_names: unicode or list
444 Class name(s) to add to the DOM element(s).
449 Class name(s) to add to the DOM element(s).
445 selector: unicode (optional)
450 selector: unicode (optional)
446 JQuery selector to select the DOM element(s) that the class(es) will
451 JQuery selector to select the DOM element(s) that the class(es) will
447 be added to.
452 be added to.
448 """
453 """
449 class_list = class_names
454 class_list = class_names
450 if isinstance(class_list, list):
455 if isinstance(class_list, list):
451 class_list = ' '.join(class_list)
456 class_list = ' '.join(class_list)
452
457
453 self.send({
458 self.send({
454 "msg_type" : "add_class",
459 "msg_type" : "add_class",
455 "class_list" : class_list,
460 "class_list" : class_list,
456 "selector" : selector
461 "selector" : selector
457 })
462 })
458
463
459 def remove_class(self, class_names, selector=""):
464 def remove_class(self, class_names, selector=""):
460 """Remove class[es] from a DOM element.
465 """Remove class[es] from a DOM element.
461
466
462 Parameters
467 Parameters
463 ----------
468 ----------
464 class_names: unicode or list
469 class_names: unicode or list
465 Class name(s) to remove from the DOM element(s).
470 Class name(s) to remove from the DOM element(s).
466 selector: unicode (optional)
471 selector: unicode (optional)
467 JQuery selector to select the DOM element(s) that the class(es) will
472 JQuery selector to select the DOM element(s) that the class(es) will
468 be removed from.
473 be removed from.
469 """
474 """
470 class_list = class_names
475 class_list = class_names
471 if isinstance(class_list, list):
476 if isinstance(class_list, list):
472 class_list = ' '.join(class_list)
477 class_list = ' '.join(class_list)
473
478
474 self.send({
479 self.send({
475 "msg_type" : "remove_class",
480 "msg_type" : "remove_class",
476 "class_list" : class_list,
481 "class_list" : class_list,
477 "selector" : selector,
482 "selector" : selector,
478 })
483 })
General Comments 0
You need to be logged in to leave comments. Login now