##// END OF EJS Templates
Merge pull request #4933 from jdfreder/widget-model-name...
Brian E. Granger -
r15005:f55d3710 merge
parent child Browse files
Show More
@@ -1,204 +1,204
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 (_, 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(model_type, model_name) {
41 _.each(WidgetManager._model_types, function(model_type, model_name) {
42 that.comm_manager.register_target(model_name, $.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 // Displays a view for a particular model.
75 // Displays a view for a particular model.
76 var cell = this.get_msg_cell(msg.parent_header.msg_id);
76 var cell = this.get_msg_cell(msg.parent_header.msg_id);
77 if (cell === null) {
77 if (cell === null) {
78 console.log("Could not determine where the display" +
78 console.log("Could not determine where the display" +
79 " message was from. Widget will not be displayed");
79 " message was from. Widget will not be displayed");
80 } else {
80 } else {
81 var view = this.create_view(model, {cell: cell});
81 var view = this.create_view(model, {cell: cell});
82 if (view === null) {
82 if (view === null) {
83 console.error("View creation failed", model);
83 console.error("View creation failed", model);
84 }
84 }
85 if (cell.widget_subarea) {
85 if (cell.widget_subarea) {
86 cell.widget_area.show();
86 cell.widget_area.show();
87 this._handle_display_view(view);
87 this._handle_display_view(view);
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._handle_display_view = function (view) {
93 WidgetManager.prototype._handle_display_view = function (view) {
94 // Have the IPython keyboard manager disable its event
94 // Have the IPython keyboard manager disable its event
95 // handling so the widget can capture keyboard input.
95 // handling so the widget can capture keyboard input.
96 // Note, this is only done on the outer most widget.
96 // Note, this is only done on the outer most widget.
97 IPython.keyboard_manager.register_events(view.$el);
97 IPython.keyboard_manager.register_events(view.$el);
98 };
98 };
99
99
100 WidgetManager.prototype.create_view = function(model, options, view) {
100 WidgetManager.prototype.create_view = function(model, options, view) {
101 // Creates a view for a particular model.
101 // Creates a view for a particular model.
102 var view_name = model.get('_view_name');
102 var view_name = model.get('_view_name');
103 var ViewType = WidgetManager._view_types[view_name];
103 var ViewType = WidgetManager._view_types[view_name];
104 if (ViewType) {
104 if (ViewType) {
105
105
106 // If a view is passed into the method, use that view's cell as
106 // If a view is passed into the method, use that view's cell as
107 // the cell for the view that is created.
107 // the cell for the view that is created.
108 options = options || {};
108 options = options || {};
109 if (view !== undefined) {
109 if (view !== undefined) {
110 options.cell = view.options.cell;
110 options.cell = view.options.cell;
111 }
111 }
112
112
113 // Create and render the view...
113 // Create and render the view...
114 var parameters = {model: model, options: options};
114 var parameters = {model: model, options: options};
115 view = new ViewType(parameters);
115 view = new ViewType(parameters);
116 view.render();
116 view.render();
117 model.views.push(view);
117 model.views.push(view);
118 model.on('destroy', view.remove, view);
118 model.on('destroy', view.remove, view);
119 return view;
119 return view;
120 }
120 }
121 return null;
121 return null;
122 };
122 };
123
123
124 WidgetManager.prototype.get_msg_cell = function (msg_id) {
124 WidgetManager.prototype.get_msg_cell = function (msg_id) {
125 var cell = null;
125 var cell = null;
126 // First, check to see if the msg was triggered by cell execution.
126 // First, check to see if the msg was triggered by cell execution.
127 if (IPython.notebook) {
127 if (IPython.notebook) {
128 cell = IPython.notebook.get_msg_cell(msg_id);
128 cell = IPython.notebook.get_msg_cell(msg_id);
129 }
129 }
130 if (cell !== null) {
130 if (cell !== null) {
131 return cell;
131 return cell;
132 }
132 }
133 // Second, check to see if a get_cell callback was defined
133 // Second, check to see if a get_cell callback was defined
134 // for the message. get_cell callbacks are registered for
134 // for the message. get_cell callbacks are registered for
135 // widget messages, so this block is actually checking to see if the
135 // widget messages, so this block is actually checking to see if the
136 // message was triggered by a widget.
136 // message was triggered by a widget.
137 var kernel = this.comm_manager.kernel;
137 var kernel = this.comm_manager.kernel;
138 if (kernel) {
138 if (kernel) {
139 var callbacks = kernel.get_callbacks_for_msg(msg_id);
139 var callbacks = kernel.get_callbacks_for_msg(msg_id);
140 if (callbacks && callbacks.iopub &&
140 if (callbacks && callbacks.iopub &&
141 callbacks.iopub.get_cell !== undefined) {
141 callbacks.iopub.get_cell !== undefined) {
142 return callbacks.iopub.get_cell();
142 return callbacks.iopub.get_cell();
143 }
143 }
144 }
144 }
145
145
146 // Not triggered by a cell or widget (no get_cell callback
146 // Not triggered by a cell or widget (no get_cell callback
147 // exists).
147 // exists).
148 return null;
148 return null;
149 };
149 };
150
150
151 WidgetManager.prototype.callbacks = function (view) {
151 WidgetManager.prototype.callbacks = function (view) {
152 // callback handlers specific a view
152 // callback handlers specific a view
153 var callbacks = {};
153 var callbacks = {};
154 if (view && view.options.cell) {
154 if (view && view.options.cell) {
155
155
156 // Try to get output handlers
156 // Try to get output handlers
157 var cell = view.options.cell;
157 var cell = view.options.cell;
158 var handle_output = null;
158 var handle_output = null;
159 var handle_clear_output = null;
159 var handle_clear_output = null;
160 if (cell.output_area) {
160 if (cell.output_area) {
161 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
161 handle_output = $.proxy(cell.output_area.handle_output, cell.output_area);
162 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
162 handle_clear_output = $.proxy(cell.output_area.handle_clear_output, cell.output_area);
163 }
163 }
164
164
165 // Create callback dict using what is known
165 // Create callback dict using what is known
166 var that = this;
166 var that = this;
167 callbacks = {
167 callbacks = {
168 iopub : {
168 iopub : {
169 output : handle_output,
169 output : handle_output,
170 clear_output : handle_clear_output,
170 clear_output : handle_clear_output,
171
171
172 // Special function only registered by widget messages.
172 // Special function only registered by widget messages.
173 // Allows us to get the cell for a message so we know
173 // Allows us to get the cell for a message so we know
174 // where to add widgets if the code requires it.
174 // where to add widgets if the code requires it.
175 get_cell : function () {
175 get_cell : function () {
176 return cell;
176 return cell;
177 },
177 },
178 },
178 },
179 };
179 };
180 }
180 }
181 return callbacks;
181 return callbacks;
182 };
182 };
183
183
184 WidgetManager.prototype.get_model = function (model_id) {
184 WidgetManager.prototype.get_model = function (model_id) {
185 // Look-up a model instance by its id.
185 // Look-up a model instance by its id.
186 var model = this._models[model_id];
186 var model = this._models[model_id];
187 if (model !== undefined && model.id == model_id) {
187 if (model !== undefined && model.id == model_id) {
188 return model;
188 return model;
189 }
189 }
190 return null;
190 return null;
191 };
191 };
192
192
193 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
193 WidgetManager.prototype._handle_comm_open = function (comm, msg) {
194 // Handle when a comm is opened.
194 // Handle when a comm is opened.
195 var model_id = comm.comm_id;
195 var model_id = comm.comm_id;
196 var widget_type_name = msg.content.target_name;
196 var widget_type_name = msg.content.target_name;
197 var widget_model = new WidgetManager._model_types[widget_type_name](this, model_id, comm);
197 var widget_model = new WidgetManager._model_types[widget_type_name](this, model_id, comm);
198 this._models[model_id] = widget_model;
198 this._models[model_id] = widget_model;
199 };
199 };
200
200
201 IPython.WidgetManager = WidgetManager;
201 IPython.WidgetManager = WidgetManager;
202 return IPython.WidgetManager;
202 return IPython.WidgetManager;
203 });
203 });
204 }());
204 }());
@@ -1,418 +1,418
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, _, 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 // Constructor
24 // Constructor
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 = 3;
36 this.msg_throttle = 3;
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 this.pending_msgs++;
59 this.pending_msgs++;
60 }
60 }
61 },
61 },
62
62
63 _handle_comm_closed: function (msg) {
63 _handle_comm_closed: function (msg) {
64 // Handle when a widget is closed.
64 // Handle when a widget is closed.
65 this.trigger('comm:close');
65 this.trigger('comm:close');
66 delete this.comm.model; // Delete ref so GC will collect widget model.
66 delete this.comm.model; // Delete ref so GC will collect widget model.
67 delete this.comm;
67 delete this.comm;
68 delete this.model_id; // Delete id from model so widget manager cleans up.
68 delete this.model_id; // Delete id from model so widget manager cleans up.
69 _.each(this.views, function(view, i) {
69 _.each(this.views, function(view, i) {
70 view.remove();
70 view.remove();
71 });
71 });
72 },
72 },
73
73
74 _handle_comm_msg: function (msg) {
74 _handle_comm_msg: function (msg) {
75 // Handle incoming comm msg.
75 // Handle incoming comm msg.
76 var method = msg.content.data.method;
76 var method = msg.content.data.method;
77 switch (method) {
77 switch (method) {
78 case 'update':
78 case 'update':
79 this.apply_update(msg.content.data.state);
79 this.apply_update(msg.content.data.state);
80 break;
80 break;
81 case 'custom':
81 case 'custom':
82 this.trigger('msg:custom', msg.content.data.content);
82 this.trigger('msg:custom', msg.content.data.content);
83 break;
83 break;
84 case 'display':
84 case 'display':
85 this.widget_manager.display_view(msg, this);
85 this.widget_manager.display_view(msg, this);
86 break;
86 break;
87 }
87 }
88 },
88 },
89
89
90 apply_update: function (state) {
90 apply_update: function (state) {
91 // Handle when a widget is updated via the python side.
91 // Handle when a widget is updated via the python side.
92 var that = this;
92 var that = this;
93 _.each(state, function(value, key) {
93 _.each(state, function(value, key) {
94 that.key_value_lock = [key, value];
94 that.key_value_lock = [key, value];
95 try {
95 try {
96 that.set(key, that._unpack_models(value));
96 that.set(key, that._unpack_models(value));
97 } finally {
97 } finally {
98 that.key_value_lock = null;
98 that.key_value_lock = null;
99 }
99 }
100 });
100 });
101 },
101 },
102
102
103 _handle_status: function (msg, callbacks) {
103 _handle_status: function (msg, callbacks) {
104 // Handle status msgs.
104 // Handle status msgs.
105
105
106 // execution_state : ('busy', 'idle', 'starting')
106 // execution_state : ('busy', 'idle', 'starting')
107 if (this.comm !== undefined) {
107 if (this.comm !== undefined) {
108 if (msg.content.execution_state ==='idle') {
108 if (msg.content.execution_state ==='idle') {
109 // Send buffer if this message caused another message to be
109 // Send buffer if this message caused another message to be
110 // throttled.
110 // throttled.
111 if (this.msg_buffer !== null &&
111 if (this.msg_buffer !== null &&
112 this.msg_throttle === this.pending_msgs) {
112 this.msg_throttle === this.pending_msgs) {
113 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
113 var data = {method: 'backbone', sync_method: 'update', sync_data: this.msg_buffer};
114 this.comm.send(data, callbacks);
114 this.comm.send(data, callbacks);
115 this.msg_buffer = null;
115 this.msg_buffer = null;
116 } else {
116 } else {
117 --this.pending_msgs;
117 --this.pending_msgs;
118 }
118 }
119 }
119 }
120 }
120 }
121 },
121 },
122
122
123 callbacks: function(view) {
123 callbacks: function(view) {
124 // Create msg callbacks for a comm msg.
124 // Create msg callbacks for a comm msg.
125 var callbacks = this.widget_manager.callbacks(view);
125 var callbacks = this.widget_manager.callbacks(view);
126
126
127 if (callbacks.iopub === undefined) {
127 if (callbacks.iopub === undefined) {
128 callbacks.iopub = {};
128 callbacks.iopub = {};
129 }
129 }
130
130
131 var that = this;
131 var that = this;
132 callbacks.iopub.status = function (msg) {
132 callbacks.iopub.status = function (msg) {
133 that._handle_status(msg, callbacks);
133 that._handle_status(msg, callbacks);
134 };
134 };
135 return callbacks;
135 return callbacks;
136 },
136 },
137
137
138 sync: function (method, model, options) {
138 sync: function (method, model, options) {
139 // Handle sync to the back-end. Called when a model.save() is called.
139 // Handle sync to the back-end. Called when a model.save() is called.
140
140
141 // Make sure a comm exists.
141 // Make sure a comm exists.
142 var error = options.error || function() {
142 var error = options.error || function() {
143 console.error('Backbone sync error:', arguments);
143 console.error('Backbone sync error:', arguments);
144 };
144 };
145 if (this.comm === undefined) {
145 if (this.comm === undefined) {
146 error();
146 error();
147 return false;
147 return false;
148 }
148 }
149
149
150 // Delete any key value pairs that the back-end already knows about.
150 // Delete any key value pairs that the back-end already knows about.
151 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
151 var attrs = (method === 'patch') ? options.attrs : model.toJSON(options);
152 if (this.key_value_lock !== null) {
152 if (this.key_value_lock !== null) {
153 var key = this.key_value_lock[0];
153 var key = this.key_value_lock[0];
154 var value = this.key_value_lock[1];
154 var value = this.key_value_lock[1];
155 if (attrs[key] === value) {
155 if (attrs[key] === value) {
156 delete attrs[key];
156 delete attrs[key];
157 }
157 }
158 }
158 }
159
159
160 // Only sync if there are attributes to send to the back-end.
160 // Only sync if there are attributes to send to the back-end.
161 if (_.size(attrs) > 0) {
161 if (_.size(attrs) > 0) {
162
162
163 // If this message was sent via backbone itself, it will not
163 // If this message was sent via backbone itself, it will not
164 // have any callbacks. It's important that we create callbacks
164 // have any callbacks. It's important that we create callbacks
165 // so we can listen for status messages, etc...
165 // so we can listen for status messages, etc...
166 var callbacks = options.callbacks || this.callbacks();
166 var callbacks = options.callbacks || this.callbacks();
167
167
168 // Check throttle.
168 // Check throttle.
169 if (this.pending_msgs >= this.msg_throttle) {
169 if (this.pending_msgs >= this.msg_throttle) {
170 // The throttle has been exceeded, buffer the current msg so
170 // The throttle has been exceeded, buffer the current msg so
171 // it can be sent once the kernel has finished processing
171 // it can be sent once the kernel has finished processing
172 // some of the existing messages.
172 // some of the existing messages.
173
173
174 // Combine updates if it is a 'patch' sync, otherwise replace updates
174 // Combine updates if it is a 'patch' sync, otherwise replace updates
175 switch (method) {
175 switch (method) {
176 case 'patch':
176 case 'patch':
177 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
177 this.msg_buffer = $.extend(this.msg_buffer || {}, attrs);
178 break;
178 break;
179 case 'update':
179 case 'update':
180 case 'create':
180 case 'create':
181 this.msg_buffer = attrs;
181 this.msg_buffer = attrs;
182 break;
182 break;
183 default:
183 default:
184 error();
184 error();
185 return false;
185 return false;
186 }
186 }
187 this.msg_buffer_callbacks = callbacks;
187 this.msg_buffer_callbacks = callbacks;
188
188
189 } else {
189 } else {
190 // We haven't exceeded the throttle, send the message like
190 // We haven't exceeded the throttle, send the message like
191 // normal.
191 // normal.
192 var data = {method: 'backbone', sync_data: attrs};
192 var data = {method: 'backbone', sync_data: attrs};
193 this.comm.send(data, callbacks);
193 this.comm.send(data, callbacks);
194 this.pending_msgs++;
194 this.pending_msgs++;
195 }
195 }
196 }
196 }
197 // Since the comm is a one-way communication, assume the message
197 // Since the comm is a one-way communication, assume the message
198 // arrived. Don't call success since we don't have a model back from the server
198 // arrived. Don't call success since we don't have a model back from the server
199 // this means we miss out on the 'sync' event.
199 // this means we miss out on the 'sync' event.
200 },
200 },
201
201
202 save_changes: function(callbacks) {
202 save_changes: function(callbacks) {
203 // Push this model's state to the back-end
203 // Push this model's state to the back-end
204 //
204 //
205 // This invokes a Backbone.Sync.
205 // This invokes a Backbone.Sync.
206 this.save(this.changedAttributes(), {patch: true, callbacks: callbacks});
206 this.save(this.changedAttributes(), {patch: true, callbacks: callbacks});
207 },
207 },
208
208
209 _pack_models: function(value) {
209 _pack_models: function(value) {
210 // Replace models with model ids recursively.
210 // Replace models with model ids recursively.
211 if (value instanceof Backbone.Model) {
211 if (value instanceof Backbone.Model) {
212 return value.id;
212 return value.id;
213 } else if (value instanceof Object) {
213 } else if (value instanceof Object) {
214 var packed = {};
214 var packed = {};
215 var that = this;
215 var that = this;
216 _.each(value, function(sub_value, key) {
216 _.each(value, function(sub_value, key) {
217 packed[key] = that._pack_models(sub_value);
217 packed[key] = that._pack_models(sub_value);
218 });
218 });
219 return packed;
219 return packed;
220 } else {
220 } else {
221 return value;
221 return value;
222 }
222 }
223 },
223 },
224
224
225 _unpack_models: function(value) {
225 _unpack_models: function(value) {
226 // Replace model ids with models recursively.
226 // Replace model ids with models recursively.
227 if (value instanceof Object) {
227 if (value instanceof Object) {
228 var unpacked = {};
228 var unpacked = {};
229 var that = this;
229 var that = this;
230 _.each(value, function(sub_value, key) {
230 _.each(value, function(sub_value, key) {
231 unpacked[key] = that._unpack_models(sub_value);
231 unpacked[key] = that._unpack_models(sub_value);
232 });
232 });
233 return unpacked;
233 return unpacked;
234 } else {
234 } else {
235 var model = this.widget_manager.get_model(value);
235 var model = this.widget_manager.get_model(value);
236 if (model) {
236 if (model) {
237 return model;
237 return model;
238 } else {
238 } else {
239 return value;
239 return value;
240 }
240 }
241 }
241 }
242 },
242 },
243
243
244 });
244 });
245 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
245 WidgetManager.register_widget_model('WidgetModel', WidgetModel);
246
246
247
247
248 var WidgetView = Backbone.View.extend({
248 var WidgetView = Backbone.View.extend({
249 initialize: function(parameters) {
249 initialize: function(parameters) {
250 // Public constructor.
250 // Public constructor.
251 this.model.on('change',this.update,this);
251 this.model.on('change',this.update,this);
252 this.options = parameters.options;
252 this.options = parameters.options;
253 this.child_views = [];
253 this.child_views = [];
254 this.model.views.push(this);
254 this.model.views.push(this);
255 },
255 },
256
256
257 update: function(){
257 update: function(){
258 // Triggered on model change.
258 // Triggered on model change.
259 //
259 //
260 // Update view to be consistent with this.model
260 // Update view to be consistent with this.model
261 },
261 },
262
262
263 create_child_view: function(child_model, options) {
263 create_child_view: function(child_model, options) {
264 // Create and return a child view.
264 // Create and return a child view.
265 //
265 //
266 // -given a model and (optionally) a view name if the view name is
266 // -given a model and (optionally) a view name if the view name is
267 // not given, it defaults to the model's default view attribute.
267 // not given, it defaults to the model's default view attribute.
268
268
269 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
269 // TODO: this is hacky, and makes the view depend on this cell attribute and widget manager behavior
270 // it would be great to have the widget manager add the cell metadata
270 // it would be great to have the widget manager add the cell metadata
271 // to the subview without having to add it here.
271 // to the subview without having to add it here.
272 var child_view = this.model.widget_manager.create_view(child_model, options || {}, this);
272 var child_view = this.model.widget_manager.create_view(child_model, options || {}, this);
273 this.child_views[child_model.id] = child_view;
273 this.child_views[child_model.id] = child_view;
274 return child_view;
274 return child_view;
275 },
275 },
276
276
277 delete_child_view: function(child_model, options) {
277 delete_child_view: function(child_model, options) {
278 // Delete a child view that was previously created using create_child_view.
278 // Delete a child view that was previously created using create_child_view.
279 var view = this.child_views[child_model.id];
279 var view = this.child_views[child_model.id];
280 if (view !== undefined) {
280 if (view !== undefined) {
281 delete this.child_views[child_model.id];
281 delete this.child_views[child_model.id];
282 view.remove();
282 view.remove();
283 }
283 }
284 },
284 },
285
285
286 do_diff: function(old_list, new_list, removed_callback, added_callback) {
286 do_diff: function(old_list, new_list, removed_callback, added_callback) {
287 // Difference a changed list and call remove and add callbacks for
287 // Difference a changed list and call remove and add callbacks for
288 // each removed and added item in the new list.
288 // each removed and added item in the new list.
289 //
289 //
290 // Parameters
290 // Parameters
291 // ----------
291 // ----------
292 // old_list : array
292 // old_list : array
293 // new_list : array
293 // new_list : array
294 // removed_callback : Callback(item)
294 // removed_callback : Callback(item)
295 // Callback that is called for each item removed.
295 // Callback that is called for each item removed.
296 // added_callback : Callback(item)
296 // added_callback : Callback(item)
297 // Callback that is called for each item added.
297 // Callback that is called for each item added.
298
298
299
299
300 // removed items
300 // removed items
301 _.each(_.difference(old_list, new_list), function(item, index, list) {
301 _.each(_.difference(old_list, new_list), function(item, index, list) {
302 removed_callback(item);
302 removed_callback(item);
303 }, this);
303 }, this);
304
304
305 // added items
305 // added items
306 _.each(_.difference(new_list, old_list), function(item, index, list) {
306 _.each(_.difference(new_list, old_list), function(item, index, list) {
307 added_callback(item);
307 added_callback(item);
308 }, this);
308 }, this);
309 },
309 },
310
310
311 callbacks: function(){
311 callbacks: function(){
312 // Create msg callbacks for a comm msg.
312 // Create msg callbacks for a comm msg.
313 return this.model.callbacks(this);
313 return this.model.callbacks(this);
314 },
314 },
315
315
316 render: function(){
316 render: function(){
317 // Render the view.
317 // Render the view.
318 //
318 //
319 // By default, this is only called the first time the view is created
319 // By default, this is only called the first time the view is created
320 },
320 },
321
321
322 send: function (content) {
322 send: function (content) {
323 // Send a custom msg associated with this view.
323 // Send a custom msg associated with this view.
324 this.model.send(content, this.callbacks());
324 this.model.send(content, this.callbacks());
325 },
325 },
326
326
327 touch: function () {
327 touch: function () {
328 this.model.save_changes(this.callbacks());
328 this.model.save_changes(this.callbacks());
329 },
329 },
330 });
330 });
331
331
332
332
333 var DOMWidgetView = WidgetView.extend({
333 var DOMWidgetView = WidgetView.extend({
334 initialize: function (options) {
334 initialize: function (options) {
335 // Public constructor
335 // Public constructor
336
336
337 // In the future we may want to make changes more granular
337 // In the future we may want to make changes more granular
338 // (e.g., trigger on visible:change).
338 // (e.g., trigger on visible:change).
339 this.model.on('change', this.update, this);
339 this.model.on('change', this.update, this);
340 this.model.on('msg:custom', this.on_msg, this);
340 this.model.on('msg:custom', this.on_msg, this);
341 DOMWidgetView.__super__.initialize.apply(this, arguments);
341 DOMWidgetView.__super__.initialize.apply(this, arguments);
342 },
342 },
343
343
344 on_msg: function(msg) {
344 on_msg: function(msg) {
345 // Handle DOM specific msgs.
345 // Handle DOM specific msgs.
346 switch(msg.msg_type) {
346 switch(msg.msg_type) {
347 case 'add_class':
347 case 'add_class':
348 this.add_class(msg.selector, msg.class_list);
348 this.add_class(msg.selector, msg.class_list);
349 break;
349 break;
350 case 'remove_class':
350 case 'remove_class':
351 this.remove_class(msg.selector, msg.class_list);
351 this.remove_class(msg.selector, msg.class_list);
352 break;
352 break;
353 }
353 }
354 },
354 },
355
355
356 add_class: function (selector, class_list) {
356 add_class: function (selector, class_list) {
357 // Add a DOM class to an element.
357 // Add a DOM class to an element.
358 this._get_selector_element(selector).addClass(class_list);
358 this._get_selector_element(selector).addClass(class_list);
359 },
359 },
360
360
361 remove_class: function (selector, class_list) {
361 remove_class: function (selector, class_list) {
362 // Remove a DOM class from an element.
362 // Remove a DOM class from an element.
363 this._get_selector_element(selector).removeClass(class_list);
363 this._get_selector_element(selector).removeClass(class_list);
364 },
364 },
365
365
366 update: function () {
366 update: function () {
367 // Update the contents of this view
367 // Update the contents of this view
368 //
368 //
369 // Called when the model is changed. The model may have been
369 // Called when the model is changed. The model may have been
370 // changed by another view or by a state update from the back-end.
370 // changed by another view or by a state update from the back-end.
371 // The very first update seems to happen before the element is
371 // The very first update seems to happen before the element is
372 // finished rendering so we use setTimeout to give the element time
372 // finished rendering so we use setTimeout to give the element time
373 // to render
373 // to render
374 var e = this.$el;
374 var e = this.$el;
375 var visible = this.model.get('visible');
375 var visible = this.model.get('visible');
376 setTimeout(function() {e.toggle(visible);},0);
376 setTimeout(function() {e.toggle(visible);},0);
377
377
378 var css = this.model.get('_css');
378 var css = this.model.get('_css');
379 if (css === undefined) {return;}
379 if (css === undefined) {return;}
380 var that = this;
380 var that = this;
381 _.each(css, function(css_traits, selector){
381 _.each(css, function(css_traits, selector){
382 // Apply the css traits to all elements that match the selector.
382 // Apply the css traits to all elements that match the selector.
383 var elements = that._get_selector_element(selector);
383 var elements = that._get_selector_element(selector);
384 if (elements.length > 0) {
384 if (elements.length > 0) {
385 _.each(css_traits, function(css_value, css_key){
385 _.each(css_traits, function(css_value, css_key){
386 elements.css(css_key, css_value);
386 elements.css(css_key, css_value);
387 });
387 });
388 }
388 }
389 });
389 });
390 },
390 },
391
391
392 _get_selector_element: function (selector) {
392 _get_selector_element: function (selector) {
393 // Get the elements via the css selector.
393 // Get the elements via the css selector.
394
394
395 // If the selector is blank, apply the style to the $el_to_style
395 // If the selector is blank, apply the style to the $el_to_style
396 // element. If the $el_to_style element is not defined, use apply
396 // element. If the $el_to_style element is not defined, use apply
397 // the style to the view's element.
397 // the style to the view's element.
398 var elements;
398 var elements;
399 if (!selector) {
399 if (!selector) {
400 if (this.$el_to_style === undefined) {
400 if (this.$el_to_style === undefined) {
401 elements = this.$el;
401 elements = this.$el;
402 } else {
402 } else {
403 elements = this.$el_to_style;
403 elements = this.$el_to_style;
404 }
404 }
405 } else {
405 } else {
406 elements = this.$el.find(selector);
406 elements = this.$el.find(selector);
407 }
407 }
408 return elements;
408 return elements;
409 },
409 },
410 });
410 });
411
411
412 IPython.WidgetModel = WidgetModel;
412 IPython.WidgetModel = WidgetModel;
413 IPython.WidgetView = WidgetView;
413 IPython.WidgetView = WidgetView;
414 IPython.DOMWidgetView = DOMWidgetView;
414 IPython.DOMWidgetView = DOMWidgetView;
415
415
416 // Pass through WidgetManager namespace.
416 // Pass through WidgetManager namespace.
417 return WidgetManager;
417 return WidgetManager;
418 });
418 });
@@ -1,423 +1,423
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
16
17 from IPython.kernel.comm import Comm
17 from IPython.kernel.comm import Comm
18 from IPython.config import LoggingConfigurable
18 from IPython.config import LoggingConfigurable
19 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple
19 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple
20 from IPython.utils.py3compat import string_types
20 from IPython.utils.py3compat import string_types
21
21
22 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
23 # Classes
23 # Classes
24 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
25 class CallbackDispatcher(LoggingConfigurable):
25 class CallbackDispatcher(LoggingConfigurable):
26 """A structure for registering and running callbacks"""
26 """A structure for registering and running callbacks"""
27 callbacks = List()
27 callbacks = List()
28
28
29 def __call__(self, *args, **kwargs):
29 def __call__(self, *args, **kwargs):
30 """Call all of the registered callbacks."""
30 """Call all of the registered callbacks."""
31 value = None
31 value = None
32 for callback in self.callbacks:
32 for callback in self.callbacks:
33 try:
33 try:
34 local_value = callback(*args, **kwargs)
34 local_value = callback(*args, **kwargs)
35 except Exception as e:
35 except Exception as e:
36 self.log.warn("Exception in callback %s: %s", callback, e)
36 self.log.warn("Exception in callback %s: %s", callback, e)
37 else:
37 else:
38 value = local_value if local_value is not None else value
38 value = local_value if local_value is not None else value
39 return value
39 return value
40
40
41 def register_callback(self, callback, remove=False):
41 def register_callback(self, callback, remove=False):
42 """(Un)Register a callback
42 """(Un)Register a callback
43
43
44 Parameters
44 Parameters
45 ----------
45 ----------
46 callback: method handle
46 callback: method handle
47 Method to be registered or unregistered.
47 Method to be registered or unregistered.
48 remove=False: bool
48 remove=False: bool
49 Whether to unregister the callback."""
49 Whether to unregister the callback."""
50
50
51 # (Un)Register the callback.
51 # (Un)Register the callback.
52 if remove and callback in self.callbacks:
52 if remove and callback in self.callbacks:
53 self.callbacks.remove(callback)
53 self.callbacks.remove(callback)
54 elif not remove and callback not in self.callbacks:
54 elif not remove and callback not in self.callbacks:
55 self.callbacks.append(callback)
55 self.callbacks.append(callback)
56
56
57
57
58 class Widget(LoggingConfigurable):
58 class Widget(LoggingConfigurable):
59 #-------------------------------------------------------------------------
59 #-------------------------------------------------------------------------
60 # Class attributes
60 # Class attributes
61 #-------------------------------------------------------------------------
61 #-------------------------------------------------------------------------
62 _widget_construction_callback = None
62 _widget_construction_callback = None
63 widgets = {}
63 widgets = {}
64
64
65 @staticmethod
65 @staticmethod
66 def on_widget_constructed(callback):
66 def on_widget_constructed(callback):
67 """Registers a callback to be called when a widget is constructed.
67 """Registers a callback to be called when a widget is constructed.
68
68
69 The callback must have the following signature:
69 The callback must have the following signature:
70 callback(widget)"""
70 callback(widget)"""
71 Widget._widget_construction_callback = callback
71 Widget._widget_construction_callback = callback
72
72
73 @staticmethod
73 @staticmethod
74 def _call_widget_constructed(widget):
74 def _call_widget_constructed(widget):
75 """Static method, called when a widget is constructed."""
75 """Static method, called when a widget is constructed."""
76 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
76 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
77 Widget._widget_construction_callback(widget)
77 Widget._widget_construction_callback(widget)
78
78
79 #-------------------------------------------------------------------------
79 #-------------------------------------------------------------------------
80 # Traits
80 # Traits
81 #-------------------------------------------------------------------------
81 #-------------------------------------------------------------------------
82 model_name = Unicode('WidgetModel', help="""Name of the backbone model
82 _model_name = Unicode('WidgetModel', help="""Name of the backbone model
83 registered in the front-end to create and sync this widget with.""")
83 registered in the front-end to create and sync this widget with.""")
84 _view_name = Unicode(help="""Default view registered in the front-end
84 _view_name = Unicode(help="""Default view registered in the front-end
85 to use to represent the widget.""", sync=True)
85 to use to represent the widget.""", sync=True)
86 _comm = Instance('IPython.kernel.comm.Comm')
86 _comm = Instance('IPython.kernel.comm.Comm')
87
87
88 closed = Bool(False)
88 closed = Bool(False)
89
89
90 keys = List()
90 keys = List()
91 def _keys_default(self):
91 def _keys_default(self):
92 return [name for name in self.traits(sync=True)]
92 return [name for name in self.traits(sync=True)]
93
93
94 _property_lock = Tuple((None, None))
94 _property_lock = Tuple((None, None))
95
95
96 _display_callbacks = Instance(CallbackDispatcher, ())
96 _display_callbacks = Instance(CallbackDispatcher, ())
97 _msg_callbacks = Instance(CallbackDispatcher, ())
97 _msg_callbacks = Instance(CallbackDispatcher, ())
98
98
99 #-------------------------------------------------------------------------
99 #-------------------------------------------------------------------------
100 # (Con/de)structor
100 # (Con/de)structor
101 #-------------------------------------------------------------------------
101 #-------------------------------------------------------------------------
102 def __init__(self, **kwargs):
102 def __init__(self, **kwargs):
103 """Public constructor"""
103 """Public constructor"""
104 super(Widget, self).__init__(**kwargs)
104 super(Widget, self).__init__(**kwargs)
105
105
106 self.on_trait_change(self._handle_property_changed, self.keys)
106 self.on_trait_change(self._handle_property_changed, self.keys)
107 Widget._call_widget_constructed(self)
107 Widget._call_widget_constructed(self)
108
108
109 def __del__(self):
109 def __del__(self):
110 """Object disposal"""
110 """Object disposal"""
111 self.close()
111 self.close()
112
112
113 #-------------------------------------------------------------------------
113 #-------------------------------------------------------------------------
114 # Properties
114 # Properties
115 #-------------------------------------------------------------------------
115 #-------------------------------------------------------------------------
116
116
117 @property
117 @property
118 def comm(self):
118 def comm(self):
119 """Gets the Comm associated with this widget.
119 """Gets the Comm associated with this widget.
120
120
121 If a Comm doesn't exist yet, a Comm will be created automagically."""
121 If a Comm doesn't exist yet, a Comm will be created automagically."""
122 if self._comm is None:
122 if self._comm is None:
123 # Create a comm.
123 # Create a comm.
124 self._comm = Comm(target_name=self.model_name)
124 self._comm = Comm(target_name=self._model_name)
125 self._comm.on_msg(self._handle_msg)
125 self._comm.on_msg(self._handle_msg)
126 self._comm.on_close(self._close)
126 self._comm.on_close(self._close)
127 Widget.widgets[self.model_id] = self
127 Widget.widgets[self.model_id] = self
128
128
129 # first update
129 # first update
130 self.send_state()
130 self.send_state()
131 return self._comm
131 return self._comm
132
132
133 @property
133 @property
134 def model_id(self):
134 def model_id(self):
135 """Gets the model id of this widget.
135 """Gets the model id of this widget.
136
136
137 If a Comm doesn't exist yet, a Comm will be created automagically."""
137 If a Comm doesn't exist yet, a Comm will be created automagically."""
138 return self.comm.comm_id
138 return self.comm.comm_id
139
139
140 #-------------------------------------------------------------------------
140 #-------------------------------------------------------------------------
141 # Methods
141 # Methods
142 #-------------------------------------------------------------------------
142 #-------------------------------------------------------------------------
143 def _close(self):
143 def _close(self):
144 """Private close - cleanup objects, registry entries"""
144 """Private close - cleanup objects, registry entries"""
145 del Widget.widgets[self.model_id]
145 del Widget.widgets[self.model_id]
146 self._comm = None
146 self._comm = None
147 self.closed = True
147 self.closed = True
148
148
149 def close(self):
149 def close(self):
150 """Close method.
150 """Close method.
151
151
152 Closes the widget which closes the underlying comm.
152 Closes the widget which closes the underlying comm.
153 When the comm is closed, all of the widget views are automatically
153 When the comm is closed, all of the widget views are automatically
154 removed from the front-end."""
154 removed from the front-end."""
155 if not self.closed:
155 if not self.closed:
156 self._comm.close()
156 self._comm.close()
157 self._close()
157 self._close()
158
158
159 def send_state(self, key=None):
159 def send_state(self, key=None):
160 """Sends the widget state, or a piece of it, to the front-end.
160 """Sends the widget state, or a piece of it, to the front-end.
161
161
162 Parameters
162 Parameters
163 ----------
163 ----------
164 key : unicode (optional)
164 key : unicode (optional)
165 A single property's name to sync with the front-end.
165 A single property's name to sync with the front-end.
166 """
166 """
167 self._send({
167 self._send({
168 "method" : "update",
168 "method" : "update",
169 "state" : self.get_state()
169 "state" : self.get_state()
170 })
170 })
171
171
172 def get_state(self, key=None):
172 def get_state(self, key=None):
173 """Gets the widget state, or a piece of it.
173 """Gets the widget state, or a piece of it.
174
174
175 Parameters
175 Parameters
176 ----------
176 ----------
177 key : unicode (optional)
177 key : unicode (optional)
178 A single property's name to get.
178 A single property's name to get.
179 """
179 """
180 keys = self.keys if key is None else [key]
180 keys = self.keys if key is None else [key]
181 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
181 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
182
182
183 def send(self, content):
183 def send(self, content):
184 """Sends a custom msg to the widget model in the front-end.
184 """Sends a custom msg to the widget model in the front-end.
185
185
186 Parameters
186 Parameters
187 ----------
187 ----------
188 content : dict
188 content : dict
189 Content of the message to send.
189 Content of the message to send.
190 """
190 """
191 self._send({"method": "custom", "content": content})
191 self._send({"method": "custom", "content": content})
192
192
193 def on_msg(self, callback, remove=False):
193 def on_msg(self, callback, remove=False):
194 """(Un)Register a custom msg receive callback.
194 """(Un)Register a custom msg receive callback.
195
195
196 Parameters
196 Parameters
197 ----------
197 ----------
198 callback: callable
198 callback: callable
199 callback will be passed two arguments when a message arrives::
199 callback will be passed two arguments when a message arrives::
200
200
201 callback(widget, content)
201 callback(widget, content)
202
202
203 remove: bool
203 remove: bool
204 True if the callback should be unregistered."""
204 True if the callback should be unregistered."""
205 self._msg_callbacks.register_callback(callback, remove=remove)
205 self._msg_callbacks.register_callback(callback, remove=remove)
206
206
207 def on_displayed(self, callback, remove=False):
207 def on_displayed(self, callback, remove=False):
208 """(Un)Register a widget displayed callback.
208 """(Un)Register a widget displayed callback.
209
209
210 Parameters
210 Parameters
211 ----------
211 ----------
212 callback: method handler
212 callback: method handler
213 Must have a signature of::
213 Must have a signature of::
214
214
215 callback(widget, **kwargs)
215 callback(widget, **kwargs)
216
216
217 kwargs from display are passed through without modification.
217 kwargs from display are passed through without modification.
218 remove: bool
218 remove: bool
219 True if the callback should be unregistered."""
219 True if the callback should be unregistered."""
220 self._display_callbacks.register_callback(callback, remove=remove)
220 self._display_callbacks.register_callback(callback, remove=remove)
221
221
222 #-------------------------------------------------------------------------
222 #-------------------------------------------------------------------------
223 # Support methods
223 # Support methods
224 #-------------------------------------------------------------------------
224 #-------------------------------------------------------------------------
225 @contextmanager
225 @contextmanager
226 def _lock_property(self, key, value):
226 def _lock_property(self, key, value):
227 """Lock a property-value pair.
227 """Lock a property-value pair.
228
228
229 NOTE: This, in addition to the single lock for all state changes, is
229 NOTE: This, in addition to the single lock for all state changes, is
230 flawed. In the future we may want to look into buffering state changes
230 flawed. In the future we may want to look into buffering state changes
231 back to the front-end."""
231 back to the front-end."""
232 self._property_lock = (key, value)
232 self._property_lock = (key, value)
233 try:
233 try:
234 yield
234 yield
235 finally:
235 finally:
236 self._property_lock = (None, None)
236 self._property_lock = (None, None)
237
237
238 def _should_send_property(self, key, value):
238 def _should_send_property(self, key, value):
239 """Check the property lock (property_lock)"""
239 """Check the property lock (property_lock)"""
240 return key != self._property_lock[0] or \
240 return key != self._property_lock[0] or \
241 value != self._property_lock[1]
241 value != self._property_lock[1]
242
242
243 # Event handlers
243 # Event handlers
244 def _handle_msg(self, msg):
244 def _handle_msg(self, msg):
245 """Called when a msg is received from the front-end"""
245 """Called when a msg is received from the front-end"""
246 data = msg['content']['data']
246 data = msg['content']['data']
247 method = data['method']
247 method = data['method']
248 if not method in ['backbone', 'custom']:
248 if not method in ['backbone', 'custom']:
249 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
249 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
250
250
251 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
251 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
252 if method == 'backbone' and 'sync_data' in data:
252 if method == 'backbone' and 'sync_data' in data:
253 sync_data = data['sync_data']
253 sync_data = data['sync_data']
254 self._handle_receive_state(sync_data) # handles all methods
254 self._handle_receive_state(sync_data) # handles all methods
255
255
256 # Handle a custom msg from the front-end
256 # Handle a custom msg from the front-end
257 elif method == 'custom':
257 elif method == 'custom':
258 if 'content' in data:
258 if 'content' in data:
259 self._handle_custom_msg(data['content'])
259 self._handle_custom_msg(data['content'])
260
260
261 def _handle_receive_state(self, sync_data):
261 def _handle_receive_state(self, sync_data):
262 """Called when a state is received from the front-end."""
262 """Called when a state is received from the front-end."""
263 for name in self.keys:
263 for name in self.keys:
264 if name in sync_data:
264 if name in sync_data:
265 value = self._unpack_widgets(sync_data[name])
265 value = self._unpack_widgets(sync_data[name])
266 with self._lock_property(name, value):
266 with self._lock_property(name, value):
267 setattr(self, name, value)
267 setattr(self, name, value)
268
268
269 def _handle_custom_msg(self, content):
269 def _handle_custom_msg(self, content):
270 """Called when a custom msg is received."""
270 """Called when a custom msg is received."""
271 self._msg_callbacks(self, content)
271 self._msg_callbacks(self, content)
272
272
273 def _handle_property_changed(self, name, old, new):
273 def _handle_property_changed(self, name, old, new):
274 """Called when a property has been changed."""
274 """Called when a property has been changed."""
275 # Make sure this isn't information that the front-end just sent us.
275 # Make sure this isn't information that the front-end just sent us.
276 if self._should_send_property(name, new):
276 if self._should_send_property(name, new):
277 # Send new state to front-end
277 # Send new state to front-end
278 self.send_state(key=name)
278 self.send_state(key=name)
279
279
280 def _handle_displayed(self, **kwargs):
280 def _handle_displayed(self, **kwargs):
281 """Called when a view has been displayed for this widget instance"""
281 """Called when a view has been displayed for this widget instance"""
282 self._display_callbacks(self, **kwargs)
282 self._display_callbacks(self, **kwargs)
283
283
284 def _pack_widgets(self, x):
284 def _pack_widgets(self, x):
285 """Recursively converts all widget instances to model id strings.
285 """Recursively converts all widget instances to model id strings.
286
286
287 Children widgets will be stored and transmitted to the front-end by
287 Children widgets will be stored and transmitted to the front-end by
288 their model ids. Return value must be JSON-able."""
288 their model ids. Return value must be JSON-able."""
289 if isinstance(x, dict):
289 if isinstance(x, dict):
290 return {k: self._pack_widgets(v) for k, v in x.items()}
290 return {k: self._pack_widgets(v) for k, v in x.items()}
291 elif isinstance(x, list):
291 elif isinstance(x, list):
292 return [self._pack_widgets(v) for v in x]
292 return [self._pack_widgets(v) for v in x]
293 elif isinstance(x, Widget):
293 elif isinstance(x, Widget):
294 return x.model_id
294 return x.model_id
295 else:
295 else:
296 return x # Value must be JSON-able
296 return x # Value must be JSON-able
297
297
298 def _unpack_widgets(self, x):
298 def _unpack_widgets(self, x):
299 """Recursively converts all model id strings to widget instances.
299 """Recursively converts all model id strings to widget instances.
300
300
301 Children widgets will be stored and transmitted to the front-end by
301 Children widgets will be stored and transmitted to the front-end by
302 their model ids."""
302 their model ids."""
303 if isinstance(x, dict):
303 if isinstance(x, dict):
304 return {k: self._unpack_widgets(v) for k, v in x.items()}
304 return {k: self._unpack_widgets(v) for k, v in x.items()}
305 elif isinstance(x, list):
305 elif isinstance(x, list):
306 return [self._unpack_widgets(v) for v in x]
306 return [self._unpack_widgets(v) for v in x]
307 elif isinstance(x, string_types):
307 elif isinstance(x, string_types):
308 return x if x not in Widget.widgets else Widget.widgets[x]
308 return x if x not in Widget.widgets else Widget.widgets[x]
309 else:
309 else:
310 return x
310 return x
311
311
312 def _ipython_display_(self, **kwargs):
312 def _ipython_display_(self, **kwargs):
313 """Called when `IPython.display.display` is called on the widget."""
313 """Called when `IPython.display.display` is called on the widget."""
314 # Show view. By sending a display message, the comm is opened and the
314 # Show view. By sending a display message, the comm is opened and the
315 # initial state is sent.
315 # initial state is sent.
316 self._send({"method": "display"})
316 self._send({"method": "display"})
317 self._handle_displayed(**kwargs)
317 self._handle_displayed(**kwargs)
318
318
319 def _send(self, msg):
319 def _send(self, msg):
320 """Sends a message to the model in the front-end."""
320 """Sends a message to the model in the front-end."""
321 self.comm.send(msg)
321 self.comm.send(msg)
322
322
323
323
324 class DOMWidget(Widget):
324 class DOMWidget(Widget):
325 visible = Bool(True, help="Whether the widget is visible.", sync=True)
325 visible = Bool(True, help="Whether the widget is visible.", sync=True)
326 _css = Dict(sync=True) # Internal CSS property dict
326 _css = Dict(sync=True) # Internal CSS property dict
327
327
328 def get_css(self, key, selector=""):
328 def get_css(self, key, selector=""):
329 """Get a CSS property of the widget.
329 """Get a CSS property of the widget.
330
330
331 Note: This function does not actually request the CSS from the
331 Note: This function does not actually request the CSS from the
332 front-end; Only properties that have been set with set_css can be read.
332 front-end; Only properties that have been set with set_css can be read.
333
333
334 Parameters
334 Parameters
335 ----------
335 ----------
336 key: unicode
336 key: unicode
337 CSS key
337 CSS key
338 selector: unicode (optional)
338 selector: unicode (optional)
339 JQuery selector used when the CSS key/value was set.
339 JQuery selector used when the CSS key/value was set.
340 """
340 """
341 if selector in self._css and key in self._css[selector]:
341 if selector in self._css and key in self._css[selector]:
342 return self._css[selector][key]
342 return self._css[selector][key]
343 else:
343 else:
344 return None
344 return None
345
345
346 def set_css(self, dict_or_key, value=None, selector=''):
346 def set_css(self, dict_or_key, value=None, selector=''):
347 """Set one or more CSS properties of the widget.
347 """Set one or more CSS properties of the widget.
348
348
349 This function has two signatures:
349 This function has two signatures:
350 - set_css(css_dict, selector='')
350 - set_css(css_dict, selector='')
351 - set_css(key, value, selector='')
351 - set_css(key, value, selector='')
352
352
353 Parameters
353 Parameters
354 ----------
354 ----------
355 css_dict : dict
355 css_dict : dict
356 CSS key/value pairs to apply
356 CSS key/value pairs to apply
357 key: unicode
357 key: unicode
358 CSS key
358 CSS key
359 value:
359 value:
360 CSS value
360 CSS value
361 selector: unicode (optional, kwarg only)
361 selector: unicode (optional, kwarg only)
362 JQuery selector to use to apply the CSS key/value. If no selector
362 JQuery selector to use to apply the CSS key/value. If no selector
363 is provided, an empty selector is used. An empty selector makes the
363 is provided, an empty selector is used. An empty selector makes the
364 front-end try to apply the css to a default element. The default
364 front-end try to apply the css to a default element. The default
365 element is an attribute unique to each view, which is a DOM element
365 element is an attribute unique to each view, which is a DOM element
366 of the view that should be styled with common CSS (see
366 of the view that should be styled with common CSS (see
367 `$el_to_style` in the Javascript code).
367 `$el_to_style` in the Javascript code).
368 """
368 """
369 if not selector in self._css:
369 if not selector in self._css:
370 self._css[selector] = {}
370 self._css[selector] = {}
371 my_css = self._css[selector]
371 my_css = self._css[selector]
372
372
373 if value is None:
373 if value is None:
374 css_dict = dict_or_key
374 css_dict = dict_or_key
375 else:
375 else:
376 css_dict = {dict_or_key: value}
376 css_dict = {dict_or_key: value}
377
377
378 for (key, value) in css_dict.items():
378 for (key, value) in css_dict.items():
379 if not (key in my_css and value == my_css[key]):
379 if not (key in my_css and value == my_css[key]):
380 my_css[key] = value
380 my_css[key] = value
381 self.send_state('_css')
381 self.send_state('_css')
382
382
383 def add_class(self, class_names, selector=""):
383 def add_class(self, class_names, selector=""):
384 """Add class[es] to a DOM element.
384 """Add class[es] to a DOM element.
385
385
386 Parameters
386 Parameters
387 ----------
387 ----------
388 class_names: unicode or list
388 class_names: unicode or list
389 Class name(s) to add to the DOM element(s).
389 Class name(s) to add to the DOM element(s).
390 selector: unicode (optional)
390 selector: unicode (optional)
391 JQuery selector to select the DOM element(s) that the class(es) will
391 JQuery selector to select the DOM element(s) that the class(es) will
392 be added to.
392 be added to.
393 """
393 """
394 class_list = class_names
394 class_list = class_names
395 if isinstance(class_list, list):
395 if isinstance(class_list, list):
396 class_list = ' '.join(class_list)
396 class_list = ' '.join(class_list)
397
397
398 self.send({
398 self.send({
399 "msg_type" : "add_class",
399 "msg_type" : "add_class",
400 "class_list" : class_list,
400 "class_list" : class_list,
401 "selector" : selector
401 "selector" : selector
402 })
402 })
403
403
404 def remove_class(self, class_names, selector=""):
404 def remove_class(self, class_names, selector=""):
405 """Remove class[es] from a DOM element.
405 """Remove class[es] from a DOM element.
406
406
407 Parameters
407 Parameters
408 ----------
408 ----------
409 class_names: unicode or list
409 class_names: unicode or list
410 Class name(s) to remove from the DOM element(s).
410 Class name(s) to remove from the DOM element(s).
411 selector: unicode (optional)
411 selector: unicode (optional)
412 JQuery selector to select the DOM element(s) that the class(es) will
412 JQuery selector to select the DOM element(s) that the class(es) will
413 be removed from.
413 be removed from.
414 """
414 """
415 class_list = class_names
415 class_list = class_names
416 if isinstance(class_list, list):
416 if isinstance(class_list, list):
417 class_list = ' '.join(class_list)
417 class_list = ' '.join(class_list)
418
418
419 self.send({
419 self.send({
420 "msg_type" : "remove_class",
420 "msg_type" : "remove_class",
421 "class_list" : class_list,
421 "class_list" : class_list,
422 "selector" : selector,
422 "selector" : selector,
423 })
423 })
General Comments 0
You need to be logged in to leave comments. Login now