Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve UI widgets #98

Merged
merged 22 commits into from
Dec 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
3b726d7
Add docstrings to export option items and use this in the Qt UI.
Carifio24 Dec 19, 2024
3d1b73f
Add info icons.
Carifio24 Dec 20, 2024
a6caad7
Put help text into info buttons.
Carifio24 Dec 20, 2024
2b438c5
Get sizing of info buttons in Qt dialogs working correctly.
Carifio24 Dec 20, 2024
6a4ce40
Add missing spaces to callback property docstrings.
Carifio24 Dec 20, 2024
d3ad15e
Add info buttons to Jupyter dialog. Still a WIP, need to adjust posit…
Carifio24 Dec 20, 2024
77cdebd
Tooltips are now displaying.
Carifio24 Dec 20, 2024
273ac14
Update Jupyter dialog sliders to show docstrings.
Carifio24 Dec 21, 2024
db46d1e
Adjust layout for layer options in Jupyter dialog.
Carifio24 Dec 21, 2024
9e13010
Refactor Qt callback property widget functionality out of dialog. Upd…
Carifio24 Dec 22, 2024
95c0330
Some code cleanup.
Carifio24 Dec 22, 2024
08f1944
Do the same type of refactoring for the Jupyter widgets and dialog.
Carifio24 Dec 22, 2024
ce74364
More code cleanup for the Qt widgets.
Carifio24 Dec 22, 2024
62f736c
Codestyle fixes.
Carifio24 Dec 22, 2024
09c2f05
Update Qt dialog test to account for layer layout widget type change.
Carifio24 Dec 22, 2024
9820365
Add importorskip to Jupyter widgets test.
Carifio24 Dec 22, 2024
63f06d8
We only need to set up/tear down the application once.
Carifio24 Dec 22, 2024
8c364ad
Use pytest-qt for testing Qt widgets.
Carifio24 Dec 22, 2024
c1408f6
Codestyle fixes.
Carifio24 Dec 22, 2024
cff9ff1
Reconfigure pytest-qt testing setup.
Carifio24 Dec 22, 2024
56ffbad
Add pytest-qt to 'all' environments as well.
Carifio24 Dec 22, 2024
1b760ed
Center-align widgets in rows in Jupyter dialog layer options layout.
Carifio24 Dec 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion glue_ar/common/scatter_export_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@


class ARVispyScatterExportOptions(State):
resolution = RangedCallbackProperty(default=10, min_value=3, max_value=50, resolution=1)
resolution = RangedCallbackProperty(
default=10,
min_value=3,
max_value=50,
resolution=1,
docstring="Controls the resolution of the sphere meshes used for scatter points. "
"Higher means better resolution, but a larger filesize.",
)


class ARIpyvolumeScatterExportOptions(State):
Expand Down
6 changes: 3 additions & 3 deletions glue_ar/common/tests/test_base_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@


class DummyState(State):
cb_int = CallbackProperty(2)
cb_float = CallbackProperty(0.7)
cb_bool = CallbackProperty(False)
cb_int = CallbackProperty(2, docstring="Integer callback property")
cb_float = CallbackProperty(0.7, docstring="Float callback property")
cb_bool = CallbackProperty(False, docstring="Boolean callback property")


class BaseExportDialogTest:
Expand Down
25 changes: 22 additions & 3 deletions glue_ar/common/volume_export_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,28 @@


class ARIsosurfaceExportOptions(State):
isosurface_count = RangedCallbackProperty(default=20, min_value=1, max_value=50)
isosurface_count = RangedCallbackProperty(
default=20,
min_value=1,
max_value=50,
docstring="The number of isosurfaces used in the export.",
)


