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