##// END OF EJS Templates
Added check on widget close to make sure comm isn't already none.
Jonathan Frederic -
Show More
@@ -1,353 +1,356 b''
1 """Base Widget class. Allows user to create widgets in the backend that render
1 """Base Widget class. Allows user to create widgets in the backend that render
2 in the IPython notebook frontend.
2 in the IPython notebook frontend.
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 copy import copy
15 from copy import copy
16 from glob import glob
16 from glob import glob
17 import uuid
17 import uuid
18 import sys
18 import sys
19 import os
19 import os
20 import inspect
20 import inspect
21 import types
21 import types
22
22
23 import IPython
23 import IPython
24 from IPython.kernel.comm import Comm
24 from IPython.kernel.comm import Comm
25 from IPython.config import LoggingConfigurable
25 from IPython.config import LoggingConfigurable
26 from IPython.utils.traitlets import Unicode, Dict, List, Instance, Bool
26 from IPython.utils.traitlets import Unicode, Dict, List, Instance, Bool
27 from IPython.display import Javascript, display
27 from IPython.display import Javascript, display
28 from IPython.utils.py3compat import string_types
28 from IPython.utils.py3compat import string_types
29
29
30
30
31 #-----------------------------------------------------------------------------
31 #-----------------------------------------------------------------------------
32 # Classes
32 # Classes
33 #-----------------------------------------------------------------------------
33 #-----------------------------------------------------------------------------
34 class Widget(LoggingConfigurable):
34 class Widget(LoggingConfigurable):
35
35
36 # Shared declarations
36 # Shared declarations
37 _keys = []
37 _keys = []
38
38
39 # Public declarations
39 # Public declarations
40 target_name = Unicode('widget', help="""Name of the backbone model
40 target_name = Unicode('widget', help="""Name of the backbone model
41 registered in the frontend to create and sync this widget with.""")
41 registered in the frontend to create and sync this widget with.""")
42 default_view_name = Unicode(help="""Default view registered in the frontend
42 default_view_name = Unicode(help="""Default view registered in the frontend
43 to use to represent the widget.""")
43 to use to represent the widget.""")
44 parent = Instance('IPython.html.widgets.widget.Widget')
44 parent = Instance('IPython.html.widgets.widget.Widget')
45 visible = Bool(True, help="Whether or not the widget is visible.")
45 visible = Bool(True, help="Whether or not the widget is visible.")
46
46
47 def _parent_changed(self, name, old, new):
47 def _parent_changed(self, name, old, new):
48 if self._displayed:
48 if self._displayed:
49 raise Exception('Parent cannot be set because widget has been displayed.')
49 raise Exception('Parent cannot be set because widget has been displayed.')
50 elif new == self:
50 elif new == self:
51 raise Exception('Parent cannot be set to self.')
51 raise Exception('Parent cannot be set to self.')
52 else:
52 else:
53
53
54 # Parent/child association
54 # Parent/child association
55 if new is not None and not self in new._children:
55 if new is not None and not self in new._children:
56 new._children.append(self)
56 new._children.append(self)
57 if old is not None and self in old._children:
57 if old is not None and self in old._children:
58 old._children.remove(self)
58 old._children.remove(self)
59
59
60 # Private/protected declarations
60 # Private/protected declarations
61 _property_lock = False
61 _property_lock = False
62 _css = Dict() # Internal CSS property dict
62 _css = Dict() # Internal CSS property dict
63 _add_class = List() # Used to add a js class to a DOM element (call#, selector, class_name)
63 _add_class = List() # Used to add a js class to a DOM element (call#, selector, class_name)
64 _remove_class = List() # Used to remove a js class from a DOM element (call#, selector, class_name)
64 _remove_class = List() # Used to remove a js class from a DOM element (call#, selector, class_name)
65 _displayed = False
65 _displayed = False
66 _comm = None
66 _comm = None
67
67
68
68
69 def __init__(self, **kwargs):
69 def __init__(self, **kwargs):
70 """Public constructor
70 """Public constructor
71
71
72 Parameters
72 Parameters
73 ----------
73 ----------
74 parent : Widget instance (optional)
74 parent : Widget instance (optional)
75 Widget that this widget instance is child of. When the widget is
75 Widget that this widget instance is child of. When the widget is
76 displayed in the frontend, it's corresponding view will be made
76 displayed in the frontend, it's corresponding view will be made
77 child of the parent's view if the parent's view exists already. If
77 child of the parent's view if the parent's view exists already. If
78 the parent's view is displayed, it will automatically display this
78 the parent's view is displayed, it will automatically display this
79 widget's default view as it's child. The default view can be set
79 widget's default view as it's child. The default view can be set
80 via the default_view_name property.
80 via the default_view_name property.
81 """
81 """
82 self._children = []
82 self._children = []
83 self._add_class = [0]
83 self._add_class = [0]
84 self._remove_class = [0]
84 self._remove_class = [0]
85 self._display_callbacks = []
85 self._display_callbacks = []
86 super(Widget, self).__init__(**kwargs)
86 super(Widget, self).__init__(**kwargs)
87
87
88 # Register after init to allow default values to be specified
88 # Register after init to allow default values to be specified
89 self.on_trait_change(self._handle_property_changed, self.keys)
89 self.on_trait_change(self._handle_property_changed, self.keys)
90
90
91
91
92 def __del__(self):
92 def __del__(self):
93 """Object disposal"""
93 """Object disposal"""
94 self.close()
94 self.close()
95
95
96
96
97 def close(self):
97 def close(self):
98 """Close method. Closes the widget which closes the underlying comm.
98 """Close method. Closes the widget which closes the underlying comm.
99 When the comm is closed, all of the widget views are automatically
99 When the comm is closed, all of the widget views are automatically
100 removed from the frontend."""
100 removed from the frontend."""
101 try:
101 self._comm.close()
102 self._comm.close()
102 del self._comm
103 del self._comm
104 except:
105 pass # Comm doesn't exist and/or is already closed.
103
106
104
107
105 # Properties
108 # Properties
106 def _get_keys(self):
109 def _get_keys(self):
107 keys = ['visible', '_css', '_add_class', '_remove_class']
110 keys = ['visible', '_css', '_add_class', '_remove_class']
108 keys.extend(self._keys)
111 keys.extend(self._keys)
109 return keys
112 return keys
110 keys = property(_get_keys)
113 keys = property(_get_keys)
111
114
112
115
113 # Event handlers
116 # Event handlers
114 def _handle_msg(self, msg):
117 def _handle_msg(self, msg):
115 """Called when a msg is recieved from the frontend"""
118 """Called when a msg is recieved from the frontend"""
116 # Handle backbone sync methods CREATE, PATCH, and UPDATE
119 # Handle backbone sync methods CREATE, PATCH, and UPDATE
117 sync_method = msg['content']['data']['sync_method']
120 sync_method = msg['content']['data']['sync_method']
118 sync_data = msg['content']['data']['sync_data']
121 sync_data = msg['content']['data']['sync_data']
119 self._handle_recieve_state(sync_data) # handles all methods
122 self._handle_recieve_state(sync_data) # handles all methods
120
123
121
124
122 def _handle_recieve_state(self, sync_data):
125 def _handle_recieve_state(self, sync_data):
123 """Called when a state is recieved from the frontend."""
126 """Called when a state is recieved from the frontend."""
124 self._property_lock = True
127 self._property_lock = True
125 try:
128 try:
126
129
127 # Use _keys instead of keys - Don't get retrieve the css from the client side.
130 # Use _keys instead of keys - Don't get retrieve the css from the client side.
128 for name in self._keys:
131 for name in self._keys:
129 if name in sync_data:
132 if name in sync_data:
130 setattr(self, name, sync_data[name])
133 setattr(self, name, sync_data[name])
131 finally:
134 finally:
132 self._property_lock = False
135 self._property_lock = False
133
136
134
137
135 def _handle_property_changed(self, name, old, new):
138 def _handle_property_changed(self, name, old, new):
136 """Called when a proeprty has been changed."""
139 """Called when a proeprty has been changed."""
137 if not self._property_lock and self._comm is not None:
140 if not self._property_lock and self._comm is not None:
138 # TODO: Validate properties.
141 # TODO: Validate properties.
139 # Send new state to frontend
142 # Send new state to frontend
140 self.send_state(key=name)
143 self.send_state(key=name)
141
144
142
145
143 def _handle_close(self):
146 def _handle_close(self):
144 """Called when the comm is closed by the frontend."""
147 """Called when the comm is closed by the frontend."""
145 self._comm = None
148 self._comm = None
146
149
147
150
148 # Public methods
151 # Public methods
149 def send_state(self, key=None):
152 def send_state(self, key=None):
150 """Sends the widget state, or a piece of it, to the frontend.
153 """Sends the widget state, or a piece of it, to the frontend.
151
154
152 Parameters
155 Parameters
153 ----------
156 ----------
154 key : unicode (optional)
157 key : unicode (optional)
155 A single property's name to sync with the frontend.
158 A single property's name to sync with the frontend.
156 """
159 """
157 if self._comm is not None:
160 if self._comm is not None:
158 state = {}
161 state = {}
159
162
160 # If a key is provided, just send the state of that key.
163 # If a key is provided, just send the state of that key.
161 keys = []
164 keys = []
162 if key is None:
165 if key is None:
163 keys.extend(self.keys)
166 keys.extend(self.keys)
164 else:
167 else:
165 keys.append(key)
168 keys.append(key)
166 for key in self.keys:
169 for key in self.keys:
167 try:
170 try:
168 state[key] = getattr(self, key)
171 state[key] = getattr(self, key)
169 except Exception as e:
172 except Exception as e:
170 pass # Eat errors, nom nom nom
173 pass # Eat errors, nom nom nom
171 self._comm.send({"method": "update",
174 self._comm.send({"method": "update",
172 "state": state})
175 "state": state})
173
176
174
177
175 def get_css(self, key, selector=""):
178 def get_css(self, key, selector=""):
176 """Get a CSS property of the widget. Note, this function does not
179 """Get a CSS property of the widget. Note, this function does not
177 actually request the CSS from the front-end; Only properties that have
180 actually request the CSS from the front-end; Only properties that have
178 been set with set_css can be read.
181 been set with set_css can be read.
179
182
180 Parameters
183 Parameters
181 ----------
184 ----------
182 key: unicode
185 key: unicode
183 CSS key
186 CSS key
184 selector: unicode (optional)
187 selector: unicode (optional)
185 JQuery selector used when the CSS key/value was set.
188 JQuery selector used when the CSS key/value was set.
186 """
189 """
187 if selector in self._css and key in self._css[selector]:
190 if selector in self._css and key in self._css[selector]:
188 return self._css[selector][key]
191 return self._css[selector][key]
189 else:
192 else:
190 return None
193 return None
191
194
192
195
193 def set_css(self, *args, **kwargs):
196 def set_css(self, *args, **kwargs):
194 """Set one or more CSS properties of the widget (shared among all of the
197 """Set one or more CSS properties of the widget (shared among all of the
195 views). This function has two signatures:
198 views). This function has two signatures:
196 - set_css(css_dict, [selector=''])
199 - set_css(css_dict, [selector=''])
197 - set_css(key, value, [selector=''])
200 - set_css(key, value, [selector=''])
198
201
199 Parameters
202 Parameters
200 ----------
203 ----------
201 css_dict : dict
204 css_dict : dict
202 CSS key/value pairs to apply
205 CSS key/value pairs to apply
203 key: unicode
206 key: unicode
204 CSS key
207 CSS key
205 value
208 value
206 CSS value
209 CSS value
207 selector: unicode (optional)
210 selector: unicode (optional)
208 JQuery selector to use to apply the CSS key/value.
211 JQuery selector to use to apply the CSS key/value.
209 """
212 """
210 selector = kwargs.get('selector', '')
213 selector = kwargs.get('selector', '')
211
214
212 # Signature 1: set_css(css_dict, [selector=''])
215 # Signature 1: set_css(css_dict, [selector=''])
213 if len(args) == 1:
216 if len(args) == 1:
214 if isinstance(args[0], dict):
217 if isinstance(args[0], dict):
215 for (key, value) in args[0].items():
218 for (key, value) in args[0].items():
216 self.set_css(key, value, selector=selector)
219 self.set_css(key, value, selector=selector)
217 else:
220 else:
218 raise Exception('css_dict must be a dict.')
221 raise Exception('css_dict must be a dict.')
219
222
220 # Signature 2: set_css(key, value, [selector=''])
223 # Signature 2: set_css(key, value, [selector=''])
221 elif len(args) == 2 or len(args) == 3:
224 elif len(args) == 2 or len(args) == 3:
222
225
223 # Selector can be a positional arg if it's the 3rd value
226 # Selector can be a positional arg if it's the 3rd value
224 if len(args) == 3:
227 if len(args) == 3:
225 selector = args[2]
228 selector = args[2]
226 if selector not in self._css:
229 if selector not in self._css:
227 self._css[selector] = {}
230 self._css[selector] = {}
228
231
229 # Only update the property if it has changed.
232 # Only update the property if it has changed.
230 key = args[0]
233 key = args[0]
231 value = args[1]
234 value = args[1]
232 if not (key in self._css[selector] and value in self._css[selector][key]):
235 if not (key in self._css[selector] and value in self._css[selector][key]):
233 self._css[selector][key] = value
236 self._css[selector][key] = value
234 self.send_state('_css') # Send new state to client.
237 self.send_state('_css') # Send new state to client.
235 else:
238 else:
236 raise Exception('set_css only accepts 1-3 arguments')
239 raise Exception('set_css only accepts 1-3 arguments')
237
240
238
241
239 def add_class(self, class_name, selector=""):
242 def add_class(self, class_name, selector=""):
240 """Add class[es] to a DOM element
243 """Add class[es] to a DOM element
241
244
242 Parameters
245 Parameters
243 ----------
246 ----------
244 class_name: unicode
247 class_name: unicode
245 Class name(s) to add to the DOM element(s). Multiple class names
248 Class name(s) to add to the DOM element(s). Multiple class names
246 must be space separated.
249 must be space separated.
247 selector: unicode (optional)
250 selector: unicode (optional)
248 JQuery selector to select the DOM element(s) that the class(es) will
251 JQuery selector to select the DOM element(s) that the class(es) will
249 be added to.
252 be added to.
250 """
253 """
251 self._add_class = [self._add_class[0] + 1, selector, class_name]
254 self._add_class = [self._add_class[0] + 1, selector, class_name]
252 self.send_state(key='_add_class')
255 self.send_state(key='_add_class')
253
256
254
257
255 def remove_class(self, class_name, selector=""):
258 def remove_class(self, class_name, selector=""):
256 """Remove class[es] from a DOM element
259 """Remove class[es] from a DOM element
257
260
258 Parameters
261 Parameters
259 ----------
262 ----------
260 class_name: unicode
263 class_name: unicode
261 Class name(s) to remove from the DOM element(s). Multiple class
264 Class name(s) to remove from the DOM element(s). Multiple class
262 names must be space separated.
265 names must be space separated.
263 selector: unicode (optional)
266 selector: unicode (optional)
264 JQuery selector to select the DOM element(s) that the class(es) will
267 JQuery selector to select the DOM element(s) that the class(es) will
265 be removed from.
268 be removed from.
266 """
269 """
267 self._remove_class = [self._remove_class[0] + 1, selector, class_name]
270 self._remove_class = [self._remove_class[0] + 1, selector, class_name]
268 self.send_state(key='_remove_class')
271 self.send_state(key='_remove_class')
269
272
270
273
271 def on_displayed(self, callback, remove=False):
274 def on_displayed(self, callback, remove=False):
272 """Register a callback to be called when the widget has been displayed
275 """Register a callback to be called when the widget has been displayed
273
276
274 Parameters
277 Parameters
275 ----------
278 ----------
276 callback: method handler
279 callback: method handler
277 Can have a signature of:
280 Can have a signature of:
278 - callback()
281 - callback()
279 - callback(sender)
282 - callback(sender)
280 - callback(sender, view_name)
283 - callback(sender, view_name)
281 remove: bool
284 remove: bool
282 True if the callback should be unregistered."""
285 True if the callback should be unregistered."""
283 if remove:
286 if remove:
284 self._display_callbacks.remove(callback)
287 self._display_callbacks.remove(callback)
285 elif not callback in self._display_callbacks:
288 elif not callback in self._display_callbacks:
286 self._display_callbacks.append(callback)
289 self._display_callbacks.append(callback)
287
290
288
291
289 def handle_displayed(self, view_name):
292 def handle_displayed(self, view_name):
290 """Called when a view has been displayed for this widget instance
293 """Called when a view has been displayed for this widget instance
291
294
292 Parameters
295 Parameters
293 ----------
296 ----------
294 view_name: unicode
297 view_name: unicode
295 Name of the view that was displayed."""
298 Name of the view that was displayed."""
296 for handler in self._display_callbacks:
299 for handler in self._display_callbacks:
297 if callable(handler):
300 if callable(handler):
298 argspec = inspect.getargspec(handler)
301 argspec = inspect.getargspec(handler)
299 nargs = len(argspec[0])
302 nargs = len(argspec[0])
300
303
301 # Bound methods have an additional 'self' argument
304 # Bound methods have an additional 'self' argument
302 if isinstance(handler, types.MethodType):
305 if isinstance(handler, types.MethodType):
303 nargs -= 1
306 nargs -= 1
304
307
305 # Call the callback
308 # Call the callback
306 if nargs == 0:
309 if nargs == 0:
307 handler()
310 handler()
308 elif nargs == 1:
311 elif nargs == 1:
309 handler(self)
312 handler(self)
310 elif nargs == 2:
313 elif nargs == 2:
311 handler(self, view_name)
314 handler(self, view_name)
312 else:
315 else:
313 raise TypeError('Widget display callback must ' \
316 raise TypeError('Widget display callback must ' \
314 'accept 0-2 arguments, not %d.' % nargs)
317 'accept 0-2 arguments, not %d.' % nargs)
315
318
316
319
317 # Support methods
320 # Support methods
318 def _repr_widget_(self, view_name=None):
321 def _repr_widget_(self, view_name=None):
319 """Function that is called when `IPython.display.display` is called on
322 """Function that is called when `IPython.display.display` is called on
320 the widget.
323 the widget.
321
324
322 Parameters
325 Parameters
323 ----------
326 ----------
324 view_name: unicode (optional)
327 view_name: unicode (optional)
325 View to display in the frontend. Overrides default_view_name."""
328 View to display in the frontend. Overrides default_view_name."""
326
329
327 if not view_name:
330 if not view_name:
328 view_name = self.default_view_name
331 view_name = self.default_view_name
329
332
330 # Create a comm.
333 # Create a comm.
331 if self._comm is None:
334 if self._comm is None:
332 self._comm = Comm(target_name=self.target_name)
335 self._comm = Comm(target_name=self.target_name)
333 self._comm.on_msg(self._handle_msg)
336 self._comm.on_msg(self._handle_msg)
334 self._comm.on_close(self._handle_close)
337 self._comm.on_close(self._handle_close)
335
338
336 # Make sure model is syncronized
339 # Make sure model is syncronized
337 self.send_state()
340 self.send_state()
338
341
339 # Show view.
342 # Show view.
340 if self.parent is None or self.parent._comm is None:
343 if self.parent is None or self.parent._comm is None:
341 self._comm.send({"method": "display", "view_name": view_name})
344 self._comm.send({"method": "display", "view_name": view_name})
342 else:
345 else:
343 self._comm.send({"method": "display",
346 self._comm.send({"method": "display",
344 "view_name": view_name,
347 "view_name": view_name,
345 "parent": self.parent._comm.comm_id})
348 "parent": self.parent._comm.comm_id})
346 self._displayed = True
349 self._displayed = True
347 self.handle_displayed(view_name)
350 self.handle_displayed(view_name)
348
351
349 # Now display children if any.
352 # Now display children if any.
350 for child in self._children:
353 for child in self._children:
351 if child != self:
354 if child != self:
352 child._repr_widget_()
355 child._repr_widget_()
353 return None
356 return None
General Comments 0
You need to be logged in to leave comments. Login now