Repository: zauberzeug/nicegui
Pull request #2761 (2024-03-25 03:25:03)

More elegant way to terminate On Air connection
This pull-request get's rid of hacky signal handler registration to terminate the On Air connection. This also fixes issues with auto-reload when On Air is active.


Patch:
diff --git a/nicegui/air.py b/nicegui/air.py
index 3844a47c..00958bca 100644
--- a/nicegui/air.py
+++ b/nicegui/air.py
@@ -162,9 +162,6 @@ class Air:
 
     async def connect(self) -> None:
         """Connect to the NiceGUI On Air server."""
-        # ensure that the connection is closed when the process is terminated
-        signal.signal(signal.SIGINT, lambda signum, frame: disconnect())
-        signal.signal(signal.SIGTERM, lambda signum, frame: disconnect())
         if self.connecting:
             return
         self.connecting = True
@@ -192,6 +189,7 @@ class Air:
 
     async def disconnect(self) -> None:
         """Disconnect from the NiceGUI On Air server."""
+        await self.relay.disconnect()
         for stream in self.streams.values():
             await stream.response.aclose()
         self.streams.clear()
diff --git a/nicegui/elements/choice_element.py b/nicegui/elements/choice_element.py
index 45d1e7bd..a4db13cb 100644
--- a/nicegui/elements/choice_element.py
+++ b/nicegui/elements/choice_element.py
@@ -25,8 +25,8 @@ class ChoiceElement(ValueElement):
     def _update_options(self) -> None:
         before_value = self.value
         self._props['options'] = [{'value': index, 'label': option} for index, option in enumerate(self._labels)]
+        self._props[self.VALUE_PROP] = self._value_to_model_value(before_value)
         if not isinstance(before_value, list):  # NOTE: no need to update value in case of multi-select
-            self._props[self.VALUE_PROP] = self._value_to_model_value(before_value)
             self.value = before_value if before_value in self._values else None
 
     def update(self) -> None:
diff --git a/nicegui/elements/range.py b/nicegui/elements/range.py
new file mode 100644
index 00000000..69afe616
--- /dev/null
+++ b/nicegui/elements/range.py
@@ -0,0 +1,29 @@
+from typing import Any, Callable, Dict, Optional
+
+from .mixins.disableable_element import DisableableElement
+from .mixins.value_element import ValueElement
+
+
+class Range(ValueElement, DisableableElement):
+
+    def __init__(self, *,
+                 min: float,  # pylint: disable=redefined-builtin
+                 max: float,  # pylint: disable=redefined-builtin
+                 step: float = 1.0,
+                 value: Optional[Dict[str, int]] = None,
+                 on_change: Optional[Callable[..., Any]] = None,
+                 ) -> None:
+        """Range
+
+        This element is based on Quasar's `QRange <https://quasar.dev/vue-components/range>`_ component.
+
+        :param min: lower bound of the range
+        :param max: upper bound of the range
+        :param step: step size
+        :param value: initial value to set min and max position of the range
+        :param on_change: callback which is invoked when the user releases the range
+        """
+        super().__init__(tag='q-range', value=value, on_value_change=on_change, throttle=0.05)
+        self._props['min'] = min
+        self._props['max'] = max
+        self._props['step'] = step
diff --git a/nicegui/ui.py b/nicegui/ui.py
index 7074f18f..2082dc7b 100644
--- a/nicegui/ui.py
+++ b/nicegui/ui.py
@@ -60,6 +60,7 @@ __all__ = [
     'pyplot',
     'query',
     'radio',
+    'range',
     'restructured_text',
     'row',
     'scene',
@@ -173,6 +174,7 @@ from .elements.progress import LinearProgress as linear_progress
 from .elements.pyplot import Pyplot as pyplot
 from .elements.query import Query as query
 from .elements.radio import Radio as radio
+from .elements.range import Range as range
 from .elements.restructured_text import ReStructuredText as restructured_text
 from .elements.row import Row as row
 from .elements.scene import Scene as scene
diff --git a/website/documentation/content/range_documentation.py b/website/documentation/content/range_documentation.py
new file mode 100644
index 00000000..02e87489
--- /dev/null
+++ b/website/documentation/content/range_documentation.py
@@ -0,0 +1,26 @@
+from nicegui import ui
+
+from . import doc
+
+
+@doc.demo(ui.range)
+def main_demo() -> None:
+    min_max_range = ui.range(min=0, max=100, value={'min': 20, 'max': 80})
+    ui.label().bind_text_from(min_max_range, 'value',
+                              backward=lambda v: f'min: {v["min"]}, max: {v["max"]}')
+
+
+@doc.demo('Customize labels', '''
+        You can customize the colors of the range and its labels by setting them individually or for the range in total.
+''')
+def customize_labels():
+    ui.label('Color the entire range')
+    ui.range(min=0, max=100, value={'min': 20, 'max': 80}) \
+        .props('label snap color="secondary"')
+
+    ui.label('Customize the color of the labels')
+    ui.range(min=0, max=100, value={'min': 40, 'max': 80}) \
+        .props('label-always snap label-color="secondary" right-label-text-color="black"')
+
+
+doc.reference(ui.range)
diff --git a/website/documentation/content/section_controls.py b/website/documentation/content/section_controls.py
index 28a73a7d..a4f06c4b 100644
--- a/website/documentation/content/section_controls.py
+++ b/website/documentation/content/section_controls.py
@@ -1,6 +1,6 @@
 from . import (badge_documentation, button_documentation, checkbox_documentation, color_input_documentation,
                color_picker_documentation, date_documentation, doc, input_documentation, joystick_documentation,
-               knob_documentation, number_documentation, radio_documentation, select_documentation,
+               knob_documentation, number_documentation, radio_documentation, range_documentation, select_documentation,
                slider_documentation, switch_documentation, textarea_documentation, time_documentation,
                toggle_documentation, upload_documentation)
 
@@ -14,6 +14,7 @@ doc.intro(select_documentation)
 doc.intro(checkbox_documentation)
 doc.intro(switch_documentation)
 doc.intro(slider_documentation)
+doc.intro(range_documentation)
 doc.intro(joystick_documentation)
 doc.intro(input_documentation)
 doc.intro(textarea_documentation)


Test patch:
diff --git a/tests/test_range.py b/tests/test_range.py
new file mode 100644
index 00000000..69fb6f5d
--- /dev/null
+++ b/tests/test_range.py
@@ -0,0 +1,10 @@
+from nicegui import ui
+from nicegui.testing import Screen
+
+
+def test_range(screen: Screen):
+    r = ui.range(min=0, max=100, value={'min': 20, 'max': 80})
+    ui.label().bind_text_from(r, 'value', backward=lambda v: f'min: {v["min"]}, max: {v["max"]}')
+
+    screen.open('/')
+    screen.should_contain('min: 20, max: 80')