##// END OF EJS Templates
More fixes
Jonathan Frederic -
Show More
@@ -1,121 +1,126 b''
1 1 //----------------------------------------------------------------------------
2 2 // Copyright (C) 2013 The IPython Development Team
3 3 //
4 4 // Distributed under the terms of the BSD License. The full license is in
5 5 // the file COPYING, distributed as part of this software.
6 6 //----------------------------------------------------------------------------
7 7
8 8 //============================================================================
9 9 // BoolWidget
10 10 //============================================================================
11 11
12 12 /**
13 13 * @module IPython
14 14 * @namespace IPython
15 15 **/
16 16
17 17 define(["notebook/js/widgets/widget"], function(widget_manager){
18 18 var CheckBoxView = IPython.DOMWidgetView.extend({
19 19
20 20 // Called when view is rendered.
21 21 render : function(){
22 22 this.$el
23 23 .addClass('widget-hbox-single');
24 24 this.$label = $('<div />')
25 25 .addClass('widget-hlabel')
26 26 .appendTo(this.$el)
27 27 .hide();
28 var that = this;
29 28 this.$checkbox = $('<input />')
30 29 .attr('type', 'checkbox')
31 .click(function(el) {
32
33 // Calling model.set will trigger all of the other views of the
34 // model to update.
35 that.model.set('value', that.$checkbox.prop('checked'), {updated_view: this});
36 that.touch();
37 })
38 .appendTo(this.$el);
30 .appendTo(this.$el)
31 .click($.proxy(this.handle_click, this));
39 32
40 33 this.$el_to_style = this.$checkbox; // Set default element to style
41 34 this.update(); // Set defaults.
42 35 },
43 36
37 handle_click: function() {
38 // Calling model.set will trigger all of the other views of the
39 // model to update.
40 var value = this.model.get('value');
41 this.model.set('value', ! value, {updated_view: this});
42 this.touch();
43 },
44
44 45 update : function(options){
45 46 // Update the contents of this view
46 47 //
47 48 // Called when the model is changed. The model may have been
48 49 // changed by another view or by a state update from the back-end.
49 if (options === undefined || options.updated_view != this) {
50 50 this.$checkbox.prop('checked', this.model.get('value'));
51 51
52 if (options === undefined || options.updated_view != this) {
52 53 var disabled = this.model.get('disabled');
53 54 this.$checkbox.prop('disabled', disabled);
54 55
55 56 var description = this.model.get('description');
56 57 if (description.length === 0) {
57 58 this.$label.hide();
58 59 } else {
59 60 this.$label.html(description);
60 61 this.$label.show();
61 62 }
62 63 }
63 64 return CheckBoxView.__super__.update.apply(this);
64 65 },
65 66
66 67 });
67 68
68 69 widget_manager.register_widget_view('CheckBoxView', CheckBoxView);
69 70
70 71 var ToggleButtonView = IPython.DOMWidgetView.extend({
71 72
72 73 // Called when view is rendered.
73 74 render : function(){
75 var that = this;
74 76 this.setElement($('<button />')
75 77 .addClass('btn')
76 78 .attr('type', 'button')
77 .attr('data-toggle', 'button'));
79 .on('click', function (e) {
80 e.preventDefault();
81 that.handle_click();
82 }));
78 83
79 84 this.update(); // Set defaults.
80 85 },
81 86
82 87 update : function(options){
83 88 // Update the contents of this view
84 89 //
85 90 // Called when the model is changed. The model may have been
86 91 // changed by another view or by a state update from the back-end.
87 92 if (this.model.get('value')) {
88 93 this.$el.addClass('active');
89 94 } else {
90 95 this.$el.removeClass('active');
91 96 }
92 97
93 98 if (options === undefined || options.updated_view != this) {
99
94 100 var disabled = this.model.get('disabled');
95 101 this.$el.prop('disabled', disabled);
96 102
97 103 var description = this.model.get('description');
98 104 if (description.length === 0) {
99 105 this.$el.html(' '); // Preserve button height
100 106 } else {
101 107 this.$el.html(description);
102 108 }
103 109 }
104 110 return ToggleButtonView.__super__.update.apply(this);
105 111 },
106 112
107 events: {"click" : "handleClick"},
108
109 113 // Handles and validates user input.
110 handleClick: function(e) {
114 handle_click: function(e) {
111 115
112 116 // Calling model.set will trigger all of the other views of the
113 117 // model to update.
114 this.model.set('value', ! $(this.$el).hasClass('active'), {updated_view: this});
118 var value = this.model.get('value');
119 this.model.set('value', ! value, {updated_view: this});
115 120 this.touch();
116 121 },
117 122 });
118 123
119 124 widget_manager.register_widget_view('ToggleButtonView', ToggleButtonView);
120 125
121 126 });
@@ -1,126 +1,76 b''
1 1 // Test the widget framework.
2 2 casper.notebook_test(function () {
3 3 var index;
4 4
5 5 this.then(function () {
6 6
7 7 // Check if the WidgetManager class is defined.
8 8 this.test.assert(this.evaluate(function() {
9 9 return IPython.WidgetManager != undefined;
10 10 }), 'WidgetManager class is defined');
11 11 });
12 12
13 13 index = this.append_cell(
14 14 'from IPython.html import widgets\n' +
15 15 'from IPython.display import display, clear_output\n' +
16 16 'print("Success")');
17 17 this.execute_cell_then(index);
18 18
19 19 this.wait(500); // Wait for require.js async callbacks to load dependencies.
20 20
21 21 this.then(function () {
22 22 // Check if the widget manager has been instanciated.
23 23 this.test.assert(this.evaluate(function() {
24 24 return IPython.widget_manager != undefined;
25 25 }), 'Notebook widget manager instanciated');
26 26 });
27 27
28 index = this.append_cell(
29 'names = [name for name in dir(widgets)' +
30 ' if name.endswith("Widget") and name!= "Widget" and name!= "DOMWidget"]\n' +
31 'for name in names:\n' +
32 ' print(name)\n');
33 this.execute_cell_then(index, function(index){
34
35 // Get the widget names that are registered with the widget manager. Assume
36 // a 1 to 1 mapping of model and widgets names (model names just have 'model'
37 // suffixed).
38 var javascript_names = this.evaluate(function () {
39 names = [];
40 for (var name in IPython.widget_manager._model_types) {
41 names.push(name.replace('Model',''));
42 }
43 return names;
44 });
45
46 // Get the widget names registered in python.
47 var python_names = this.get_output_cell(index).text.split('\n');
48
49 // Make sure the two lists have the same items.
50 for (var i in javascript_names) {
51 var javascript_name = javascript_names[i];
52 var found = false;
53 for (var j in python_names) {
54 var python_name = python_names[j];
55 if (python_name==javascript_name) {
56 found = true;
57 break;
58 }
59 }
60 this.test.assert(found, javascript_name + ' exists in python');
61 }
62 for (var i in python_names) {
63 var python_name = python_names[i];
64 if (python_name.length > 0) {
65 var found = false;
66 for (var j in javascript_names) {
67 var javascript_name = javascript_names[j];
68 if (python_name==javascript_name) {
69 found = true;
70 break;
71 }
72 }
73 this.test.assert(found, python_name + ' exists in javascript');
74 }
75 }
76 });
77
78 28 throttle_index = this.append_cell(
79 29 'import time\n' +
80 'textbox = widgets.StringWidget()\n' +
30 'textbox = widgets.TextBoxWidget()\n' +
81 31 'display(textbox)\n'+
82 32 'textbox.add_class("my-throttle-textbox")\n' +
83 33 'def handle_change(name, old, new):\n' +
84 34 ' print(len(new))\n' +
85 35 ' time.sleep(0.5)\n' +
86 36 'textbox.on_trait_change(handle_change)\n' +
87 37 'print("Success")');
88 38 this.execute_cell_then(throttle_index, function(index){
89 39 this.test.assert(this.get_output_cell(index).text == 'Success\n',
90 40 'Test throttling cell executed with correct output');
91 41
92 42 this.test.assert(this.cell_element_exists(index,
93 43 '.widget-area .widget-subarea'),
94 44 'Widget subarea exists.');
95 45
96 46 this.test.assert(this.cell_element_exists(index,
97 47 '.my-throttle-textbox'), 'Textbox exists.');
98 48
99 49 // Send 20 characters
100 50 this.sendKeys('.my-throttle-textbox', '....................');
101 51 });
102 52
103 53 this.wait(2000); // Wait for clicks to execute in kernel
104 54
105 55 this.then(function(){
106 56 var resume = true;
107 57 var i = 0;
108 58 while (resume) {
109 59 i++;
110 60 var output = this.get_output_cell(throttle_index, i);
111 61 if (output === undefined || output === null) {
112 62 resume = false;
113 63 i--;
114 64 }
115 65 }
116 66
117 67 // Only 4 outputs should have printed, but because of timing, sometimes
118 68 // 5 outputs will print. All we need to do is verify num outputs <= 5
119 69 // because that is much less than 20.
120 70 this.test.assert(i <= 5, 'Messages throttled.');
121 71
122 72 // We also need to verify that the last state sent was correct.
123 73 var last_state = this.get_output_cell(throttle_index, i).text;
124 74 this.test.assert(last_state == "20\n", "Last state sent when throttling.");
125 75 })
126 76 });
@@ -1,98 +1,86 b''
1 1 // Test widget bool class
2 2 casper.notebook_test(function () {
3 3 index = this.append_cell(
4 4 'from IPython.html import widgets\n' +
5 5 'from IPython.display import display, clear_output\n' +
6 6 'print("Success")');
7 7 this.execute_cell_then(index);
8 8
9 9 var bool_index = this.append_cell(
10 'bool_widgets = [widgets.BoolWidget(description="Title", value=True) for i in range(2)]\n' +
10 'bool_widgets = [widgets.CheckBoxWidget(description="Title", value=True),\n' +
11 ' widgets.ToggleButtonWidget(description="Title", value=True)]\n' +
11 12 'display(bool_widgets[0])\n' +
12 'bool_widgets[1].view_name = "ToggleButtonView"\n' +
13 13 'display(bool_widgets[1])\n' +
14 'for widget in bool_widgets:\n' +
15 ' def handle_change(name,old,new):\n' +
16 ' for other_widget in bool_widgets:\n' +
17 ' other_widget.value = new\n' +
18 ' widget.on_trait_change(handle_change, "value")\n' +
19 14 'print("Success")');
20 15 this.execute_cell_then(bool_index, function(index){
21 16
22 17 this.test.assert(this.get_output_cell(index).text == 'Success\n',
23 18 'Create bool widget cell executed with correct output.');
24 19
25 20 this.test.assert(this.cell_element_exists(index,
26 21 '.widget-area .widget-subarea'),
27 22 'Widget subarea exists.');
28 23
29 24 this.test.assert(this.cell_element_exists(index,
30 25 '.widget-area .widget-subarea .widget-hbox-single input'),
31 26 'Checkbox exists.');
32 27
33 28 this.test.assert(this.cell_element_function(index,
34 29 '.widget-area .widget-subarea .widget-hbox-single input', 'prop', ['checked']),
35 30 'Checkbox is checked.');
36 31
37 32 this.test.assert(this.cell_element_exists(index,
38 33 '.widget-area .widget-subarea .widget-hbox-single .widget-hlabel'),
39 34 'Checkbox label exists.');
40 35
41 36 this.test.assert(this.cell_element_function(index,
42 37 '.widget-area .widget-subarea .widget-hbox-single .widget-hlabel', 'html')=="Title",
43 38 'Checkbox labeled correctly.');
44 39
45 40 this.test.assert(this.cell_element_exists(index,
46 41 '.widget-area .widget-subarea button'),
47 42 'Toggle button exists.');
48 43
49 44 this.test.assert(this.cell_element_function(index,
50 45 '.widget-area .widget-subarea button', 'html')=="Title",
51 46 'Toggle button labeled correctly.');
52 47
53 48 this.test.assert(this.cell_element_function(index,
54 49 '.widget-area .widget-subarea button', 'hasClass', ['active']),
55 50 'Toggle button is toggled.');
56 51
57 52 });
58 53
59 54 index = this.append_cell(
60 55 'bool_widgets[0].value = False\n' +
56 'bool_widgets[1].value = False\n' +
61 57 'print("Success")');
62 58 this.execute_cell_then(index, function(index){
63 59
64 60 this.test.assert(this.get_output_cell(index).text == 'Success\n',
65 61 'Change bool widget value cell executed with correct output.');
66 62
67 63 this.test.assert(! this.cell_element_function(bool_index,
68 64 '.widget-area .widget-subarea .widget-hbox-single input', 'prop', ['checked']),
69 65 'Checkbox is not checked. (1)');
70 66
71 67 this.test.assert(! this.cell_element_function(bool_index,
72 68 '.widget-area .widget-subarea button', 'hasClass', ['active']),
73 69 'Toggle button is not toggled. (1)');
74 70
75 // Try toggling the bool by clicking on the toggle button.
76 this.cell_element_function(bool_index, '.widget-area .widget-subarea button', 'click');
71 // Try toggling the bool by clicking on the checkbox.
72 this.cell_element_function(bool_index, '.widget-area .widget-subarea .widget-hbox-single input', 'click');
77 73
78 74 this.test.assert(this.cell_element_function(bool_index,
79 75 '.widget-area .widget-subarea .widget-hbox-single input', 'prop', ['checked']),
80 76 'Checkbox is checked. (2)');
81 77
82 this.test.assert(this.cell_element_function(bool_index,
83 '.widget-area .widget-subarea button', 'hasClass', ['active']),
84 'Toggle button is toggled. (2)');
85
86 // Try toggling the bool by clicking on the checkbox.
87 this.cell_element_function(bool_index, '.widget-area .widget-subarea .widget-hbox-single input', 'click');
88
89 this.test.assert(! this.cell_element_function(bool_index,
90 '.widget-area .widget-subarea .widget-hbox-single input', 'prop', ['checked']),
91 'Checkbox is not checked. (3)');
78 // Try toggling the bool by clicking on the toggle button.
79 this.cell_element_function(bool_index, '.widget-area .widget-subarea button', 'click');
92 80
93 this.test.assert(! this.cell_element_function(bool_index,
81 this.test.assert(this.cell_element_function(bool_index,
94 82 '.widget-area .widget-subarea button', 'hasClass', ['active']),
95 'Toggle button is not toggled. (3)');
83 'Toggle button is toggled. (3)');
96 84
97 85 });
98 86 }); No newline at end of file
@@ -1,447 +1,447 b''
1 1 """Base Widget class. Allows user to create widgets in the back-end that render
2 2 in the IPython notebook front-end.
3 3 """
4 4 #-----------------------------------------------------------------------------
5 5 # Copyright (c) 2013, the IPython Development Team.
6 6 #
7 7 # Distributed under the terms of the Modified BSD License.
8 8 #
9 9 # The full license is in the file COPYING.txt, distributed with this software.
10 10 #-----------------------------------------------------------------------------
11 11
12 12 #-----------------------------------------------------------------------------
13 13 # Imports
14 14 #-----------------------------------------------------------------------------
15 15 from contextlib import contextmanager
16 16 import inspect
17 17 import types
18 18
19 19 from IPython.kernel.comm import Comm
20 20 from IPython.config import LoggingConfigurable
21 21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool
22 22 from IPython.utils.py3compat import string_types
23 23
24 24 #-----------------------------------------------------------------------------
25 25 # Classes
26 26 #-----------------------------------------------------------------------------
27 27 class Widget(LoggingConfigurable):
28 28
29 29 # Shared declarations (Class level)
30 30 widget_construction_callback = None
31 31 widgets = {}
32 32
33 33 def on_widget_constructed(callback):
34 34 """Class method, registers a callback to be called when a widget is
35 35 constructed. The callback must have the following signature:
36 36 callback(widget)"""
37 37 Widget.widget_construction_callback = callback
38 38
39 39 def _call_widget_constructed(widget):
40 40 """Class method, called when a widget is constructed."""
41 41 if Widget.widget_construction_callback is not None and callable(Widget.widget_construction_callback):
42 42 Widget.widget_construction_callback(widget)
43 43
44 44
45 45
46 46 # Public declarations (Instance level)
47 47 model_name = Unicode('WidgetModel', help="""Name of the backbone model
48 48 registered in the front-end to create and sync this widget with.""")
49 49 view_name = Unicode(help="""Default view registered in the front-end
50 50 to use to represent the widget.""", sync=True)
51 51
52 52 @contextmanager
53 53 def property_lock(self, key, value):
54 54 """Lock a property-value pair.
55 55
56 56 NOTE: This, in addition to the single lock for all state changes, is
57 57 flawed. In the future we may want to look into buffering state changes
58 58 back to the front-end."""
59 59 self._property_lock = (key, value)
60 60 try:
61 61 yield
62 62 finally:
63 63 self._property_lock = (None, None)
64 64
65 65 def should_send_property(self, key, value):
66 66 """Check the property lock (property_lock)"""
67 67 return key != self._property_lock[0] or \
68 68 value != self._property_lock[1]
69 69
70 70 @property
71 71 def keys(self):
72 72 if self._keys is None:
73 73 self._keys = []
74 74 for trait_name in self.trait_names():
75 75 if self.trait_metadata(trait_name, 'sync'):
76 76 self._keys.append(trait_name)
77 77 return self._keys
78 78
79 79 # Private/protected declarations
80 80 _comm = Instance('IPython.kernel.comm.Comm')
81 81
82 82 def __init__(self, **kwargs):
83 83 """Public constructor
84 84 """
85 85 self.closed = False
86 86 self._property_lock = (None, None)
87 87 self._display_callbacks = []
88 88 self._msg_callbacks = []
89 89 self._keys = None
90 90 super(Widget, self).__init__(**kwargs)
91 91
92 92 self.on_trait_change(self._handle_property_changed, self.keys)
93 93 Widget._call_widget_constructed(self)
94 94
95 95 def __del__(self):
96 96 """Object disposal"""
97 97 self.close()
98 98
99 99 def close(self):
100 100 """Close method. Closes the widget which closes the underlying comm.
101 101 When the comm is closed, all of the widget views are automatically
102 102 removed from the front-end."""
103 103 if not self.closed:
104 104 self._comm.close()
105 105 self._close()
106 106
107 107
108 108 def _close(self):
109 109 """Unsafe close"""
110 110 del Widget.widgets[self.model_id]
111 111 self._comm = None
112 112 self.closed = True
113 113
114 114
115 115 @property
116 116 def comm(self):
117 117 if self._comm is None:
118 118 # Create a comm.
119 119 self._comm = Comm(target_name=self.model_name)
120 120 self._comm.on_msg(self._handle_msg)
121 121 self._comm.on_close(self._close)
122 122 Widget.widgets[self.model_id] = self
123 123
124 124 # first update
125 125 self.send_state()
126 126 return self._comm
127 127
128 128 @property
129 129 def model_id(self):
130 130 return self.comm.comm_id
131 131
132 132 # Event handlers
133 133 def _handle_msg(self, msg):
134 134 """Called when a msg is received from the front-end"""
135 135 data = msg['content']['data']
136 136 method = data['method']
137 137 if not method in ['backbone', 'custom']:
138 138 self.log.error('Unknown front-end to back-end widget msg with method "%s"' % method)
139 139
140 140 # Handle backbone sync methods CREATE, PATCH, and UPDATE all in one.
141 141 if method == 'backbone' and 'sync_data' in data:
142 142 sync_data = data['sync_data']
143 143 self._handle_receive_state(sync_data) # handles all methods
144 144
145 145 # Handle a custom msg from the front-end
146 146 elif method == 'custom':
147 147 if 'custom_content' in data:
148 148 self._handle_custom_msg(data['custom_content'])
149 149
150 150
151 151 def _handle_receive_state(self, sync_data):
152 152 """Called when a state is received from the front-end."""
153 153 for name in self.keys:
154 154 if name in sync_data:
155 155 value = self._unpack_widgets(sync_data[name])
156 156 with self.property_lock(name, value):
157 157 setattr(self, name, value)
158 158
159 159
160 160 def _handle_custom_msg(self, content):
161 161 """Called when a custom msg is received."""
162 162 for handler in self._msg_callbacks:
163 163 handler(self, content)
164 164
165 165
166 166 def _handle_property_changed(self, name, old, new):
167 167 """Called when a property has been changed."""
168 168 # Make sure this isn't information that the front-end just sent us.
169 169 if self.should_send_property(name, new):
170 170 # Send new state to front-end
171 171 self.send_state(key=name)
172 172
173 173 def _handle_displayed(self, **kwargs):
174 174 """Called when a view has been displayed for this widget instance"""
175 175 for handler in self._display_callbacks:
176 176 handler(self, **kwargs)
177 177
178 178 # Public methods
179 179 def send_state(self, key=None):
180 180 """Sends the widget state, or a piece of it, to the front-end.
181 181
182 182 Parameters
183 183 ----------
184 184 key : unicode (optional)
185 185 A single property's name to sync with the front-end.
186 186 """
187 187 self._send({"method": "update",
188 188 "state": self.get_state()})
189 189
190 190 def get_state(self, key=None):
191 191 """Gets the widget state, or a piece of it.
192 192
193 193 Parameters
194 194 ----------
195 195 key : unicode (optional)
196 196 A single property's name to get.
197 197 """
198 198 keys = self.keys if key is None else [key]
199 199 return {k: self._pack_widgets(getattr(self, k)) for k in keys}
200 200
201 201
202 202 def _pack_widgets(self, values):
203 203 """This function recursively converts all widget instances to model id
204 204 strings.
205 205
206 206 Children widgets will be stored and transmitted to the front-end by
207 207 their model ids."""
208 208 if isinstance(values, dict):
209 209 new_dict = {}
210 210 for key, value in values.items():
211 211 new_dict[key] = self._pack_widgets(value)
212 212 return new_dict
213 213 elif isinstance(values, list):
214 214 new_list = []
215 215 for value in values:
216 216 new_list.append(self._pack_widgets(value))
217 217 return new_list
218 218 elif isinstance(values, Widget):
219 219 return values.model_id
220 220 else:
221 221 return values
222 222
223 223
224 224 def _unpack_widgets(self, values):
225 225 """This function recursively converts all model id strings to widget
226 226 instances.
227 227
228 228 Children widgets will be stored and transmitted to the front-end by
229 229 their model ids."""
230 230 if isinstance(values, dict):
231 231 new_dict = {}
232 232 for key, values in values.items():
233 233 new_dict[key] = self._unpack_widgets(values[key])
234 234 return new_dict
235 235 elif isinstance(values, list):
236 236 new_list = []
237 237 for value in values:
238 238 new_list.append(self._unpack_widgets(value))
239 239 return new_list
240 240 elif isinstance(values, string_types):
241 if widget.model_id in Widget.widgets:
242 return Widget.widgets[widget.model_id]
241 if values in Widget.widgets:
242 return Widget.widgets[values]
243 243 else:
244 244 return values
245 245 else:
246 246 return values
247 247
248 248
249 249 def send(self, content):
250 250 """Sends a custom msg to the widget model in the front-end.
251 251
252 252 Parameters
253 253 ----------
254 254 content : dict
255 255 Content of the message to send.
256 256 """
257 257 self._send({"method": "custom", "custom_content": content})
258 258
259 259
260 260 def on_msg(self, callback, remove=False):
261 261 """Register or unregister a callback for when a custom msg is recieved
262 262 from the front-end.
263 263
264 264 Parameters
265 265 ----------
266 266 callback: method handler
267 267 Can have a signature of:
268 268 - callback(content)
269 269 - callback(sender, content)
270 270 remove: bool
271 271 True if the callback should be unregistered."""
272 272 if remove and callback in self._msg_callbacks:
273 273 self._msg_callbacks.remove(callback)
274 274 elif not remove and not callback in self._msg_callbacks:
275 275 if callable(callback):
276 276 argspec = inspect.getargspec(callback)
277 277 nargs = len(argspec[0])
278 278
279 279 # Bound methods have an additional 'self' argument
280 280 if isinstance(callback, types.MethodType):
281 281 nargs -= 1
282 282
283 283 # Call the callback
284 284 if nargs == 1:
285 285 self._msg_callbacks.append(lambda sender, content: callback(content))
286 286 elif nargs == 2:
287 287 self._msg_callbacks.append(callback)
288 288 else:
289 289 raise TypeError('Widget msg callback must ' \
290 290 'accept 1 or 2 arguments, not %d.' % nargs)
291 291 else:
292 292 raise Exception('Callback must be callable.')
293 293
294 294
295 295 def on_displayed(self, callback, remove=False):
296 296 """Register or unregister a callback to be called when the widget has
297 297 been displayed.
298 298
299 299 Parameters
300 300 ----------
301 301 callback: method handler
302 302 Can have a signature of:
303 303 - callback(sender, **kwargs)
304 304 kwargs from display call passed through without modification.
305 305 remove: bool
306 306 True if the callback should be unregistered."""
307 307 if remove and callback in self._display_callbacks:
308 308 self._display_callbacks.remove(callback)
309 309 elif not remove and not callback in self._display_callbacks:
310 310 if callable(handler):
311 311 self._display_callbacks.append(callback)
312 312 else:
313 313 raise Exception('Callback must be callable.')
314 314
315 315
316 316 # Support methods
317 317 def _ipython_display_(self, **kwargs):
318 318 """Function that is called when `IPython.display.display` is called on
319 319 the widget."""
320 320
321 321 # Show view. By sending a display message, the comm is opened and the
322 322 # initial state is sent.
323 323 self._send({"method": "display"})
324 324 self._handle_displayed(**kwargs)
325 325
326 326
327 327 def _send(self, msg):
328 328 """Sends a message to the model in the front-end"""
329 329 self.comm.send(msg)
330 330
331 331
332 332 class DOMWidget(Widget):
333 333 visible = Bool(True, help="Whether or not the widget is visible.", sync=True)
334 334
335 335 # Private/protected declarations
336 336 _css = Dict(sync=True) # Internal CSS property dict
337 337
338 338 def get_css(self, key, selector=""):
339 339 """Get a CSS property of the widget.
340 340
341 341 Note: This function does not actually request the CSS from the
342 342 front-end; Only properties that have been set with set_css can be read.
343 343
344 344 Parameters
345 345 ----------
346 346 key: unicode
347 347 CSS key
348 348 selector: unicode (optional)
349 349 JQuery selector used when the CSS key/value was set.
350 350 """
351 351 if selector in self._css and key in self._css[selector]:
352 352 return self._css[selector][key]
353 353 else:
354 354 return None
355 355
356 356 def set_css(self, *args, **kwargs):
357 357 """Set one or more CSS properties of the widget.
358 358
359 359 This function has two signatures:
360 360 - set_css(css_dict, selector='')
361 361 - set_css(key, value, selector='')
362 362
363 363 Parameters
364 364 ----------
365 365 css_dict : dict
366 366 CSS key/value pairs to apply
367 367 key: unicode
368 368 CSS key
369 369 value
370 370 CSS value
371 371 selector: unicode (optional)
372 372 JQuery selector to use to apply the CSS key/value. If no selector
373 373 is provided, an empty selector is used. An empty selector makes the
374 374 front-end try to apply the css to a default element. The default
375 375 element is an attribute unique to each view, which is a DOM element
376 376 of the view that should be styled with common CSS (see
377 377 `$el_to_style` in the Javascript code).
378 378 """
379 379 selector = kwargs.get('selector', '')
380 380
381 381 # Signature 1: set_css(css_dict, selector='')
382 382 if len(args) == 1:
383 383 if isinstance(args[0], dict):
384 384 for (key, value) in args[0].items():
385 385 if not (key in self._css[selector] and value == self._css[selector][key]):
386 386 self._css[selector][key] = value
387 387 self.send_state('_css')
388 388 else:
389 389 raise Exception('css_dict must be a dict.')
390 390
391 391 # Signature 2: set_css(key, value, selector='')
392 392 elif len(args) == 2 or len(args) == 3:
393 393
394 394 # Selector can be a positional arg if it's the 3rd value
395 395 if len(args) == 3:
396 396 selector = args[2]
397 397 if selector not in self._css:
398 398 self._css[selector] = {}
399 399
400 400 # Only update the property if it has changed.
401 401 key = args[0]
402 402 value = args[1]
403 403 if not (key in self._css[selector] and value == self._css[selector][key]):
404 404 self._css[selector][key] = value
405 405 self.send_state('_css') # Send new state to client.
406 406 else:
407 407 raise Exception('set_css only accepts 1-3 arguments')
408 408
409 409
410 410 def add_class(self, class_names, selector=""):
411 411 """Add class[es] to a DOM element
412 412
413 413 Parameters
414 414 ----------
415 415 class_names: unicode or list
416 416 Class name(s) to add to the DOM element(s).
417 417 selector: unicode (optional)
418 418 JQuery selector to select the DOM element(s) that the class(es) will
419 419 be added to.
420 420 """
421 421 class_list = class_names
422 422 if isinstance(class_list, list):
423 423 class_list = ' '.join(class_list)
424 424
425 425 self.send({"msg_type": "add_class",
426 426 "class_list": class_list,
427 427 "selector": selector})
428 428
429 429
430 430 def remove_class(self, class_names, selector=""):
431 431 """Remove class[es] from a DOM element
432 432
433 433 Parameters
434 434 ----------
435 435 class_names: unicode or list
436 436 Class name(s) to remove from the DOM element(s).
437 437 selector: unicode (optional)
438 438 JQuery selector to select the DOM element(s) that the class(es) will
439 439 be removed from.
440 440 """
441 441 class_list = class_names
442 442 if isinstance(class_list, list):
443 443 class_list = ' '.join(class_list)
444 444
445 445 self.send({"msg_type": "remove_class",
446 446 "class_list": class_list,
447 447 "selector": selector})
@@ -1,94 +1,94 b''
1 1 """StringWidget class.
2 2
3 3 Represents a unicode string using a widget.
4 4 """
5 5 #-----------------------------------------------------------------------------
6 6 # Copyright (c) 2013, the IPython Development Team.
7 7 #
8 8 # Distributed under the terms of the Modified BSD License.
9 9 #
10 10 # The full license is in the file COPYING.txt, distributed with this software.
11 11 #-----------------------------------------------------------------------------
12 12
13 13 #-----------------------------------------------------------------------------
14 14 # Imports
15 15 #-----------------------------------------------------------------------------
16 16 import inspect
17 17 import types
18 18
19 19 from .widget import DOMWidget
20 20 from IPython.utils.traitlets import Unicode, Bool, List, Int
21 21
22 22 #-----------------------------------------------------------------------------
23 23 # Classes
24 24 #-----------------------------------------------------------------------------
25 25 class HTMLWidget(DOMWidget):
26 26 view_name = Unicode('HTMLView', sync=True)
27 27
28 28 # Keys
29 29 value = Unicode(help="String value", sync=True)
30 30 disabled = Bool(False, help="Enable or disable user changes", sync=True)
31 31 description = Unicode(help="Description of the value this widget represents", sync=True)
32 32
33 33
34 34 class LatexWidget(HTMLWidget):
35 35 view_name = Unicode('LatexView', sync=True)
36 36
37 37
38 38 class TextAreaWidget(HTMLWidget):
39 39 view_name = Unicode('TextAreaView', sync=True)
40 40
41 41 def scroll_to_bottom(self):
42 42 self.send({"method": "scroll_to_bottom"})
43 43
44 44
45 45 class TextBoxWidget(HTMLWidget):
46 46 view_name = Unicode('TextBoxView', sync=True)
47 47
48 48 def __init__(self, **kwargs):
49 super(StringWidget, self).__init__(**kwargs)
49 super(TextBoxWidget, self).__init__(**kwargs)
50 50 self._submission_callbacks = []
51 51 self.on_msg(self._handle_string_msg)
52 52
53 53 def _handle_string_msg(self, content):
54 54 """Handle a msg from the front-end
55 55
56 56 Parameters
57 57 ----------
58 58 content: dict
59 59 Content of the msg."""
60 60 if 'event' in content and content['event'] == 'submit':
61 61 for handler in self._submission_callbacks:
62 62 handler(self)
63 63
64 64 def on_submit(self, callback, remove=False):
65 65 """Register a callback to handle text submission (triggered when the
66 66 user clicks enter).
67 67
68 68 Parameters
69 69 callback: Method handle
70 70 Function to be called when the text has been submitted. Function
71 71 can have two possible signatures:
72 72 callback()
73 73 callback(sender)
74 74 remove: bool (optional)
75 75 Whether or not to unregister the callback"""
76 76 if remove and callback in self._submission_callbacks:
77 77 self._submission_callbacks.remove(callback)
78 78 elif not remove and not callback in self._submission_callbacks:
79 79 if callable(callback):
80 80 argspec = inspect.getargspec(callback)
81 81 nargs = len(argspec[0])
82 82
83 83 # Bound methods have an additional 'self' argument
84 84 if isinstance(callback, types.MethodType):
85 85 nargs -= 1
86 86
87 87 # Call the callback
88 88 if nargs == 0:
89 89 self._submission_callbacks.append(lambda sender: callback())
90 90 elif nargs == 1:
91 91 self._submission_callbacks.append(callback)
92 92 else:
93 raise TypeError('StringWidget submit callback must ' \
93 raise TypeError('TextBoxWidget submit callback must ' \
94 94 'accept 0 or 1 arguments.')
General Comments 0
You need to be logged in to leave comments. Login now