From 11fb9c06527bcb2e2880de28d9b3cb538c71500e Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 26 Jul 2017 22:41:03 -0400 Subject: [PATCH 01/11] Add a continuous_update option for int/float text inputs. --- ipywidgets/widgets/widget_float.py | 2 + ipywidgets/widgets/widget_int.py | 2 + ipywidgets/widgets/widget_string.py | 2 + packages/controls/src/widget_float.ts | 2 + packages/controls/src/widget_int.ts | 54 +++++++++++++------------- packages/controls/src/widget_string.ts | 6 ++- 6 files changed, 38 insertions(+), 30 deletions(-) diff --git a/ipywidgets/widgets/widget_float.py b/ipywidgets/widgets/widget_float.py index 6d4a7ba27a..c561cac735 100644 --- a/ipywidgets/widgets/widget_float.py +++ b/ipywidgets/widgets/widget_float.py @@ -74,6 +74,7 @@ class FloatText(_Float): _view_name = Unicode('FloatTextView').tag(sync=True) _model_name = Unicode('FloatTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) + continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) @register @@ -96,6 +97,7 @@ class BoundedFloatText(_BoundedFloat): _view_name = Unicode('FloatTextView').tag(sync=True) _model_name = Unicode('BoundedFloatTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) + continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) @register diff --git a/ipywidgets/widgets/widget_int.py b/ipywidgets/widgets/widget_int.py index 59b9a12c3a..fdcdee7e7d 100644 --- a/ipywidgets/widgets/widget_int.py +++ b/ipywidgets/widgets/widget_int.py @@ -127,6 +127,7 @@ class IntText(_Int): _view_name = Unicode('IntTextView').tag(sync=True) _model_name = Unicode('IntTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) + continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) @register @@ -137,6 +138,7 @@ class BoundedIntText(_BoundedInt): _view_name = Unicode('IntTextView').tag(sync=True) _model_name = Unicode('BoundedIntTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) + continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) @register diff --git a/ipywidgets/widgets/widget_string.py b/ipywidgets/widgets/widget_string.py index 534545dfb0..b7ece05f56 100644 --- a/ipywidgets/widgets/widget_string.py +++ b/ipywidgets/widgets/widget_string.py @@ -64,6 +64,7 @@ class Textarea(_String): _model_name = Unicode('TextareaModel').tag(sync=True) rows = Int(None, allow_none=True, help="The number of rows to display.").tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) + continuous_update = Bool(True, help="Update the value of the widget as the user types.").tag(sync=True) def scroll_to_bottom(self): self.send({"method": "scroll_to_bottom"}) @@ -75,6 +76,7 @@ class Text(_String): _view_name = Unicode('TextView').tag(sync=True) _model_name = Unicode('TextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) + continuous_update = Bool(True, help="Update the value of the widget as the user types.").tag(sync=True) def __init__(self, *args, **kwargs): super(Text, self).__init__(*args, **kwargs) diff --git a/packages/controls/src/widget_float.ts b/packages/controls/src/widget_float.ts index 0d2763f937..51ca403cf8 100644 --- a/packages/controls/src/widget_float.ts +++ b/packages/controls/src/widget_float.ts @@ -108,6 +108,7 @@ class FloatTextModel extends FloatModel { _model_name: "FloatTextModel", _view_name: "FloatTextView", disabled: false, + continuous_update: false, }); } } @@ -119,6 +120,7 @@ class BoundedFloatTextModel extends BoundedFloatModel { _model_name: "BoundedFloatTextModel", _view_name: "FloatTextView", disabled: false, + continuous_update: false, }); } } diff --git a/packages/controls/src/widget_int.ts b/packages/controls/src/widget_int.ts index 3ac9b6c661..a8ea8d35f3 100644 --- a/packages/controls/src/widget_int.ts +++ b/packages/controls/src/widget_int.ts @@ -474,7 +474,8 @@ class IntTextModel extends IntModel { return _.extend(super.defaults(), { _model_name: 'IntTextModel', _view_name: 'IntTextView', - disabled: false + disabled: false, + continuous_update: false, }); } } @@ -485,7 +486,8 @@ class BoundedIntTextModel extends BoundedIntModel { return _.extend(super.defaults(), { _model_name: 'BoundedIntTextModel', _view_name: 'IntTextView', - disabled: false + disabled: false, + continuous_update: false, }); } } @@ -499,7 +501,8 @@ class IntTextView extends DescriptionView { this.el.classList.add('widget-text'); this.textbox = document.createElement('input'); - this.textbox.setAttribute('type', 'text'); + this.textbox.type = 'number'; + this.textbox.required = true; this.textbox.id = this.label.htmlFor = uuid(); this.el.appendChild(this.textbox); @@ -519,21 +522,18 @@ class IntTextView extends DescriptionView { if (this._parse_value(this.textbox.value) !== value) { this.textbox.value = value.toString(); } + this.textbox.min = this.model.get('min'); + this.textbox.max = this.model.get('max'); this.textbox.disabled = this.model.get('disabled'); } return super.update(); } - events(): {[e: string]: string} { + events() { return { - // Dictionary of events and their handlers. 'keydown input' : 'handleKeyDown', 'keypress input' : 'handleKeypress', - 'keyup input' : 'handleChanging', - 'paste input' : 'handleChanging', - 'cut input' : 'handleChanging', - - // Fires only when control is validated or looses focus. + 'input input' : 'handleChanging', 'change input' : 'handleChanged' }; } @@ -557,9 +557,8 @@ class IntTextView extends DescriptionView { } /** - * Handles and validates user input. - * - * Try to parse value as an int. + * Call the submit handler if continuous update is true and we are not + * obviously incomplete. */ handleChanging(e) { let trimmed = e.target.value.trim(); @@ -567,41 +566,40 @@ class IntTextView extends DescriptionView { // incomplete number return; } + + if (this.model.get('continuous_update')) { + this.handleChanged(e); + } + } + + /** + * Applies validated input. + */ + handleChanged(e) { let numericalValue = this._parse_value(e.target.value); // If parse failed, reset value to value stored in model. if (isNaN(numericalValue)) { e.target.value = this.model.get('value'); } else { - // Handle both the IntTextModel and the BoundedIntTextModel by + // Handle both the unbounded and bounded case by // checking to see if the max/min properties are defined - if (this.model.get('max') !== void 0) { + if (this.model.get('max') !== undefined) { numericalValue = Math.min(this.model.get('max'), numericalValue); } - if (this.model.get('min') !== void 0) { + if (this.model.get('min') !== undefined) { numericalValue = Math.max(this.model.get('min'), numericalValue); } + e.target.value = numericalValue; // Apply the value if it has changed. if (numericalValue !== this.model.get('value')) { - - // Calling model.set will trigger all of the other views of the - // model to update. this.model.set('value', numericalValue, {updated_view: this}); this.touch(); } } } - /** - * Applies validated input. - */ - handleChanged(e) { - if (e.target.value.trim() === '' || e.target.value !== this.model.get('value')) { - e.target.value = this.model.get('value'); - } - } - _parse_value = parseInt textbox: HTMLInputElement; } diff --git a/packages/controls/src/widget_string.ts b/packages/controls/src/widget_string.ts index 7f4bdba6ad..5877b97d88 100644 --- a/packages/controls/src/widget_string.ts +++ b/packages/controls/src/widget_string.ts @@ -146,7 +146,8 @@ class TextareaModel extends StringModel { return _.extend(super.defaults(), { _view_name: 'TextareaView', _model_name: 'TextareaModel', - rows: null + rows: null, + continuous_update: true, }); } } @@ -264,7 +265,8 @@ class TextModel extends StringModel { defaults() { return _.extend(super.defaults(), { _view_name: 'TextView', - _model_name: 'TextModel' + _model_name: 'TextModel', + continuous_update: true, }); } } From cb61165496816168678d90ee90110432961203bc Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 26 Jul 2017 22:58:20 -0400 Subject: [PATCH 02/11] Add a step for the bounded int/float textboxes to make them compatible with sliders. This also is needed to make the spinners in the new number inputs work appropriately, especially with floating point numbers. --- ipywidgets/widgets/widget_float.py | 5 +++-- ipywidgets/widgets/widget_int.py | 1 + packages/controls/src/widget_float.ts | 2 ++ packages/controls/src/widget_int.ts | 3 +++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ipywidgets/widgets/widget_float.py b/ipywidgets/widgets/widget_float.py index c561cac735..929d0683dc 100644 --- a/ipywidgets/widgets/widget_float.py +++ b/ipywidgets/widgets/widget_float.py @@ -91,6 +91,8 @@ class BoundedFloatText(_BoundedFloat): minimal value of the range of possible values displayed max : float maximal value of the range of possible values displayed + step : float + step of the increment description : str description displayed next to the textbox """ @@ -98,6 +100,7 @@ class BoundedFloatText(_BoundedFloat): _model_name = Unicode('BoundedFloatTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) + step = CFloat(0.1, help="Minimum step to increment the value").tag(sync=True) @register @@ -151,8 +154,6 @@ class FloatProgress(_BoundedFloat): minimal position of the slider max : float maximal position of the slider - step : float - step of the progress bar description : str name of the progress bar orientation : {'horizontal', 'vertical'} diff --git a/ipywidgets/widgets/widget_int.py b/ipywidgets/widgets/widget_int.py index fdcdee7e7d..ffd7824ffc 100644 --- a/ipywidgets/widgets/widget_int.py +++ b/ipywidgets/widgets/widget_int.py @@ -139,6 +139,7 @@ class BoundedIntText(_BoundedInt): _model_name = Unicode('BoundedIntTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) + step = CInt(1, help="Minimum step to increment the value").tag(sync=True) @register diff --git a/packages/controls/src/widget_float.ts b/packages/controls/src/widget_float.ts index 51ca403cf8..c115222693 100644 --- a/packages/controls/src/widget_float.ts +++ b/packages/controls/src/widget_float.ts @@ -121,6 +121,7 @@ class BoundedFloatTextModel extends BoundedFloatModel { _view_name: "FloatTextView", disabled: false, continuous_update: false, + step: 0.1 }); } } @@ -128,6 +129,7 @@ class BoundedFloatTextModel extends BoundedFloatModel { export class FloatTextView extends IntTextView { _parse_value = parseFloat; + _default_step = 0.1; } export diff --git a/packages/controls/src/widget_int.ts b/packages/controls/src/widget_int.ts index a8ea8d35f3..7509e1c37f 100644 --- a/packages/controls/src/widget_int.ts +++ b/packages/controls/src/widget_int.ts @@ -488,6 +488,7 @@ class BoundedIntTextModel extends BoundedIntModel { _view_name: 'IntTextView', disabled: false, continuous_update: false, + step: 1, }); } } @@ -524,6 +525,7 @@ class IntTextView extends DescriptionView { } this.textbox.min = this.model.get('min'); this.textbox.max = this.model.get('max'); + this.textbox.step = this.model.get('step') || this._default_step; this.textbox.disabled = this.model.get('disabled'); } return super.update(); @@ -601,6 +603,7 @@ class IntTextView extends DescriptionView { } _parse_value = parseInt + _default_step = 1; textbox: HTMLInputElement; } From 6b9e7883f770d5fd912e9a17c7c02e08d1429f41 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 27 Jul 2017 08:40:53 -0400 Subject: [PATCH 03/11] Fix the number input styling. --- packages/controls/css/widgets-base.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/controls/css/widgets-base.css b/packages/controls/css/widgets-base.css index afb55a4b53..a0299e37a3 100644 --- a/packages/controls/css/widgets-base.css +++ b/packages/controls/css/widgets-base.css @@ -408,16 +408,16 @@ width: var(--jp-widgets-inline-width); } -.widget-text input[type="text"] { +.widget-text input[type="text"], .widget-text input[type="number"]{ height: var(--jp-widgets-inline-height); line-height: var(--jp-widgets-inline-height); } -.widget-text input[type="text"]:disabled, .widget-textarea textarea:disabled { +.widget-text input[type="text"]:disabled, .widget-text input[type="number"]:disabled, .widget-textarea textarea:disabled { opacity: var(--jp-widgets-disabled-opacity); } -.widget-text input[type="text"], .widget-textarea textarea { +.widget-text input[type="text"], .widget-text input[type="number"], .widget-textarea textarea { box-sizing: border-box; border: var(--jp-widgets-input-border-width) solid var(--jp-widgets-input-border-color); background-color: var(--jp-widgets-input-background-color); From 8f0e944bd6c3b832544f9983b94570b21a27f586 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 27 Jul 2017 08:52:53 -0400 Subject: [PATCH 04/11] Add continuous_update option to Text and Textarea. Deprecate the Text on_submit handler. --- ipywidgets/widgets/widget_string.py | 6 +- packages/controls/src/widget_string.ts | 77 +++++++++++--------------- 2 files changed, 37 insertions(+), 46 deletions(-) diff --git a/ipywidgets/widgets/widget_string.py b/ipywidgets/widgets/widget_string.py index b7ece05f56..805d194cfc 100644 --- a/ipywidgets/widgets/widget_string.py +++ b/ipywidgets/widgets/widget_string.py @@ -64,7 +64,7 @@ class Textarea(_String): _model_name = Unicode('TextareaModel').tag(sync=True) rows = Int(None, allow_none=True, help="The number of rows to display.").tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) - continuous_update = Bool(True, help="Update the value of the widget as the user types.").tag(sync=True) + continuous_update = Bool(True, help="Update the value as the user types.").tag(sync=True) def scroll_to_bottom(self): self.send({"method": "scroll_to_bottom"}) @@ -76,7 +76,7 @@ class Text(_String): _view_name = Unicode('TextView').tag(sync=True) _model_name = Unicode('TextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) - continuous_update = Bool(True, help="Update the value of the widget as the user types.").tag(sync=True) + continuous_update = Bool(True, help="Update the value as the user types.").tag(sync=True) def __init__(self, *args, **kwargs): super(Text, self).__init__(*args, **kwargs) @@ -106,6 +106,8 @@ def on_submit(self, callback, remove=False): remove: bool (optional) Whether to unregister the callback """ + import warnings + warnings.warn("on_submit is deprecated. Instead, set the .continuous_update attribute to False and observe the value changing with: mywidget.observe(callback, 'value').", DeprecationWarning) self._submission_callbacks.register_callback(callback, remove=remove) diff --git a/packages/controls/src/widget_string.ts b/packages/controls/src/widget_string.ts index 5877b97d88..aecd8e4644 100644 --- a/packages/controls/src/widget_string.ts +++ b/packages/controls/src/widget_string.ts @@ -217,15 +217,13 @@ class TextareaView extends DescriptionView { return super.update(); } - events(): {[e: string]: string} { + events() { return { - // Dictionary of events and their handlers. - 'keydown textarea' : 'handleKeyDown', - 'keypress textarea' : 'handleKeypress', - 'keyup textarea' : 'handleChanging', - 'paste textarea' : 'handleChanging', - 'cut textarea' : 'handleChanging' - } + 'keydown input' : 'handleKeyDown', + 'keypress input' : 'handleKeypress', + 'input textarea' : 'handleChanging', + 'change textarea' : 'handleChanged' + }; } /** @@ -247,16 +245,23 @@ class TextareaView extends DescriptionView { } /** - * Handles and validates user input. - * - * Calling model.set will trigger all of the other views of the - * model to update. + * Triggered on input change */ handleChanging(e) { + if (this.model.get('continuous_update')) { + this.handleChanged(e); + } + } + + /** + * Sync the value with the kernel. + * + * @param e Event + */ + handleChanged(e) { this.model.set('value', e.target.value, {updated_view: this}); this.touch(); } - textbox: HTMLTextAreaElement; } @@ -319,17 +324,13 @@ class TextView extends DescriptionView { return super.update(); } - events(): {[e: string]: string} { + events() { return { - // Dictionary of events and their handlers. 'keydown input' : 'handleKeyDown', 'keypress input' : 'handleKeypress', - 'keyup input' : 'handleChanging', - 'paste input' : 'handleChanging', - 'cut input' : 'handleChanging', - 'blur input' : 'handleBlur', - 'focusout input' : 'handleFocusOut' - } + 'input input' : 'handleChanging', + 'change input' : 'handleChanged' + }; } /** @@ -346,9 +347,9 @@ class TextView extends DescriptionView { */ handleKeypress(e) { e.stopPropagation(); + // The submit message is deprecated in widgets 7 if (e.keyCode == 13) { // Return key this.send({event: 'submit'}); - e.preventDefault(); } } @@ -359,35 +360,23 @@ class TextView extends DescriptionView { * model to update. */ handleChanging(e) { - e.stopPropagation(); - this.model.set('value', e.target.value, {updated_view: this}); - this.touch(); - } - - /** - * Prevent a blur from firing if the blur was not user intended. - * This is a workaround for the return-key focus loss bug. - * TODO: Is the original bug actually a fault of the keyboard - * manager? - */ - handleBlur(e) { - if (e.relatedTarget === null) { - e.stopPropagation(); - e.preventDefault(); + if (this.model.get('continuous_update')) { + this.handleChanged(e); } } /** - * Prevent a blur from firing if the blur was not user intended. - * This is a workaround for the return-key focus loss bug. + * Handles user input. + * + * Calling model.set will trigger all of the other views of the + * model to update. */ - handleFocusOut(e) { - if (e.relatedTarget === null) { - e.stopPropagation(); - e.preventDefault(); - } + handleChanged(e) { + this.model.set('value', e.target.value, {updated_view: this}); + this.touch(); } + protected readonly inputType: string = 'text'; textbox: HTMLInputElement; } From a8201972da69c53a62dfc53440739b3fa4ee3857 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 27 Jul 2017 09:25:03 -0400 Subject: [PATCH 05/11] =?UTF-8?q?Delete=20the=20textarea=20scroll=5Fto=5Fb?= =?UTF-8?q?ottom=20function,=20which=20hasn=E2=80=99t=20worked=20for=20a?= =?UTF-8?q?=20while.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ipywidgets/widgets/widget_string.py | 4 ---- packages/controls/src/widget_string.ts | 19 ------------------- 2 files changed, 23 deletions(-) diff --git a/ipywidgets/widgets/widget_string.py b/ipywidgets/widgets/widget_string.py index 805d194cfc..7ab6962ad1 100644 --- a/ipywidgets/widgets/widget_string.py +++ b/ipywidgets/widgets/widget_string.py @@ -66,10 +66,6 @@ class Textarea(_String): disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) continuous_update = Bool(True, help="Update the value as the user types.").tag(sync=True) - def scroll_to_bottom(self): - self.send({"method": "scroll_to_bottom"}) - - @register class Text(_String): """Single line textbox widget.""" diff --git a/packages/controls/src/widget_string.ts b/packages/controls/src/widget_string.ts index aecd8e4644..b729a92ff8 100644 --- a/packages/controls/src/widget_string.ts +++ b/packages/controls/src/widget_string.ts @@ -169,9 +169,6 @@ class TextareaView extends DescriptionView { this.el.appendChild(this.textbox); this.update(); // Set defaults. - this.listenTo(this.model, 'msg:custom', (content) => { - this._handle_textarea_msg(content) - }); this.listenTo(this.model, 'change:placeholder', function(model, value, options) { @@ -181,27 +178,11 @@ class TextareaView extends DescriptionView { this.update_placeholder(); } - /** - * Handle when a custom msg is recieved from the back-end. - */ - _handle_textarea_msg (content) { - if (content.method == 'scroll_to_bottom') { - this.scroll_to_bottom(); - } - } - update_placeholder(value?) { value = value || this.model.get('placeholder'); this.textbox.setAttribute('placeholder', value.toString()); } - /** - * Scroll the text-area view to the bottom. - */ - scroll_to_bottom () { - //this.$textbox.scrollTop(this.$textbox[0].scrollHeight); // DW TODO - } - /** * Update the contents of this view * From a0658331b0454617f2cfb30e0f7a77ae62dd7f32 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 27 Jul 2017 09:47:20 -0400 Subject: [PATCH 06/11] Update changelog --- docs/source/changelog.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/source/changelog.md b/docs/source/changelog.md index 3953010ec0..93b30a61ea 100644 --- a/docs/source/changelog.md +++ b/docs/source/changelog.md @@ -21,7 +21,7 @@ Major user-visible changes in ipywidgets 7.0 include: ``` - Removed the version validation check since it was causing too many false warnings about the widget javascript not being installed or the wrong version number. It is now up to the user to ensure that the ipywidgets and widgetsnbextension packages are compatible. ([#1219](https://github.com/jupyter-widgets/ipywidgets/pull/1219)) - Play range is now inclusive (max value is max, instead of max-1), to be consistent with Sliders -- A refactoring of the text, slider, slider range, and progress widgets in resulted in the `BoundedIntText` and `BoundedFloatText` losing their `step` attribute (which was previously ignored), and a number of these widgets changing their `_model_name` and/or `_view_name` attributes ([#1290](https://github.com/jupyter-widgets/ipywidgets/pull/1290)) +- A refactoring of the text, slider, slider range, and progress widgets in resulted in the progress widgets losing their `step` attribute (which was previously ignored), and a number of these widgets changing their `_model_name` and/or `_view_name` attributes ([#1290](https://github.com/jupyter-widgets/ipywidgets/pull/1290)) - The `Checkbox` description is now on the right of the checkbox and is clickable. The `Checkbox` widget has a new `indent` attribute (defaults to `True`) to line up nicely with controls that have descriptions. To make the checkbox align to the left, set `indent` to `False`. ([#1346](https://github.com/jupyter-widgets/ipywidgets/pull/1346)) - The `Play` widget now has an optional repeat toggle button (visible by default). ([#1190](https://github.com/jupyter-widgets/ipywidgets/pull/1190)) - A new Password widget, which behaves exactly like the Text widget, but hides the typed text: `Password()`. ([#1310](https://github.com/jupyter-widgets/ipywidgets/pull/1310)) @@ -32,6 +32,10 @@ Major user-visible changes in ipywidgets 7.0 include: - ToggleButtons have a new `.style.button_width` attribute to set the CSS width of the buttons. Set this to `'initial'` to have buttons that individually size to the content width. ([#1257](https://github.com/jupyter-widgets/ipywidgets/pull/1257)) - Selection container widgets (`Accordion`, `Tabs`) can have their `.selected_index` set to `None` to deselect all items. ([#1495](https://github.com/jupyter-widgets/ipywidgets/pull/1495)) - The `IntRangeSlider` widget now has a `.readout_format` trait to control the formatting of the readout. ([#1446](https://github.com/jupyter-widgets/ipywidgets/pull/1446)) +- The `Text`, `Textarea`, `IntText`, `BoundedIntText`, `FloatText`, and `BoundedFloatText` widgets all gained a `continuous_update` attribute (defaults to `True` for `Text` and `TextArea`, and `False` for the others). ([#1545](https://github.com/jupyter-widgets/ipywidgets/pull/1545)) +- The `IntText`, `BoundedIntText`, `FloatText`, and `BoundedFloatText` widgets are now rendered as HTML number inputs, and have a `step` attribute that controls the resolution. ([#1545](https://github.com/jupyter-widgets/ipywidgets/pull/1545)) +- The `Text.on_submit` callback is deprecated; instead, set `continuous_update` to `False` and observe the `value` attribute: `mywidget.observe(callback, 'value')`. The `Textarea.scroll_to_bottom` method was removed. ([#1545](https://github.com/jupyter-widgets/ipywidgets/pull/1545)) + Major changes developers should be aware of include: From 387a4b2394cb3d4b8154c16e8299baa1ad0bc47e Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 27 Jul 2017 11:05:38 -0400 Subject: [PATCH 07/11] Fix an issue with typing in numbers with continuous update. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Basically, if you typed ‘3.’, it would parse the number as 3 and reset the value, so you could never type the decimal. Also, fixed some issues with min, max, and step being set, especially if they were undefined. --- packages/controls/src/widget_float.ts | 2 +- packages/controls/src/widget_int.ts | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/controls/src/widget_float.ts b/packages/controls/src/widget_float.ts index c115222693..36340436b7 100644 --- a/packages/controls/src/widget_float.ts +++ b/packages/controls/src/widget_float.ts @@ -129,7 +129,7 @@ class BoundedFloatTextModel extends BoundedFloatModel { export class FloatTextView extends IntTextView { _parse_value = parseFloat; - _default_step = 0.1; + _default_step = 'any'; } export diff --git a/packages/controls/src/widget_int.ts b/packages/controls/src/widget_int.ts index 7509e1c37f..cc58dc0b43 100644 --- a/packages/controls/src/widget_int.ts +++ b/packages/controls/src/widget_int.ts @@ -523,9 +523,17 @@ class IntTextView extends DescriptionView { if (this._parse_value(this.textbox.value) !== value) { this.textbox.value = value.toString(); } - this.textbox.min = this.model.get('min'); - this.textbox.max = this.model.get('max'); - this.textbox.step = this.model.get('step') || this._default_step; + if (this.model.get('min') !== undefined) { + this.textbox.min = this.model.get('min'); + } + if (this.model.get('max') !== undefined) { + this.textbox.max = this.model.get('max'); + } + if (this.model.get('step') !== undefined) { + this.textbox.step = this.model.get('step'); + } else { + this.textbox.step = this._default_step; + } this.textbox.disabled = this.model.get('disabled'); } return super.update(); @@ -586,14 +594,18 @@ class IntTextView extends DescriptionView { } else { // Handle both the unbounded and bounded case by // checking to see if the max/min properties are defined + let boundedValue = numericalValue; if (this.model.get('max') !== undefined) { - numericalValue = Math.min(this.model.get('max'), numericalValue); + boundedValue = Math.min(this.model.get('max'), boundedValue); } if (this.model.get('min') !== undefined) { - numericalValue = Math.max(this.model.get('min'), numericalValue); + boundedValue = Math.max(this.model.get('min'), boundedValue); + } + if (boundedValue !== numericalValue) { + e.target.value = boundedValue; + numericalValue = boundedValue; } - e.target.value = numericalValue; // Apply the value if it has changed. if (numericalValue !== this.model.get('value')) { this.model.set('value', numericalValue, {updated_view: this}); @@ -603,7 +615,7 @@ class IntTextView extends DescriptionView { } _parse_value = parseInt - _default_step = 1; + _default_step = '1'; textbox: HTMLInputElement; } From 7d53455910f0aa28f361a8d86e322c999a9eb578 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 27 Jul 2017 11:15:13 -0400 Subject: [PATCH 08/11] Give the FloatText and IntText a step attribute. For floats, a step of None means any step is allowed. --- ipywidgets/widgets/widget_float.py | 8 +++++--- ipywidgets/widgets/widget_int.py | 1 + packages/controls/src/widget_int.ts | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ipywidgets/widgets/widget_float.py b/ipywidgets/widgets/widget_float.py index 929d0683dc..6af4913241 100644 --- a/ipywidgets/widgets/widget_float.py +++ b/ipywidgets/widgets/widget_float.py @@ -68,6 +68,8 @@ class FloatText(_Float): ---------- value : float value displayed + step : float + step of the increment (if None, any step is allowed) description : str description displayed next to the text box """ @@ -75,6 +77,7 @@ class FloatText(_Float): _model_name = Unicode('FloatTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) + step = CFloat(None, allow_none=True, help="Minimum step to increment the value").tag(sync=True) @register @@ -92,7 +95,7 @@ class BoundedFloatText(_BoundedFloat): max : float maximal value of the range of possible values displayed step : float - step of the increment + step of the increment (if None, any step is allowed) description : str description displayed next to the textbox """ @@ -100,8 +103,7 @@ class BoundedFloatText(_BoundedFloat): _model_name = Unicode('BoundedFloatTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) - step = CFloat(0.1, help="Minimum step to increment the value").tag(sync=True) - + step = CFloat(None, allow_none=True, help="Minimum step to increment the value").tag(sync=True) @register class FloatSlider(_BoundedFloat): diff --git a/ipywidgets/widgets/widget_int.py b/ipywidgets/widgets/widget_int.py index ffd7824ffc..3136e237bf 100644 --- a/ipywidgets/widgets/widget_int.py +++ b/ipywidgets/widgets/widget_int.py @@ -128,6 +128,7 @@ class IntText(_Int): _model_name = Unicode('IntTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) + step = CInt(1, help="Minimum step to increment the value").tag(sync=True) @register diff --git a/packages/controls/src/widget_int.ts b/packages/controls/src/widget_int.ts index cc58dc0b43..91d8fba138 100644 --- a/packages/controls/src/widget_int.ts +++ b/packages/controls/src/widget_int.ts @@ -529,7 +529,8 @@ class IntTextView extends DescriptionView { if (this.model.get('max') !== undefined) { this.textbox.max = this.model.get('max'); } - if (this.model.get('step') !== undefined) { + if (this.model.get('step') !== undefined + && this.model.get('step') !== null) { this.textbox.step = this.model.get('step'); } else { this.textbox.step = this._default_step; From 53515bb44a024169b70a0819207c62f358b9e4a5 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 27 Jul 2017 15:23:11 -0400 Subject: [PATCH 09/11] Delete the docs for the on_submit method. --- docs/source/examples/Widget Events.ipynb | 43 ------------------------ 1 file changed, 43 deletions(-) diff --git a/docs/source/examples/Widget Events.ipynb b/docs/source/examples/Widget Events.ipynb index 8d1e96539d..29d8caa53f 100644 --- a/docs/source/examples/Widget Events.ipynb +++ b/docs/source/examples/Widget Events.ipynb @@ -116,49 +116,6 @@ "button.on_click(on_button_clicked)" ] }, - { - "cell_type": "markdown", - "metadata": { - "slideshow": { - "slide_type": "slide" - } - }, - "source": [ - "### on_submit" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "The `Text` widget also has a special `on_submit` event. The `on_submit` event fires when the user hits return." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "cab0a6538a00491f95c48e77707e9ad8" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "text = widgets.Text()\n", - "display(text)\n", - "\n", - "def handle_submit(sender):\n", - " print(text.value)\n", - "\n", - "text.on_submit(handle_submit)" - ] - }, { "cell_type": "markdown", "metadata": { From c2ddf2fddd047520413cd832b8207d3a3e20b2fb Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 27 Jul 2017 17:01:57 -0400 Subject: [PATCH 10/11] Add a section about the continuous_update parameter. --- docs/source/examples/Widget Events.ipynb | 278 +++++------------------ 1 file changed, 56 insertions(+), 222 deletions(-) diff --git a/docs/source/examples/Widget Events.ipynb b/docs/source/examples/Widget Events.ipynb index 29d8caa53f..d0b720f26d 100644 --- a/docs/source/examples/Widget Events.ipynb +++ b/docs/source/examples/Widget Events.ipynb @@ -29,7 +29,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "collapsed": true }, @@ -47,26 +47,9 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Register a callback to execute when the button is clicked.\n", - "\n", - " The callback will be called with one argument, the clicked button\n", - " widget instance.\n", - "\n", - " Parameters\n", - " ----------\n", - " remove: bool (optional)\n", - " Set to true to remove the callback from the list of callbacks.\n", - " \n" - ] - } - ], + "outputs": [], "source": [ "import ipywidgets as widgets\n", "print(widgets.Button.on_click.__doc__)" @@ -92,19 +75,9 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9cdef1f1216f4ae08225bcf64fc47f1b" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from IPython.display import display\n", "button = widgets.Button(description=\"Click Me!\")\n", @@ -136,41 +109,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Setup a handler to be called when a trait changes.\n", - "\n", - " This is used to setup dynamic notifications of trait changes.\n", - "\n", - " Parameters\n", - " ----------\n", - " handler : callable\n", - " A callable that is called when a trait changes. Its\n", - " signature should be ``handler(change)``, where ``change`` is a\n", - " dictionary. The change dictionary at least holds a 'type' key.\n", - " * ``type``: the type of notification.\n", - " Other keys may be passed depending on the value of 'type'. In the\n", - " case where type is 'change', we also have the following keys:\n", - " * ``owner`` : the HasTraits instance\n", - " * ``old`` : the old value of the modified trait attribute\n", - " * ``new`` : the new value of the modified trait attribute\n", - " * ``name`` : the name of the modified trait attribute.\n", - " names : list, str, All\n", - " If names is All, the handler will apply to all traits. If a list\n", - " of str, handler will apply to all names in the list. If a\n", - " str, the handler will apply just to that name.\n", - " type : str, All (default: 'change')\n", - " The type of notification to filter by. If equal to All, then all\n", - " notifications are passed to the observe handler.\n", - " \n" - ] - } - ], + "outputs": [], "source": [ "print(widgets.Widget.observe.__doc__)" ] @@ -197,19 +138,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "01bd94cb03fb402ab86fbd6ace38d09b" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "int_range = widgets.IntSlider()\n", "display(int_range)\n", @@ -245,7 +176,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": { "collapsed": true }, @@ -256,37 +187,9 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "58f809d3255944028e26010a7b820b7b" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "53298b8e30a34daf89bc08036ee90e73" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "052bb821482c4e43b4a4cccc38dc8265" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "caption = widgets.Label(value='The values of slider1 and slider2 are synchronized')\n", "sliders1, slider2 = widgets.IntSlider(description='Slider 1'),\\\n", @@ -297,37 +200,9 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "969463073307481a8035894210978797" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8b92997bff2641109f3367794c3c1bc4" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9fbf05bb01524a4883dca5c6ee3b862b" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "caption = widgets.Label(value='Changes in source values are reflected in target1')\n", "source, target1 = widgets.IntSlider(description='Source'),\\\n", @@ -345,7 +220,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": { "collapsed": true }, @@ -375,28 +250,9 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8761630b797042fb9ed2f23495f46905" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e17348dfdaa34206aa8e4a02a71c4c09" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "caption = widgets.Label(value='The values of range1 and range2 are synchronized')\n", "slider = widgets.IntSlider(min=-5, max=5, value=1, description='Slider')\n", @@ -429,37 +285,9 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "85f446392a39465692ab2223d0006eb2" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2c7acc51bfec4098b2aefebae8b465ce" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "480c329cfef84ac6a1c22f6f586e257e" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "caption = widgets.Label(value='The values of range1 and range2 are synchronized')\n", "range1, range2 = widgets.IntSlider(description='Range 1'),\\\n", @@ -470,37 +298,9 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "42ea4119375b4fb2a347f406fdfdb0d3" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6f8c3948a7fb485fa139a034758e8267" - } - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "8a7e0233e1b84a578fb71af0c05ce03a" - } - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "caption = widgets.Label(value='Changes in source_range values are reflected in target_range1')\n", "source_range, target_range1 = widgets.IntSlider(description='Source range'),\\\n", @@ -518,7 +318,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": { "collapsed": true }, @@ -541,6 +341,40 @@ "To see the difference between the two, go to the [static version of this page in the ipywidgets documentation](http://ipywidgets.readthedocs.io/en/latest/examples/Widget%20Events.html) and try out the sliders near the bottom. The ones linked in the kernel with `link` and `dlink` are no longer linked, but the ones linked in the browser with `jslink` and `jsdlink` are still linked." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Continuous updates\n", + "\n", + "Some widgets offer a choice with their `continuous_update` attribute between continually updating values or only updating values when a user submits the value (for example, by pressing Enter or navigating away from the control). In the next example, we see the \"Delayed\" controls only transmit their value after the user finishes dragging the slider or submitting the textbox. The \"Continuous\" controls continually transmit their values as they are changed. Try typing a two-digit number into each of the text boxes, or dragging each of the sliders, to see the difference." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import traitlets\n", + "a = widgets.IntSlider(description=\"Delayed\", continuous_update=False)\n", + "b = widgets.IntText(description=\"Delayed\", continuous_update=False)\n", + "c = widgets.IntSlider(description=\"Continuous\", continuous_update=True)\n", + "d = widgets.IntText(description=\"Continuous\", continuous_update=True)\n", + "\n", + "traitlets.link((a, 'value'), (b, 'value'))\n", + "traitlets.link((a, 'value'), (c, 'value'))\n", + "traitlets.link((a, 'value'), (d, 'value'))\n", + "widgets.VBox([a,b,c,d])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Sliders, `Text`, and `Textarea` controls default to `continuous_update=True`. `IntText` and other text boxes for entering integer or float numbers default to `continuous_update=False` (since often you'll want to type an entire number before submitting the value by pressing enter or navigating out of the box)." + ] + }, { "cell_type": "markdown", "metadata": { From 21ad3c887205d352570a0a25ad7de6b65732aa71 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 27 Jul 2017 17:11:10 -0400 Subject: [PATCH 11/11] Update continuous_update docs to indicate what sort of things are submitting a value. --- ipywidgets/widgets/widget_float.py | 4 ++-- ipywidgets/widgets/widget_int.py | 4 ++-- ipywidgets/widgets/widget_string.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ipywidgets/widgets/widget_float.py b/ipywidgets/widgets/widget_float.py index 6af4913241..1b642fa97c 100644 --- a/ipywidgets/widgets/widget_float.py +++ b/ipywidgets/widgets/widget_float.py @@ -76,7 +76,7 @@ class FloatText(_Float): _view_name = Unicode('FloatTextView').tag(sync=True) _model_name = Unicode('FloatTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) - continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) + continuous_update = Bool(False, help="Update the value as the user types. If False, update on submission, e.g., pressing Enter or navigating away.").tag(sync=True) step = CFloat(None, allow_none=True, help="Minimum step to increment the value").tag(sync=True) @@ -102,7 +102,7 @@ class BoundedFloatText(_BoundedFloat): _view_name = Unicode('FloatTextView').tag(sync=True) _model_name = Unicode('BoundedFloatTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) - continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) + continuous_update = Bool(False, help="Update the value as the user types. If False, update on submission, e.g., pressing Enter or navigating away.").tag(sync=True) step = CFloat(None, allow_none=True, help="Minimum step to increment the value").tag(sync=True) @register diff --git a/ipywidgets/widgets/widget_int.py b/ipywidgets/widgets/widget_int.py index 3136e237bf..8126fb05c1 100644 --- a/ipywidgets/widgets/widget_int.py +++ b/ipywidgets/widgets/widget_int.py @@ -127,7 +127,7 @@ class IntText(_Int): _view_name = Unicode('IntTextView').tag(sync=True) _model_name = Unicode('IntTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) - continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) + continuous_update = Bool(False, help="Update the value as the user types. If False, update on submission, e.g., pressing Enter or navigating away.").tag(sync=True) step = CInt(1, help="Minimum step to increment the value").tag(sync=True) @@ -139,7 +139,7 @@ class BoundedIntText(_BoundedInt): _view_name = Unicode('IntTextView').tag(sync=True) _model_name = Unicode('BoundedIntTextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) - continuous_update = Bool(False, help="Update the value as the user types.").tag(sync=True) + continuous_update = Bool(False, help="Update the value as the user types. If False, update on submission, e.g., pressing Enter or navigating away.").tag(sync=True) step = CInt(1, help="Minimum step to increment the value").tag(sync=True) diff --git a/ipywidgets/widgets/widget_string.py b/ipywidgets/widgets/widget_string.py index 7ab6962ad1..c3ebf0fbd6 100644 --- a/ipywidgets/widgets/widget_string.py +++ b/ipywidgets/widgets/widget_string.py @@ -64,7 +64,7 @@ class Textarea(_String): _model_name = Unicode('TextareaModel').tag(sync=True) rows = Int(None, allow_none=True, help="The number of rows to display.").tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) - continuous_update = Bool(True, help="Update the value as the user types.").tag(sync=True) + continuous_update = Bool(True, help="Update the value as the user types. If False, update on submission, e.g., pressing Enter or navigating away.").tag(sync=True) @register class Text(_String): @@ -72,7 +72,7 @@ class Text(_String): _view_name = Unicode('TextView').tag(sync=True) _model_name = Unicode('TextModel').tag(sync=True) disabled = Bool(False, help="Enable or disable user changes").tag(sync=True) - continuous_update = Bool(True, help="Update the value as the user types.").tag(sync=True) + continuous_update = Bool(True, help="Update the value as the user types. If False, update on submission, e.g., pressing Enter or navigating away.").tag(sync=True) def __init__(self, *args, **kwargs): super(Text, self).__init__(*args, **kwargs)