class ARVoxelExportOptions(State):
opacity_cutoff = RangedCallbackProperty(default=0.1, min_value=0.01, max_value=1, resolution=0.01)
opacity_resolution = RangedCallbackProperty(default=0.02, min_value=0.005, max_value=1, resolution=0.005)
opacity_cutoff = RangedCallbackProperty(
default=0.1,
min_value=0.01,
max_value=1,
resolution=0.01,
docstring="The minimum opacity voxels to retain. Voxels with a lower opacity will be "
"omitted from the export.",
)
opacity_resolution = RangedCallbackProperty(
default=0.02,
min_value=0.005,
max_value=1,
resolution=0.005,
docstring="The resolution of the opacity in the exported figure. Opacity values will be "
"rounded to the nearest integer multiple of this value.",
)
55 changes: 15 additions & 40 deletions glue_ar/jupyter/export_dialog.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import ipyvuetify as v # noqa
from ipyvuetify.VuetifyTemplate import VuetifyTemplate
from ipywidgets import DOMWidget, widget_serialization
from ipywidgets import widget_serialization
import traitlets
from typing import Callable, List, Optional
from typing import Callable, Optional

from echo import HasCallbackProperties
from glue.core.state_objects import State
from glue.viewers.common.viewer import Viewer
from glue_jupyter.link import link
from glue_jupyter.vuetify_helpers import link_glue_choices

from glue_ar.common.export_dialog_base import ARExportDialogBase
from glue_ar.jupyter.widgets import widgets_for_callback_property


class JupyterARExportDialog(ARExportDialogBase, VuetifyTemplate):
Expand All @@ -31,7 +31,7 @@ class JupyterARExportDialog(ARExportDialogBase, VuetifyTemplate):
method_items = traitlets.List().tag(sync=True)
method_selected = traitlets.Int().tag(sync=True)

layer_layout = traitlets.Instance(v.Container).tag(sync=True, **widget_serialization)
layer_layout = traitlets.Instance(v.Col).tag(sync=True, **widget_serialization)
has_layer_options = traitlets.Bool().tag(sync=True)

modelviewer = traitlets.Bool(True).tag(sync=True)
Expand All @@ -42,8 +42,9 @@ def __init__(self,
display: Optional[bool] = False,
on_cancel: Optional[Callable] = None,
on_export: Optional[Callable] = None):

ARExportDialogBase.__init__(self, viewer=viewer)
self.layer_layout = v.Container()
self.layer_layout = v.Col()
VuetifyTemplate.__init__(self)

self._on_layer_change(self.state.layer)
Expand All @@ -65,12 +66,17 @@ def _update_layer_ui(self, state: State):
for widget in self.layer_layout.children:
widget.close()

widgets = []
rows = []
input_widgets = []
self.layer_layout = v.Col()
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, v.Slider)]
self.layer_layout = v.Container(children=widgets, px_0=True, py_0=True)
widgets = widgets_for_callback_property(state, property, name)
input_widgets.extend(w for w in widgets if isinstance(w, v.Slider))
rows.append(v.Row(children=widgets, align="center"))

self.layer_layout.children = rows
self.input_widgets = input_widgets
self.has_layer_options = len(self.layer_layout.children) > 0

def _on_method_change(self, method_name: str):
Expand All @@ -84,37 +90,6 @@ def _on_filetype_change(self, filetype: str):
self.show_compression = gl
self.show_modelviewer = gl

def widgets_for_property(self,
instance: HasCallbackProperties,
property: str,
display_name: str) -> List[DOMWidget]:

value = getattr(instance, property)
t = type(value)
if t is bool:
widget = v.Checkbox(label=display_name)
link((instance, property), (widget, 'value'))
return [widget]
elif t in (int, float):
instance_type = type(instance)
cb_property = getattr(instance_type, property)
min = getattr(cb_property, 'min_value', 1 if t is int else 0.01)
max = getattr(cb_property, 'max_value', 100 * min)
step = getattr(cb_property, 'resolution', None)
if step is None:
step = 1 if t is int else 0.01
widget = v.Slider(min=min,
max=max,
step=step,
label=display_name,
thumb_label=f"{value:g}")
link((instance, property),
(widget, 'v_model'))

return [widget]
else:
return []

def vue_cancel_dialog(self, *args):
self.state_dictionary = {}
self.dialog_open = False
Expand Down
25 changes: 0 additions & 25 deletions glue_ar/jupyter/tests/test_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
# 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, Slider

from glue_ar.common.tests.test_base_dialog import BaseExportDialogTest, DummyState
from glue_ar.jupyter.export_dialog import JupyterARExportDialog
Expand Down Expand Up @@ -89,30 +88,6 @@ def test_filetype_change(self):
assert self.dialog.show_compression
assert self.dialog.show_modelviewer

