##// END OF EJS Templates
Remove redundent _handle_close method
Jonathan Frederic -
Show More
@@ -1,449 +1,447 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 # Classes
32 32 #-----------------------------------------------------------------------------
33 33 class Widget(LoggingConfigurable):
34 34
35 35 # Shared declarations (Class level)
36 36 _keys = []
37 37 widget_construction_callback = None
38 38
39 39 def on_widget_constructed(callback):
40 40 """Class method, registers a callback to be called when a widget is
41 41 constructed. The callback must have the following signature:
42 42 callback(widget)"""
43 43 Widget.widget_construction_callback = callback
44 44
45 45 def _handle_widget_constructed(widget):
46 46 """Class method, called when a widget is constructed."""
47 47 if Widget.widget_construction_callback is not None and callable(Widget.widget_construction_callback):
48 48 Widget.widget_construction_callback(widget)
49 49
50 50
51 51
52 52 # Public declarations (Instance level)
53 53 target_name = Unicode('widget', help="""Name of the backbone model
54 54 registered in the frontend to create and sync this widget with.""")
55 55 default_view_name = Unicode(help="""Default view registered in the frontend
56 56 to use to represent the widget.""")
57 57 parent = Instance('IPython.html.widgets.widget.Widget')
58 58 visible = Bool(True, help="Whether or not the widget is visible.")
59 59
60 60 def _parent_changed(self, name, old, new):
61 61 if self._displayed:
62 62 raise Exception('Parent cannot be set because widget has been displayed.')
63 63 elif new == self:
64 64 raise Exception('Parent cannot be set to self.')
65 65 else:
66 66
67 67 # Parent/child association
68 68 if new is not None and not self in new._children:
69 69 new._children.append(self)
70 70 if old is not None and self in old._children:
71 71 old._children.remove(self)
72 72
73 73 # Private/protected declarations
74 74 _property_lock = (None, None) # Last updated (key, value) from the front-end. Prevents echo.
75 75 _css = Dict() # Internal CSS property dict
76 76 _displayed = False
77 77
78 78
79 79 def __init__(self, **kwargs):
80 80 """Public constructor
81 81
82 82 Parameters
83 83 ----------
84 84 parent : Widget instance (optional)
85 85 Widget that this widget instance is child of. When the widget is
86 86 displayed in the frontend, it's corresponding view will be made
87 87 child of the parent's view if the parent's view exists already. If
88 88 the parent's view is displayed, it will automatically display this
89 89 widget's default view as it's child. The default view can be set
90 90 via the default_view_name property.
91 91 """
92 92 self._children = []
93 93 self._display_callbacks = []
94 94 self._msg_callbacks = []
95 95 super(Widget, self).__init__(**kwargs)
96 96
97 97 # Register after init to allow default values to be specified
98 98 self.on_trait_change(self._handle_property_changed, self.keys)
99 99
100 100 Widget._handle_widget_constructed(self)
101 101
102 102
103 103 def __del__(self):
104 104 """Object disposal"""
105 105 self.close()
106 106
107 107
108 108 def close(self):
109 109 """Close method. Closes the widget which closes the underlying comm.
110 110 When the comm is closed, all of the widget views are automatically
111 111 removed from the frontend."""
112 112 self._close_communication()
113 113
114 114
115 115 # Properties
116 116 def _get_keys(self):
117 117 keys = ['visible', '_css']
118 118 keys.extend(self._keys)
119 119 return keys
120 120 keys = property(_get_keys)
121 121
122 122
123 123 # Event handlers
124 124 def _handle_msg(self, msg):
125 125 """Called when a msg is recieved from the frontend"""
126 126 data = msg['content']['data']
127 127 method = data['method']
128 128
129 129 # Handle backbone sync methods CREATE, PATCH, and UPDATE
130 130 if method == 'backbone':
131 131 if 'sync_method' in data and 'sync_data' in data:
132 132 sync_method = data['sync_method']
133 133 sync_data = data['sync_data']
134 134 self._handle_recieve_state(sync_data) # handles all methods
135 135
136 136 # Handle a custom msg from the front-end
137 137 elif method == 'custom':
138 138 if 'custom_content' in data:
139 139 self._handle_custom_msg(data['custom_content'])
140 140
141 141
142 142 def _handle_custom_msg(self, content):
143 143 """Called when a custom msg is recieved."""
144 144 for handler in self._msg_callbacks:
145 145 if callable(handler):
146 146 argspec = inspect.getargspec(handler)
147 147 nargs = len(argspec[0])
148 148
149 149 # Bound methods have an additional 'self' argument
150 150 if isinstance(handler, types.MethodType):
151 151 nargs -= 1
152 152
153 153 # Call the callback
154 154 if nargs == 1:
155 155 handler(content)
156 156 elif nargs == 2:
157 157 handler(self, content)
158 158 else:
159 159 raise TypeError('Widget msg callback must ' \
160 160 'accept 1 or 2 arguments, not %d.' % nargs)
161 161
162 162
163 163 def _handle_recieve_state(self, sync_data):
164 164 """Called when a state is recieved from the frontend."""
165 165 # Use _keys instead of keys - Don't get retrieve the css from the client side.
166 166 for name in self._keys:
167 167 if name in sync_data:
168 168 try:
169 169 self._property_lock = (name, sync_data[name])
170 170 setattr(self, name, sync_data[name])
171 171 finally:
172 172 self._property_lock = (None, None)
173 173
174 174
175 175 def _handle_property_changed(self, name, old, new):
176 176 """Called when a proeprty has been changed."""
177 177 # Make sure this isn't information that the front-end just sent us.
178 178 if self._property_lock[0] != name and self._property_lock[1] != new:
179 179 # Send new state to frontend
180 180 self.send_state(key=name)
181 181
182 182
183 183 def _handle_displayed(self, view_name, parent=None):
184 184 """Called when a view has been displayed for this widget instance
185 185
186 186 Parameters
187 187 ----------
188 188 view_name: unicode
189 189 Name of the view that was displayed.
190 190 parent: Widget instance [optional]
191 191 Widget that this widget should be displayed as a child of."""
192 192 for handler in self._display_callbacks:
193 193 if callable(handler):
194 194 argspec = inspect.getargspec(handler)
195 195 nargs = len(argspec[0])
196 196
197 197 # Bound methods have an additional 'self' argument
198 198 if isinstance(handler, types.MethodType):
199 199 nargs -= 1
200 200
201 201 # Call the callback
202 202 if nargs == 0:
203 203 handler()
204 204 elif nargs == 1:
205 205 handler(self)
206 206 elif nargs == 2:
207 207 handler(self, view_name)
208 208 elif nargs == 3:
209 209 handler(self, view_name, parent)
210 210 else:
211 211 raise TypeError('Widget display callback must ' \
212 212 'accept 0-2 arguments, not %d.' % nargs)
213 213
214 214
215 215 # Public methods
216 216 def send_state(self, key=None):
217 217 """Sends the widget state, or a piece of it, to the frontend.
218 218
219 219 Parameters
220 220 ----------
221 221 key : unicode (optional)
222 222 A single property's name to sync with the frontend.
223 223 """
224 224 state = {}
225 225
226 226 # If a key is provided, just send the state of that key.
227 227 keys = []
228 228 if key is None:
229 229 keys.extend(self.keys)
230 230 else:
231 231 keys.append(key)
232 232 for key in self.keys:
233 233 try:
234 234 state[key] = getattr(self, key)
235 235 except Exception as e:
236 236 pass # Eat errors, nom nom nom
237 237 self._send({"method": "update",
238 238 "state": state})
239 239
240 240
241 241 def get_css(self, key, selector=""):
242 242 """Get a CSS property of the widget. Note, this function does not
243 243 actually request the CSS from the front-end; Only properties that have
244 244 been set with set_css can be read.
245 245
246 246 Parameters
247 247 ----------
248 248 key: unicode
249 249 CSS key
250 250 selector: unicode (optional)
251 251 JQuery selector used when the CSS key/value was set.
252 252 """
253 253 if selector in self._css and key in self._css[selector]:
254 254 return self._css[selector][key]
255 255 else:
256 256 return None
257 257
258 258
259 259 def set_css(self, *args, **kwargs):
260 260 """Set one or more CSS properties of the widget (shared among all of the
261 261 views). This function has two signatures:
262 262 - set_css(css_dict, [selector=''])
263 263 - set_css(key, value, [selector=''])
264 264
265 265 Parameters
266 266 ----------
267 267 css_dict : dict
268 268 CSS key/value pairs to apply
269 269 key: unicode
270 270 CSS key
271 271 value
272 272 CSS value
273 273 selector: unicode (optional)
274 274 JQuery selector to use to apply the CSS key/value.
275 275 """
276 276 selector = kwargs.get('selector', '')
277 277
278 278 # Signature 1: set_css(css_dict, [selector=''])
279 279 if len(args) == 1:
280 280 if isinstance(args[0], dict):
281 281 for (key, value) in args[0].items():
282 282 self.set_css(key, value, selector=selector)
283 283 else:
284 284 raise Exception('css_dict must be a dict.')
285 285
286 286 # Signature 2: set_css(key, value, [selector=''])
287 287 elif len(args) == 2 or len(args) == 3:
288 288
289 289 # Selector can be a positional arg if it's the 3rd value
290 290 if len(args) == 3:
291 291 selector = args[2]
292 292 if selector not in self._css:
293 293 self._css[selector] = {}
294 294
295 295 # Only update the property if it has changed.
296 296 key = args[0]
297 297 value = args[1]
298 298 if not (key in self._css[selector] and value in self._css[selector][key]):
299 299 self._css[selector][key] = value
300 300 self.send_state('_css') # Send new state to client.
301 301 else:
302 302 raise Exception('set_css only accepts 1-3 arguments')
303 303
304 304
305 305 def add_class(self, class_name, selector=""):
306 306 """Add class[es] to a DOM element
307 307
308 308 Parameters
309 309 ----------
310 310 class_name: unicode
311 311 Class name(s) to add to the DOM element(s). Multiple class names
312 312 must be space separated.
313 313 selector: unicode (optional)
314 314 JQuery selector to select the DOM element(s) that the class(es) will
315 315 be added to.
316 316 """
317 317 self._send({"method": "add_class",
318 318 "class_list": class_name,
319 319 "selector": selector})
320 320
321 321
322 322 def remove_class(self, class_name, selector=""):
323 323 """Remove class[es] from a DOM element
324 324
325 325 Parameters
326 326 ----------
327 327 class_name: unicode
328 328 Class name(s) to remove from the DOM element(s). Multiple class
329 329 names must be space separated.
330 330 selector: unicode (optional)
331 331 JQuery selector to select the DOM element(s) that the class(es) will
332 332 be removed from.
333 333 """
334 334 self._send({"method": "remove_class",
335 335 "class_list": class_name,
336 336 "selector": selector})
337 337
338 338
339 339 def send(self, content):
340 340 """Sends a custom msg to the widget model in the front-end.
341 341
342 342 Parameters
343 343 ----------
344 344 content : dict
345 345 Content of the message to send.
346 346 """
347 347 self._send({"method": "custom",
348 348 "custom_content": content})
349 349
350 350
351 351 def on_msg(self, callback, remove=False):
352 352 """Register a callback for when a custom msg is recieved from the front-end
353 353
354 354 Parameters
355 355 ----------
356 356 callback: method handler
357 357 Can have a signature of:
358 358 - callback(content)
359 359 - callback(sender, content)
360 360 remove: bool
361 361 True if the callback should be unregistered."""
362 362 if remove and callback in self._msg_callbacks:
363 363 self._msg_callbacks.remove(callback)
364 364 elif not remove and not callback in self._msg_callbacks:
365 365 self._msg_callbacks.append(callback)
366 366
367 367
368 368 def on_displayed(self, callback, remove=False):
369 369 """Register a callback to be called when the widget has been displayed
370 370
371 371 Parameters
372 372 ----------
373 373 callback: method handler
374 374 Can have a signature of:
375 375 - callback()
376 376 - callback(sender)
377 377 - callback(sender, view_name)
378 378 - callback(sender, view_name, parent)
379 379 remove: bool
380 380 True if the callback should be unregistered."""
381 381 if remove and callback in self._display_callbacks:
382 382 self._display_callbacks.remove(callback)
383 383 elif not remove and not callback in self._display_callbacks:
384 384 self._display_callbacks.append(callback)
385 385
386 386
387 387 # Support methods
388 388 def _repr_widget_(self, view_name=None):
389 389 """Function that is called when `IPython.display.display` is called on
390 390 the widget.
391 391
392 392 Parameters
393 393 ----------
394 394 view_name: unicode (optional)
395 395 View to display in the frontend. Overrides default_view_name."""
396 396 if not view_name:
397 397 view_name = self.default_view_name
398 398
399 399 # Create a communication.
400 400 self._open_communication()
401 401
402 402 # Make sure model is syncronized
403 403 self.send_state()
404 404
405 405 # Show view.
406 406 if self.parent is None or self.parent._comm is None:
407 407 self._send({"method": "display", "view_name": view_name})
408 408 self._handle_displayed(view_name)
409 409 else:
410 410 self._send({"method": "display",
411 411 "view_name": view_name,
412 412 "parent": self.parent._comm.comm_id})
413 413 self._handle_displayed(view_name, self.parent)
414 414 self._displayed = True
415 415
416 416 # Now display children if any.
417 417 for child in self._children:
418 418 if child != self:
419 419 child._repr_widget_()
420 420 return None
421 421
422 422
423 423 def _open_communication(self):
424 424 """Opens a communication with the front-end."""
425 425 # Create a comm.
426 426 if not hasattr(self, '_comm') or self._comm is None:
427 427 self._comm = Comm(target_name=self.target_name)
428 428 self._comm.on_msg(self._handle_msg)
429 self._comm.on_close(self._handle_close)
430
431
432 def _handle_close(self):
433 """Called when the comm is closed by the front-end."""
434 self._close_communication()
429 self._comm.on_close(self._close_communication)
435 430
436 431
437 432 def _close_communication(self):
438 433 """Closes a communication with the front-end."""
439 434 if hasattr(self, '_comm') and self._comm is not None:
440 self._comm.close()
435 try:
436 self._comm.close()
437 finally:
438 self._comm = None
441 439
442 440
443 441 def _send(self, msg):
444 442 """Sends a message to the model in the front-end"""
445 443 if hasattr(self, '_comm') and self._comm is not None:
446 444 self._comm.send(msg)
447 445 return True
448 446 else:
449 447 return False
General Comments 0
You need to be logged in to leave comments. Login now