From 3b726d7d1b103e6f73e565980ea0509a57632a06 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Thu, 19 Dec 2024 13:22:33 -0500 Subject: [PATCH 01/22] Add docstrings to export option items and use this in the Qt UI. --- glue_ar/common/scatter_export_options.py | 9 ++- glue_ar/common/volume_export_options.py | 25 +++++++- glue_ar/qt/export_dialog.py | 44 +++++++++---- glue_ar/qt/export_dialog.ui | 79 +++++++++++++----------- 4 files changed, 104 insertions(+), 53 deletions(-) diff --git a/glue_ar/common/scatter_export_options.py b/glue_ar/common/scatter_export_options.py index 681e7e8..ea7a7f2 100644 --- a/glue_ar/common/scatter_export_options.py +++ b/glue_ar/common/scatter_export_options.py @@ -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): diff --git a/glue_ar/common/volume_export_options.py b/glue_ar/common/volume_export_options.py index 155bd55..8676f69 100644 --- a/glue_ar/common/volume_export_options.py +++ b/glue_ar/common/volume_export_options.py @@ -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.", + ) diff --git a/glue_ar/qt/export_dialog.py b/glue_ar/qt/export_dialog.py index 7c9c5ba..43b943c 100644 --- a/glue_ar/qt/export_dialog.py +++ b/glue_ar/qt/export_dialog.py @@ -1,6 +1,6 @@ from math import floor, log import os -from typing import List +from typing import List, Tuple from echo import HasCallbackProperties, add_callback from echo.core import remove_callback @@ -10,7 +10,7 @@ from glue_ar.common.export_dialog_base import ARExportDialogBase from qtpy.QtCore import Qt -from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLabel, QLayout, QSizePolicy, QSlider, QWidget +from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QVBoxLayout, QLabel, QLayout, QSizePolicy, QSlider, QWidget __all__ = ['QtARExportDialog'] @@ -32,22 +32,38 @@ def __init__(self, parent=None, viewer=None): self.ui.button_cancel.clicked.connect(self.reject) self.ui.button_ok.clicked.connect(self.accept) + def _doc_label(self, cb_property) -> QLabel: + label = QLabel(text=cb_property.__doc__) + label.setWordWrap(True) + return label + def _widgets_for_property(self, instance: HasCallbackProperties, property: str, - display_name: str) -> List[QWidget]: + display_name: str) -> List[Tuple[QWidget]]: value = getattr(instance, property) + instance_type = type(instance) + cb_property = getattr(instance_type, property) t = type(value) + widgets: List[Tuple[QWidget]] = [] if t is bool: widget = QCheckBox() widget.setChecked(value) widget.setText(display_name) self._layer_connections.append(connect_checkable_button(instance, property, widget)) - return [widget] + if cb_property.__doc__: + label = self._doc_label(cb_property) + widgets.append((label,)) + widgets.append((widget,)) elif t in (int, float): + widgets: List[Tuple[QWidget]] = [] label = QLabel() prompt = f"{display_name}:" label.setText(prompt) + widgets.append((label,)) + if cb_property.__doc__: + label = self._doc_label(cb_property) + widgets.append((label,)) widget = QSlider() policy = QSizePolicy() policy.setHorizontalPolicy(QSizePolicy.Policy.Expanding) @@ -57,8 +73,6 @@ def _widgets_for_property(self, widget.setSizePolicy(policy) value_label = QLabel() - 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) @@ -86,9 +100,9 @@ def on_widget_destroyed(widget, cb=remove_label_callback): widget.setMinimum(0) widget.setMaximum(steps) self._layer_connections.append(connect_value(instance, property, widget, value_range=(min, max))) - return [label, widget, value_label] - else: - return [] + widgets.append((widget, value_label)) + + return widgets def _clear_layout(self, layout: QLayout): if layout is not None: @@ -119,15 +133,19 @@ def _on_layer_change(self, layer_name: str): multiple_methods = len(self.state.method_helper.choices) > 1 self.ui.label_method.setVisible(multiple_methods) self.ui.combosel_method.setVisible(multiple_methods) + self.ui.line_2.setVisible(multiple_methods) def _update_layer_ui(self, state: State): self._clear_layer_layout() for property in state.callback_properties(): - row = QHBoxLayout() + row = QVBoxLayout() name = self.display_name(property) - widgets = self._widgets_for_property(state, property, name) - for widget in widgets: - row.addWidget(widget) + widget_tuples = self._widgets_for_property(state, property, name) + for widgets in widget_tuples: + subrow = QHBoxLayout() + for widget in widgets: + subrow.addWidget(widget) + row.addLayout(subrow) self.ui.layer_layout.addRow(row) def _on_filetype_change(self, filetype: str): diff --git a/glue_ar/qt/export_dialog.ui b/glue_ar/qt/export_dialog.ui index 01e98c8..c5b678d 100644 --- a/glue_ar/qt/export_dialog.ui +++ b/glue_ar/qt/export_dialog.ui @@ -21,64 +21,67 @@ - + - - - - Qt::Horizontal + + + + + + + Compression method + + + + + + + Export model-viewer HTML + + + true - - + + - Select export options + <html><head/><body><p align="center"><span style=" font-weight:600;">File Options</span></p></body></html> - + + + + Filetype - - - - - - - - - - - - <html><head/><body><p align="center"><span style=" font-weight:600;">File Options</span></p></body></html> + + + Qt::Horizontal - - + + - Export method: + Select export options - - - - Compression method - - + + - + @@ -117,13 +120,17 @@ - - + + - Export model-viewer HTML + Export method: - - true + + + + + + Qt::Horizontal From 3d1b73f73632a02b83a93f7642b7865ecac5196f Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 20 Dec 2024 10:27:48 -0500 Subject: [PATCH 02/22] Add info icons. --- glue_ar/common/info.png | Bin 0 -> 1492 bytes glue_ar/common/info.svg | 1 + 2 files changed, 1 insertion(+) create mode 100644 glue_ar/common/info.png create mode 100644 glue_ar/common/info.svg diff --git a/glue_ar/common/info.png b/glue_ar/common/info.png new file mode 100644 index 0000000000000000000000000000000000000000..4a2f32f793b8ba3e0771608abc1c1a02090249f4 GIT binary patch literal 1492 zcmV;_1uOcAP)Px)j!8s8RCr$PooiOxFc5|9Y=J^yHHDS6tc-LuP$*l7dThsu?ZlZoPl;6gXb+9; zk?uDyDX!f%^(U(WZC2~mcvAsnc7h6^0?4dDX7e=yDuB!iWHw(TpaQt8!0X4yV}HbM z)BoMJ>BsSmcKEDG+y2kg{+?ZJv)ex0-u9oB`cqs4jDokR{n<35z*_mzwuAHV{pOeU zO4jEW0iY9o5WM&Cy;oaLb-dmHqbVdhvH~~??=AW|%#@nvoRN_QKncRnk>pEJp#aJg zet{u1&(WzV1VHJ+FB4Q`T5<<4h`)L9Dyg2=jR4^a;PubvueNRW-HlYLNsqtT&3gK2 zkH1_m3~L6h?QU)1Tcq6p=)_Ok)TZZ7MD0SV2eaEe4huM0x8(`J1;E?;-xvO52hp{H zQ*kDDjI_PK`}Ra#pZmEf0BN!H^sqHSq##ned$M#AQvh%8pNZoEIfP%1j}O8&63hVb z(oRge;Wz_#{8s761OUg2O7MD%v}838HGpdoe}ob}9~A)BV2z6xC%tfwjhvSX09tgd zI|r_ylH)lmc+LF808lTn%9RokwdddvMwu5Pwz0I!x6cv^@qX`g zKy_aE;13wNQNRK~O}-m|rvTmcsWIfX-`%6c*`|O2AR*}O{R4pYt?mf>P!?b;I&lC% z=i}{teGRzq6=#Hf$_e&~2#x|609AY=yl+}8=bjk5yGmyRJPk1bT;A)C@;;V zcjS760)Xrbkr7~#=PH181=Ii{J+6e^(AjnfdFwsBKm`DlwFOa93bJsB3P713wGIIQ zxO}U5^eR|8VWr;8robcxRRC+yY8P}ynaHMq0#Mfh*7;^}z5jA0Tv82)0btPUS~_9{ zaH1w%>8}+4cL;%%(BHsgy&S(aJV^k9fB>p9708NIj|u;6%Y!g@Dz%K}Jp85k~+U zg2GFzJ)627M|^FqQvv89klV7y>1qc{31{(n^nBC+t_ec4_~JJ7oTnju@+21E|3_=# z)zmfkkK45S{=*~!#tHHul+mVgyndXgrsyu6!4v?ecufbAqc%u{?^gKMPxmziAdQ6a zya*8dbaNP;OWJe+z%9(DIoj?$FXL0yH7^D7bPP8DdI!x5Rwzxvf6Vz%>G \ No newline at end of file From a6caad7e446983016d4facd214c7d1f1c5c293ee Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 20 Dec 2024 15:05:06 -0500 Subject: [PATCH 03/22] Put help text into info buttons. --- glue_ar/common/info.png | Bin 1492 -> 0 bytes glue_ar/qt/export_dialog.py | 48 ++++++--- glue_ar/qt/export_dialog.ui | 138 ++++++++++++------------- glue_ar/resources/info.png | Bin 0 -> 1621 bytes glue_ar/{common => resources}/info.svg | 2 +- 5 files changed, 105 insertions(+), 83 deletions(-) delete mode 100644 glue_ar/common/info.png create mode 100644 glue_ar/resources/info.png rename glue_ar/{common => resources}/info.svg (88%) diff --git a/glue_ar/common/info.png b/glue_ar/common/info.png deleted file mode 100644 index 4a2f32f793b8ba3e0771608abc1c1a02090249f4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1492 zcmV;_1uOcAP)Px)j!8s8RCr$PooiOxFc5|9Y=J^yHHDS6tc-LuP$*l7dThsu?ZlZoPl;6gXb+9; zk?uDyDX!f%^(U(WZC2~mcvAsnc7h6^0?4dDX7e=yDuB!iWHw(TpaQt8!0X4yV}HbM z)BoMJ>BsSmcKEDG+y2kg{+?ZJv)ex0-u9oB`cqs4jDokR{n<35z*_mzwuAHV{pOeU zO4jEW0iY9o5WM&Cy;oaLb-dmHqbVdhvH~~??=AW|%#@nvoRN_QKncRnk>pEJp#aJg zet{u1&(WzV1VHJ+FB4Q`T5<<4h`)L9Dyg2=jR4^a;PubvueNRW-HlYLNsqtT&3gK2 zkH1_m3~L6h?QU)1Tcq6p=)_Ok)TZZ7MD0SV2eaEe4huM0x8(`J1;E?;-xvO52hp{H zQ*kDDjI_PK`}Ra#pZmEf0BN!H^sqHSq##ned$M#AQvh%8pNZoEIfP%1j}O8&63hVb z(oRge;Wz_#{8s761OUg2O7MD%v}838HGpdoe}ob}9~A)BV2z6xC%tfwjhvSX09tgd zI|r_ylH)lmc+LF808lTn%9RokwdddvMwu5Pwz0I!x6cv^@qX`g zKy_aE;13wNQNRK~O}-m|rvTmcsWIfX-`%6c*`|O2AR*}O{R4pYt?mf>P!?b;I&lC% z=i}{teGRzq6=#Hf$_e&~2#x|609AY=yl+}8=bjk5yGmyRJPk1bT;A)C@;;V zcjS760)Xrbkr7~#=PH181=Ii{J+6e^(AjnfdFwsBKm`DlwFOa93bJsB3P713wGIIQ zxO}U5^eR|8VWr;8robcxRRC+yY8P}ynaHMq0#Mfh*7;^}z5jA0Tv82)0btPUS~_9{ zaH1w%>8}+4cL;%%(BHsgy&S(aJV^k9fB>p9708NIj|u;6%Y!g@Dz%K}Jp85k~+U zg2GFzJ)627M|^FqQvv89klV7y>1qc{31{(n^nBC+t_ec4_~JJ7oTnju@+21E|3_=# z)zmfkkK45S{=*~!#tHHul+mVgyndXgrsyu6!4v?ecufbAqc%u{?^gKMPxmziAdQ6a zya*8dbaNP;OWJe+z%9(DIoj?$FXL0yH7^D7bPP8DdI!x5Rwzxvf6Vz%>G QLabel: - label = QLabel(text=cb_property.__doc__) - label.setWordWrap(True) - return label + def _doc_button(self, cb_property) -> QPushButton: + button = QPushButton() + button.setCheckable(False) + button.setFlat(True) + icon_path = os.path.join(RESOURCES_DIR, "info.png") + icon = QIcon(icon_path) + button.setIcon(icon) + button.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)) + + # We want the tooltip to show immediately, rather than have a delay + button.enterEvent = lambda event: self._doc_enter_event(event, cb_property=cb_property) + button.leaveEvent = self._doc_leave_event + + return button + + def _doc_enter_event(self, event: QEnterEvent, cb_property: CallbackProperty): + print(dir(event)) + QToolTip.showText(QCursor.pos(), cb_property.__doc__ or "") + + def _doc_leave_event(self, _event: QEvent): + QToolTip.hideText() def _widgets_for_property(self, instance: HasCallbackProperties, @@ -52,17 +72,19 @@ def _widgets_for_property(self, widget.setText(display_name) self._layer_connections.append(connect_checkable_button(instance, property, widget)) if cb_property.__doc__: - label = self._doc_label(cb_property) - widgets.append((label,)) - widgets.append((widget,)) + label = self._doc_button(cb_property) + widgets.append((label, widget)) + else: + widgets.append((widget,)) elif t in (int, float): widgets: List[Tuple[QWidget]] = [] label = QLabel() prompt = f"{display_name}:" label.setText(prompt) - widgets.append((label,)) if cb_property.__doc__: - label = self._doc_label(cb_property) + info_button = self._doc_button(cb_property) + widgets.append((label, info_button)) + else: widgets.append((label,)) widget = QSlider() policy = QSizePolicy() @@ -146,7 +168,7 @@ def _update_layer_ui(self, state: State): for widget in widgets: subrow.addWidget(widget) row.addLayout(subrow) - self.ui.layer_layout.addRow(row) + self.ui.layer_layout.addLayout(row) def _on_filetype_change(self, filetype: str): super()._on_filetype_change(filetype) diff --git a/glue_ar/qt/export_dialog.ui b/glue_ar/qt/export_dialog.ui index c5b678d..323ea87 100644 --- a/glue_ar/qt/export_dialog.ui +++ b/glue_ar/qt/export_dialog.ui @@ -14,74 +14,7 @@ Export 3D File - - - - <html><head/><body><p align="center"><span style=" font-weight:600;">Layer Options</span></p></body></html> - - - - - - - - - - - - - Compression method - - - - - - Export model-viewer HTML - - - true - - - - - - - - - - <html><head/><body><p align="center"><span style=" font-weight:600;">File Options</span></p></body></html> - - - - - - - - - - Filetype - - - - - - - Qt::Horizontal - - - - - - - Select export options - - - - - - - @@ -120,14 +53,81 @@ - + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">File Options</span></p></body></html> + + + + + + + Qt::Horizontal + + + + + + + + + + <html><head/><body><p align="center"><span style=" font-weight:600;">Layer Options</span></p></body></html> + + + + + + + Compression method + + + + Export method: - + + + + + + + + + + Select export options + + + + + + + + + + Export model-viewer HTML + + + true + + + + + + + Filetype + + + + + + + Qt::Horizontal diff --git a/glue_ar/resources/info.png b/glue_ar/resources/info.png new file mode 100644 index 0000000000000000000000000000000000000000..9e078d8881655d03dc59fc7b2e37eca82de9a224 GIT binary patch literal 1621 zcmV-b2CDgqP)Px*4@pEpRCr$Poeh!{Hw=ZFlLStZ~N%pYrSI54lTHw{x-goE61~AzPHh>LavIQo0zawA+m~4T`-R}t404^=? z`Zx~z`suv0{T+V(eEuHa?dOlp@BW!=i~tz*x8pc|Ilp@CXHecB#{n=)0a*or z8U5|)@x4aBdOv_H17kLT-~W}33Zp*)Vle(3N&KqqSpXm}e-(JUYFB`GH$$GP0YGYC zcmP=ODhE*oz%>4k6|H_}Ovn+CB}?GLhFt*w-v0Nn8H{Tf#s>tz7

YfCLN4>A5UX zS{rSRkJk`gxNLoZpRs1ZzX9J~kBnh>k{$^Fh6jMiWfT99AOjie3P|UWN@)auXrdkN zZB105{Pji##`kN2Lwx6u*#Iyp=)Grh6^6f!!e~*D4j~gj#Q4KPThzS>h*Suf0O0v* zeUJSZg!3ww*LeWpO~&A}06eqt&5qt^KzWMQbJvvt00~W9V9k~s#aN~D=>_0fWI=M4 z+6um)p{`%Lw=Q}BAY?x&E_LetvlzdXBxQbGNx27ry3l$i&%gC2H4?)4APPX0o@dZ} z7Q|{!2sv65fKv60EHxp2_F;~F7L1By)b6DMAvE6z0LA-5gJG`NY`Wdk<}NKJeXH9m z?kI==P`n?Dlu_Pzm3wA@Z`rvQXkGwHQW~xM@cu`8O>P5-QD?U)QUocKKSS^WxSG5T z(%Yuf2LF}5SDSq!`&UYDmlJpft7!jd?+2_a2@IEm4lhttK+Xjo07~8OY0oj|?{2;F z!jZYBU7)coci9m=24kV`(!Qwmn@Y(3cLhQo$+zS~^s|2!1+olz0DL7ztMI-~ZfS2< za)Bg6DgZ{jLVMG!m(&m|$0Zq30kF<0`m< zOP=&f0GM-9+b9vh!i=u=UNC^}Z5jmtOEl8HcOrz9B7jwhbeBK@z)CIHmUi?40WinJ zl<6*k0ssxMQj}`l3k1NVnI>S^Lf71a$#h!N))6Y>(W?h763(p-Q{=$fU*K~ zg`vj~9aK!x;Rgp7*+_x*bAM*Yl0C+Fk zinW=#l9JmzW8a|2ZCA8MB?z6WQP6#&$XFXcY+aq7tM0$;tp@3Hz{RpVDEc|oSLN2sg4#%6e?@+4xLFC>Bb#mHraosN8-SIk zc9@dKODjM`JU&(PN9BNfa)bx*sK;48ii(Vf?wHYg9tt#Uhj&B-w8o5t`8^m&dWeuO z=ZkhsnH>rRBij)+_->E9iaJ0$hM`s4XaFWj%JGjLxvIFgWCdRVz##NUQiZ{9y*}gF z1$5k10HW&!I)UUHAax^8U;JUX)c{rnQ6e|@oMjdO8w>W4B2Tl(a{P+Y_j0VW0kjd3 z`TWSMNt@A2G@g0NDgfGXqKrh%Za~-^l-HLsdOP410o=l9Yy6+j7wvcWZfk+twE@-) zcAlI9Y05pT1DIlz%1$?cN^9TgYXjH-CR<=~_d5bMfXNn^-2IM#4PdebCU^f0hT?p1 T5apPD00000NkvXXu0mjfkP_>F literal 0 HcmV?d00001 diff --git a/glue_ar/common/info.svg b/glue_ar/resources/info.svg similarity index 88% rename from glue_ar/common/info.svg rename to glue_ar/resources/info.svg index 0d4efd1..42037e2 100644 --- a/glue_ar/common/info.svg +++ b/glue_ar/resources/info.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From 2b438c5f58c086c0d442c47057caec567b3ecfe6 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 20 Dec 2024 15:18:03 -0500 Subject: [PATCH 04/22] Get sizing of info buttons in Qt dialogs working correctly. --- glue_ar/common/scatter_export_options.py | 2 +- glue_ar/qt/export_dialog.py | 22 +++++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/glue_ar/common/scatter_export_options.py b/glue_ar/common/scatter_export_options.py index ea7a7f2..4ef894a 100644 --- a/glue_ar/common/scatter_export_options.py +++ b/glue_ar/common/scatter_export_options.py @@ -12,7 +12,7 @@ class ARVispyScatterExportOptions(State): min_value=3, max_value=50, resolution=1, - docstring="Controls the resolution of the sphere meshes used for scatter points." + docstring="Controls the resolution of the sphere meshes used for scatter points. " "Higher means better resolution, but a larger filesize.", ) diff --git a/glue_ar/qt/export_dialog.py b/glue_ar/qt/export_dialog.py index 871c7fb..4465e28 100644 --- a/glue_ar/qt/export_dialog.py +++ b/glue_ar/qt/export_dialog.py @@ -11,7 +11,7 @@ from glue_ar.common.export_dialog_base import ARExportDialogBase from qtpy.QtCore import Qt, QEvent -from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QPushButton, QToolTip, QVBoxLayout, QLabel, QLayout, QSizePolicy, QSlider, QWidget +from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLayoutItem, QPushButton, QSpacerItem, QToolTip, QVBoxLayout, QLabel, QLayout, QSizePolicy, QSlider, QWidget from glue_ar.utils import RESOURCES_DIR @@ -51,12 +51,15 @@ def _doc_button(self, cb_property) -> QPushButton: return button def _doc_enter_event(self, event: QEnterEvent, cb_property: CallbackProperty): - print(dir(event)) - QToolTip.showText(QCursor.pos(), cb_property.__doc__ or "") + # Make the tooltip be rich text so that it will line wrap + QToolTip.showText(QCursor.pos(), f"{cb_property.__doc__}" or "") def _doc_leave_event(self, _event: QEvent): QToolTip.hideText() + def _horizontal_spacer(self) -> QSpacerItem: + return QSpacerItem(20, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + def _widgets_for_property(self, instance: HasCallbackProperties, property: str, @@ -72,8 +75,9 @@ def _widgets_for_property(self, widget.setText(display_name) self._layer_connections.append(connect_checkable_button(instance, property, widget)) if cb_property.__doc__: - label = self._doc_button(cb_property) - widgets.append((label, widget)) + button = self._doc_button(cb_property) + spacer = self._horizontal_spacer() + widgets.append((widget, button, spacer)) else: widgets.append((widget,)) elif t in (int, float): @@ -83,7 +87,8 @@ def _widgets_for_property(self, label.setText(prompt) if cb_property.__doc__: info_button = self._doc_button(cb_property) - widgets.append((label, info_button)) + spacer = self._horizontal_spacer() + widgets.append((label, info_button, spacer)) else: widgets.append((label,)) widget = QSlider() @@ -166,7 +171,10 @@ def _update_layer_ui(self, state: State): for widgets in widget_tuples: subrow = QHBoxLayout() for widget in widgets: - subrow.addWidget(widget) + if isinstance(widget, QWidget): + subrow.addWidget(widget) + elif isinstance(widget, QLayoutItem): + subrow.addItem(widget) row.addLayout(subrow) self.ui.layer_layout.addLayout(row) From 6a4ce400511c4a14a97995991a4269ae08907cdd Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 20 Dec 2024 16:46:08 -0500 Subject: [PATCH 05/22] Add missing spaces to callback property docstrings. --- glue_ar/common/volume_export_options.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glue_ar/common/volume_export_options.py b/glue_ar/common/volume_export_options.py index 8676f69..eb19400 100644 --- a/glue_ar/common/volume_export_options.py +++ b/glue_ar/common/volume_export_options.py @@ -21,7 +21,7 @@ class ARVoxelExportOptions(State): min_value=0.01, max_value=1, resolution=0.01, - docstring="The minimum opacity voxels to retain. Voxels with a lower opacity will be" + docstring="The minimum opacity voxels to retain. Voxels with a lower opacity will be " "omitted from the export.", ) opacity_resolution = RangedCallbackProperty( @@ -29,6 +29,6 @@ class ARVoxelExportOptions(State): min_value=0.005, max_value=1, resolution=0.005, - docstring="The resolution of the opacity in the exported figure. Opacity values will be" + docstring="The resolution of the opacity in the exported figure. Opacity values will be " "rounded to the nearest integer multiple of this value.", ) From d3ad15ee38284234a70cd425ecf6d19b3813f353 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 20 Dec 2024 17:14:09 -0500 Subject: [PATCH 06/22] Add info buttons to Jupyter dialog. Still a WIP, need to adjust positioning and tooltips. --- glue_ar/jupyter/export_dialog.py | 37 ++++++++++++++++++++++++++------ glue_ar/qt/export_dialog.py | 8 +++---- 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/glue_ar/jupyter/export_dialog.py b/glue_ar/jupyter/export_dialog.py index e6f329c..9bc4d07 100644 --- a/glue_ar/jupyter/export_dialog.py +++ b/glue_ar/jupyter/export_dialog.py @@ -1,16 +1,19 @@ import ipyvuetify as v # noqa from ipyvuetify.VuetifyTemplate import VuetifyTemplate from ipywidgets import DOMWidget, widget_serialization +import os import traitlets from typing import Callable, List, Optional -from echo import HasCallbackProperties +from echo import CallbackProperty, HasCallbackProperties from glue.core.state_objects import State from glue.viewers.common.viewer import Viewer +from glue_jupyter.common.toolbar_vuetify import read_icon 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.utils import RESOURCES_DIR class JupyterARExportDialog(ARExportDialogBase, VuetifyTemplate): @@ -84,20 +87,37 @@ def _on_filetype_change(self, filetype: str): self.show_compression = gl self.show_modelviewer = gl + def _doc_button(self, cb_property: CallbackProperty) -> v.Tooltip: + img_path = os.path.join(RESOURCES_DIR, "info.png") + icon_src = read_icon(img_path, "image/png") + button = v.Tooltip( + top=True, + v_slots=[{ + "name": "activator", + "variable": "tooltip", + "children": [ + v.Img(src=icon_src, height=20, width=20) + ], + }], + children=[cb_property.__doc__], + ) + return button + def widgets_for_property(self, instance: HasCallbackProperties, property: str, display_name: str) -> List[DOMWidget]: value = getattr(instance, property) + instance_type = type(instance) + cb_property = getattr(instance_type, property) t = type(value) + widgets = [] if t is bool: widget = v.Checkbox(label=display_name) link((instance, property), (widget, 'value')) - return [widget] + widgets.append(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) @@ -111,9 +131,12 @@ def widgets_for_property(self, link((instance, property), (widget, 'v_model')) - return [widget] - else: - return [] + widgets.append(widget) + + if cb_property.__doc__: + widgets.append(self._doc_button(cb_property)) + + return widgets def vue_cancel_dialog(self, *args): self.state_dictionary = {} diff --git a/glue_ar/qt/export_dialog.py b/glue_ar/qt/export_dialog.py index 4465e28..dc95645 100644 --- a/glue_ar/qt/export_dialog.py +++ b/glue_ar/qt/export_dialog.py @@ -35,7 +35,7 @@ def __init__(self, parent=None, viewer=None): self.ui.button_cancel.clicked.connect(self.reject) self.ui.button_ok.clicked.connect(self.accept) - def _doc_button(self, cb_property) -> QPushButton: + def _doc_button(self, cb_property: CallbackProperty) -> QPushButton: button = QPushButton() button.setCheckable(False) button.setFlat(True) @@ -58,7 +58,7 @@ def _doc_leave_event(self, _event: QEvent): QToolTip.hideText() def _horizontal_spacer(self) -> QSpacerItem: - return QSpacerItem(20, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + return QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) def _widgets_for_property(self, instance: HasCallbackProperties, @@ -77,7 +77,7 @@ def _widgets_for_property(self, if cb_property.__doc__: button = self._doc_button(cb_property) spacer = self._horizontal_spacer() - widgets.append((widget, button, spacer)) + widgets.append((widget, spacer, button)) else: widgets.append((widget,)) elif t in (int, float): @@ -88,7 +88,7 @@ def _widgets_for_property(self, if cb_property.__doc__: info_button = self._doc_button(cb_property) spacer = self._horizontal_spacer() - widgets.append((label, info_button, spacer)) + widgets.append((label, spacer, info_button)) else: widgets.append((label,)) widget = QSlider() From 77cdebdf25739997de887d1ea65d307de36dc9ee Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Fri, 20 Dec 2024 17:15:35 -0500 Subject: [PATCH 07/22] Tooltips are now displaying. --- glue_ar/jupyter/export_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue_ar/jupyter/export_dialog.py b/glue_ar/jupyter/export_dialog.py index 9bc4d07..cffe007 100644 --- a/glue_ar/jupyter/export_dialog.py +++ b/glue_ar/jupyter/export_dialog.py @@ -96,7 +96,7 @@ def _doc_button(self, cb_property: CallbackProperty) -> v.Tooltip: "name": "activator", "variable": "tooltip", "children": [ - v.Img(src=icon_src, height=20, width=20) + v.Img(v_on="tooltip.on", src=icon_src, height=20, width=20) ], }], children=[cb_property.__doc__], From 273ac14029b307719859793749f821af3af306d7 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sat, 21 Dec 2024 15:52:48 -0500 Subject: [PATCH 08/22] Update Jupyter dialog sliders to show docstrings. --- glue_ar/jupyter/export_dialog.py | 49 ++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/glue_ar/jupyter/export_dialog.py b/glue_ar/jupyter/export_dialog.py index cffe007..59275e8 100644 --- a/glue_ar/jupyter/export_dialog.py +++ b/glue_ar/jupyter/export_dialog.py @@ -34,7 +34,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.Row).tag(sync=True, **widget_serialization) has_layer_options = traitlets.Bool().tag(sync=True) modelviewer = traitlets.Bool(True).tag(sync=True) @@ -45,8 +45,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.Row() VuetifyTemplate.__init__(self) self._on_layer_change(self.state.layer) @@ -73,7 +74,7 @@ def _update_layer_ui(self, state: State): 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) + self.layer_layout = v.Row(children=widgets, px_0=True, py_0=True) self.has_layer_options = len(self.layer_layout.children) > 0 def _on_method_change(self, method_name: str): @@ -96,7 +97,10 @@ def _doc_button(self, cb_property: CallbackProperty) -> v.Tooltip: "name": "activator", "variable": "tooltip", "children": [ - v.Img(v_on="tooltip.on", src=icon_src, height=20, width=20) + v.Img(v_on="tooltip.on", src=icon_src, + height=20, width=20, + max_width=20, max_height=20 + ), ], }], children=[cb_property.__doc__], @@ -123,11 +127,38 @@ def widgets_for_property(self, 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}") + widget = v.Slider( + min=min, + max=max, + step=step, + label=display_name, + thumb_label=f"{value:g}", + v_slots=[{ + "name": "append", + "children": [ + v.Tooltip( + top=True, + dark=v.theme.dark, + disabled=not cb_property.__doc__, + children=[v.Html(tag="p", + children=[(cb_property.__doc__ or "").replace(". ", ".
")] + ), + ], + v_slots=[{ + "name": "activator", + "variable": "tooltip", + "children": [ + v.Icon( + small=True, + v_on="tooltip.on", + src="mdi-info", + ) + ], + }], + ), + ], + }], + ) link((instance, property), (widget, 'v_model')) From db46d1e06d14b15f1295dad7946279ec26cbcde6 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sat, 21 Dec 2024 16:49:15 -0500 Subject: [PATCH 09/22] Adjust layout for layer options in Jupyter dialog. --- glue_ar/jupyter/export_dialog.py | 49 ++++++++++---------------------- 1 file changed, 15 insertions(+), 34 deletions(-) diff --git a/glue_ar/jupyter/export_dialog.py b/glue_ar/jupyter/export_dialog.py index 59275e8..7d46008 100644 --- a/glue_ar/jupyter/export_dialog.py +++ b/glue_ar/jupyter/export_dialog.py @@ -34,7 +34,7 @@ class JupyterARExportDialog(ARExportDialogBase, VuetifyTemplate): method_items = traitlets.List().tag(sync=True) method_selected = traitlets.Int().tag(sync=True) - layer_layout = traitlets.Instance(v.Row).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) @@ -47,7 +47,7 @@ def __init__(self, on_export: Optional[Callable] = None): ARExportDialogBase.__init__(self, viewer=viewer) - self.layer_layout = v.Row() + self.layer_layout = v.Col() VuetifyTemplate.__init__(self) self._on_layer_change(self.state.layer) @@ -69,12 +69,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.Row(children=widgets, px_0=True, py_0=True) + widgets = self.widgets_for_property(state, property, name) + input_widgets.extend(w for w in widgets if isinstance(w, v.Slider)) + rows.append(v.Row(children=widgets)) + + 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): @@ -132,41 +137,17 @@ def widgets_for_property(self, max=max, step=step, label=display_name, + hide_details=True, thumb_label=f"{value:g}", - v_slots=[{ - "name": "append", - "children": [ - v.Tooltip( - top=True, - dark=v.theme.dark, - disabled=not cb_property.__doc__, - children=[v.Html(tag="p", - children=[(cb_property.__doc__ or "").replace(". ", ".
")] - ), - ], - v_slots=[{ - "name": "activator", - "variable": "tooltip", - "children": [ - v.Icon( - small=True, - v_on="tooltip.on", - src="mdi-info", - ) - ], - }], - ), - ], - }], ) link((instance, property), (widget, 'v_model')) widgets.append(widget) - if cb_property.__doc__: - widgets.append(self._doc_button(cb_property)) - + if cb_property.__doc__: + widgets.append(self._doc_button(cb_property)) + return widgets def vue_cancel_dialog(self, *args): From 9e13010c8af0bbe1ee9d718664c2723e2f47115c Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 00:45:25 -0500 Subject: [PATCH 10/22] Refactor Qt callback property widget functionality out of dialog. Update and add additional tests. --- glue_ar/common/tests/test_base_dialog.py | 6 +- glue_ar/qt/export_dialog.py | 106 +---------------- glue_ar/qt/tests/test_dialog.py | 31 ----- glue_ar/qt/tests/test_widgets.py | 142 +++++++++++++++++++++++ glue_ar/qt/widget_utils.py | 137 ++++++++++++++++++++++ 5 files changed, 287 insertions(+), 135 deletions(-) create mode 100644 glue_ar/qt/tests/test_widgets.py create mode 100644 glue_ar/qt/widget_utils.py diff --git a/glue_ar/common/tests/test_base_dialog.py b/glue_ar/common/tests/test_base_dialog.py index 1ed3081..c6fea5f 100644 --- a/glue_ar/common/tests/test_base_dialog.py +++ b/glue_ar/common/tests/test_base_dialog.py @@ -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: diff --git a/glue_ar/qt/export_dialog.py b/glue_ar/qt/export_dialog.py index dc95645..b8c8d7c 100644 --- a/glue_ar/qt/export_dialog.py +++ b/glue_ar/qt/export_dialog.py @@ -7,13 +7,12 @@ 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 qtpy.QtGui import QCursor, QEnterEvent, QIcon from glue_ar.common.export_dialog_base import ARExportDialogBase -from qtpy.QtCore import Qt, QEvent -from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLayoutItem, QPushButton, QSpacerItem, QToolTip, QVBoxLayout, QLabel, QLayout, QSizePolicy, QSlider, QWidget +from qtpy.QtCore import Qt +from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLayoutItem, QVBoxLayout, QLabel, QLayout, QSizePolicy, QSlider, QWidget -from glue_ar.utils import RESOURCES_DIR +from glue_ar.qt.widget_utils import horizontal_spacer, info_button, widgets_for_callback_property __all__ = ['QtARExportDialog'] @@ -35,102 +34,6 @@ def __init__(self, parent=None, viewer=None): self.ui.button_cancel.clicked.connect(self.reject) self.ui.button_ok.clicked.connect(self.accept) - def _doc_button(self, cb_property: CallbackProperty) -> QPushButton: - button = QPushButton() - button.setCheckable(False) - button.setFlat(True) - icon_path = os.path.join(RESOURCES_DIR, "info.png") - icon = QIcon(icon_path) - button.setIcon(icon) - button.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)) - - # We want the tooltip to show immediately, rather than have a delay - button.enterEvent = lambda event: self._doc_enter_event(event, cb_property=cb_property) - button.leaveEvent = self._doc_leave_event - - return button - - def _doc_enter_event(self, event: QEnterEvent, cb_property: CallbackProperty): - # Make the tooltip be rich text so that it will line wrap - QToolTip.showText(QCursor.pos(), f"{cb_property.__doc__}" or "") - - def _doc_leave_event(self, _event: QEvent): - QToolTip.hideText() - - def _horizontal_spacer(self) -> QSpacerItem: - return QSpacerItem(40, 20, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) - - def _widgets_for_property(self, - instance: HasCallbackProperties, - property: str, - display_name: str) -> List[Tuple[QWidget]]: - value = getattr(instance, property) - instance_type = type(instance) - cb_property = getattr(instance_type, property) - t = type(value) - widgets: List[Tuple[QWidget]] = [] - if t is bool: - widget = QCheckBox() - widget.setChecked(value) - widget.setText(display_name) - self._layer_connections.append(connect_checkable_button(instance, property, widget)) - if cb_property.__doc__: - button = self._doc_button(cb_property) - spacer = self._horizontal_spacer() - widgets.append((widget, spacer, button)) - else: - widgets.append((widget,)) - elif t in (int, float): - widgets: List[Tuple[QWidget]] = [] - label = QLabel() - prompt = f"{display_name}:" - label.setText(prompt) - if cb_property.__doc__: - info_button = self._doc_button(cb_property) - spacer = self._horizontal_spacer() - widgets.append((label, spacer, info_button)) - else: - widgets.append((label,)) - widget = QSlider() - policy = QSizePolicy() - policy.setHorizontalPolicy(QSizePolicy.Policy.Expanding) - policy.setVerticalPolicy(QSizePolicy.Policy.Fixed) - widget.setOrientation(Qt.Orientation.Horizontal) - - widget.setSizePolicy(policy) - - value_label = QLabel() - 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 - places = -floor(log(step, 10)) - - def update_label(value): - value_label.setText(f"{value:.{places}f}") - - def remove_label_callback(widget, update_label=update_label): - try: - remove_callback(instance, property, update_label) - except ValueError: - pass - - def on_widget_destroyed(widget, cb=remove_label_callback): - cb(widget) - - update_label(value) - add_callback(instance, property, update_label) - widget.destroyed.connect(on_widget_destroyed) - - steps = round((max - min) / step) - widget.setMinimum(0) - widget.setMaximum(steps) - self._layer_connections.append(connect_value(instance, property, widget, value_range=(min, max))) - widgets.append((widget, value_label)) - - return widgets - def _clear_layout(self, layout: QLayout): if layout is not None: while layout.count(): @@ -167,7 +70,8 @@ def _update_layer_ui(self, state: State): for property in state.callback_properties(): row = QVBoxLayout() name = self.display_name(property) - widget_tuples = self._widgets_for_property(state, property, name) + widget_tuples, connection = widgets_for_callback_property(state, property, name) + self._layer_connections.append(connection) for widgets in widget_tuples: subrow = QHBoxLayout() for widget in widgets: diff --git a/glue_ar/qt/tests/test_dialog.py b/glue_ar/qt/tests/test_dialog.py index ad7557f..063f6c8 100644 --- a/glue_ar/qt/tests/test_dialog.py +++ b/glue_ar/qt/tests/test_dialog.py @@ -6,7 +6,6 @@ from glue_qt.app import GlueApplication from glue_vispy_viewers.volume.qt.volume_viewer import VispyVolumeViewer -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 @@ -72,36 +71,6 @@ def test_filetype_change(self): assert ui.combosel_compression.isVisible() assert ui.label_compression_message.isVisible() - def test_widgets_for_property(self): - state = DummyState() - - int_widgets = self.dialog._widgets_for_property(state, "cb_int", "Int CB") - assert len(int_widgets) == 3 - label, slider, value_label = int_widgets - assert isinstance(label, QLabel) - assert label.text() == "Int CB:" - assert isinstance(slider, QSlider) - assert slider.value() == 1 # 2 is the second (index 1) step value - 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) == 3 - label, slider, value_label = float_widgets - assert isinstance(label, QLabel) - assert label.text() == "Float CB:" - assert isinstance(slider, QSlider) - assert slider.value() == 69 # Another value -> index thing (see above comment) - assert isinstance(value_label, QLabel) - assert value_label.text() == "0.70" - - bool_widgets = self.dialog._widgets_for_property(state, "cb_bool", "Bool CB") - assert len(bool_widgets) == 1 - box = bool_widgets[0] - assert isinstance(box, QCheckBox) - assert box.text() == "Bool CB" - assert not box.isChecked() - def test_update_layer_ui(self): state = DummyState() self.dialog._update_layer_ui(state) diff --git a/glue_ar/qt/tests/test_widgets.py b/glue_ar/qt/tests/test_widgets.py new file mode 100644 index 0000000..45061da --- /dev/null +++ b/glue_ar/qt/tests/test_widgets.py @@ -0,0 +1,142 @@ +from pytest import importorskip + +importorskip("glue_qt") + +import sys + +from echo import CallbackProperty +from echo.qt import connect_checkable_button, connect_value +from qtpy.QtWidgets import QApplication, QPushButton, QSpacerItem, QCheckBox, QLabel, QSlider, QSpacerItem + +from glue_ar.common.tests.test_base_dialog import DummyState +from glue_ar.qt.widget_utils import boolean_callback_widgets, horizontal_spacer, info_button, info_tooltip, widgets_for_callback_property + + +class TestQtWidgets: + + def setup_method(self, method): + self.app = QApplication(sys.argv) + + def teardown_method(self, method): + self.app.quit() + + def test_info_tooltip(self): + 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_horizontal_spacer(self): + spacer = horizontal_spacer(width=60, height=80) + assert isinstance(spacer, QSpacerItem) + + + def test_info_button(self): + state = DummyState() + for property in state.callback_properties(): + print(property) + cb_property: CallbackProperty = getattr(DummyState, property) + button = info_button(cb_property) + assert isinstance(button, QPushButton) + + + def test_boolean_callback_widgets(self): + state = DummyState() + + widget_tuples, connection = boolean_callback_widgets(state, "cb_bool", "Bool CB") + assert isinstance(connection, connect_checkable_button) + + assert len(widget_tuples) == 1 + box, spacer, info_button = widget_tuples[0] + assert isinstance(box, QCheckBox) + assert box.text() == "Bool CB" + assert not box.isChecked() + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) + + + def test_integer_callback_widgets(self): + state = DummyState() + widget_rows, connection = widgets_for_callback_property(state, "cb_int", "Int CB") + + assert isinstance(connection, connect_value) + + assert len(widget_rows) == 2 + label, spacer, info_button = widget_rows[0] + assert isinstance(label, QLabel) + assert label.text() == "Int CB:" + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) + + slider, value_label = widget_rows[1] + assert isinstance(slider, QSlider) + assert slider.value() == 1 # 2 is the second (index 1) step value + assert isinstance(value_label, QLabel) + assert value_label.text() == "2" + + + def test_float_callback_widgets(self): + state = DummyState() + widget_rows, connection = widgets_for_callback_property(state, "cb_float", "Float CB") + + assert isinstance(connection, connect_value) + assert len(widget_rows) == 2 + + label, spacer, info_button = widget_rows[0] + assert isinstance(label, QLabel) + assert label.text() == "Float CB:" + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) + + slider, value_label = widget_rows[1] + assert isinstance(slider, QSlider) + assert slider.value() == 69 # Another value -> index thing (see above comment) + assert isinstance(value_label, QLabel) + assert value_label.text() == "0.70" + + + def test_widgets_for_callback_property(self): + state = DummyState() + + int_widget_rows, connection = widgets_for_callback_property(state, "cb_int", "Int CB") + assert isinstance(connection, connect_value) + assert len(int_widget_rows) == 2 + + label, spacer, info_button = int_widget_rows[0] + assert isinstance(label, QLabel) + assert label.text() == "Int CB:" + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) + + slider, value_label = int_widget_rows[1] + assert isinstance(slider, QSlider) + assert slider.value() == 1 # 2 is the second (index 1) step value + assert isinstance(value_label, QLabel) + assert value_label.text() == "2" + + float_widget_rows, connection = widgets_for_callback_property(state, "cb_float", "Float CB") + assert isinstance(connection, connect_value) + assert len(float_widget_rows) == 2 + + label, spacer, info_button = float_widget_rows[0] + assert isinstance(label, QLabel) + assert label.text() == "Float CB:" + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) + + slider, value_label = float_widget_rows[1] + assert isinstance(slider, QSlider) + assert slider.value() == 69 # Another value -> index thing (see above comment) + assert isinstance(value_label, QLabel) + assert value_label.text() == "0.70" + + bool_widget_rows, connection = widgets_for_callback_property(state, "cb_bool", "Bool CB") + assert isinstance(connection, connect_checkable_button) + assert len(bool_widget_rows) == 1 + + box, spacer, info_button = bool_widget_rows[0] + assert isinstance(box, QCheckBox) + assert box.text() == "Bool CB" + assert not box.isChecked() + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) diff --git a/glue_ar/qt/widget_utils.py b/glue_ar/qt/widget_utils.py new file mode 100644 index 0000000..7d74e42 --- /dev/null +++ b/glue_ar/qt/widget_utils.py @@ -0,0 +1,137 @@ +from math import floor, log +from os.path import join +from typing import Tuple + +from echo import CallbackProperty, HasCallbackProperties, add_callback, remove_callback +from echo.qt import BaseConnection, connect_checkable_button, connect_value +from qtpy.QtGui import QCursor, QEnterEvent, QIcon +from qtpy.QtCore import Qt, QEvent +from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLayoutItem, QPushButton, QSpacerItem, QToolTip, QVBoxLayout, QLabel, QLayout, QSizePolicy, QSlider, QWidget + +from glue_ar.utils import RESOURCES_DIR + + +def info_tooltip(cb_property: CallbackProperty) -> str: + return f"{cb_property.__doc__ or ''}" + + +def info_enter_event_handler(_event: QEnterEvent, cb_property: CallbackProperty): + # Make the tooltip be rich text so that it will line wrap + QToolTip.showText(QCursor.pos(), info_tooltip(cb_property)) + + +def info_leave_event_handler(_event: QEvent): + QToolTip.hideText() + + +def horizontal_spacer(width: int = 40, height: int = 20) -> QSpacerItem: + return QSpacerItem(width, height, QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Minimum) + + +def info_button(cb_property: CallbackProperty) -> QPushButton: + button = QPushButton() + button.setCheckable(False) + button.setFlat(True) + icon_path = join(RESOURCES_DIR, "info.png") + icon = QIcon(icon_path) + button.setIcon(icon) + button.setSizePolicy(QSizePolicy(QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Minimum)) + + # We want the tooltip to show immediately, rather than have a delay + if cb_property.__doc__: + button.enterEvent = lambda event: info_enter_event_handler(event, cb_property=cb_property) + button.leaveEvent = info_leave_event_handler + + return button + + +def boolean_callback_widgets(instance: HasCallbackProperties, + property: str, + display_name: str) -> Tuple[Tuple[Tuple[QWidget]], connect_checkable_button]: + + value = getattr(instance, property) + instance_type = type(instance) + cb_property: CallbackProperty = getattr(instance_type, property) + + box = QCheckBox() + box.setChecked(value) + box.setText(display_name) + connection = connect_checkable_button(instance, property, box) + if cb_property.__doc__: + spacer = horizontal_spacer(width=40, height=20) + button = info_button(cb_property) + return ((box, spacer, button),), connection + else: + return ((box,),), connection + + +def number_callback_widgets(instance: HasCallbackProperties, + property: str, + display_name: str) -> Tuple[Tuple[Tuple[QWidget]], connect_value]: + + value = getattr(instance, property) + instance_type = type(instance) + cb_property: CallbackProperty = getattr(instance_type, property) + t = type(value) + + label = QLabel() + prompt = f"{display_name}:" + label.setText(prompt) + + slider = QSlider() + policy = QSizePolicy() + policy.setHorizontalPolicy(QSizePolicy.Policy.Expanding) + policy.setVerticalPolicy(QSizePolicy.Policy.Fixed) + slider.setOrientation(Qt.Orientation.Horizontal) + slider.setSizePolicy(policy) + + value_label = QLabel() + 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 + places = -floor(log(step, 10)) + + + def update_label(value): + value_label.setText(f"{value:.{places}f}") + + def remove_label_callback(widget, update_label=update_label): + try: + remove_callback(instance, property, update_label) + except ValueError: + pass + + def on_widget_destroyed(widget, cb=remove_label_callback): + cb(widget) + + update_label(value) + add_callback(instance, property, update_label) + slider.destroyed.connect(on_widget_destroyed) + + steps = round((max - min) / step) + slider.setMinimum(0) + slider.setMaximum(steps) + connection = connect_value(instance, property, slider, value_range=(min, max)) + + value_widgets = (slider, value_label) + if cb_property.__doc__: + button = info_button(cb_property) + spacer = horizontal_spacer(width=40, height=20) + return ((label, spacer, button), value_widgets), connection + else: + return ((label,), value_widgets), connection + + +def widgets_for_callback_property(instance: HasCallbackProperties, + property: str, + display_name: str) -> Tuple[Tuple[Tuple[QWidget]], BaseConnection]: + value = getattr(instance, property) + t = type(value) + if t is bool: + return boolean_callback_widgets(instance, property, display_name) + elif t in (int, float): + return number_callback_widgets(instance, property, display_name) + else: + raise ValueError("Unsupported callback property type!") From 95c03308e1fe220e8c3ee8b916f6cc52aa00eec2 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 00:59:18 -0500 Subject: [PATCH 11/22] Some code cleanup. --- glue_ar/qt/export_dialog.py | 11 +++-------- glue_ar/qt/tests/test_widgets.py | 8 +------- glue_ar/qt/{widget_utils.py => widgets.py} | 0 3 files changed, 4 insertions(+), 15 deletions(-) rename glue_ar/qt/{widget_utils.py => widgets.py} (100%) diff --git a/glue_ar/qt/export_dialog.py b/glue_ar/qt/export_dialog.py index b8c8d7c..4651013 100644 --- a/glue_ar/qt/export_dialog.py +++ b/glue_ar/qt/export_dialog.py @@ -1,18 +1,13 @@ -from math import floor, log import os -from typing import List, Tuple -from echo import CallbackProperty, HasCallbackProperties, add_callback -from echo.core import remove_callback -from echo.qt import autoconnect_callbacks_to_qt, connect_checkable_button, connect_value +from echo.qt import autoconnect_callbacks_to_qt 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.QtCore import Qt -from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLayoutItem, QVBoxLayout, QLabel, QLayout, QSizePolicy, QSlider, QWidget +from qtpy.QtWidgets import QDialog, QFormLayout, QHBoxLayout, QLayoutItem, QVBoxLayout, QLayout, QWidget -from glue_ar.qt.widget_utils import horizontal_spacer, info_button, widgets_for_callback_property +from glue_ar.qt.widgets import widgets_for_callback_property __all__ = ['QtARExportDialog'] diff --git a/glue_ar/qt/tests/test_widgets.py b/glue_ar/qt/tests/test_widgets.py index 45061da..f949e6c 100644 --- a/glue_ar/qt/tests/test_widgets.py +++ b/glue_ar/qt/tests/test_widgets.py @@ -9,7 +9,7 @@ from qtpy.QtWidgets import QApplication, QPushButton, QSpacerItem, QCheckBox, QLabel, QSlider, QSpacerItem from glue_ar.common.tests.test_base_dialog import DummyState -from glue_ar.qt.widget_utils import boolean_callback_widgets, horizontal_spacer, info_button, info_tooltip, widgets_for_callback_property +from glue_ar.qt.widgets import boolean_callback_widgets, horizontal_spacer, info_button, info_tooltip, widgets_for_callback_property class TestQtWidgets: @@ -25,12 +25,10 @@ def test_info_tooltip(self): assert info_tooltip(DummyState.cb_float) == "Float callback property" assert info_tooltip(DummyState.cb_bool) == "Boolean callback property" - def test_horizontal_spacer(self): spacer = horizontal_spacer(width=60, height=80) assert isinstance(spacer, QSpacerItem) - def test_info_button(self): state = DummyState() for property in state.callback_properties(): @@ -39,7 +37,6 @@ def test_info_button(self): button = info_button(cb_property) assert isinstance(button, QPushButton) - def test_boolean_callback_widgets(self): state = DummyState() @@ -54,7 +51,6 @@ def test_boolean_callback_widgets(self): assert isinstance(spacer, QSpacerItem) assert isinstance(info_button, QPushButton) - def test_integer_callback_widgets(self): state = DummyState() widget_rows, connection = widgets_for_callback_property(state, "cb_int", "Int CB") @@ -74,7 +70,6 @@ def test_integer_callback_widgets(self): assert isinstance(value_label, QLabel) assert value_label.text() == "2" - def test_float_callback_widgets(self): state = DummyState() widget_rows, connection = widgets_for_callback_property(state, "cb_float", "Float CB") @@ -94,7 +89,6 @@ def test_float_callback_widgets(self): assert isinstance(value_label, QLabel) assert value_label.text() == "0.70" - def test_widgets_for_callback_property(self): state = DummyState() diff --git a/glue_ar/qt/widget_utils.py b/glue_ar/qt/widgets.py similarity index 100% rename from glue_ar/qt/widget_utils.py rename to glue_ar/qt/widgets.py From 08f1944cd84ddd3228ecff0e3ce7d6a65883c4b5 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 12:28:13 -0500 Subject: [PATCH 12/22] Do the same type of refactoring for the Jupyter widgets and dialog. --- glue_ar/jupyter/export_dialog.py | 68 +---------------- glue_ar/jupyter/tests/test_dialog.py | 25 ------- glue_ar/jupyter/tests/test_widgets.py | 94 ++++++++++++++++++++++++ glue_ar/jupyter/widgets.py | 101 ++++++++++++++++++++++++++ 4 files changed, 199 insertions(+), 89 deletions(-) create mode 100644 glue_ar/jupyter/tests/test_widgets.py create mode 100644 glue_ar/jupyter/widgets.py diff --git a/glue_ar/jupyter/export_dialog.py b/glue_ar/jupyter/export_dialog.py index 7d46008..39c7e85 100644 --- a/glue_ar/jupyter/export_dialog.py +++ b/glue_ar/jupyter/export_dialog.py @@ -1,19 +1,16 @@ import ipyvuetify as v # noqa from ipyvuetify.VuetifyTemplate import VuetifyTemplate -from ipywidgets import DOMWidget, widget_serialization -import os +from ipywidgets import widget_serialization import traitlets -from typing import Callable, List, Optional +from typing import Callable, Optional -from echo import CallbackProperty, HasCallbackProperties from glue.core.state_objects import State from glue.viewers.common.viewer import Viewer -from glue_jupyter.common.toolbar_vuetify import read_icon 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.utils import RESOURCES_DIR +from glue_ar.jupyter.widgets import widgets_for_callback_property class JupyterARExportDialog(ARExportDialogBase, VuetifyTemplate): @@ -74,7 +71,7 @@ def _update_layer_ui(self, state: State): self.layer_layout = v.Col() for property, _ in state.iter_callback_properties(): name = self.display_name(property) - widgets = self.widgets_for_property(state, property, name) + 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)) @@ -93,63 +90,6 @@ def _on_filetype_change(self, filetype: str): self.show_compression = gl self.show_modelviewer = gl - def _doc_button(self, cb_property: CallbackProperty) -> v.Tooltip: - img_path = os.path.join(RESOURCES_DIR, "info.png") - icon_src = read_icon(img_path, "image/png") - button = v.Tooltip( - top=True, - v_slots=[{ - "name": "activator", - "variable": "tooltip", - "children": [ - v.Img(v_on="tooltip.on", src=icon_src, - height=20, width=20, - max_width=20, max_height=20 - ), - ], - }], - children=[cb_property.__doc__], - ) - return button - - def widgets_for_property(self, - instance: HasCallbackProperties, - property: str, - display_name: str) -> List[DOMWidget]: - - value = getattr(instance, property) - instance_type = type(instance) - cb_property = getattr(instance_type, property) - t = type(value) - widgets = [] - if t is bool: - widget = v.Checkbox(label=display_name) - link((instance, property), (widget, 'value')) - widgets.append(widget) - elif t in (int, float): - 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, - hide_details=True, - thumb_label=f"{value:g}", - ) - link((instance, property), - (widget, 'v_model')) - - widgets.append(widget) - - if cb_property.__doc__: - widgets.append(self._doc_button(cb_property)) - - return widgets - def vue_cancel_dialog(self, *args): self.state_dictionary = {} self.dialog_open = False diff --git a/glue_ar/jupyter/tests/test_dialog.py b/glue_ar/jupyter/tests/test_dialog.py index a778b85..9bca646 100644 --- a/glue_ar/jupyter/tests/test_dialog.py +++ b/glue_ar/jupyter/tests/test_dialog.py @@ -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 @@ -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) diff --git a/glue_ar/jupyter/tests/test_widgets.py b/glue_ar/jupyter/tests/test_widgets.py new file mode 100644 index 0000000..10b180a --- /dev/null +++ b/glue_ar/jupyter/tests/test_widgets.py @@ -0,0 +1,94 @@ +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) diff --git a/glue_ar/jupyter/widgets.py b/glue_ar/jupyter/widgets.py new file mode 100644 index 0000000..9b96fd9 --- /dev/null +++ b/glue_ar/jupyter/widgets.py @@ -0,0 +1,101 @@ +from os.path import join +from typing import List, Tuple + +from echo import CallbackProperty, HasCallbackProperties +from glue_jupyter.common.toolbar_vuetify import read_icon +from glue_jupyter.link import link +import ipyvuetify as v +from ipywidgets import DOMWidget + +from glue_ar.utils import RESOURCES_DIR + + +def info_tooltip(cb_property: CallbackProperty) -> List[str]: + if cb_property.__doc__: + return cb_property.__doc__.replace(". ", ".\n").split("\n") + else: + return [] + + +def info_icon(cb_property: CallbackProperty) -> v.Tooltip: + img_path = join(RESOURCES_DIR, "info.png") + icon_src = read_icon(img_path, "image/png") + tooltip_children = [v.Html(tag="div", children=[text]) for text in info_tooltip(cb_property) if text] + button = v.Tooltip( + top=True, + v_slots=[{ + "name": "activator", + "variable": "tooltip", + "children": [ + v.Img(v_on="tooltip.on", src=icon_src, + height=20, width=20, + max_width=20, max_height=20 + ), + ], + }], + children=tooltip_children, + ) + return button + + +def boolean_callback_widgets(instance: HasCallbackProperties, + property: str, + display_name: str) -> Tuple[DOMWidget]: + + instance_type = type(instance) + cb_property = getattr(instance_type, property) + + checkbox = v.Checkbox(label=display_name) + link((instance, property), (checkbox, 'value')) + + if cb_property.__doc__: + icon = info_icon(cb_property) + return (checkbox, icon) + else: + return (checkbox,) + + +def number_callback_widgets(instance: HasCallbackProperties, + property: str, + display_name: str) -> Tuple[DOMWidget]: + + value = getattr(instance, property) + instance_type = type(instance) + cb_property = getattr(instance_type, property) + t = type(value) + + 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 + slider = v.Slider( + min=min, + max=max, + step=step, + label=display_name, + hide_details=True, + thumb_label=f"{value:g}", + ) + link((instance, property), + (slider, 'v_model')) + + if cb_property.__doc__: + icon = info_icon(cb_property) + return (slider, icon) + else: + return (slider,) + + +def widgets_for_callback_property( + instance: HasCallbackProperties, + property: str, + display_name: str) -> Tuple[DOMWidget]: + + t = type(getattr(instance, property)) + if t is bool: + return boolean_callback_widgets(instance, property, display_name) + elif t in (int, float): + return number_callback_widgets(instance, property, display_name) + else: + raise ValueError("Unsupported callback property type!") From ce74364c9c61dd78eba73cc727a1b659a57a3be5 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 12:28:37 -0500 Subject: [PATCH 13/22] More code cleanup for the Qt widgets. --- glue_ar/qt/tests/test_widgets.py | 1 - glue_ar/qt/widgets.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/glue_ar/qt/tests/test_widgets.py b/glue_ar/qt/tests/test_widgets.py index f949e6c..4775428 100644 --- a/glue_ar/qt/tests/test_widgets.py +++ b/glue_ar/qt/tests/test_widgets.py @@ -32,7 +32,6 @@ def test_horizontal_spacer(self): def test_info_button(self): state = DummyState() for property in state.callback_properties(): - print(property) cb_property: CallbackProperty = getattr(DummyState, property) button = info_button(cb_property) assert isinstance(button, QPushButton) diff --git a/glue_ar/qt/widgets.py b/glue_ar/qt/widgets.py index 7d74e42..66001d9 100644 --- a/glue_ar/qt/widgets.py +++ b/glue_ar/qt/widgets.py @@ -53,16 +53,16 @@ def boolean_callback_widgets(instance: HasCallbackProperties, instance_type = type(instance) cb_property: CallbackProperty = getattr(instance_type, property) - box = QCheckBox() - box.setChecked(value) - box.setText(display_name) - connection = connect_checkable_button(instance, property, box) + checkbox = QCheckBox() + checkbox.setChecked(value) + checkbox.setText(display_name) + connection = connect_checkable_button(instance, property, checkbox) if cb_property.__doc__: spacer = horizontal_spacer(width=40, height=20) button = info_button(cb_property) - return ((box, spacer, button),), connection + return ((checkbox, spacer, button),), connection else: - return ((box,),), connection + return ((checkbox,),), connection def number_callback_widgets(instance: HasCallbackProperties, @@ -127,8 +127,8 @@ def on_widget_destroyed(widget, cb=remove_label_callback): def widgets_for_callback_property(instance: HasCallbackProperties, property: str, display_name: str) -> Tuple[Tuple[Tuple[QWidget]], BaseConnection]: - value = getattr(instance, property) - t = type(value) + + t = type(getattr(instance, property)) if t is bool: return boolean_callback_widgets(instance, property, display_name) elif t in (int, float): From 62f736c43316c900d8895a6d9798484f81d58b28 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 12:36:39 -0500 Subject: [PATCH 14/22] Codestyle fixes. --- glue_ar/common/scatter_export_options.py | 2 +- glue_ar/jupyter/tests/test_widgets.py | 4 +++- glue_ar/jupyter/widgets.py | 7 +++---- glue_ar/qt/export_dialog.py | 2 +- glue_ar/qt/tests/test_widgets.py | 23 ++++++++++++----------- glue_ar/qt/widgets.py | 7 +++---- 6 files changed, 23 insertions(+), 22 deletions(-) diff --git a/glue_ar/common/scatter_export_options.py b/glue_ar/common/scatter_export_options.py index 4ef894a..f276584 100644 --- a/glue_ar/common/scatter_export_options.py +++ b/glue_ar/common/scatter_export_options.py @@ -13,7 +13,7 @@ class ARVispyScatterExportOptions(State): 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.", + "Higher means better resolution, but a larger filesize.", ) diff --git a/glue_ar/jupyter/tests/test_widgets.py b/glue_ar/jupyter/tests/test_widgets.py index 10b180a..4347bfe 100644 --- a/glue_ar/jupyter/tests/test_widgets.py +++ b/glue_ar/jupyter/tests/test_widgets.py @@ -2,7 +2,9 @@ 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 +from glue_ar.jupyter.widgets import boolean_callback_widgets, info_icon, \ + info_tooltip, number_callback_widgets, \ + widgets_for_callback_property def test_info_tooltip(): diff --git a/glue_ar/jupyter/widgets.py b/glue_ar/jupyter/widgets.py index 9b96fd9..f3bef1e 100644 --- a/glue_ar/jupyter/widgets.py +++ b/glue_ar/jupyter/widgets.py @@ -1,5 +1,5 @@ from os.path import join -from typing import List, Tuple +from typing import List, Tuple from echo import CallbackProperty, HasCallbackProperties from glue_jupyter.common.toolbar_vuetify import read_icon @@ -29,8 +29,7 @@ def info_icon(cb_property: CallbackProperty) -> v.Tooltip: "children": [ v.Img(v_on="tooltip.on", src=icon_src, height=20, width=20, - max_width=20, max_height=20 - ), + max_width=20, max_height=20), ], }], children=tooltip_children, @@ -96,6 +95,6 @@ def widgets_for_callback_property( if t is bool: return boolean_callback_widgets(instance, property, display_name) elif t in (int, float): - return number_callback_widgets(instance, property, display_name) + return number_callback_widgets(instance, property, display_name) else: raise ValueError("Unsupported callback property type!") diff --git a/glue_ar/qt/export_dialog.py b/glue_ar/qt/export_dialog.py index 4651013..444add6 100644 --- a/glue_ar/qt/export_dialog.py +++ b/glue_ar/qt/export_dialog.py @@ -68,7 +68,7 @@ def _update_layer_ui(self, state: State): widget_tuples, connection = widgets_for_callback_property(state, property, name) self._layer_connections.append(connection) for widgets in widget_tuples: - subrow = QHBoxLayout() + subrow = QHBoxLayout() for widget in widgets: if isinstance(widget, QWidget): subrow.addWidget(widget) diff --git a/glue_ar/qt/tests/test_widgets.py b/glue_ar/qt/tests/test_widgets.py index 4775428..f0022b2 100644 --- a/glue_ar/qt/tests/test_widgets.py +++ b/glue_ar/qt/tests/test_widgets.py @@ -6,10 +6,11 @@ from echo import CallbackProperty from echo.qt import connect_checkable_button, connect_value -from qtpy.QtWidgets import QApplication, QPushButton, QSpacerItem, QCheckBox, QLabel, QSlider, QSpacerItem +from qtpy.QtWidgets import QApplication, QPushButton, QSpacerItem, QCheckBox, QLabel, QSlider from glue_ar.common.tests.test_base_dialog import DummyState -from glue_ar.qt.widgets import boolean_callback_widgets, horizontal_spacer, info_button, info_tooltip, widgets_for_callback_property +from glue_ar.qt.widgets import boolean_callback_widgets, horizontal_spacer, \ + info_button, info_tooltip, widgets_for_callback_property class TestQtWidgets: @@ -24,24 +25,24 @@ def test_info_tooltip(self): 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_horizontal_spacer(self): spacer = horizontal_spacer(width=60, height=80) assert isinstance(spacer, QSpacerItem) - + def test_info_button(self): state = DummyState() for property in state.callback_properties(): cb_property: CallbackProperty = getattr(DummyState, property) button = info_button(cb_property) assert isinstance(button, QPushButton) - + def test_boolean_callback_widgets(self): state = DummyState() - + widget_tuples, connection = boolean_callback_widgets(state, "cb_bool", "Bool CB") assert isinstance(connection, connect_checkable_button) - + assert len(widget_tuples) == 1 box, spacer, info_button = widget_tuples[0] assert isinstance(box, QCheckBox) @@ -49,7 +50,7 @@ def test_boolean_callback_widgets(self): assert not box.isChecked() assert isinstance(spacer, QSpacerItem) assert isinstance(info_button, QPushButton) - + def test_integer_callback_widgets(self): state = DummyState() widget_rows, connection = widgets_for_callback_property(state, "cb_int", "Int CB") @@ -68,7 +69,7 @@ def test_integer_callback_widgets(self): assert slider.value() == 1 # 2 is the second (index 1) step value assert isinstance(value_label, QLabel) assert value_label.text() == "2" - + def test_float_callback_widgets(self): state = DummyState() widget_rows, connection = widgets_for_callback_property(state, "cb_float", "Float CB") @@ -81,7 +82,7 @@ def test_float_callback_widgets(self): assert label.text() == "Float CB:" assert isinstance(spacer, QSpacerItem) assert isinstance(info_button, QPushButton) - + slider, value_label = widget_rows[1] assert isinstance(slider, QSlider) assert slider.value() == 69 # Another value -> index thing (see above comment) @@ -90,7 +91,7 @@ def test_float_callback_widgets(self): def test_widgets_for_callback_property(self): state = DummyState() - + int_widget_rows, connection = widgets_for_callback_property(state, "cb_int", "Int CB") assert isinstance(connection, connect_value) assert len(int_widget_rows) == 2 diff --git a/glue_ar/qt/widgets.py b/glue_ar/qt/widgets.py index 66001d9..3326004 100644 --- a/glue_ar/qt/widgets.py +++ b/glue_ar/qt/widgets.py @@ -6,7 +6,7 @@ from echo.qt import BaseConnection, connect_checkable_button, connect_value from qtpy.QtGui import QCursor, QEnterEvent, QIcon from qtpy.QtCore import Qt, QEvent -from qtpy.QtWidgets import QCheckBox, QDialog, QFormLayout, QHBoxLayout, QLayoutItem, QPushButton, QSpacerItem, QToolTip, QVBoxLayout, QLabel, QLayout, QSizePolicy, QSlider, QWidget +from qtpy.QtWidgets import QCheckBox, QPushButton, QSpacerItem, QToolTip, QLabel, QSizePolicy, QSlider, QWidget from glue_ar.utils import RESOURCES_DIR @@ -41,8 +41,8 @@ def info_button(cb_property: CallbackProperty) -> QPushButton: if cb_property.__doc__: button.enterEvent = lambda event: info_enter_event_handler(event, cb_property=cb_property) button.leaveEvent = info_leave_event_handler - - return button + + return button def boolean_callback_widgets(instance: HasCallbackProperties, @@ -93,7 +93,6 @@ def number_callback_widgets(instance: HasCallbackProperties, step = 1 if t is int else 0.01 places = -floor(log(step, 10)) - def update_label(value): value_label.setText(f"{value:.{places}f}") From 09c2f056fcf62fa606647ba1c7355f6bdeac786a Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 12:51:39 -0500 Subject: [PATCH 15/22] Update Qt dialog test to account for layer layout widget type change. --- glue_ar/qt/tests/test_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/glue_ar/qt/tests/test_dialog.py b/glue_ar/qt/tests/test_dialog.py index 063f6c8..62b35d1 100644 --- a/glue_ar/qt/tests/test_dialog.py +++ b/glue_ar/qt/tests/test_dialog.py @@ -74,11 +74,11 @@ def test_filetype_change(self): def test_update_layer_ui(self): state = DummyState() self.dialog._update_layer_ui(state) - assert self.dialog.ui.layer_layout.rowCount() == 3 + assert self.dialog.ui.layer_layout.count() == 3 state = ARVispyScatterExportOptions() self.dialog._update_layer_ui(state) - assert self.dialog.ui.layer_layout.rowCount() == 1 + assert self.dialog.ui.layer_layout.count() == 1 def test_clear_layout(self): self.dialog._clear_layer_layout() From 9820365d7dff163a5e5e2f5ee3944a5a7128d563 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 13:01:53 -0500 Subject: [PATCH 16/22] Add importorskip to Jupyter widgets test. --- glue_ar/jupyter/tests/test_widgets.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/glue_ar/jupyter/tests/test_widgets.py b/glue_ar/jupyter/tests/test_widgets.py index 4347bfe..5d897dd 100644 --- a/glue_ar/jupyter/tests/test_widgets.py +++ b/glue_ar/jupyter/tests/test_widgets.py @@ -1,3 +1,7 @@ +from pytest import importorskip + +importorskip("glue_jupyter") + from echo import CallbackProperty from ipyvuetify import Checkbox, Img, Slider, Tooltip From 63f06d89f86fac027d674504acbcf1837ee512fc Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 13:28:47 -0500 Subject: [PATCH 17/22] We only need to set up/tear down the application once. --- glue_ar/qt/tests/test_widgets.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/glue_ar/qt/tests/test_widgets.py b/glue_ar/qt/tests/test_widgets.py index f0022b2..62ba12f 100644 --- a/glue_ar/qt/tests/test_widgets.py +++ b/glue_ar/qt/tests/test_widgets.py @@ -15,11 +15,11 @@ class TestQtWidgets: - def setup_method(self, method): - self.app = QApplication(sys.argv) + def setup_class(cls): + cls.app = QApplication(sys.argv) - def teardown_method(self, method): - self.app.quit() + def teardown_class(cls): + cls.app.quit() def test_info_tooltip(self): assert info_tooltip(DummyState.cb_int) == "Integer callback property" From 8c364ad68b68d29a1844b942d0a555690f275eb8 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 14:27:00 -0500 Subject: [PATCH 18/22] Use pytest-qt for testing Qt widgets. --- glue_ar/qt/tests/test_widgets.py | 242 +++++++++++++++---------------- setup.py | 1 + 2 files changed, 119 insertions(+), 124 deletions(-) diff --git a/glue_ar/qt/tests/test_widgets.py b/glue_ar/qt/tests/test_widgets.py index 62ba12f..c47a1b1 100644 --- a/glue_ar/qt/tests/test_widgets.py +++ b/glue_ar/qt/tests/test_widgets.py @@ -2,135 +2,129 @@ importorskip("glue_qt") -import sys - from echo import CallbackProperty from echo.qt import connect_checkable_button, connect_value -from qtpy.QtWidgets import QApplication, QPushButton, QSpacerItem, QCheckBox, QLabel, QSlider +from qtpy.QtWidgets import QPushButton, QSpacerItem, QCheckBox, QLabel, QSlider from glue_ar.common.tests.test_base_dialog import DummyState from glue_ar.qt.widgets import boolean_callback_widgets, horizontal_spacer, \ info_button, info_tooltip, widgets_for_callback_property -class TestQtWidgets: - - def setup_class(cls): - cls.app = QApplication(sys.argv) - - def teardown_class(cls): - cls.app.quit() - - def test_info_tooltip(self): - 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_horizontal_spacer(self): - spacer = horizontal_spacer(width=60, height=80) - assert isinstance(spacer, QSpacerItem) - - def test_info_button(self): - state = DummyState() - for property in state.callback_properties(): - cb_property: CallbackProperty = getattr(DummyState, property) - button = info_button(cb_property) - assert isinstance(button, QPushButton) - - def test_boolean_callback_widgets(self): - state = DummyState() - - widget_tuples, connection = boolean_callback_widgets(state, "cb_bool", "Bool CB") - assert isinstance(connection, connect_checkable_button) - - assert len(widget_tuples) == 1 - box, spacer, info_button = widget_tuples[0] - assert isinstance(box, QCheckBox) - assert box.text() == "Bool CB" - assert not box.isChecked() - assert isinstance(spacer, QSpacerItem) - assert isinstance(info_button, QPushButton) - - def test_integer_callback_widgets(self): - state = DummyState() - widget_rows, connection = widgets_for_callback_property(state, "cb_int", "Int CB") - - assert isinstance(connection, connect_value) - - assert len(widget_rows) == 2 - label, spacer, info_button = widget_rows[0] - assert isinstance(label, QLabel) - assert label.text() == "Int CB:" - assert isinstance(spacer, QSpacerItem) - assert isinstance(info_button, QPushButton) - - slider, value_label = widget_rows[1] - assert isinstance(slider, QSlider) - assert slider.value() == 1 # 2 is the second (index 1) step value - assert isinstance(value_label, QLabel) - assert value_label.text() == "2" - - def test_float_callback_widgets(self): - state = DummyState() - widget_rows, connection = widgets_for_callback_property(state, "cb_float", "Float CB") - - assert isinstance(connection, connect_value) - assert len(widget_rows) == 2 - - label, spacer, info_button = widget_rows[0] - assert isinstance(label, QLabel) - assert label.text() == "Float CB:" - assert isinstance(spacer, QSpacerItem) - assert isinstance(info_button, QPushButton) - - slider, value_label = widget_rows[1] - assert isinstance(slider, QSlider) - assert slider.value() == 69 # Another value -> index thing (see above comment) - assert isinstance(value_label, QLabel) - assert value_label.text() == "0.70" - - def test_widgets_for_callback_property(self): - state = DummyState() - - int_widget_rows, connection = widgets_for_callback_property(state, "cb_int", "Int CB") - assert isinstance(connection, connect_value) - assert len(int_widget_rows) == 2 - - label, spacer, info_button = int_widget_rows[0] - assert isinstance(label, QLabel) - assert label.text() == "Int CB:" - assert isinstance(spacer, QSpacerItem) - assert isinstance(info_button, QPushButton) - - slider, value_label = int_widget_rows[1] - assert isinstance(slider, QSlider) - assert slider.value() == 1 # 2 is the second (index 1) step value - assert isinstance(value_label, QLabel) - assert value_label.text() == "2" - - float_widget_rows, connection = widgets_for_callback_property(state, "cb_float", "Float CB") - assert isinstance(connection, connect_value) - assert len(float_widget_rows) == 2 - - label, spacer, info_button = float_widget_rows[0] - assert isinstance(label, QLabel) - assert label.text() == "Float CB:" - assert isinstance(spacer, QSpacerItem) - assert isinstance(info_button, QPushButton) - - slider, value_label = float_widget_rows[1] - assert isinstance(slider, QSlider) - assert slider.value() == 69 # Another value -> index thing (see above comment) - assert isinstance(value_label, QLabel) - assert value_label.text() == "0.70" - - bool_widget_rows, connection = widgets_for_callback_property(state, "cb_bool", "Bool CB") - assert isinstance(connection, connect_checkable_button) - assert len(bool_widget_rows) == 1 - - box, spacer, info_button = bool_widget_rows[0] - assert isinstance(box, QCheckBox) - assert box.text() == "Bool CB" - assert not box.isChecked() - assert isinstance(spacer, QSpacerItem) - assert isinstance(info_button, QPushButton) +def test_info_tooltip(qtbot): + 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_horizontal_spacer(qtbot): + spacer = horizontal_spacer(width=60, height=80) + assert isinstance(spacer, QSpacerItem) + + +def test_info_button(qtbot): + state = DummyState() + for property in state.callback_properties(): + cb_property: CallbackProperty = getattr(DummyState, property) + button = info_button(cb_property) + assert isinstance(button, QPushButton) + +def test_boolean_callback_widgets(qtbot): + state = DummyState() + + widget_tuples, connection = boolean_callback_widgets(state, "cb_bool", "Bool CB") + assert isinstance(connection, connect_checkable_button) + + assert len(widget_tuples) == 1 + box, spacer, info_button = widget_tuples[0] + assert isinstance(box, QCheckBox) + assert box.text() == "Bool CB" + assert not box.isChecked() + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) + + +def test_integer_callback_widgets(qtbot): + state = DummyState() + widget_rows, connection = widgets_for_callback_property(state, "cb_int", "Int CB") + + assert isinstance(connection, connect_value) + + assert len(widget_rows) == 2 + label, spacer, info_button = widget_rows[0] + assert isinstance(label, QLabel) + assert label.text() == "Int CB:" + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) + + slider, value_label = widget_rows[1] + assert isinstance(slider, QSlider) + assert slider.value() == 1 # 2 is the second (index 1) step value + assert isinstance(value_label, QLabel) + assert value_label.text() == "2" + +def test_float_callback_widgets(): + state = DummyState() + widget_rows, connection = widgets_for_callback_property(state, "cb_float", "Float CB") + + assert isinstance(connection, connect_value) + assert len(widget_rows) == 2 + + label, spacer, info_button = widget_rows[0] + assert isinstance(label, QLabel) + assert label.text() == "Float CB:" + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) + + slider, value_label = widget_rows[1] + assert isinstance(slider, QSlider) + assert slider.value() == 69 # Another value -> index thing (see above comment) + assert isinstance(value_label, QLabel) + assert value_label.text() == "0.70" + + +def test_widgets_for_callback_property(qtbot): + state = DummyState() + + int_widget_rows, connection = widgets_for_callback_property(state, "cb_int", "Int CB") + assert isinstance(connection, connect_value) + assert len(int_widget_rows) == 2 + + label, spacer, info_button = int_widget_rows[0] + assert isinstance(label, QLabel) + assert label.text() == "Int CB:" + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) + + slider, value_label = int_widget_rows[1] + assert isinstance(slider, QSlider) + assert slider.value() == 1 # 2 is the second (index 1) step value + assert isinstance(value_label, QLabel) + assert value_label.text() == "2" + + float_widget_rows, connection = widgets_for_callback_property(state, "cb_float", "Float CB") + assert isinstance(connection, connect_value) + assert len(float_widget_rows) == 2 + + label, spacer, info_button = float_widget_rows[0] + assert isinstance(label, QLabel) + assert label.text() == "Float CB:" + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) + + slider, value_label = float_widget_rows[1] + assert isinstance(slider, QSlider) + assert slider.value() == 69 # Another value -> index thing (see above comment) + assert isinstance(value_label, QLabel) + assert value_label.text() == "0.70" + + bool_widget_rows, connection = widgets_for_callback_property(state, "cb_bool", "Bool CB") + assert isinstance(connection, connect_checkable_button) + assert len(bool_widget_rows) == 1 + + box, spacer, info_button = bool_widget_rows[0] + assert isinstance(box, QCheckBox) + assert box.text() == "Bool CB" + assert not box.isChecked() + assert isinstance(spacer, QSpacerItem) + assert isinstance(info_button, QPushButton) diff --git a/setup.py b/setup.py index 1d2f443..36c1008 100644 --- a/setup.py +++ b/setup.py @@ -744,6 +744,7 @@ def data_files(root_directory): "flake8", "pytest", "pytest-cov", + "pytest-qt", ], "qt": [ "glue-qt", From c1408f6a17df49443934bcc9c495ebc7b754ca0f Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 14:28:11 -0500 Subject: [PATCH 19/22] Codestyle fixes. --- glue_ar/qt/tests/test_widgets.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/glue_ar/qt/tests/test_widgets.py b/glue_ar/qt/tests/test_widgets.py index c47a1b1..278ada7 100644 --- a/glue_ar/qt/tests/test_widgets.py +++ b/glue_ar/qt/tests/test_widgets.py @@ -29,6 +29,7 @@ def test_info_button(qtbot): button = info_button(cb_property) assert isinstance(button, QPushButton) + def test_boolean_callback_widgets(qtbot): state = DummyState() @@ -63,6 +64,7 @@ def test_integer_callback_widgets(qtbot): assert isinstance(value_label, QLabel) assert value_label.text() == "2" + def test_float_callback_widgets(): state = DummyState() widget_rows, connection = widgets_for_callback_property(state, "cb_float", "Float CB") From cff9ff11f356d85149e325b30f25df44f0401d15 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 14:33:16 -0500 Subject: [PATCH 20/22] Reconfigure pytest-qt testing setup. --- setup.py | 1 - tox.ini | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 36c1008..1d2f443 100644 --- a/setup.py +++ b/setup.py @@ -744,7 +744,6 @@ def data_files(root_directory): "flake8", "pytest", "pytest-cov", - "pytest-qt", ], "qt": [ "glue-qt", diff --git a/tox.ini b/tox.ini index 46b50dc..fce99d0 100644 --- a/tox.ini +++ b/tox.ini @@ -13,6 +13,7 @@ extras = qt: qt jupyter: jupyter commands = + qt: pip install pytest-qt test: pip freeze test: pytest --pyargs glue_ar --cov glue_ar {posargs} From 56ffbad8bcd7e1c21134443e971f97b3ab1eb6c5 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 14:45:42 -0500 Subject: [PATCH 21/22] Add pytest-qt to 'all' environments as well. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index fce99d0..b9cc6c0 100644 --- a/tox.ini +++ b/tox.ini @@ -14,6 +14,7 @@ extras = jupyter: jupyter commands = qt: pip install pytest-qt + all: pip install pytest-qt test: pip freeze test: pytest --pyargs glue_ar --cov glue_ar {posargs} From 1b760eda214ef1b8c300011d2688b762614d2868 Mon Sep 17 00:00:00 2001 From: Carifio24 Date: Sun, 22 Dec 2024 17:28:03 -0500 Subject: [PATCH 22/22] Center-align widgets in rows in Jupyter dialog layer options layout. --- glue_ar/jupyter/export_dialog.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/glue_ar/jupyter/export_dialog.py b/glue_ar/jupyter/export_dialog.py index 39c7e85..80f0ecc 100644 --- a/glue_ar/jupyter/export_dialog.py +++ b/glue_ar/jupyter/export_dialog.py @@ -73,7 +73,7 @@ def _update_layer_ui(self, state: State): name = self.display_name(property) 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)) + rows.append(v.Row(children=widgets, align="center")) self.layer_layout.children = rows self.input_widgets = input_widgets