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