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