def test_widgets_for_property(self):
state = DummyState()

int_widgets = self.dialog.widgets_for_property(state, "cb_int", "Int CB")
assert len(int_widgets) == 1
widget = int_widgets[0]
assert isinstance(widget, Slider)
assert widget.label == "Int CB"
assert widget.v_model == 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, Slider)
assert widget.label == "Float CB"
assert widget.v_model == 0.7

bool_widgets = self.dialog.widgets_for_property(state, "cb_bool", "Bool CB")
assert len(bool_widgets) == 1
widget = bool_widgets[0]
assert isinstance(widget, Checkbox)
assert widget.label == "Bool CB"
assert widget.value is False

def test_update_layer_ui(self):
state = DummyState()
self.dialog._update_layer_ui(state)
Expand Down
100 changes: 100 additions & 0 deletions glue_ar/jupyter/tests/test_widgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from pytest import importorskip

importorskip("glue_jupyter")

from echo import CallbackProperty
from ipyvuetify import Checkbox, Img, Slider, Tooltip

from glue_ar.common.tests.test_base_dialog import DummyState
from glue_ar.jupyter.widgets import boolean_callback_widgets, info_icon, \
info_tooltip, number_callback_widgets, \
widgets_for_callback_property


def test_info_tooltip():
assert info_tooltip(DummyState.cb_int) == ["Integer callback property"]
assert info_tooltip(DummyState.cb_float) == ["Float callback property"]
assert info_tooltip(DummyState.cb_bool) == ["Boolean callback property"]


def test_info_button():
state = DummyState()
for property in state.callback_properties():
cb_property: CallbackProperty = getattr(DummyState, property)
icon = info_icon(cb_property)
assert isinstance(icon, Tooltip)
assert len(icon.children) == 1
assert len(icon.v_slots) == 1
slot = icon.v_slots[0]
assert slot["name"] == "activator"
assert slot["variable"] == "tooltip"
assert len(slot["children"]) == 1
img = slot["children"][0]
assert isinstance(img, Img)


def test_boolean_callback_widgets():
state = DummyState()
widgets = boolean_callback_widgets(state, "cb_bool", "Bool CB")
assert len(widgets) == 2
checkbox, icon = widgets

assert isinstance(checkbox, Checkbox)
assert checkbox.label == "Bool CB"

assert not checkbox.value
assert isinstance(icon, Tooltip)


def test_integer_callback_widgets():
state = DummyState()
widgets = number_callback_widgets(state, "cb_int", "Int CB")
assert len(widgets) == 2
slider, icon = widgets

assert isinstance(slider, Slider)
assert slider.label == "Int CB"
assert slider.v_model == 2

assert isinstance(icon, Tooltip)


def test_float_callback_widgets():
state = DummyState()
widgets = number_callback_widgets(state, "cb_float", "Float CB")
assert len(widgets) == 2
slider, icon = widgets

assert isinstance(slider, Slider)
assert slider.label == "Float CB"
assert slider.v_model == 0.7

assert isinstance(icon, Tooltip)


def test_widgets_for_property():
state = DummyState()

int_widgets = widgets_for_callback_property(state, "cb_int", "Int CB")
assert len(int_widgets) == 2
slider, icon = int_widgets
assert isinstance(slider, Slider)
assert slider.label == "Int CB"
assert slider.v_model == 2
assert isinstance(icon, Tooltip)

float_widgets = widgets_for_callback_property(state, "cb_float", "Float CB")
assert len(float_widgets) == 2
slider, icon = float_widgets
assert isinstance(slider, Slider)
assert slider.label == "Float CB"
assert slider.v_model == 0.7
assert isinstance(icon, Tooltip)

bool_widgets = widgets_for_callback_property(state, "cb_bool", "Bool CB")
assert len(bool_widgets) == 2
checkbox, icon = bool_widgets
assert isinstance(checkbox, Checkbox)
assert checkbox.label == "Bool CB"
assert not checkbox.value
assert isinstance(icon, Tooltip)
Loading
Loading