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