##// END OF EJS Templates
review pass on Python-side of widgets...
MinRK -
Show More
@@ -13,37 +13,28 b' in the IPython notebook front-end.'
13 # Imports
13 # Imports
14 #-----------------------------------------------------------------------------
14 #-----------------------------------------------------------------------------
15 from contextlib import contextmanager
15 from contextlib import contextmanager
16 import inspect
17 import types
18
16
19 from IPython.kernel.comm import Comm
17 from IPython.kernel.comm import Comm
20 from IPython.config import LoggingConfigurable
18 from IPython.config import LoggingConfigurable
21 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List
19 from IPython.utils.traitlets import Unicode, Dict, Instance, Bool, List, Tuple
22 from IPython.utils.py3compat import string_types
20 from IPython.utils.py3compat import string_types
23
21
24 #-----------------------------------------------------------------------------
22 #-----------------------------------------------------------------------------
25 # Classes
23 # Classes
26 #-----------------------------------------------------------------------------
24 #-----------------------------------------------------------------------------
27 class CallbackDispatcher(LoggingConfigurable):
25 class CallbackDispatcher(LoggingConfigurable):
28 acceptable_nargs = List([], help="""List of integers.
26 """A structure for registering and running callbacks"""
29 The number of arguments in the callbacks registered must match one of
27 callbacks = List()
30 the integers in this list. If this list is empty or None, it will be
28
31 ignored.""")
29 def __call__(self, *args, **kwargs):
32
30 """Call all of the registered callbacks."""
33 def __init__(self, *pargs, **kwargs):
34 """Constructor"""
35 LoggingConfigurable.__init__(self, *pargs, **kwargs)
36 self.callbacks = {}
37
38 def __call__(self, *pargs, **kwargs):
39 """Call all of the registered callbacks that have the same number of
40 positional arguments."""
41 nargs = len(pargs)
42 self._validate_nargs(nargs)
43 value = None
31 value = None
44 if nargs in self.callbacks:
32 for callback in self.callbacks:
45 for callback in self.callbacks[nargs]:
33 try:
46 local_value = callback(*pargs, **kwargs)
34 local_value = callback(*args, **kwargs)
35 except Exception as e:
36 self.log.warn("Exception in callback %s: %s", callback, e)
37 else:
47 value = local_value if local_value is not None else value
38 value = local_value if local_value is not None else value
48 return value
39 return value
49
40
@@ -53,69 +44,37 b' class CallbackDispatcher(LoggingConfigurable):'
53 Parameters
44 Parameters
54 ----------
45 ----------
55 callback: method handle
46 callback: method handle
56 Method to be registered or unregisted.
47 Method to be registered or unregistered.
57 remove=False: bool
48 remove=False: bool
58 Whether or not to unregister the callback."""
49 Whether to unregister the callback."""
59
50
60 # Validate the number of arguments that the callback accepts.
61 nargs = self._get_nargs(callback)
62 self._validate_nargs(nargs)
63
64 # Get/create the appropriate list of callbacks.
65 if nargs not in self.callbacks:
66 self.callbacks[nargs] = []
67 callback_list = self.callbacks[nargs]
68
69 # (Un)Register the callback.
51 # (Un)Register the callback.
70 if remove and callback in callback_list:
52 if remove and callback in self.callbacks:
71 callback_list.remove(callback)
53 self.callbacks.remove(callback)
72 elif not remove and callback not in callback_list:
54 elif not remove and callback not in self.callbacks:
73 callback_list.append(callback)
55 self.callbacks.append(callback)
74
75 def _validate_nargs(self, nargs):
76 if self.acceptable_nargs is not None and \
77 len(self.acceptable_nargs) > 0 and \
78 nargs not in self.acceptable_nargs:
79
80 raise TypeError('Invalid number of positional arguments. See acceptable_nargs list.')
81
82 def _get_nargs(self, callback):
83 """Gets the number of arguments in a callback"""
84 if callable(callback):
85 argspec = inspect.getargspec(callback)
86 if argspec[0] is None:
87 nargs = 0
88 elif argspec[3] is None:
89 nargs = len(argspec[0]) # Only count vargs!
90 else:
91 nargs = len(argspec[0]) - len(argspec[3]) # Subtract number of defaults.
92
93 # Bound methods have an additional 'self' argument
94 if isinstance(callback, types.MethodType):
95 nargs -= 1
96 return nargs
97 else:
98 raise TypeError('Callback must be callable.')
99
56
100
57
101 class Widget(LoggingConfigurable):
58 class Widget(LoggingConfigurable):
102 #-------------------------------------------------------------------------
59 #-------------------------------------------------------------------------
103 # Class attributes
60 # Class attributes
104 #-------------------------------------------------------------------------
61 #-------------------------------------------------------------------------
105 widget_construction_callback = None
62 _widget_construction_callback = None
106 widgets = {}
63 widgets = {}
107
64
65 @staticmethod
108 def on_widget_constructed(callback):
66 def on_widget_constructed(callback):
109 """Registers a callback to be called when a widget is constructed.
67 """Registers a callback to be called when a widget is constructed.
110
68
111 The callback must have the following signature:
69 The callback must have the following signature:
112 callback(widget)"""
70 callback(widget)"""
113 Widget.widget_construction_callback = callback
71 Widget._widget_construction_callback = callback
114
72
73 @staticmethod
115 def _call_widget_constructed(widget):
74 def _call_widget_constructed(widget):
116 """Class method, called when a widget is constructed."""
75 """Static method, called when a widget is constructed."""
117 if Widget.widget_construction_callback is not None and callable(Widget.widget_construction_callback):
76 if Widget._widget_construction_callback is not None and callable(Widget._widget_construction_callback):
118 Widget.widget_construction_callback(widget)
77 Widget._widget_construction_callback(widget)
119
78
120 #-------------------------------------------------------------------------
79 #-------------------------------------------------------------------------
121 # Traits
80 # Traits
@@ -125,20 +84,23 b' class Widget(LoggingConfigurable):'
125 _view_name = Unicode(help="""Default view registered in the front-end
84 _view_name = Unicode(help="""Default view registered in the front-end
126 to use to represent the widget.""", sync=True)
85 to use to represent the widget.""", sync=True)
127 _comm = Instance('IPython.kernel.comm.Comm')
86 _comm = Instance('IPython.kernel.comm.Comm')
128
87
88 closed = Bool(False)
89
90 keys = List()
91 def _keys_default(self):
92 return [name for name in self.traits(sync=True)]
93
94 _property_lock = Tuple((None, None))
95
96 _display_callbacks = Instance(CallbackDispatcher, ())
97 _msg_callbacks = Instance(CallbackDispatcher, ())
98
129 #-------------------------------------------------------------------------
99 #-------------------------------------------------------------------------
130 # (Con/de)structor
100 # (Con/de)structor
131 #-------------------------------------------------------------------------
101 #-------------------------------------------------------------------------
132 def __init__(self, **kwargs):
102 def __init__(self, **kwargs):
133 """Public constructor"""
103 """Public constructor"""
134 self.closed = False
135
136 self._property_lock = (None, None)
137 self._keys = None
138
139 self._display_callbacks = CallbackDispatcher(acceptable_nargs=[0])
140 self._msg_callbacks = CallbackDispatcher(acceptable_nargs=[1, 2])
141
142 super(Widget, self).__init__(**kwargs)
104 super(Widget, self).__init__(**kwargs)
143
105
144 self.on_trait_change(self._handle_property_changed, self.keys)
106 self.on_trait_change(self._handle_property_changed, self.keys)
@@ -150,16 +112,7 b' class Widget(LoggingConfigurable):'
150
112
151 #-------------------------------------------------------------------------
113 #-------------------------------------------------------------------------
152 # Properties
114 # Properties
153 #-------------------------------------------------------------------------
115 #-------------------------------------------------------------------------
154 @property
155 def keys(self):
156 """Gets a list of the traitlets that should be synced with the front-end."""
157 if self._keys is None:
158 self._keys = []
159 for trait_name in self.trait_names():
160 if self.trait_metadata(trait_name, 'sync'):
161 self._keys.append(trait_name)
162 return self._keys
163
116
164 @property
117 @property
165 def comm(self):
118 def comm(self):
@@ -186,15 +139,21 b' class Widget(LoggingConfigurable):'
186
139
187 #-------------------------------------------------------------------------
140 #-------------------------------------------------------------------------
188 # Methods
141 # Methods
189 #-------------------------------------------------------------------------
142 #-------------------------------------------------------------------------
143 def _close(self):
144 """Private close - cleanup objects, registry entries"""
145 del Widget.widgets[self.model_id]
146 self._comm = None
147 self.closed = True
148
190 def close(self):
149 def close(self):
191 """Close method.
150 """Close method.
192
151
193 Closes the widget which closes the underlying comm.
152 Closes the widget which closes the underlying comm.
194 When the comm is closed, all of the widget views are automatically
153 When the comm is closed, all of the widget views are automatically
195 removed from the front-end."""
154 removed from the front-end."""
196 if not self.closed:
155 if not self.closed:
197 self._comm.close()
156 self._comm.close()
198 self._close()
157 self._close()
199
158
200 def send_state(self, key=None):
159 def send_state(self, key=None):
@@ -232,14 +191,13 b' class Widget(LoggingConfigurable):'
232 self._send({"method": "custom", "content": content})
191 self._send({"method": "custom", "content": content})
233
192
234 def on_msg(self, callback, remove=False):
193 def on_msg(self, callback, remove=False):
235 """(Un)Register a custom msg recieve callback.
194 """(Un)Register a custom msg receive callback.
236
195
237 Parameters
196 Parameters
238 ----------
197 ----------
239 callback: method handler
198 callback: callable
240 Can have a signature of:
199 callback will be passed two arguments when a message arrives:
241 - callback(content) Signature 1
200 callback(widget, content)
242 - callback(sender, content) Signature 2
243 remove: bool
201 remove: bool
244 True if the callback should be unregistered."""
202 True if the callback should be unregistered."""
245 self._msg_callbacks.register_callback(callback, remove=remove)
203 self._msg_callbacks.register_callback(callback, remove=remove)
@@ -250,9 +208,9 b' class Widget(LoggingConfigurable):'
250 Parameters
208 Parameters
251 ----------
209 ----------
252 callback: method handler
210 callback: method handler
253 Can have a signature of:
211 Must have a signature of:
254 - callback(sender, **kwargs)
212 callback(widget, **kwargs)
255 kwargs from display call passed through without modification.
213 kwargs from display are passed through without modification.
256 remove: bool
214 remove: bool
257 True if the callback should be unregistered."""
215 True if the callback should be unregistered."""
258 self._display_callbacks.register_callback(callback, remove=remove)
216 self._display_callbacks.register_callback(callback, remove=remove)
@@ -278,12 +236,6 b' class Widget(LoggingConfigurable):'
278 return key != self._property_lock[0] or \
236 return key != self._property_lock[0] or \
279 value != self._property_lock[1]
237 value != self._property_lock[1]
280
238
281 def _close(self):
282 """Unsafe close"""
283 del Widget.widgets[self.model_id]
284 self._comm = None
285 self.closed = True
286
287 # Event handlers
239 # Event handlers
288 def _handle_msg(self, msg):
240 def _handle_msg(self, msg):
289 """Called when a msg is received from the front-end"""
241 """Called when a msg is received from the front-end"""
@@ -312,8 +264,7 b' class Widget(LoggingConfigurable):'
312
264
313 def _handle_custom_msg(self, content):
265 def _handle_custom_msg(self, content):
314 """Called when a custom msg is received."""
266 """Called when a custom msg is received."""
315 self._msg_callbacks(content) # Signature 1
267 self._msg_callbacks(self, content)
316 self._msg_callbacks(self, content) # Signature 2
317
268
318 def _handle_property_changed(self, name, old, new):
269 def _handle_property_changed(self, name, old, new):
319 """Called when a property has been changed."""
270 """Called when a property has been changed."""
@@ -324,7 +275,7 b' class Widget(LoggingConfigurable):'
324
275
325 def _handle_displayed(self, **kwargs):
276 def _handle_displayed(self, **kwargs):
326 """Called when a view has been displayed for this widget instance"""
277 """Called when a view has been displayed for this widget instance"""
327 self._display_callbacks(**kwargs)
278 self._display_callbacks(self, **kwargs)
328
279
329 def _pack_widgets(self, x):
280 def _pack_widgets(self, x):
330 """Recursively converts all widget instances to model id strings.
281 """Recursively converts all widget instances to model id strings.
@@ -367,7 +318,7 b' class Widget(LoggingConfigurable):'
367
318
368
319
369 class DOMWidget(Widget):
320 class DOMWidget(Widget):
370 visible = Bool(True, help="Whether or not the widget is visible.", sync=True)
321 visible = Bool(True, help="Whether the widget is visible.", sync=True)
371 _css = Dict(sync=True) # Internal CSS property dict
322 _css = Dict(sync=True) # Internal CSS property dict
372
323
373 def get_css(self, key, selector=""):
324 def get_css(self, key, selector=""):
@@ -388,7 +339,7 b' class DOMWidget(Widget):'
388 else:
339 else:
389 return None
340 return None
390
341
391 def set_css(self, *args, **kwargs):
342 def set_css(self, dict_or_key, value=None, selector=''):
392 """Set one or more CSS properties of the widget.
343 """Set one or more CSS properties of the widget.
393
344
394 This function has two signatures:
345 This function has two signatures:
@@ -401,9 +352,9 b' class DOMWidget(Widget):'
401 CSS key/value pairs to apply
352 CSS key/value pairs to apply
402 key: unicode
353 key: unicode
403 CSS key
354 CSS key
404 value
355 value:
405 CSS value
356 CSS value
406 selector: unicode (optional)
357 selector: unicode (optional, kwarg only)
407 JQuery selector to use to apply the CSS key/value. If no selector
358 JQuery selector to use to apply the CSS key/value. If no selector
408 is provided, an empty selector is used. An empty selector makes the
359 is provided, an empty selector is used. An empty selector makes the
409 front-end try to apply the css to a default element. The default
360 front-end try to apply the css to a default element. The default
@@ -411,37 +362,19 b' class DOMWidget(Widget):'
411 of the view that should be styled with common CSS (see
362 of the view that should be styled with common CSS (see
412 `$el_to_style` in the Javascript code).
363 `$el_to_style` in the Javascript code).
413 """
364 """
414 selector = kwargs.get('selector', '')
415 if not selector in self._css:
365 if not selector in self._css:
416 self._css[selector] = {}
366 self._css[selector] = {}
417
367 my_css = self._css[selector]
418 # Signature 1: set_css(css_dict, selector='')
368
419 if len(args) == 1:
369 if value is None:
420 if isinstance(args[0], dict):
370 css_dict = dict_or_key
421 for (key, value) in args[0].items():
422 if not (key in self._css[selector] and value == self._css[selector][key]):
423 self._css[selector][key] = value
424 self.send_state('_css')
425 else:
426 raise Exception('css_dict must be a dict.')
427
428 # Signature 2: set_css(key, value, selector='')
429 elif len(args) == 2 or len(args) == 3:
430
431 # Selector can be a positional arg if it's the 3rd value
432 if len(args) == 3:
433 selector = args[2]
434 if selector not in self._css:
435 self._css[selector] = {}
436
437 # Only update the property if it has changed.
438 key = args[0]
439 value = args[1]
440 if not (key in self._css[selector] and value == self._css[selector][key]):
441 self._css[selector][key] = value
442 self.send_state('_css') # Send new state to client.
443 else:
371 else:
444 raise Exception('set_css only accepts 1-3 arguments')
372 css_dict = {dict_or_key: value}
373
374 for (key, value) in css_dict.items():
375 if not (key in my_css and value == my_css[key]):
376 my_css[key] = value
377 self.send_state('_css')
445
378
446 def add_class(self, class_names, selector=""):
379 def add_class(self, class_names, selector=""):
447 """Add class[es] to a DOM element.
380 """Add class[es] to a DOM element.
@@ -30,17 +30,14 b' class ButtonWidget(DOMWidget):'
30 def __init__(self, **kwargs):
30 def __init__(self, **kwargs):
31 """Constructor"""
31 """Constructor"""
32 super(ButtonWidget, self).__init__(**kwargs)
32 super(ButtonWidget, self).__init__(**kwargs)
33 self._click_handlers = CallbackDispatcher(acceptable_nargs=[0, 1])
33 self._click_handlers = CallbackDispatcher()
34 self.on_msg(self._handle_button_msg)
34 self.on_msg(self._handle_button_msg)
35
35
36 def on_click(self, callback, remove=False):
36 def on_click(self, callback, remove=False):
37 """Register a callback to execute when the button is clicked.
37 """Register a callback to execute when the button is clicked.
38
38
39 The callback can either accept no parameters or one sender parameter:
39 The callback will be called with one argument,
40 - callback()
40 the clicked button widget instance.
41 - callback(sender)
42 If the callback has a sender parameter, the ButtonWidget instance that
43 called the callback will be passed into the method as the sender.
44
41
45 Parameters
42 Parameters
46 ----------
43 ----------
@@ -48,13 +45,12 b' class ButtonWidget(DOMWidget):'
48 Set to true to remove the callback from the list of callbacks."""
45 Set to true to remove the callback from the list of callbacks."""
49 self._click_handlers.register_callback(callback, remove=remove)
46 self._click_handlers.register_callback(callback, remove=remove)
50
47
51 def _handle_button_msg(self, content):
48 def _handle_button_msg(self, _, content):
52 """Handle a msg from the front-end.
49 """Handle a msg from the front-end.
53
50
54 Parameters
51 Parameters
55 ----------
52 ----------
56 content: dict
53 content: dict
57 Content of the msg."""
54 Content of the msg."""
58 if 'event' in content and content['event'] == 'click':
55 if content.get('event', '') == 'click':
59 self._click_handlers()
60 self._click_handlers(self)
56 self._click_handlers(self)
@@ -26,7 +26,7 b' class _StringWidget(DOMWidget):'
26
26
27
27
28 class HTMLWidget(_StringWidget):
28 class HTMLWidget(_StringWidget):
29 _view_name = Unicode('HTMLView', sync=True)
29 _view_name = Unicode('HTMLView', sync=True)
30
30
31
31
32 class LatexWidget(_StringWidget):
32 class LatexWidget(_StringWidget):
@@ -45,18 +45,17 b' class TextBoxWidget(_StringWidget):'
45
45
46 def __init__(self, **kwargs):
46 def __init__(self, **kwargs):
47 super(TextBoxWidget, self).__init__(**kwargs)
47 super(TextBoxWidget, self).__init__(**kwargs)
48 self._submission_callbacks = CallbackDispatcher(acceptable_nargs=[0, 1])
48 self._submission_callbacks = CallbackDispatcher()
49 self.on_msg(self._handle_string_msg)
49 self.on_msg(self._handle_string_msg)
50
50
51 def _handle_string_msg(self, content):
51 def _handle_string_msg(self, _, content):
52 """Handle a msg from the front-end.
52 """Handle a msg from the front-end.
53
53
54 Parameters
54 Parameters
55 ----------
55 ----------
56 content: dict
56 content: dict
57 Content of the msg."""
57 Content of the msg."""
58 if 'event' in content and content['event'] == 'submit':
58 if content.get('event', '') == 'submit':
59 self._submission_callbacks()
60 self._submission_callbacks(self)
59 self._submission_callbacks(self)
61
60
62 def on_submit(self, callback, remove=False):
61 def on_submit(self, callback, remove=False):
@@ -65,11 +64,9 b' class TextBoxWidget(_StringWidget):'
65 Triggered when the user clicks enter.
64 Triggered when the user clicks enter.
66
65
67 Parameters
66 Parameters
68 callback: Method handle
67 ----------
69 Function to be called when the text has been submitted. Function
68 callback: callable
70 can have two possible signatures:
69 Will be called with exactly one argument: the Widget instance
71 callback()
72 callback(sender)
73 remove: bool (optional)
70 remove: bool (optional)
74 Whether or not to unregister the callback"""
71 Whether to unregister the callback"""
75 self._submission_callbacks.register_callback(callback, remove=remove)
72 self._submission_callbacks.register_callback(callback, remove=remove)
General Comments 0
You need to be logged in to leave comments. Login now