diff --git a/glue_ar/common/scatter_export_options.py b/glue_ar/common/scatter_export_options.py index 4cc1db9..5caf181 100644 --- a/glue_ar/common/scatter_export_options.py +++ b/glue_ar/common/scatter_export_options.py @@ -6,9 +6,7 @@ class ARVispyScatterExportOptions(State): - - theta_resolution = CallbackProperty(10) - phi_resolution = CallbackProperty(10) + resolution = CallbackProperty(10) class ARIpyvolumeScatterExportOptions(State): diff --git a/glue_ar/common/scatter_gltf.py b/glue_ar/common/scatter_gltf.py index 3dc75e3..1cdb2c9 100644 --- a/glue_ar/common/scatter_gltf.py +++ b/glue_ar/common/scatter_gltf.py @@ -350,8 +350,9 @@ def add_vispy_scatter_layer_gltf(builder: GLTFBuilder, bounds: Bounds, clip_to_bounds: bool = True): - theta_resolution = int(options.theta_resolution) - phi_resolution = int(options.phi_resolution) + resolution = int(options.resolution) + theta_resolution = resolution + phi_resolution = resolution triangles = sphere_triangles(theta_resolution=theta_resolution, phi_resolution=phi_resolution) diff --git a/glue_ar/common/scatter_stl.py b/glue_ar/common/scatter_stl.py index 524d983..46de629 100644 --- a/glue_ar/common/scatter_stl.py +++ b/glue_ar/common/scatter_stl.py @@ -59,8 +59,9 @@ def add_vispy_scatter_layer_stl(builder: STLBuilder, bounds: Bounds, clip_to_bounds: bool = True): - theta_resolution = int(options.theta_resolution) - phi_resolution = int(options.phi_resolution) + resolution = int(options.resolution) + theta_resolution = resolution + phi_resolution = resolution triangles = sphere_triangles(theta_resolution=theta_resolution, phi_resolution=phi_resolution) diff --git a/glue_ar/common/scatter_usd.py b/glue_ar/common/scatter_usd.py index 0526783..8d35fca 100644 --- a/glue_ar/common/scatter_usd.py +++ b/glue_ar/common/scatter_usd.py @@ -195,8 +195,9 @@ def add_vispy_scatter_layer_usd(builder: USDBuilder, bounds: Bounds, clip_to_bounds: bool = True): - theta_resolution = int(options.theta_resolution) - phi_resolution = int(options.phi_resolution) + resolution = int(options.resolution) + theta_resolution = resolution + phi_resolution = resolution triangles = sphere_triangles(theta_resolution=theta_resolution, phi_resolution=phi_resolution) diff --git a/glue_ar/common/tests/test_base_dialog.py b/glue_ar/common/tests/test_base_dialog.py index 1033218..1ed3081 100644 --- a/glue_ar/common/tests/test_base_dialog.py +++ b/glue_ar/common/tests/test_base_dialog.py @@ -9,8 +9,8 @@ class DummyState(State): - cb_int = CallbackProperty(0) - cb_float = CallbackProperty(1.7) + cb_int = CallbackProperty(2) + cb_float = CallbackProperty(0.7) cb_bool = CallbackProperty(False) diff --git a/glue_ar/common/tests/test_scatter_gltf.py b/glue_ar/common/tests/test_scatter_gltf.py index 29ac78e..dabc985 100644 --- a/glue_ar/common/tests/test_scatter_gltf.py +++ b/glue_ar/common/tests/test_scatter_gltf.py @@ -62,8 +62,8 @@ def test_basic_export(self, app_type: str, viewer_type: str): # TODO: 3 is the value for ipyvolume's diamond, which is the ipv default # But we should make this more robust - theta_resolution: int = getattr(options, "theta_resolution", 3) - phi_resolution: int = getattr(options, "phi_resolution", 3) + theta_resolution: int = getattr(options, "resolution", 3) + phi_resolution: int = getattr(options, "resolution", 3) triangles_count = sphere_triangles_count(theta_resolution=theta_resolution, phi_resolution=phi_resolution) points_count = sphere_points_count(theta_resolution=theta_resolution, diff --git a/glue_ar/common/tests/test_scatter_stl.py b/glue_ar/common/tests/test_scatter_stl.py index d2c3ea1..e6e14b9 100644 --- a/glue_ar/common/tests/test_scatter_stl.py +++ b/glue_ar/common/tests/test_scatter_stl.py @@ -50,8 +50,8 @@ def test_basic_export(self, app_type: str, viewer_type: str): # TODO: 3 is the value for ipyvolume's diamond, which is the ipv default # But we should make this more robust - theta_resolution: int = getattr(options, "theta_resolution", 3) - phi_resolution: int = getattr(options, "phi_resolution", 3) + theta_resolution: int = getattr(options, "resolution", 3) + phi_resolution: int = getattr(options, "resolution", 3) points_count = sphere_points_count(theta_resolution, phi_resolution) triangle_count = sphere_triangles_count(theta_resolution, phi_resolution) for index in range(self.data1.size): diff --git a/glue_ar/common/tests/test_scatter_usd.py b/glue_ar/common/tests/test_scatter_usd.py index 25ccdcd..7c8e668 100644 --- a/glue_ar/common/tests/test_scatter_usd.py +++ b/glue_ar/common/tests/test_scatter_usd.py @@ -40,8 +40,8 @@ def test_basic_export(self, app_type: str, viewer_type: str): _, options = self.state_dictionary[label] # The default ipyvolume geometry type is diamond - theta_resolution: int = getattr(options, "theta_resolution", 3) - phi_resolution: int = getattr(options, "phi_resolution", 3) + theta_resolution: int = getattr(options, "resolution", 3) + phi_resolution: int = getattr(options, "resolution", 3) sphere_pts_count = sphere_points_count(theta_resolution=theta_resolution, phi_resolution=phi_resolution) sphere_tris_count = sphere_triangles_count(theta_resolution=theta_resolution, phi_resolution=phi_resolution) expected_vert_cts = [3] * sphere_tris_count * self.n diff --git a/glue_ar/jupyter/export_dialog.py b/glue_ar/jupyter/export_dialog.py index 0044e0d..b8f1db3 100644 --- a/glue_ar/jupyter/export_dialog.py +++ b/glue_ar/jupyter/export_dialog.py @@ -4,7 +4,7 @@ import traitlets from typing import Callable, List, Optional -from echo import HasCallbackProperties +from echo import HasCallbackProperties, add_callback from glue.core.state_objects import State from glue.viewers.common.viewer import Viewer from glue_jupyter.link import link @@ -13,40 +13,6 @@ from glue_ar.common.export_dialog_base import ARExportDialogBase -# Based on https://github.com/widgetti/ipyvuetify/issues/241 -class NumberField(v.VuetifyTemplate): - label = traitlets.Unicode().tag(sync=True) - value = traitlets.Unicode().tag(sync=True) - - temp_error = traitlets.Unicode(allow_none=True, default_value=None).tag(sync=True) - - def __init__(self, type, label, error_message, *args, **kwargs): - super().__init__(*args, **kwargs) - self.number_type = type - self.label = label - self.error_message = error_message - - @traitlets.default("template") - def _template(self): - return """ - - - """ - - def vue_temp_rule(self, value): - self.temp_error = None - try: - self.number_type(value) - except ValueError: - self.temp_error = self.error_message - - class JupyterARExportDialog(ARExportDialogBase, VuetifyTemplate): template_file = (__file__, "export_dialog.vue") @@ -103,7 +69,7 @@ def _update_layer_ui(self, state: State): for property, _ in state.iter_callback_properties(): name = self.display_name(property) widgets.extend(self.widgets_for_property(state, property, name)) - self.input_widgets = [w for w in widgets if isinstance(w, NumberField)] + self.input_widgets = [w for w in widgets if isinstance(w, v.Slider)] self.layer_layout = v.Container(children=widgets, px_0=True, py_0=True) self.has_layer_options = len(self.layer_layout.children) > 0 @@ -130,12 +96,21 @@ def widgets_for_property(self, link((instance, property), (widget, 'value')) return [widget] elif t in (int, float): - name = "integer" if t is int else "number" - widget = NumberField(type=t, label=display_name, error_message=f"You must enter a valid {name}") + step = 0.01 if t is float else 1 + min = step + max = min * 100 + widget = v.Slider(min=min, + max=max, + step=step, + label=display_name, + thumb_label=f"{value:g}") link((instance, property), - (widget, 'value'), - lambda value: str(value), - lambda text: t(text)) + (widget, 'value')) + + def update_label(value): + widget.thumb_label = f"{value:g}" + add_callback(instance, property, update_label) + return [widget] else: return [] @@ -147,7 +122,7 @@ def vue_cancel_dialog(self, *args): self.on_cancel() def vue_export_viewer(self, *args): - okay = all(widget.temp_error is None for widget in self.input_widgets) + okay = all(not widget.error for widget in self.input_widgets) if not okay: return self.dialog_open = False diff --git a/glue_ar/jupyter/number_field.py b/glue_ar/jupyter/number_field.py new file mode 100644 index 0000000..101c25a --- /dev/null +++ b/glue_ar/jupyter/number_field.py @@ -0,0 +1,36 @@ +import traitlets +import ipyvuetify as v + + +# Based on https://github.com/widgetti/ipyvuetify/issues/241 +class NumberField(v.VuetifyTemplate): + label = traitlets.Unicode().tag(sync=True) + value = traitlets.Unicode().tag(sync=True) + + temp_error = traitlets.Unicode(allow_none=True, default_value=None).tag(sync=True) + + def __init__(self, type, label, error_message, *args, **kwargs): + super().__init__(*args, **kwargs) + self.number_type = type + self.label = label + self.error_message = error_message + + @traitlets.default("template") + def _template(self): + return """ + + + """ + + def vue_temp_rule(self, value): + self.temp_error = None + try: + self.number_type(value) + except ValueError: + self.temp_error = self.error_message diff --git a/glue_ar/jupyter/tests/test_dialog.py b/glue_ar/jupyter/tests/test_dialog.py index e2fd098..f6ea399 100644 --- a/glue_ar/jupyter/tests/test_dialog.py +++ b/glue_ar/jupyter/tests/test_dialog.py @@ -8,10 +8,10 @@ # We can't use the Jupyter vispy widget for these tests until # https://github.com/glue-viz/glue-vispy-viewers/pull/388 is released from glue_jupyter.ipyvolume.volume import IpyvolumeVolumeView -from ipyvuetify import Checkbox +from ipyvuetify import Checkbox, Slider from glue_ar.common.tests.test_base_dialog import BaseExportDialogTest, DummyState -from glue_ar.jupyter.export_dialog import JupyterARExportDialog, NumberField +from glue_ar.jupyter.export_dialog import JupyterARExportDialog class TestJupyterExportDialog(BaseExportDialogTest): @@ -95,20 +95,16 @@ def test_widgets_for_property(self): int_widgets = self.dialog.widgets_for_property(state, "cb_int", "Int CB") assert len(int_widgets) == 1 widget = int_widgets[0] - assert isinstance(widget, NumberField) + assert isinstance(widget, Slider) assert widget.label == "Int CB" - assert widget.value == "0" - assert widget.number_type is int - assert widget.error_message == "You must enter a valid integer" + assert widget.value == 2 float_widgets = self.dialog.widgets_for_property(state, "cb_float", "Float CB") assert len(float_widgets) == 1 widget = float_widgets[0] - assert isinstance(widget, NumberField) + assert isinstance(widget, Slider) assert widget.label == "Float CB" - assert widget.value == "1.7" - assert widget.number_type is float - assert widget.error_message == "You must enter a valid number" + assert widget.value == 0.7 bool_widgets = self.dialog.widgets_for_property(state, "cb_bool", "Bool CB") assert len(bool_widgets) == 1 diff --git a/glue_ar/qt/export_dialog.py b/glue_ar/qt/export_dialog.py index 2668e3b..931bba2 100644 --- a/glue_ar/qt/export_dialog.py +++ b/glue_ar/qt/export_dialog.py @@ -1,14 +1,14 @@ import os from typing import List -from echo import HasCallbackProperties -from echo.qt import autoconnect_callbacks_to_qt, connect_checkable_button, connect_float_text +from echo import HasCallbackProperties, add_callback +from echo.qt import autoconnect_callbacks_to_qt, connect_checkable_button, connect_value from glue.core.state_objects import State from glue_qt.utils import load_ui from glue_ar.common.export_dialog_base import ARExportDialogBase -from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLabel, QLayout, QLineEdit, QWidget -from qtpy.QtGui import QIntValidator, QDoubleValidator +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLabel, QLayout, QSizePolicy, QSlider, QWidget __all__ = ['QtARExportDialog'] @@ -46,12 +46,27 @@ def _widgets_for_property(self, label = QLabel() prompt = f"{display_name}:" label.setText(prompt) - widget = QLineEdit() - validator = QIntValidator() if t is int else QDoubleValidator() - widget.setText(str(value)) - widget.setValidator(validator) - self._layer_connections.append(connect_float_text(instance, property, widget)) - return [label, widget] + widget = QSlider() + policy = QSizePolicy() + policy.setHorizontalPolicy(QSizePolicy.Policy.Expanding) + policy.setVerticalPolicy(QSizePolicy.Policy.Fixed) + widget.setOrientation(Qt.Orientation.Horizontal) + widget.setMinimum(1) + widget.setMaximum(100) + + widget.setSizePolicy(policy) + + value_label = QLabel() + + def update_label(value): + value_label.setText(f"{value:g}") + + update_label(value) + add_callback(instance, property, update_label) + + range = (1, 100) if t is int else (0.01, 1) + self._layer_connections.append(connect_value(instance, property, widget, value_range=range)) + return [label, widget, value_label] else: return [] diff --git a/glue_ar/qt/tests/test_dialog.py b/glue_ar/qt/tests/test_dialog.py index 629478e..8c94cf7 100644 --- a/glue_ar/qt/tests/test_dialog.py +++ b/glue_ar/qt/tests/test_dialog.py @@ -6,8 +6,7 @@ from glue_qt.app import GlueApplication from glue_vispy_viewers.volume.qt.volume_viewer import VispyVolumeViewer -from qtpy.QtGui import QDoubleValidator, QIntValidator -from qtpy.QtWidgets import QCheckBox, QLabel, QLineEdit +from qtpy.QtWidgets import QCheckBox, QLabel, QSlider from glue_ar.common.tests.test_base_dialog import BaseExportDialogTest, DummyState from glue_ar.common.scatter_export_options import ARVispyScatterExportOptions @@ -77,22 +76,24 @@ def test_widgets_for_property(self): state = DummyState() int_widgets = self.dialog._widgets_for_property(state, "cb_int", "Int CB") - assert len(int_widgets) == 2 - label, edit = int_widgets + assert len(int_widgets) == 3 + label, slider, value_label = int_widgets assert isinstance(label, QLabel) assert label.text() == "Int CB:" - assert isinstance(edit, QLineEdit) - assert isinstance(edit.validator(), QIntValidator) - assert edit.text() == "0" + assert isinstance(slider, QSlider) + assert slider.value() == 2 + assert isinstance(value_label, QLabel) + assert value_label.text() == "2" float_widgets = self.dialog._widgets_for_property(state, "cb_float", "Float CB") - assert len(float_widgets) == 2 - label, edit = float_widgets + assert len(float_widgets) == 3 + label, slider, value_label = float_widgets assert isinstance(label, QLabel) assert label.text() == "Float CB:" - assert isinstance(edit, QLineEdit) - assert isinstance(edit.validator(), QDoubleValidator) - assert edit.text() == "1.7" + assert isinstance(slider, QSlider) + assert slider.value() == 70 + assert isinstance(value_label, QLabel) + assert value_label.text() == "0.7" bool_widgets = self.dialog._widgets_for_property(state, "cb_bool", "Bool CB") assert len(bool_widgets) == 1 @@ -108,7 +109,7 @@ def test_update_layer_ui(self): state = ARVispyScatterExportOptions() self.dialog._update_layer_ui(state) - assert self.dialog.ui.layer_layout.rowCount() == 2 + assert self.dialog.ui.layer_layout.rowCount() == 1 def test_clear_layout(self): self.dialog._clear_layer_layout() diff --git a/glue_ar/qt/tests/test_tool_scatter.py b/glue_ar/qt/tests/test_tool_scatter.py index c67cb82..e7460e8 100644 --- a/glue_ar/qt/tests/test_tool_scatter.py +++ b/glue_ar/qt/tests/test_tool_scatter.py @@ -95,5 +95,4 @@ def test_tool_export_call(self, extension, compression): assert len(value) == 2 assert value[0] == "Scatter" assert isinstance(value[1], ARVispyScatterExportOptions) - assert value[1].theta_resolution == 10 - assert value[1].phi_resolution == 10 + assert value[1].resolution == 10 diff --git a/glue_ar/qt/tests/test_tool_volume.py b/glue_ar/qt/tests/test_tool_volume.py index 542593e..294c212 100644 --- a/glue_ar/qt/tests/test_tool_volume.py +++ b/glue_ar/qt/tests/test_tool_volume.py @@ -99,8 +99,7 @@ def test_tool_export_call(self, extension, compression): scatter_method, scatter_state = state_dict["Scatter Data"] assert scatter_method == "Scatter" assert isinstance(scatter_state, ARVispyScatterExportOptions) - assert scatter_state.theta_resolution == 10 - assert scatter_state.phi_resolution == 10 + assert scatter_state.resolution == 10 volume_method, volume_state = state_dict["Volume Data"] assert volume_method == "Isosurface" diff --git a/glue_ar/tests/test_utils.py b/glue_ar/tests/test_utils.py index abd91b1..aeaa2ae 100644 --- a/glue_ar/tests/test_utils.py +++ b/glue_ar/tests/test_utils.py @@ -162,6 +162,8 @@ def test_clip_sides_native(): assert clip_sides(viewer_state, clip_size=clip_size) == (max_size, max_size / 2, max_size / 4) +@pytest.mark.skipif(not (GLUE_QT_INSTALLED or GLUE_JUPYTER_INSTALLED), + reason="Requires either glue-qt or glue-jupyter to create application") def test_mask_for_bounds(): x_values = range(10, 25) y_values = range(130, 145)