datalab-platform 0.0.1.dev0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- datalab/__init__.py +35 -2
- datalab/adapters_metadata/__init__.py +31 -0
- datalab/adapters_metadata/base_adapter.py +316 -0
- datalab/adapters_metadata/common.py +422 -0
- datalab/adapters_metadata/geometry_adapter.py +98 -0
- datalab/adapters_metadata/table_adapter.py +84 -0
- datalab/adapters_plotpy/__init__.py +54 -0
- datalab/adapters_plotpy/annotations.py +124 -0
- datalab/adapters_plotpy/base.py +110 -0
- datalab/adapters_plotpy/converters.py +86 -0
- datalab/adapters_plotpy/factories.py +80 -0
- datalab/adapters_plotpy/objects/__init__.py +0 -0
- datalab/adapters_plotpy/objects/base.py +197 -0
- datalab/adapters_plotpy/objects/image.py +157 -0
- datalab/adapters_plotpy/objects/scalar.py +565 -0
- datalab/adapters_plotpy/objects/signal.py +264 -0
- datalab/adapters_plotpy/roi/__init__.py +0 -0
- datalab/adapters_plotpy/roi/base.py +146 -0
- datalab/adapters_plotpy/roi/factory.py +93 -0
- datalab/adapters_plotpy/roi/image.py +207 -0
- datalab/adapters_plotpy/roi/signal.py +72 -0
- datalab/app.py +98 -0
- datalab/config.py +817 -0
- datalab/control/__init__.py +0 -0
- datalab/control/baseproxy.py +776 -0
- datalab/control/proxy.py +343 -0
- datalab/control/remote.py +1005 -0
- datalab/data/doc/DataLab_en.pdf +0 -0
- datalab/data/doc/DataLab_fr.pdf +0 -0
- datalab/data/icons/analysis/delete_results.svg +109 -0
- datalab/data/icons/analysis/fw1e2.svg +156 -0
- datalab/data/icons/analysis/fwhm.svg +156 -0
- datalab/data/icons/analysis/histogram.svg +49 -0
- datalab/data/icons/analysis/peak_detect.svg +160 -0
- datalab/data/icons/analysis/plot_results.svg +151 -0
- datalab/data/icons/analysis/show_results.svg +83 -0
- datalab/data/icons/analysis/stats.svg +49 -0
- datalab/data/icons/analysis.svg +120 -0
- datalab/data/icons/apply.svg +3 -0
- datalab/data/icons/check_all.svg +15 -0
- datalab/data/icons/collapse.svg +44 -0
- datalab/data/icons/collapse_selection.svg +63 -0
- datalab/data/icons/console.svg +101 -0
- datalab/data/icons/create/1d-normal.svg +8 -0
- datalab/data/icons/create/1d-poisson.svg +9 -0
- datalab/data/icons/create/1d-uniform.svg +8 -0
- datalab/data/icons/create/1d-zero.svg +57 -0
- datalab/data/icons/create/2d-gaussian.svg +56 -0
- datalab/data/icons/create/2d-normal.svg +38 -0
- datalab/data/icons/create/2d-poisson.svg +38 -0
- datalab/data/icons/create/2d-ramp.svg +90 -0
- datalab/data/icons/create/2d-sinc.svg +62 -0
- datalab/data/icons/create/2d-uniform.svg +38 -0
- datalab/data/icons/create/2d-zero.svg +13 -0
- datalab/data/icons/create/checkerboard.svg +39 -0
- datalab/data/icons/create/cosine.svg +12 -0
- datalab/data/icons/create/exponential.svg +55 -0
- datalab/data/icons/create/gaussian.svg +12 -0
- datalab/data/icons/create/grating.svg +29 -0
- datalab/data/icons/create/linear_chirp.svg +7 -0
- datalab/data/icons/create/logistic.svg +7 -0
- datalab/data/icons/create/lorentzian.svg +12 -0
- datalab/data/icons/create/planck.svg +12 -0
- datalab/data/icons/create/polynomial.svg +7 -0
- datalab/data/icons/create/pulse.svg +12 -0
- datalab/data/icons/create/ring.svg +18 -0
- datalab/data/icons/create/sawtooth.svg +7 -0
- datalab/data/icons/create/siemens.svg +35 -0
- datalab/data/icons/create/sinc.svg +12 -0
- datalab/data/icons/create/sine.svg +7 -0
- datalab/data/icons/create/square.svg +7 -0
- datalab/data/icons/create/square_pulse.svg +7 -0
- datalab/data/icons/create/step.svg +7 -0
- datalab/data/icons/create/step_pulse.svg +12 -0
- datalab/data/icons/create/triangle.svg +7 -0
- datalab/data/icons/create/voigt.svg +12 -0
- datalab/data/icons/edit/annotations.svg +72 -0
- datalab/data/icons/edit/annotations_copy.svg +114 -0
- datalab/data/icons/edit/annotations_delete.svg +83 -0
- datalab/data/icons/edit/annotations_edit.svg +98 -0
- datalab/data/icons/edit/annotations_export.svg +85 -0
- datalab/data/icons/edit/annotations_import.svg +85 -0
- datalab/data/icons/edit/annotations_paste.svg +100 -0
- datalab/data/icons/edit/copy_titles.svg +109 -0
- datalab/data/icons/edit/delete.svg +84 -0
- datalab/data/icons/edit/delete_all.svg +214 -0
- datalab/data/icons/edit/duplicate.svg +64 -0
- datalab/data/icons/edit/goto_source.svg +60 -0
- datalab/data/icons/edit/metadata.svg +60 -0
- datalab/data/icons/edit/metadata_add.svg +80 -0
- datalab/data/icons/edit/metadata_copy.svg +96 -0
- datalab/data/icons/edit/metadata_delete.svg +62 -0
- datalab/data/icons/edit/metadata_export.svg +68 -0
- datalab/data/icons/edit/metadata_import.svg +68 -0
- datalab/data/icons/edit/metadata_paste.svg +79 -0
- datalab/data/icons/edit/move_down.svg +55 -0
- datalab/data/icons/edit/move_up.svg +54 -0
- datalab/data/icons/edit/new_group.svg +76 -0
- datalab/data/icons/edit/recompute.svg +60 -0
- datalab/data/icons/edit/rename.svg +49 -0
- datalab/data/icons/edit.svg +16 -0
- datalab/data/icons/expand.svg +44 -0
- datalab/data/icons/expand_selection.svg +63 -0
- datalab/data/icons/fit/cdf_fit.svg +56 -0
- datalab/data/icons/fit/exponential_fit.svg +55 -0
- datalab/data/icons/fit/gaussian_fit.svg +62 -0
- datalab/data/icons/fit/interactive_fit.svg +101 -0
- datalab/data/icons/fit/linear_fit.svg +57 -0
- datalab/data/icons/fit/lorentzian_fit.svg +209 -0
- datalab/data/icons/fit/multigaussian_fit.svg +85 -0
- datalab/data/icons/fit/multilorentzian_fit.svg +85 -0
- datalab/data/icons/fit/piecewiseexponential_fit.svg +209 -0
- datalab/data/icons/fit/planckian_fit.svg +62 -0
- datalab/data/icons/fit/polynomial_fit.svg +59 -0
- datalab/data/icons/fit/sigmoid_fit.svg +56 -0
- datalab/data/icons/fit/sinusoidal_fit.svg +72 -0
- datalab/data/icons/fit/twohalfgaussian_fit.svg +63 -0
- datalab/data/icons/fit/voigt_fit.svg +57 -0
- datalab/data/icons/group.svg +56 -0
- datalab/data/icons/h5/h5array.svg +59 -0
- datalab/data/icons/h5/h5attrs.svg +75 -0
- datalab/data/icons/h5/h5browser.svg +133 -0
- datalab/data/icons/h5/h5file.svg +69 -0
- datalab/data/icons/h5/h5group.svg +49 -0
- datalab/data/icons/h5/h5scalar.svg +1 -0
- datalab/data/icons/help_pdf.svg +46 -0
- datalab/data/icons/history.svg +7 -0
- datalab/data/icons/image.svg +135 -0
- datalab/data/icons/io/fileopen_directory.svg +60 -0
- datalab/data/icons/io/fileopen_h5.svg +84 -0
- datalab/data/icons/io/fileopen_ima.svg +187 -0
- datalab/data/icons/io/fileopen_py.svg +123 -0
- datalab/data/icons/io/fileopen_sig.svg +138 -0
- datalab/data/icons/io/filesave_h5.svg +97 -0
- datalab/data/icons/io/filesave_ima.svg +200 -0
- datalab/data/icons/io/filesave_py.svg +136 -0
- datalab/data/icons/io/filesave_sig.svg +151 -0
- datalab/data/icons/io/import_text.svg +144 -0
- datalab/data/icons/io/save_to_directory.svg +134 -0
- datalab/data/icons/io.svg +84 -0
- datalab/data/icons/libre-camera-flash-off.svg +1 -0
- datalab/data/icons/libre-camera-flash-on.svg +1 -0
- datalab/data/icons/libre-gui-about.svg +1 -0
- datalab/data/icons/libre-gui-action-delete.svg +1 -0
- datalab/data/icons/libre-gui-add.svg +1 -0
- datalab/data/icons/libre-gui-arrow-down.svg +1 -0
- datalab/data/icons/libre-gui-arrow-left.svg +1 -0
- datalab/data/icons/libre-gui-arrow-right.svg +1 -0
- datalab/data/icons/libre-gui-arrow-up.svg +1 -0
- datalab/data/icons/libre-gui-close.svg +40 -0
- datalab/data/icons/libre-gui-cogs.svg +1 -0
- datalab/data/icons/libre-gui-globe.svg +1 -0
- datalab/data/icons/libre-gui-help.svg +1 -0
- datalab/data/icons/libre-gui-link.svg +1 -0
- datalab/data/icons/libre-gui-menu.svg +1 -0
- datalab/data/icons/libre-gui-pencil.svg +1 -0
- datalab/data/icons/libre-gui-plugin.svg +1 -0
- datalab/data/icons/libre-gui-questions.svg +1 -0
- datalab/data/icons/libre-gui-settings.svg +1 -0
- datalab/data/icons/libre-gui-unlink.svg +1 -0
- datalab/data/icons/libre-tech-ram.svg +1 -0
- datalab/data/icons/libre-toolbox.svg +1 -0
- datalab/data/icons/logs.svg +1 -0
- datalab/data/icons/markers.svg +74 -0
- datalab/data/icons/menu.svg +13 -0
- datalab/data/icons/new_ima.svg +148 -0
- datalab/data/icons/new_sig.svg +123 -0
- datalab/data/icons/operations/abs.svg +116 -0
- datalab/data/icons/operations/arithmetic.svg +123 -0
- datalab/data/icons/operations/average.svg +124 -0
- datalab/data/icons/operations/complex_from_magnitude_phase.svg +116 -0
- datalab/data/icons/operations/complex_from_real_imag.svg +124 -0
- datalab/data/icons/operations/constant.svg +116 -0
- datalab/data/icons/operations/constant_add.svg +109 -0
- datalab/data/icons/operations/constant_divide.svg +109 -0
- datalab/data/icons/operations/constant_multiply.svg +109 -0
- datalab/data/icons/operations/constant_subtract.svg +109 -0
- datalab/data/icons/operations/convert_dtype.svg +117 -0
- datalab/data/icons/operations/convolution.svg +46 -0
- datalab/data/icons/operations/deconvolution.svg +57 -0
- datalab/data/icons/operations/derivative.svg +127 -0
- datalab/data/icons/operations/difference.svg +52 -0
- datalab/data/icons/operations/division.svg +139 -0
- datalab/data/icons/operations/exp.svg +116 -0
- datalab/data/icons/operations/flip_horizontally.svg +69 -0
- datalab/data/icons/operations/flip_vertically.svg +74 -0
- datalab/data/icons/operations/im.svg +124 -0
- datalab/data/icons/operations/integral.svg +50 -0
- datalab/data/icons/operations/inverse.svg +143 -0
- datalab/data/icons/operations/log10.svg +109 -0
- datalab/data/icons/operations/phase.svg +116 -0
- datalab/data/icons/operations/power.svg +118 -0
- datalab/data/icons/operations/product.svg +124 -0
- datalab/data/icons/operations/profile.svg +379 -0
- datalab/data/icons/operations/profile_average.svg +399 -0
- datalab/data/icons/operations/profile_radial.svg +261 -0
- datalab/data/icons/operations/profile_segment.svg +262 -0
- datalab/data/icons/operations/quadratic_difference.svg +84 -0
- datalab/data/icons/operations/re.svg +124 -0
- datalab/data/icons/operations/rotate_left.svg +72 -0
- datalab/data/icons/operations/rotate_right.svg +72 -0
- datalab/data/icons/operations/signals_to_image.svg +314 -0
- datalab/data/icons/operations/sqrt.svg +110 -0
- datalab/data/icons/operations/std.svg +124 -0
- datalab/data/icons/operations/sum.svg +102 -0
- datalab/data/icons/play_demo.svg +9 -0
- datalab/data/icons/processing/axis_transform.svg +62 -0
- datalab/data/icons/processing/bandpass.svg +79 -0
- datalab/data/icons/processing/bandstop.svg +71 -0
- datalab/data/icons/processing/binning.svg +126 -0
- datalab/data/icons/processing/clip.svg +119 -0
- datalab/data/icons/processing/detrending.svg +173 -0
- datalab/data/icons/processing/distribute_on_grid.svg +769 -0
- datalab/data/icons/processing/edge_detection.svg +46 -0
- datalab/data/icons/processing/erase.svg +1 -0
- datalab/data/icons/processing/exposure.svg +143 -0
- datalab/data/icons/processing/fourier.svg +104 -0
- datalab/data/icons/processing/highpass.svg +59 -0
- datalab/data/icons/processing/interpolation.svg +71 -0
- datalab/data/icons/processing/level_adjustment.svg +70 -0
- datalab/data/icons/processing/lowpass.svg +60 -0
- datalab/data/icons/processing/morphology.svg +49 -0
- datalab/data/icons/processing/noise_addition.svg +114 -0
- datalab/data/icons/processing/noise_reduction.svg +38 -0
- datalab/data/icons/processing/normalize.svg +84 -0
- datalab/data/icons/processing/offset_correction.svg +131 -0
- datalab/data/icons/processing/resampling1d.svg +101 -0
- datalab/data/icons/processing/resampling2d.svg +240 -0
- datalab/data/icons/processing/reset_positions.svg +185 -0
- datalab/data/icons/processing/resize.svg +9 -0
- datalab/data/icons/processing/reverse_signal_x.svg +171 -0
- datalab/data/icons/processing/stability.svg +11 -0
- datalab/data/icons/processing/swap_x_y.svg +65 -0
- datalab/data/icons/processing/thresholding.svg +63 -0
- datalab/data/icons/processing/windowing.svg +45 -0
- datalab/data/icons/properties.svg +26 -0
- datalab/data/icons/reset.svg +9 -0
- datalab/data/icons/restore.svg +40 -0
- datalab/data/icons/roi/roi.svg +76 -0
- datalab/data/icons/roi/roi_coordinate.svg +78 -0
- datalab/data/icons/roi/roi_copy.svg +112 -0
- datalab/data/icons/roi/roi_delete.svg +81 -0
- datalab/data/icons/roi/roi_export.svg +87 -0
- datalab/data/icons/roi/roi_graphical.svg +78 -0
- datalab/data/icons/roi/roi_grid.svg +67 -0
- datalab/data/icons/roi/roi_ima.svg +188 -0
- datalab/data/icons/roi/roi_import.svg +87 -0
- datalab/data/icons/roi/roi_new.svg +81 -0
- datalab/data/icons/roi/roi_new_circle.svg +95 -0
- datalab/data/icons/roi/roi_new_polygon.svg +110 -0
- datalab/data/icons/roi/roi_new_rectangle.svg +70 -0
- datalab/data/icons/roi/roi_paste.svg +98 -0
- datalab/data/icons/roi/roi_sig.svg +124 -0
- datalab/data/icons/shapes.svg +134 -0
- datalab/data/icons/signal.svg +103 -0
- datalab/data/icons/table.svg +85 -0
- datalab/data/icons/table_unavailable.svg +102 -0
- datalab/data/icons/to_signal.svg +124 -0
- datalab/data/icons/tour/next.svg +44 -0
- datalab/data/icons/tour/previous.svg +44 -0
- datalab/data/icons/tour/rewind.svg +51 -0
- datalab/data/icons/tour/stop.svg +47 -0
- datalab/data/icons/tour/tour.svg +16 -0
- datalab/data/icons/uncheck_all.svg +78 -0
- datalab/data/icons/view/curve_antialiasing.svg +50 -0
- datalab/data/icons/view/new_window.svg +98 -0
- datalab/data/icons/view/refresh-auto.svg +57 -0
- datalab/data/icons/view/refresh-manual.svg +51 -0
- datalab/data/icons/view/reset_curve_styles.svg +96 -0
- datalab/data/icons/view/show_first.svg +55 -0
- datalab/data/icons/view/show_titles.svg +46 -0
- datalab/data/icons/visualization.svg +51 -0
- datalab/data/logo/DataLab-Banner-150.png +0 -0
- datalab/data/logo/DataLab-Banner-200.png +0 -0
- datalab/data/logo/DataLab-Banner2-100.png +0 -0
- datalab/data/logo/DataLab-Splash.png +0 -0
- datalab/data/logo/DataLab-watermark.png +0 -0
- datalab/data/logo/DataLab.svg +83 -0
- datalab/data/tests/reordering_test.h5 +0 -0
- datalab/data/tutorials/fabry_perot/fabry-perot1.jpg +0 -0
- datalab/data/tutorials/fabry_perot/fabry-perot2.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_13.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_18.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_23.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_30.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_35.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_40.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_45.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_50.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_55.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_60.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_65.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_70.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_75.jpg +0 -0
- datalab/data/tutorials/laser_beam/TEM00_z_80.jpg +0 -0
- datalab/env.py +542 -0
- datalab/gui/__init__.py +89 -0
- datalab/gui/actionhandler.py +1701 -0
- datalab/gui/docks.py +473 -0
- datalab/gui/h5io.py +150 -0
- datalab/gui/macroeditor.py +310 -0
- datalab/gui/main.py +2081 -0
- datalab/gui/newobject.py +217 -0
- datalab/gui/objectview.py +766 -0
- datalab/gui/panel/__init__.py +48 -0
- datalab/gui/panel/base.py +3254 -0
- datalab/gui/panel/image.py +157 -0
- datalab/gui/panel/macro.py +607 -0
- datalab/gui/panel/signal.py +164 -0
- datalab/gui/plothandler.py +800 -0
- datalab/gui/processor/__init__.py +84 -0
- datalab/gui/processor/base.py +2456 -0
- datalab/gui/processor/catcher.py +75 -0
- datalab/gui/processor/image.py +1214 -0
- datalab/gui/processor/signal.py +755 -0
- datalab/gui/profiledialog.py +333 -0
- datalab/gui/roieditor.py +633 -0
- datalab/gui/roigrideditor.py +208 -0
- datalab/gui/settings.py +612 -0
- datalab/gui/tour.py +908 -0
- datalab/h5/__init__.py +12 -0
- datalab/h5/common.py +314 -0
- datalab/h5/generic.py +580 -0
- datalab/h5/native.py +39 -0
- datalab/h5/utils.py +95 -0
- datalab/objectmodel.py +640 -0
- datalab/plugins/_readme_.txt +9 -0
- datalab/plugins/datalab_imageformats.py +175 -0
- datalab/plugins/datalab_testdata.py +190 -0
- datalab/plugins.py +355 -0
- datalab/tests/__init__.py +199 -0
- datalab/tests/backbone/__init__.py +1 -0
- datalab/tests/backbone/config_unit_test.py +170 -0
- datalab/tests/backbone/config_versioning_unit_test.py +34 -0
- datalab/tests/backbone/dictlistserial_app_test.py +38 -0
- datalab/tests/backbone/errorcatcher_unit_test.py +69 -0
- datalab/tests/backbone/errormsgbox_unit_test.py +50 -0
- datalab/tests/backbone/execenv_unit.py +262 -0
- datalab/tests/backbone/loadtest_gdi.py +147 -0
- datalab/tests/backbone/long_callback.py +96 -0
- datalab/tests/backbone/main_app_test.py +137 -0
- datalab/tests/backbone/memory_leak.py +43 -0
- datalab/tests/backbone/procisolation1_unit.py +128 -0
- datalab/tests/backbone/procisolation2_unit.py +171 -0
- datalab/tests/backbone/procisolation_unit_test.py +22 -0
- datalab/tests/backbone/profiling_app.py +27 -0
- datalab/tests/backbone/strings_unit_test.py +65 -0
- datalab/tests/backbone/title_formatting_unit_test.py +82 -0
- datalab/tests/conftest.py +131 -0
- datalab/tests/features/__init__.py +1 -0
- datalab/tests/features/applauncher/__init__.py +1 -0
- datalab/tests/features/applauncher/launcher1_app_test.py +28 -0
- datalab/tests/features/applauncher/launcher2_app_test.py +30 -0
- datalab/tests/features/common/__init__.py +1 -0
- datalab/tests/features/common/add_metadata_app_test.py +134 -0
- datalab/tests/features/common/add_metadata_unit_test.py +267 -0
- datalab/tests/features/common/annotations_management_unit_test.py +152 -0
- datalab/tests/features/common/auto_analysis_recompute_unit_test.py +240 -0
- datalab/tests/features/common/createobject_unit_test.py +50 -0
- datalab/tests/features/common/geometry_results_app_test.py +135 -0
- datalab/tests/features/common/interactive_processing_test.py +1109 -0
- datalab/tests/features/common/io_app_test.py +75 -0
- datalab/tests/features/common/large_results_app_test.py +187 -0
- datalab/tests/features/common/metadata_all_patterns_test.py +103 -0
- datalab/tests/features/common/metadata_app_test.py +139 -0
- datalab/tests/features/common/metadata_io_unit_test.py +60 -0
- datalab/tests/features/common/misc_app_test.py +236 -0
- datalab/tests/features/common/multiple_geometry_results_unit_test.py +122 -0
- datalab/tests/features/common/multiple_table_results_unit_test.py +64 -0
- datalab/tests/features/common/operation_modes_app_test.py +392 -0
- datalab/tests/features/common/plot_results_app_test.py +278 -0
- datalab/tests/features/common/reorder_app_test.py +75 -0
- datalab/tests/features/common/result_deletion_unit_test.py +96 -0
- datalab/tests/features/common/result_merged_label_unit_test.py +154 -0
- datalab/tests/features/common/result_shape_settings_unit_test.py +223 -0
- datalab/tests/features/common/roi_plotitem_unit_test.py +64 -0
- datalab/tests/features/common/roieditor_unit_test.py +102 -0
- datalab/tests/features/common/save_to_dir_app_test.py +163 -0
- datalab/tests/features/common/save_to_dir_unit_test.py +474 -0
- datalab/tests/features/common/stat_app_test.py +40 -0
- datalab/tests/features/common/stats_tools_unit_test.py +77 -0
- datalab/tests/features/common/table_results_app_test.py +52 -0
- datalab/tests/features/common/textimport_unit_test.py +131 -0
- datalab/tests/features/common/uuid_preservation_test.py +281 -0
- datalab/tests/features/common/worker_unit_test.py +402 -0
- datalab/tests/features/control/__init__.py +1 -0
- datalab/tests/features/control/connect_dialog.py +28 -0
- datalab/tests/features/control/embedded1_unit_test.py +304 -0
- datalab/tests/features/control/embedded2_unit_test.py +52 -0
- datalab/tests/features/control/remoteclient_app_test.py +219 -0
- datalab/tests/features/control/remoteclient_unit.py +75 -0
- datalab/tests/features/control/simpleclient_unit_test.py +321 -0
- datalab/tests/features/hdf5/__init__.py +1 -0
- datalab/tests/features/hdf5/h5browser1_unit_test.py +31 -0
- datalab/tests/features/hdf5/h5browser2_unit.py +55 -0
- datalab/tests/features/hdf5/h5browser_app_test.py +77 -0
- datalab/tests/features/hdf5/h5import_app_test.py +25 -0
- datalab/tests/features/hdf5/h5importer_app_test.py +34 -0
- datalab/tests/features/image/__init__.py +1 -0
- datalab/tests/features/image/annotations_app_test.py +28 -0
- datalab/tests/features/image/annotations_unit_test.py +80 -0
- datalab/tests/features/image/average_app_test.py +46 -0
- datalab/tests/features/image/background_dialog_test.py +70 -0
- datalab/tests/features/image/blobs_app_test.py +50 -0
- datalab/tests/features/image/contour_app_test.py +42 -0
- datalab/tests/features/image/contour_fabryperot_app_test.py +51 -0
- datalab/tests/features/image/denoise_app_test.py +31 -0
- datalab/tests/features/image/distribute_on_grid_app_test.py +95 -0
- datalab/tests/features/image/edges_app_test.py +31 -0
- datalab/tests/features/image/erase_app_test.py +21 -0
- datalab/tests/features/image/fft2d_app_test.py +27 -0
- datalab/tests/features/image/flatfield_app_test.py +40 -0
- datalab/tests/features/image/geometry_transform_unit_test.py +396 -0
- datalab/tests/features/image/imagetools_app_test.py +51 -0
- datalab/tests/features/image/imagetools_unit_test.py +27 -0
- datalab/tests/features/image/load_app_test.py +73 -0
- datalab/tests/features/image/morph_app_test.py +32 -0
- datalab/tests/features/image/offsetcorrection_app_test.py +30 -0
- datalab/tests/features/image/peak2d_app_test.py +53 -0
- datalab/tests/features/image/profile_app_test.py +73 -0
- datalab/tests/features/image/profile_dialog_test.py +56 -0
- datalab/tests/features/image/roi_app_test.py +98 -0
- datalab/tests/features/image/roi_circ_app_test.py +62 -0
- datalab/tests/features/image/roi_manipulation_app_test.py +268 -0
- datalab/tests/features/image/roigrid_unit_test.py +60 -0
- datalab/tests/features/image/side_by_side_app_test.py +52 -0
- datalab/tests/features/macro/__init__.py +1 -0
- datalab/tests/features/macro/macro_app_test.py +28 -0
- datalab/tests/features/macro/macroeditor_unit_test.py +102 -0
- datalab/tests/features/signal/__init__.py +1 -0
- datalab/tests/features/signal/baseline_dialog_test.py +53 -0
- datalab/tests/features/signal/deltax_dialog_unit_test.py +34 -0
- datalab/tests/features/signal/fft1d_app_test.py +26 -0
- datalab/tests/features/signal/filter_app_test.py +44 -0
- datalab/tests/features/signal/fitdialog_unit_test.py +50 -0
- datalab/tests/features/signal/interpolation_app_test.py +110 -0
- datalab/tests/features/signal/loadbigsignal_app_test.py +80 -0
- datalab/tests/features/signal/multiple_rois_unit_test.py +132 -0
- datalab/tests/features/signal/pulse_features_app_test.py +118 -0
- datalab/tests/features/signal/pulse_features_roi_app_test.py +55 -0
- datalab/tests/features/signal/roi_app_test.py +78 -0
- datalab/tests/features/signal/roi_manipulation_app_test.py +261 -0
- datalab/tests/features/signal/select_xy_cursor_unit_test.py +46 -0
- datalab/tests/features/signal/signalpeakdetection_dialog_test.py +33 -0
- datalab/tests/features/signal/signals_to_image_app_test.py +98 -0
- datalab/tests/features/signal/xarray_compat_app_test.py +128 -0
- datalab/tests/features/tour_unit_test.py +22 -0
- datalab/tests/features/utilities/__init__.py +1 -0
- datalab/tests/features/utilities/installconf_unit_test.py +21 -0
- datalab/tests/features/utilities/logview_app_test.py +21 -0
- datalab/tests/features/utilities/logview_error.py +24 -0
- datalab/tests/features/utilities/logview_unit_test.py +21 -0
- datalab/tests/features/utilities/memstatus_app_test.py +42 -0
- datalab/tests/features/utilities/settings_unit_test.py +88 -0
- datalab/tests/scenarios/__init__.py +1 -0
- datalab/tests/scenarios/beautiful_app.py +121 -0
- datalab/tests/scenarios/common.py +463 -0
- datalab/tests/scenarios/demo.py +212 -0
- datalab/tests/scenarios/example_app_test.py +47 -0
- datalab/tests/scenarios/scenario_h5_app_test.py +75 -0
- datalab/tests/scenarios/scenario_ima1_app_test.py +34 -0
- datalab/tests/scenarios/scenario_ima2_app_test.py +34 -0
- datalab/tests/scenarios/scenario_mac_app_test.py +58 -0
- datalab/tests/scenarios/scenario_sig1_app_test.py +36 -0
- datalab/tests/scenarios/scenario_sig2_app_test.py +35 -0
- datalab/utils/__init__.py +1 -0
- datalab/utils/conf.py +304 -0
- datalab/utils/dephash.py +105 -0
- datalab/utils/qthelpers.py +633 -0
- datalab/utils/strings.py +34 -0
- datalab/utils/tests.py +0 -0
- datalab/widgets/__init__.py +1 -0
- datalab/widgets/connection.py +138 -0
- datalab/widgets/filedialog.py +91 -0
- datalab/widgets/fileviewer.py +84 -0
- datalab/widgets/fitdialog.py +788 -0
- datalab/widgets/h5browser.py +1048 -0
- datalab/widgets/imagebackground.py +111 -0
- datalab/widgets/instconfviewer.py +175 -0
- datalab/widgets/logviewer.py +80 -0
- datalab/widgets/signalbaseline.py +90 -0
- datalab/widgets/signalcursor.py +208 -0
- datalab/widgets/signaldeltax.py +151 -0
- datalab/widgets/signalpeak.py +199 -0
- datalab/widgets/status.py +249 -0
- datalab/widgets/textimport.py +786 -0
- datalab/widgets/warningerror.py +223 -0
- datalab/widgets/wizard.py +286 -0
- datalab_platform-1.0.1.dist-info/METADATA +121 -0
- datalab_platform-1.0.1.dist-info/RECORD +494 -0
- datalab_platform-0.0.1.dev0.dist-info/METADATA +0 -67
- datalab_platform-0.0.1.dev0.dist-info/RECORD +0 -7
- {datalab_platform-0.0.1.dev0.dist-info → datalab_platform-1.0.1.dist-info}/WHEEL +0 -0
- {datalab_platform-0.0.1.dev0.dist-info → datalab_platform-1.0.1.dist-info}/entry_points.txt +0 -0
- {datalab_platform-0.0.1.dev0.dist-info → datalab_platform-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {datalab_platform-0.0.1.dev0.dist-info → datalab_platform-1.0.1.dist-info}/top_level.txt +0 -0
datalab/gui/main.py
ADDED
|
@@ -0,0 +1,2081 @@
|
|
|
1
|
+
# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Main window
|
|
5
|
+
===========
|
|
6
|
+
|
|
7
|
+
The :mod:`datalab.gui.main` module provides the main window of the
|
|
8
|
+
DataLab project.
|
|
9
|
+
|
|
10
|
+
.. autoclass:: DLMainWindow
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import abc
|
|
18
|
+
import base64
|
|
19
|
+
import functools
|
|
20
|
+
import os
|
|
21
|
+
import os.path as osp
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
import webbrowser
|
|
25
|
+
from typing import TYPE_CHECKING
|
|
26
|
+
|
|
27
|
+
import guidata.dataset as gds
|
|
28
|
+
import numpy as np
|
|
29
|
+
import scipy.ndimage as spi
|
|
30
|
+
import scipy.signal as sps
|
|
31
|
+
from guidata import qthelpers as guidata_qth
|
|
32
|
+
from guidata.configtools import get_icon
|
|
33
|
+
from guidata.qthelpers import add_actions, create_action
|
|
34
|
+
from guidata.widgets.console import DockableConsole
|
|
35
|
+
from plotpy import config as plotpy_config
|
|
36
|
+
from plotpy.builder import make
|
|
37
|
+
from plotpy.constants import PlotType
|
|
38
|
+
from qtpy import QtCore as QC
|
|
39
|
+
from qtpy import QtGui as QG
|
|
40
|
+
from qtpy import QtWidgets as QW
|
|
41
|
+
from qtpy.compat import getopenfilenames, getsavefilename
|
|
42
|
+
from sigima.config import options as sigima_options
|
|
43
|
+
from sigima.objects import ImageObj, SignalObj, create_image, create_signal
|
|
44
|
+
|
|
45
|
+
import datalab
|
|
46
|
+
from datalab import __docurl__, __homeurl__, __supporturl__, env
|
|
47
|
+
from datalab.adapters_metadata.common import have_geometry_results
|
|
48
|
+
from datalab.adapters_plotpy import create_adapter_from_object
|
|
49
|
+
from datalab.config import (
|
|
50
|
+
APP_DESC,
|
|
51
|
+
APP_NAME,
|
|
52
|
+
DATAPATH,
|
|
53
|
+
DEBUG,
|
|
54
|
+
TEST_SEGFAULT_ERROR,
|
|
55
|
+
Conf,
|
|
56
|
+
_,
|
|
57
|
+
)
|
|
58
|
+
from datalab.control.baseproxy import AbstractDLControl
|
|
59
|
+
from datalab.control.remote import RemoteServer
|
|
60
|
+
from datalab.env import execenv
|
|
61
|
+
from datalab.gui.actionhandler import ActionCategory
|
|
62
|
+
from datalab.gui.docks import DockablePlotWidget
|
|
63
|
+
from datalab.gui.h5io import H5InputOutput
|
|
64
|
+
from datalab.gui.panel import base, image, macro, signal
|
|
65
|
+
from datalab.gui.settings import edit_settings
|
|
66
|
+
from datalab.objectmodel import ObjectGroup
|
|
67
|
+
from datalab.plugins import PluginRegistry, discover_plugins, discover_v020_plugins
|
|
68
|
+
from datalab.utils import qthelpers as qth
|
|
69
|
+
from datalab.utils.qthelpers import (
|
|
70
|
+
add_corner_menu,
|
|
71
|
+
bring_to_front,
|
|
72
|
+
configure_menu_about_to_show,
|
|
73
|
+
)
|
|
74
|
+
from datalab.widgets import instconfviewer, logviewer, status
|
|
75
|
+
from datalab.widgets.warningerror import go_to_error
|
|
76
|
+
|
|
77
|
+
if TYPE_CHECKING:
|
|
78
|
+
from typing import Literal
|
|
79
|
+
|
|
80
|
+
from datalab.gui.panel.base import AbstractPanel, BaseDataPanel
|
|
81
|
+
from datalab.gui.panel.image import ImagePanel
|
|
82
|
+
from datalab.gui.panel.macro import MacroPanel
|
|
83
|
+
from datalab.gui.panel.signal import SignalPanel
|
|
84
|
+
from datalab.plugins import PluginBase
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def remote_controlled(func):
|
|
88
|
+
"""Decorator for remote-controlled methods"""
|
|
89
|
+
|
|
90
|
+
@functools.wraps(func)
|
|
91
|
+
def method_wrapper(*args, **kwargs):
|
|
92
|
+
"""Decorator wrapper function"""
|
|
93
|
+
win = args[0] # extracting 'self' from method arguments
|
|
94
|
+
already_busy = not win.ready_flag
|
|
95
|
+
win.ready_flag = False
|
|
96
|
+
try:
|
|
97
|
+
output = func(*args, **kwargs)
|
|
98
|
+
finally:
|
|
99
|
+
if not already_busy:
|
|
100
|
+
win.SIG_READY.emit()
|
|
101
|
+
win.ready_flag = True
|
|
102
|
+
QW.QApplication.processEvents()
|
|
103
|
+
return output
|
|
104
|
+
|
|
105
|
+
return method_wrapper
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DLMainWindowMeta(type(QW.QMainWindow), abc.ABCMeta):
|
|
109
|
+
"""Mixed metaclass to avoid conflicts"""
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class DLMainWindow(QW.QMainWindow, AbstractDLControl, metaclass=DLMainWindowMeta):
|
|
113
|
+
"""DataLab main window
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
console: enable internal console
|
|
117
|
+
hide_on_close: True to hide window on close
|
|
118
|
+
"""
|
|
119
|
+
|
|
120
|
+
__instance = None
|
|
121
|
+
|
|
122
|
+
SIG_READY = QC.Signal()
|
|
123
|
+
SIG_SEND_OBJECT = QC.Signal(object)
|
|
124
|
+
SIG_SEND_OBJECTLIST = QC.Signal(object)
|
|
125
|
+
SIG_CLOSING = QC.Signal()
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def get_instance(console=None, hide_on_close=False):
|
|
129
|
+
"""Return singleton instance"""
|
|
130
|
+
if DLMainWindow.__instance is None:
|
|
131
|
+
return DLMainWindow(console, hide_on_close)
|
|
132
|
+
return DLMainWindow.__instance
|
|
133
|
+
|
|
134
|
+
def __init__(self, console=None, hide_on_close=False):
|
|
135
|
+
"""Initialize main window"""
|
|
136
|
+
DLMainWindow.__instance = self
|
|
137
|
+
super().__init__()
|
|
138
|
+
self.setObjectName(APP_NAME)
|
|
139
|
+
self.setWindowIcon(get_icon("DataLab.svg"))
|
|
140
|
+
|
|
141
|
+
execenv.log(self, "Starting initialization")
|
|
142
|
+
|
|
143
|
+
self.ready_flag = True
|
|
144
|
+
|
|
145
|
+
self.hide_on_close = hide_on_close
|
|
146
|
+
self.__old_size: tuple[int, int] | None = None
|
|
147
|
+
self.__memory_warning = False
|
|
148
|
+
self.memorystatus: status.MemoryStatus | None = None
|
|
149
|
+
|
|
150
|
+
self.consolestatus: status.ConsoleStatus | None = None
|
|
151
|
+
self.console: DockableConsole | None = None
|
|
152
|
+
self.macropanel: MacroPanel | None = None
|
|
153
|
+
|
|
154
|
+
self.main_toolbar: QW.QToolBar | None = None
|
|
155
|
+
self.signalpanel_toolbar: QW.QToolBar | None = None
|
|
156
|
+
self.imagepanel_toolbar: QW.QToolBar | None = None
|
|
157
|
+
self.signalpanel: SignalPanel | None = None
|
|
158
|
+
self.imagepanel: ImagePanel | None = None
|
|
159
|
+
self.signalview: DockablePlotWidget | None = None
|
|
160
|
+
self.imageview: DockablePlotWidget | None = None
|
|
161
|
+
self.tabwidget: QW.QTabWidget | None = None
|
|
162
|
+
self.tabmenu: QW.QMenu | None = None
|
|
163
|
+
self.docks: dict[AbstractPanel | DockableConsole, QW.QDockWidget] | None = None
|
|
164
|
+
self.h5inputoutput = H5InputOutput(self)
|
|
165
|
+
|
|
166
|
+
self.openh5_action: QW.QAction | None = None
|
|
167
|
+
self.saveh5_action: QW.QAction | None = None
|
|
168
|
+
self.browseh5_action: QW.QAction | None = None
|
|
169
|
+
self.settings_action: QW.QAction | None = None
|
|
170
|
+
self.quit_action: QW.QAction | None = None
|
|
171
|
+
self.autorefresh_action: QW.QAction | None = None
|
|
172
|
+
self.showfirstonly_action: QW.QAction | None = None
|
|
173
|
+
self.showlabel_action: QW.QAction | None = None
|
|
174
|
+
|
|
175
|
+
self.file_menu: QW.QMenu | None = None
|
|
176
|
+
self.create_menu: QW.QMenu | None = None
|
|
177
|
+
self.edit_menu: QW.QMenu | None = None
|
|
178
|
+
self.roi_menu: QW.QMenu | None = None
|
|
179
|
+
self.operation_menu: QW.QMenu | None = None
|
|
180
|
+
self.processing_menu: QW.QMenu | None = None
|
|
181
|
+
self.analysis_menu: QW.QMenu | None = None
|
|
182
|
+
self.plugins_menu: QW.QMenu | None = None
|
|
183
|
+
self.view_menu: QW.QMenu | None = None
|
|
184
|
+
self.help_menu: QW.QMenu | None = None
|
|
185
|
+
|
|
186
|
+
self.__update_color_mode(startup=True)
|
|
187
|
+
|
|
188
|
+
self.__is_modified = False
|
|
189
|
+
self.set_modified(False)
|
|
190
|
+
|
|
191
|
+
# Starting XML-RPC server thread
|
|
192
|
+
self.remote_server = RemoteServer(self)
|
|
193
|
+
if Conf.main.rpc_server_enabled.get():
|
|
194
|
+
self.remote_server.SIG_SERVER_PORT.connect(self.xmlrpc_server_started)
|
|
195
|
+
self.remote_server.start()
|
|
196
|
+
|
|
197
|
+
# Setup actions and menus
|
|
198
|
+
if console is None:
|
|
199
|
+
console = Conf.console.console_enabled.get()
|
|
200
|
+
self.setup(console)
|
|
201
|
+
|
|
202
|
+
self.__restore_pos_and_size()
|
|
203
|
+
execenv.log(self, "Initialization done")
|
|
204
|
+
|
|
205
|
+
# ------API related to XML-RPC remote control
|
|
206
|
+
@staticmethod
|
|
207
|
+
def xmlrpc_server_started(port):
|
|
208
|
+
"""XML-RPC server has started, writing comm port in configuration file"""
|
|
209
|
+
Conf.main.rpc_server_port.set(port)
|
|
210
|
+
|
|
211
|
+
def __get_current_basedatapanel(self) -> BaseDataPanel:
|
|
212
|
+
"""Return the current BaseDataPanel,
|
|
213
|
+
or the signal panel if macro panel is active
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
BaseDataPanel: current panel
|
|
217
|
+
"""
|
|
218
|
+
panel = self.tabwidget.currentWidget()
|
|
219
|
+
if not isinstance(panel, base.BaseDataPanel):
|
|
220
|
+
panel = self.signalpanel
|
|
221
|
+
return panel
|
|
222
|
+
|
|
223
|
+
def __get_datapanel(
|
|
224
|
+
self, panel: Literal["signal", "image"] | None
|
|
225
|
+
) -> BaseDataPanel:
|
|
226
|
+
"""Return a specific BaseDataPanel.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
panel: panel name. If None, current panel is used.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Panel widget
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
ValueError: if panel is unknown
|
|
236
|
+
"""
|
|
237
|
+
if not panel:
|
|
238
|
+
return self.__get_current_basedatapanel()
|
|
239
|
+
if panel == "signal":
|
|
240
|
+
return self.signalpanel
|
|
241
|
+
if panel == "image":
|
|
242
|
+
return self.imagepanel
|
|
243
|
+
raise ValueError(f"Unknown panel: {panel}")
|
|
244
|
+
|
|
245
|
+
@remote_controlled
|
|
246
|
+
def get_group_titles_with_object_info(
|
|
247
|
+
self,
|
|
248
|
+
) -> tuple[list[str], list[list[str]], list[list[str]]]:
|
|
249
|
+
"""Return groups titles and lists of inner objects uuids and titles.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Groups titles, lists of inner objects uuids and titles
|
|
253
|
+
"""
|
|
254
|
+
panel = self.__get_current_basedatapanel()
|
|
255
|
+
return panel.objmodel.get_group_titles_with_object_info()
|
|
256
|
+
|
|
257
|
+
@remote_controlled
|
|
258
|
+
def get_object_titles(
|
|
259
|
+
self, panel: Literal["signal", "image", "macro"] | None = None
|
|
260
|
+
) -> list[str]:
|
|
261
|
+
"""Get object (signal/image) list for current panel.
|
|
262
|
+
Objects are sorted by group number and object index in group.
|
|
263
|
+
|
|
264
|
+
Args:
|
|
265
|
+
panel: panel name. If None, current data panel is used (i.e. signal or
|
|
266
|
+
image panel).
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
List of object titles
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ValueError: if panel is unknown
|
|
273
|
+
"""
|
|
274
|
+
if not panel or panel in ("signal", "image"):
|
|
275
|
+
return self.__get_datapanel(panel).objmodel.get_object_titles()
|
|
276
|
+
if panel == "macro":
|
|
277
|
+
return self.macropanel.get_macro_titles()
|
|
278
|
+
raise ValueError(f"Unknown panel: {panel}")
|
|
279
|
+
|
|
280
|
+
@remote_controlled
|
|
281
|
+
def get_object(
|
|
282
|
+
self,
|
|
283
|
+
nb_id_title: int | str | None = None,
|
|
284
|
+
panel: Literal["signal", "image"] | None = None,
|
|
285
|
+
) -> SignalObj | ImageObj:
|
|
286
|
+
"""Get object (signal/image) from index.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
nb_id_title: Object number, or object id, or object title.
|
|
290
|
+
Defaults to None (current object).
|
|
291
|
+
panel: Panel name. Defaults to None (current panel).
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
Object
|
|
295
|
+
|
|
296
|
+
Raises:
|
|
297
|
+
KeyError: if object not found
|
|
298
|
+
TypeError: if index_id_title type is invalid
|
|
299
|
+
"""
|
|
300
|
+
panelw = self.__get_datapanel(panel)
|
|
301
|
+
if nb_id_title is None:
|
|
302
|
+
return panelw.objview.get_current_object()
|
|
303
|
+
if isinstance(nb_id_title, int):
|
|
304
|
+
return panelw.objmodel.get_object_from_number(nb_id_title)
|
|
305
|
+
if isinstance(nb_id_title, str):
|
|
306
|
+
try:
|
|
307
|
+
return panelw.objmodel[nb_id_title]
|
|
308
|
+
except KeyError:
|
|
309
|
+
try:
|
|
310
|
+
return panelw.objmodel.get_object_from_title(nb_id_title)
|
|
311
|
+
except KeyError as exc:
|
|
312
|
+
raise KeyError(
|
|
313
|
+
f"Invalid object index, id or title: {nb_id_title}"
|
|
314
|
+
) from exc
|
|
315
|
+
raise TypeError(f"Invalid index_id_title type: {type(nb_id_title)}")
|
|
316
|
+
|
|
317
|
+
def find_object_by_uuid(
|
|
318
|
+
self, uuid: str
|
|
319
|
+
) -> SignalObj | ImageObj | ObjectGroup | None:
|
|
320
|
+
"""Find an object by UUID, searching across all panels.
|
|
321
|
+
|
|
322
|
+
This method searches for an object in both signal and image panels,
|
|
323
|
+
making it suitable for cross-panel operations (e.g., radial profile that
|
|
324
|
+
takes an ImageObj and produces a SignalObj).
|
|
325
|
+
|
|
326
|
+
Difference from get_object():
|
|
327
|
+
- get_object() requires specifying a panel and accepts number/id/title
|
|
328
|
+
- find_object_by_uuid() searches all panels automatically using only UUID
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
uuid: UUID of the object to find
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
The object if found in any panel, None otherwise
|
|
335
|
+
"""
|
|
336
|
+
for panel in (self.signalpanel, self.imagepanel):
|
|
337
|
+
if panel is not None:
|
|
338
|
+
try:
|
|
339
|
+
return panel.objmodel[uuid]
|
|
340
|
+
except KeyError:
|
|
341
|
+
continue
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
@remote_controlled
|
|
345
|
+
def get_object_uuids(
|
|
346
|
+
self,
|
|
347
|
+
panel: Literal["signal", "image"] | None = None,
|
|
348
|
+
group: int | str | None = None,
|
|
349
|
+
) -> list[str]:
|
|
350
|
+
"""Get object (signal/image) uuid list for current panel.
|
|
351
|
+
Objects are sorted by group number and object index in group.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
panel: panel name. If None, current panel is used.
|
|
355
|
+
group: Group number, or group id, or group title.
|
|
356
|
+
Defaults to None (all groups).
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
List of object uuids
|
|
360
|
+
|
|
361
|
+
Raises:
|
|
362
|
+
ValueError: if panel is unknown
|
|
363
|
+
"""
|
|
364
|
+
objmodel = self.__get_datapanel(panel).objmodel
|
|
365
|
+
if group is None:
|
|
366
|
+
return objmodel.get_object_ids()
|
|
367
|
+
if isinstance(group, int):
|
|
368
|
+
grp = objmodel.get_group_from_number(group)
|
|
369
|
+
else:
|
|
370
|
+
try:
|
|
371
|
+
grp = objmodel.get_group(group)
|
|
372
|
+
except KeyError:
|
|
373
|
+
grp = objmodel.get_group_from_title(group)
|
|
374
|
+
if grp is None:
|
|
375
|
+
raise KeyError(f"Invalid group index, id or title: {group}")
|
|
376
|
+
return grp.get_object_ids()
|
|
377
|
+
|
|
378
|
+
@remote_controlled
|
|
379
|
+
def get_sel_object_uuids(self, include_groups: bool = False) -> list[str]:
|
|
380
|
+
"""Return selected objects uuids.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
include_groups: If True, also return objects from selected groups.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
List of selected objects uuids.
|
|
387
|
+
"""
|
|
388
|
+
panel = self.__get_current_basedatapanel()
|
|
389
|
+
return panel.objview.get_sel_object_uuids(include_groups)
|
|
390
|
+
|
|
391
|
+
@remote_controlled
|
|
392
|
+
def add_group(
|
|
393
|
+
self,
|
|
394
|
+
title: str,
|
|
395
|
+
panel: Literal["signal", "image"] | None = None,
|
|
396
|
+
select: bool = False,
|
|
397
|
+
) -> None:
|
|
398
|
+
"""Add group to DataLab.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
title: Group title
|
|
402
|
+
panel: Panel name. Defaults to None.
|
|
403
|
+
select: Select the group after creation. Defaults to False.
|
|
404
|
+
"""
|
|
405
|
+
self.__get_datapanel(panel).add_group(title, select)
|
|
406
|
+
|
|
407
|
+
@remote_controlled
|
|
408
|
+
def select_objects(
|
|
409
|
+
self,
|
|
410
|
+
selection: list[int | str],
|
|
411
|
+
panel: Literal["signal", "image"] | None = None,
|
|
412
|
+
) -> None:
|
|
413
|
+
"""Select objects in current panel.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
selection: List of object numbers (1 to N) or uuids to select
|
|
417
|
+
panel: panel name. If None, current panel is used. Defaults to None.
|
|
418
|
+
"""
|
|
419
|
+
panel = self.__get_datapanel(panel)
|
|
420
|
+
panel.objview.select_objects(selection)
|
|
421
|
+
|
|
422
|
+
@remote_controlled
|
|
423
|
+
def select_groups(
|
|
424
|
+
self,
|
|
425
|
+
selection: list[int | str] | None = None,
|
|
426
|
+
panel: Literal["signal", "image"] | None = None,
|
|
427
|
+
) -> None:
|
|
428
|
+
"""Select groups in current panel.
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
selection: List of group numbers (1 to N), or list of group uuids,
|
|
432
|
+
or None to select all groups. Defaults to None.
|
|
433
|
+
panel: panel name. If None, current panel is used. Defaults to None.
|
|
434
|
+
"""
|
|
435
|
+
panel = self.__get_datapanel(panel)
|
|
436
|
+
panel.objview.select_groups(selection)
|
|
437
|
+
|
|
438
|
+
@remote_controlled
|
|
439
|
+
def delete_metadata(
|
|
440
|
+
self, refresh_plot: bool = True, keep_roi: bool = False
|
|
441
|
+
) -> None:
|
|
442
|
+
"""Delete metadata of selected objects
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
refresh_plot: Refresh plot. Defaults to True.
|
|
446
|
+
keep_roi: Keep ROI. Defaults to False.
|
|
447
|
+
"""
|
|
448
|
+
panel = self.__get_current_basedatapanel()
|
|
449
|
+
panel.delete_metadata(refresh_plot, keep_roi)
|
|
450
|
+
|
|
451
|
+
@remote_controlled
|
|
452
|
+
def get_object_shapes(
|
|
453
|
+
self,
|
|
454
|
+
nb_id_title: int | str | None = None,
|
|
455
|
+
panel: Literal["signal", "image"] | None = None,
|
|
456
|
+
) -> list:
|
|
457
|
+
"""Get plot item shapes associated to object (signal/image).
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
nb_id_title: Object number, or object id, or object title.
|
|
461
|
+
Defaults to None (current object).
|
|
462
|
+
panel: Panel name. Defaults to None (current panel).
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
List of plot item shapes
|
|
466
|
+
"""
|
|
467
|
+
obj = self.get_object(nb_id_title, panel)
|
|
468
|
+
return list(create_adapter_from_object(obj).iterate_shape_items(editable=False))
|
|
469
|
+
|
|
470
|
+
@remote_controlled
|
|
471
|
+
def add_annotations_from_items(
|
|
472
|
+
self,
|
|
473
|
+
items: list,
|
|
474
|
+
refresh_plot: bool = True,
|
|
475
|
+
panel: Literal["signal", "image"] | None = None,
|
|
476
|
+
) -> None:
|
|
477
|
+
"""Add object annotations (annotation plot items).
|
|
478
|
+
|
|
479
|
+
Args:
|
|
480
|
+
items: annotation plot items
|
|
481
|
+
refresh_plot: refresh plot. Defaults to True.
|
|
482
|
+
panel: panel name. If None, current panel is used.
|
|
483
|
+
"""
|
|
484
|
+
panel = self.__get_datapanel(panel)
|
|
485
|
+
panel.add_annotations_from_items(items, refresh_plot)
|
|
486
|
+
|
|
487
|
+
@remote_controlled
|
|
488
|
+
def add_label_with_title(
|
|
489
|
+
self, title: str | None = None, panel: Literal["signal", "image"] | None = None
|
|
490
|
+
) -> None:
|
|
491
|
+
"""Add a label with object title on the associated plot
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
title: Label title. Defaults to None.
|
|
495
|
+
If None, the title is the object title.
|
|
496
|
+
panel: panel name. If None, current panel is used.
|
|
497
|
+
"""
|
|
498
|
+
self.__get_datapanel(panel).add_label_with_title(title)
|
|
499
|
+
|
|
500
|
+
@remote_controlled
|
|
501
|
+
def run_macro(self, number_or_title: int | str | None = None) -> None:
|
|
502
|
+
"""Run macro.
|
|
503
|
+
|
|
504
|
+
Args:
|
|
505
|
+
number: Number of the macro (starting at 1). Defaults to None (run
|
|
506
|
+
current macro, or does nothing if there is no macro).
|
|
507
|
+
"""
|
|
508
|
+
self.macropanel.run_macro(number_or_title)
|
|
509
|
+
|
|
510
|
+
@remote_controlled
|
|
511
|
+
def stop_macro(self, number_or_title: int | str | None = None) -> None:
|
|
512
|
+
"""Stop macro.
|
|
513
|
+
|
|
514
|
+
Args:
|
|
515
|
+
number: Number of the macro (starting at 1). Defaults to None (stop
|
|
516
|
+
current macro, or does nothing if there is no macro).
|
|
517
|
+
"""
|
|
518
|
+
self.macropanel.stop_macro(number_or_title)
|
|
519
|
+
|
|
520
|
+
@remote_controlled
|
|
521
|
+
def import_macro_from_file(self, filename: str) -> None:
|
|
522
|
+
"""Import macro from file
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
filename: Filename.
|
|
526
|
+
"""
|
|
527
|
+
self.macropanel.import_macro_from_file(filename)
|
|
528
|
+
|
|
529
|
+
# ------Misc.
|
|
530
|
+
@property
|
|
531
|
+
def panels(self) -> tuple[AbstractPanel, ...]:
|
|
532
|
+
"""Return the tuple of implemented panels (signal, image)
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Tuple of panels
|
|
536
|
+
"""
|
|
537
|
+
return (self.signalpanel, self.imagepanel, self.macropanel)
|
|
538
|
+
|
|
539
|
+
def __set_low_memory_state(self, state: bool) -> None:
|
|
540
|
+
"""Set memory warning state"""
|
|
541
|
+
self.__memory_warning = state
|
|
542
|
+
|
|
543
|
+
def confirm_memory_state(self) -> bool: # pragma: no cover
|
|
544
|
+
"""Check memory warning state and eventually show a warning dialog
|
|
545
|
+
|
|
546
|
+
Returns:
|
|
547
|
+
True if memory state is ok
|
|
548
|
+
"""
|
|
549
|
+
if not env.execenv.unattended and self.__memory_warning:
|
|
550
|
+
threshold = Conf.main.available_memory_threshold.get()
|
|
551
|
+
answer = QW.QMessageBox.critical(
|
|
552
|
+
self,
|
|
553
|
+
_("Warning"),
|
|
554
|
+
_("Available memory is below %d MB.<br><br>Do you want to continue?")
|
|
555
|
+
% threshold,
|
|
556
|
+
QW.QMessageBox.Yes | QW.QMessageBox.No,
|
|
557
|
+
)
|
|
558
|
+
return answer == QW.QMessageBox.Yes
|
|
559
|
+
return True
|
|
560
|
+
|
|
561
|
+
def check_stable_release(self) -> None: # pragma: no cover
|
|
562
|
+
"""Check if this is a stable release"""
|
|
563
|
+
if datalab.__version__.replace(".", "").isdigit():
|
|
564
|
+
# This is a stable release
|
|
565
|
+
return
|
|
566
|
+
if "b" in datalab.__version__:
|
|
567
|
+
# This is a beta release
|
|
568
|
+
rel = _(
|
|
569
|
+
"This software is in the <b>beta stage</b> of its release cycle. "
|
|
570
|
+
"The focus of beta testing is providing a feature complete "
|
|
571
|
+
"software for users interested in trying new features before "
|
|
572
|
+
"the final release. However, <u>beta software may not behave as "
|
|
573
|
+
"expected and will probably have more bugs or performance issues "
|
|
574
|
+
"than completed software</u>."
|
|
575
|
+
)
|
|
576
|
+
else:
|
|
577
|
+
# This is an alpha release
|
|
578
|
+
rel = _(
|
|
579
|
+
"This software is in the <b>alpha stage</b> of its release cycle. "
|
|
580
|
+
"The focus of alpha testing is providing an incomplete software "
|
|
581
|
+
"for early testing of specific features by users. "
|
|
582
|
+
"Please note that <u>alpha software was not thoroughly tested</u> "
|
|
583
|
+
"by the developer before it is released."
|
|
584
|
+
)
|
|
585
|
+
txtlist = [
|
|
586
|
+
f"<b>{APP_NAME}</b> v{datalab.__version__}:",
|
|
587
|
+
"",
|
|
588
|
+
_("<i>This is not a stable release.</i>"),
|
|
589
|
+
"",
|
|
590
|
+
rel,
|
|
591
|
+
]
|
|
592
|
+
if not env.execenv.unattended:
|
|
593
|
+
QW.QMessageBox.warning(
|
|
594
|
+
self, APP_NAME, "<br>".join(txtlist), QW.QMessageBox.Ok
|
|
595
|
+
)
|
|
596
|
+
|
|
597
|
+
def check_for_previous_crash(self) -> None: # pragma: no cover
|
|
598
|
+
"""Check for previous crash"""
|
|
599
|
+
if execenv.unattended and not execenv.do_not_quit:
|
|
600
|
+
# Showing the log viewer for testing purpose (unattended mode) but only
|
|
601
|
+
# if option 'do_not_quit' is not set, to avoid blocking the test suite
|
|
602
|
+
self.__show_logviewer()
|
|
603
|
+
elif Conf.main.faulthandler_log_available.get(
|
|
604
|
+
False
|
|
605
|
+
) or Conf.main.traceback_log_available.get(False):
|
|
606
|
+
txt = "<br>".join(
|
|
607
|
+
[
|
|
608
|
+
logviewer.get_log_prompt_message(),
|
|
609
|
+
"",
|
|
610
|
+
_("Do you want to see available log files?"),
|
|
611
|
+
]
|
|
612
|
+
)
|
|
613
|
+
btns = QW.QMessageBox.StandardButton.Yes | QW.QMessageBox.StandardButton.No
|
|
614
|
+
choice = QW.QMessageBox.warning(self, APP_NAME, txt, btns)
|
|
615
|
+
if choice == QW.QMessageBox.StandardButton.Yes:
|
|
616
|
+
self.__show_logviewer()
|
|
617
|
+
|
|
618
|
+
def check_for_v020_plugins(self) -> None: # pragma: no cover
|
|
619
|
+
"""Check for v0.20 plugins and warn user if any are found"""
|
|
620
|
+
if Conf.main.v020_plugins_warning_ignore.get(False):
|
|
621
|
+
return
|
|
622
|
+
|
|
623
|
+
v020_plugins = discover_v020_plugins()
|
|
624
|
+
if execenv.unattended or not v020_plugins:
|
|
625
|
+
return
|
|
626
|
+
|
|
627
|
+
# Build plugin list with clickable directory paths
|
|
628
|
+
plugin_items = []
|
|
629
|
+
for name, directory_path in v020_plugins:
|
|
630
|
+
if directory_path:
|
|
631
|
+
# Create clickable file:// link to directory
|
|
632
|
+
dir_url = QC.QUrl.fromLocalFile(directory_path).toString()
|
|
633
|
+
plugin_items.append(
|
|
634
|
+
f'<li>{name} (<a href="{dir_url}">{directory_path}</a>)</li>'
|
|
635
|
+
)
|
|
636
|
+
else:
|
|
637
|
+
plugin_items.append(f"<li>{name}</li>")
|
|
638
|
+
plugin_list = "<ul>" + "".join(plugin_items) + "</ul>"
|
|
639
|
+
|
|
640
|
+
txtlist = [
|
|
641
|
+
"<b>" + _("DataLab v0.20 plugins detected") + "</b>",
|
|
642
|
+
"",
|
|
643
|
+
_("The following plugins are using the old DataLab v0.20 format:"),
|
|
644
|
+
plugin_list,
|
|
645
|
+
_(
|
|
646
|
+
"These plugins will <b>not be loaded</b> in DataLab v1.0 because "
|
|
647
|
+
"they are not compatible with the new architecture."
|
|
648
|
+
),
|
|
649
|
+
"",
|
|
650
|
+
_(
|
|
651
|
+
"To use these plugins with DataLab v1.0, you need to update them. "
|
|
652
|
+
"Please refer to the migration guide on the DataLab website "
|
|
653
|
+
)
|
|
654
|
+
+ '(<a href="https://datalab-platform.com/en/features/advanced/'
|
|
655
|
+
'migration_v020_to_v100.html">Migration guide</a>)'
|
|
656
|
+
+ _(" or in the PDF documentation."),
|
|
657
|
+
"",
|
|
658
|
+
_("Choosing to ignore this message will prevent it from appearing again."),
|
|
659
|
+
]
|
|
660
|
+
|
|
661
|
+
answer = QW.QMessageBox.question(
|
|
662
|
+
self,
|
|
663
|
+
APP_NAME,
|
|
664
|
+
"<br>".join(txtlist),
|
|
665
|
+
QW.QMessageBox.Ok | QW.QMessageBox.Ignore,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
if answer == QW.QMessageBox.Ignore:
|
|
669
|
+
Conf.main.v020_plugins_warning_ignore.set(True)
|
|
670
|
+
|
|
671
|
+
def execute_post_show_actions(self) -> None:
|
|
672
|
+
"""Execute post-show actions"""
|
|
673
|
+
self.check_stable_release()
|
|
674
|
+
self.check_for_previous_crash()
|
|
675
|
+
self.check_for_v020_plugins()
|
|
676
|
+
tour = Conf.main.tour_enabled.get()
|
|
677
|
+
if tour:
|
|
678
|
+
Conf.main.tour_enabled.set(False)
|
|
679
|
+
self.show_tour()
|
|
680
|
+
|
|
681
|
+
def take_screenshot(self, name: str) -> None: # pragma: no cover
|
|
682
|
+
"""Take main window screenshot"""
|
|
683
|
+
# For esthetic reasons, we set the central widget width to a lower value:
|
|
684
|
+
old_width = self.tabwidget.maximumWidth()
|
|
685
|
+
self.tabwidget.setMaximumWidth(500)
|
|
686
|
+
# To avoid having screenshot depending on memory status, we set demo mode ON:
|
|
687
|
+
self.memorystatus.set_demo_mode(True)
|
|
688
|
+
qth.grab_save_window(self, f"{name}")
|
|
689
|
+
# Restore previous state:
|
|
690
|
+
self.memorystatus.set_demo_mode(False)
|
|
691
|
+
self.tabwidget.setMaximumWidth(old_width)
|
|
692
|
+
|
|
693
|
+
def take_menu_screenshots(self) -> None: # pragma: no cover
|
|
694
|
+
"""Take menu screenshots"""
|
|
695
|
+
for panel in self.panels:
|
|
696
|
+
if isinstance(panel, base.BaseDataPanel):
|
|
697
|
+
self.tabwidget.setCurrentWidget(panel)
|
|
698
|
+
for name in (
|
|
699
|
+
"file",
|
|
700
|
+
"create",
|
|
701
|
+
"edit",
|
|
702
|
+
"roi",
|
|
703
|
+
"view",
|
|
704
|
+
"operation",
|
|
705
|
+
"processing",
|
|
706
|
+
"analysis",
|
|
707
|
+
"help",
|
|
708
|
+
):
|
|
709
|
+
menu = getattr(self, f"{name}_menu")
|
|
710
|
+
menu.popup(self.pos())
|
|
711
|
+
qth.grab_save_window(menu, f"{panel.objectName()}_{name}")
|
|
712
|
+
menu.close()
|
|
713
|
+
if panel in (self.signalpanel, self.imagepanel):
|
|
714
|
+
panel: BaseDataPanel
|
|
715
|
+
# Take screenshots of Edit menu submenus (Metadata and Annotations)
|
|
716
|
+
for submenu, suffix in (
|
|
717
|
+
(panel.acthandler.metadata_submenu, "_edit_metadata"),
|
|
718
|
+
(panel.acthandler.annotations_submenu, "_edit_annotations"),
|
|
719
|
+
):
|
|
720
|
+
submenu.popup(self.pos())
|
|
721
|
+
qth.grab_save_window(submenu, f"{panel.objectName()}{suffix}")
|
|
722
|
+
submenu.close()
|
|
723
|
+
|
|
724
|
+
# ------GUI setup
|
|
725
|
+
def __restore_pos_and_size(self) -> None:
|
|
726
|
+
"""Restore main window position and size from configuration"""
|
|
727
|
+
pos = Conf.main.window_position.get(None)
|
|
728
|
+
if pos is not None:
|
|
729
|
+
posx, posy = pos
|
|
730
|
+
self.move(QC.QPoint(posx, posy))
|
|
731
|
+
size = Conf.main.window_size.get(None)
|
|
732
|
+
if size is None:
|
|
733
|
+
sgeo = self.screen().availableGeometry()
|
|
734
|
+
sw, sh = sgeo.width(), sgeo.height()
|
|
735
|
+
w = max(1200, min(1800, int(sw * 0.8)))
|
|
736
|
+
h = max(700, min(1100, int(sh * 0.8)))
|
|
737
|
+
size = (w, h)
|
|
738
|
+
if pos is None:
|
|
739
|
+
cx = sgeo.x() + (sw - w) // 2
|
|
740
|
+
cy = sgeo.y() + (sh - h) // 2
|
|
741
|
+
self.move(QC.QPoint(cx, cy))
|
|
742
|
+
width, height = size
|
|
743
|
+
self.resize(QC.QSize(width, height))
|
|
744
|
+
if pos is not None and size is not None:
|
|
745
|
+
sgeo = self.screen().availableGeometry()
|
|
746
|
+
out_inf = posx < -int(0.9 * width) or posy < -int(0.9 * height)
|
|
747
|
+
out_sup = posx > int(0.9 * sgeo.width()) or posy > int(0.9 * sgeo.height())
|
|
748
|
+
if len(QW.QApplication.screens()) == 1 and (out_inf or out_sup):
|
|
749
|
+
# Main window is offscreen
|
|
750
|
+
posx = min(max(posx, 0), sgeo.width() - width)
|
|
751
|
+
posy = min(max(posy, 0), sgeo.height() - height)
|
|
752
|
+
self.move(QC.QPoint(posx, posy))
|
|
753
|
+
|
|
754
|
+
def __restore_state(self) -> None:
|
|
755
|
+
"""Restore main window state from configuration"""
|
|
756
|
+
state = Conf.main.window_state.get(None)
|
|
757
|
+
if state is not None:
|
|
758
|
+
state = base64.b64decode(state)
|
|
759
|
+
self.restoreState(QC.QByteArray(state))
|
|
760
|
+
for widget in self.children():
|
|
761
|
+
if isinstance(widget, QW.QDockWidget):
|
|
762
|
+
self.restoreDockWidget(widget)
|
|
763
|
+
|
|
764
|
+
def __save_pos_size_and_state(self) -> None:
|
|
765
|
+
"""Save main window position, size and state to configuration"""
|
|
766
|
+
is_maximized = self.windowState() == QC.Qt.WindowMaximized
|
|
767
|
+
Conf.main.window_maximized.set(is_maximized)
|
|
768
|
+
if not is_maximized:
|
|
769
|
+
size = self.size()
|
|
770
|
+
Conf.main.window_size.set((size.width(), size.height()))
|
|
771
|
+
pos = self.pos()
|
|
772
|
+
Conf.main.window_position.set((pos.x(), pos.y()))
|
|
773
|
+
# Encoding window state into base64 string to avoid sending binary data
|
|
774
|
+
# to the configuration file:
|
|
775
|
+
state = base64.b64encode(self.saveState().data()).decode("ascii")
|
|
776
|
+
Conf.main.window_state.set(state)
|
|
777
|
+
|
|
778
|
+
def setup(self, console: bool = False) -> None:
|
|
779
|
+
"""Setup main window
|
|
780
|
+
|
|
781
|
+
Args:
|
|
782
|
+
console: True to setup console
|
|
783
|
+
"""
|
|
784
|
+
self.__register_plugins()
|
|
785
|
+
self.__configure_statusbar(console)
|
|
786
|
+
self.__setup_global_actions()
|
|
787
|
+
self.__add_signal_image_panels()
|
|
788
|
+
self.__create_plugins_actions()
|
|
789
|
+
self.__setup_central_widget()
|
|
790
|
+
self.__add_menus()
|
|
791
|
+
if console:
|
|
792
|
+
self.__setup_console()
|
|
793
|
+
self.__update_actions(update_other_data_panel=True)
|
|
794
|
+
self.__add_macro_panel()
|
|
795
|
+
self.__configure_panels()
|
|
796
|
+
# Now that everything is set up, we can restore the window state:
|
|
797
|
+
self.__restore_state()
|
|
798
|
+
|
|
799
|
+
def __register_plugins(self) -> None:
|
|
800
|
+
"""Register plugins"""
|
|
801
|
+
with qth.try_or_log_error("Discovering plugins"):
|
|
802
|
+
# Discovering plugins
|
|
803
|
+
plugin_nb = len(discover_plugins())
|
|
804
|
+
execenv.log(self, f"{plugin_nb} plugin(s) found")
|
|
805
|
+
for plugin_class in PluginRegistry.get_plugin_classes():
|
|
806
|
+
with qth.try_or_log_error(f"Instantiating plugin {plugin_class.__name__}"):
|
|
807
|
+
# Instantiating plugin
|
|
808
|
+
plugin: PluginBase = plugin_class()
|
|
809
|
+
with qth.try_or_log_error(f"Registering plugin {plugin.info.name}"):
|
|
810
|
+
# Registering plugin
|
|
811
|
+
plugin.register(self)
|
|
812
|
+
|
|
813
|
+
def __create_plugins_actions(self) -> None:
|
|
814
|
+
"""Create plugins actions"""
|
|
815
|
+
with self.signalpanel.acthandler.new_category(ActionCategory.PLUGINS):
|
|
816
|
+
with self.imagepanel.acthandler.new_category(ActionCategory.PLUGINS):
|
|
817
|
+
for plugin in PluginRegistry.get_plugins():
|
|
818
|
+
with qth.try_or_log_error(f"Create actions for {plugin.info.name}"):
|
|
819
|
+
plugin.create_actions()
|
|
820
|
+
|
|
821
|
+
@staticmethod
|
|
822
|
+
def __unregister_plugins() -> None:
|
|
823
|
+
"""Unregister plugins"""
|
|
824
|
+
with qth.try_or_log_error("Unregistering plugins"):
|
|
825
|
+
PluginRegistry.unregister_all_plugins()
|
|
826
|
+
|
|
827
|
+
def __configure_statusbar(self, console: bool) -> None:
|
|
828
|
+
"""Configure status bar
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
console: True if console is enabled
|
|
832
|
+
"""
|
|
833
|
+
self.statusBar().showMessage(_("Welcome to %s!") % APP_NAME, 5000)
|
|
834
|
+
if console:
|
|
835
|
+
# Console status
|
|
836
|
+
self.consolestatus = status.ConsoleStatus()
|
|
837
|
+
self.statusBar().addPermanentWidget(self.consolestatus)
|
|
838
|
+
# Plugin status
|
|
839
|
+
pluginstatus = status.PluginStatus()
|
|
840
|
+
self.statusBar().addPermanentWidget(pluginstatus)
|
|
841
|
+
# XML-RPC server status
|
|
842
|
+
xmlrpcstatus = status.XMLRPCStatus()
|
|
843
|
+
xmlrpcstatus.set_port(self.remote_server.port)
|
|
844
|
+
self.statusBar().addPermanentWidget(xmlrpcstatus)
|
|
845
|
+
# Memory status
|
|
846
|
+
threshold = Conf.main.available_memory_threshold.get()
|
|
847
|
+
self.memorystatus = status.MemoryStatus(threshold)
|
|
848
|
+
self.memorystatus.SIG_MEMORY_ALARM.connect(self.__set_low_memory_state)
|
|
849
|
+
self.statusBar().addPermanentWidget(self.memorystatus)
|
|
850
|
+
|
|
851
|
+
def __add_toolbar(
|
|
852
|
+
self, title: str, position: Literal["top", "bottom", "left", "right"], name: str
|
|
853
|
+
) -> QW.QToolBar:
|
|
854
|
+
"""Add toolbar to main window
|
|
855
|
+
|
|
856
|
+
Args:
|
|
857
|
+
title: toolbar title
|
|
858
|
+
position: toolbar position
|
|
859
|
+
name: toolbar name (Qt object name)
|
|
860
|
+
"""
|
|
861
|
+
toolbar = QW.QToolBar(title, self)
|
|
862
|
+
toolbar.setObjectName(name)
|
|
863
|
+
area = getattr(QC.Qt, f"{position.capitalize()}ToolBarArea")
|
|
864
|
+
self.addToolBar(area, toolbar)
|
|
865
|
+
return toolbar
|
|
866
|
+
|
|
867
|
+
def __setup_global_actions(self) -> None:
|
|
868
|
+
"""Setup global actions"""
|
|
869
|
+
self.openh5_action = create_action(
|
|
870
|
+
self,
|
|
871
|
+
_("Open HDF5 files..."),
|
|
872
|
+
icon=get_icon("fileopen_h5.svg"),
|
|
873
|
+
tip=_("Open one or more HDF5 files"),
|
|
874
|
+
triggered=lambda checked=False: self.open_h5_files(import_all=True),
|
|
875
|
+
)
|
|
876
|
+
self.saveh5_action = create_action(
|
|
877
|
+
self,
|
|
878
|
+
_("Save to HDF5 file..."),
|
|
879
|
+
icon=get_icon("filesave_h5.svg"),
|
|
880
|
+
tip=_("Save to HDF5 file"),
|
|
881
|
+
triggered=self.save_to_h5_file,
|
|
882
|
+
)
|
|
883
|
+
self.browseh5_action = create_action(
|
|
884
|
+
self,
|
|
885
|
+
_("Browse HDF5 file..."),
|
|
886
|
+
icon=get_icon("h5browser.svg"),
|
|
887
|
+
tip=_("Browse an HDF5 file"),
|
|
888
|
+
triggered=lambda checked=False: self.open_h5_files(import_all=None),
|
|
889
|
+
)
|
|
890
|
+
self.settings_action = create_action(
|
|
891
|
+
self,
|
|
892
|
+
_("Settings..."),
|
|
893
|
+
icon=get_icon("libre-gui-settings.svg"),
|
|
894
|
+
tip=_("Open settings dialog"),
|
|
895
|
+
triggered=self.__edit_settings,
|
|
896
|
+
)
|
|
897
|
+
self.main_toolbar = self.__add_toolbar(
|
|
898
|
+
_("Main Toolbar"), "left", "main_toolbar"
|
|
899
|
+
)
|
|
900
|
+
add_actions(
|
|
901
|
+
self.main_toolbar,
|
|
902
|
+
[
|
|
903
|
+
self.openh5_action,
|
|
904
|
+
self.saveh5_action,
|
|
905
|
+
self.browseh5_action,
|
|
906
|
+
None,
|
|
907
|
+
self.settings_action,
|
|
908
|
+
],
|
|
909
|
+
)
|
|
910
|
+
# Quit action for "File menu" (added when populating menu on demand)
|
|
911
|
+
if self.hide_on_close:
|
|
912
|
+
quit_text = _("Hide window")
|
|
913
|
+
quit_tip = _("Hide DataLab window")
|
|
914
|
+
else:
|
|
915
|
+
quit_text = _("Quit")
|
|
916
|
+
quit_tip = _("Quit application")
|
|
917
|
+
if sys.platform != "darwin":
|
|
918
|
+
# On macOS, the "Quit" action is automatically added to the application menu
|
|
919
|
+
self.quit_action = create_action(
|
|
920
|
+
self,
|
|
921
|
+
quit_text,
|
|
922
|
+
shortcut=QG.QKeySequence(QG.QKeySequence.Quit),
|
|
923
|
+
icon=get_icon("libre-gui-close.svg"),
|
|
924
|
+
tip=quit_tip,
|
|
925
|
+
triggered=self.close,
|
|
926
|
+
)
|
|
927
|
+
# View menu actions
|
|
928
|
+
self.autorefresh_action = create_action(
|
|
929
|
+
self,
|
|
930
|
+
_("Auto-refresh"),
|
|
931
|
+
icon=get_icon("refresh-auto.svg"),
|
|
932
|
+
tip=_("Auto-refresh plot when object is modified, added or removed"),
|
|
933
|
+
toggled=self.handle_autorefresh_action,
|
|
934
|
+
)
|
|
935
|
+
self.showfirstonly_action = create_action(
|
|
936
|
+
self,
|
|
937
|
+
_("Show first object only"),
|
|
938
|
+
icon=get_icon("show_first.svg"),
|
|
939
|
+
tip=_("Show only the first selected object (signal or image)"),
|
|
940
|
+
toggled=self.toggle_show_first_only,
|
|
941
|
+
)
|
|
942
|
+
self.showlabel_action = create_action(
|
|
943
|
+
self,
|
|
944
|
+
_("Show graphical object titles"),
|
|
945
|
+
icon=get_icon("show_titles.svg"),
|
|
946
|
+
tip=_("Show or hide ROI and other graphical object titles or subtitles"),
|
|
947
|
+
toggled=self.toggle_show_titles,
|
|
948
|
+
)
|
|
949
|
+
|
|
950
|
+
def __add_signal_panel(self) -> None:
|
|
951
|
+
"""Setup signal toolbar, widgets and panel"""
|
|
952
|
+
self.signalpanel_toolbar = self.__add_toolbar(
|
|
953
|
+
_("Signal Panel Toolbar"), "left", "signalpanel_toolbar"
|
|
954
|
+
)
|
|
955
|
+
dpw = DockablePlotWidget(self, PlotType.CURVE)
|
|
956
|
+
self.signalpanel = signal.SignalPanel(self, dpw, self.signalpanel_toolbar)
|
|
957
|
+
self.signalpanel.SIG_STATUS_MESSAGE.connect(self.statusBar().showMessage)
|
|
958
|
+
plot = dpw.get_plot()
|
|
959
|
+
plot.add_item(make.legend("TR"))
|
|
960
|
+
plot.SIG_ITEM_PARAMETERS_CHANGED.connect(
|
|
961
|
+
self.signalpanel.plot_item_parameters_changed
|
|
962
|
+
)
|
|
963
|
+
plot.SIG_ITEM_MOVED.connect(self.signalpanel.plot_item_moved)
|
|
964
|
+
return dpw
|
|
965
|
+
|
|
966
|
+
def __add_image_panel(self) -> None:
|
|
967
|
+
"""Setup image toolbar, widgets and panel"""
|
|
968
|
+
self.imagepanel_toolbar = self.__add_toolbar(
|
|
969
|
+
_("Image Panel Toolbar"), "left", "imagepanel_toolbar"
|
|
970
|
+
)
|
|
971
|
+
dpw = DockablePlotWidget(self, PlotType.IMAGE)
|
|
972
|
+
self.imagepanel = image.ImagePanel(self, dpw, self.imagepanel_toolbar)
|
|
973
|
+
# -----------------------------------------------------------------------------
|
|
974
|
+
# # Before eventually disabling the "peritem" mode by default, wait for the
|
|
975
|
+
# # plotpy bug to be fixed (peritem mode is not compatible with multiple image
|
|
976
|
+
# # items):
|
|
977
|
+
# for cspanel in (
|
|
978
|
+
# self.imagepanel.plotwidget.get_xcs_panel(),
|
|
979
|
+
# self.imagepanel.plotwidget.get_ycs_panel(),
|
|
980
|
+
# ):
|
|
981
|
+
# cspanel.peritem_ac.setChecked(False)
|
|
982
|
+
# -----------------------------------------------------------------------------
|
|
983
|
+
self.imagepanel.SIG_STATUS_MESSAGE.connect(self.statusBar().showMessage)
|
|
984
|
+
plot = dpw.get_plot()
|
|
985
|
+
plot.SIG_ITEM_PARAMETERS_CHANGED.connect(
|
|
986
|
+
self.imagepanel.plot_item_parameters_changed
|
|
987
|
+
)
|
|
988
|
+
plot.SIG_ITEM_MOVED.connect(self.imagepanel.plot_item_moved)
|
|
989
|
+
plot.SIG_LUT_CHANGED.connect(self.imagepanel.plot_lut_changed)
|
|
990
|
+
return dpw
|
|
991
|
+
|
|
992
|
+
def __update_tab_menu(self) -> None:
|
|
993
|
+
"""Update tab menu"""
|
|
994
|
+
current_panel: BaseDataPanel = self.tabwidget.currentWidget()
|
|
995
|
+
add_actions(self.tabmenu, current_panel.get_context_menu().actions())
|
|
996
|
+
|
|
997
|
+
def __add_signal_image_panels(self) -> None:
|
|
998
|
+
"""Add signal and image panels"""
|
|
999
|
+
self.tabwidget = QW.QTabWidget()
|
|
1000
|
+
self.tabmenu = add_corner_menu(self.tabwidget)
|
|
1001
|
+
configure_menu_about_to_show(self.tabmenu, self.__update_tab_menu)
|
|
1002
|
+
self.signalview = self.__add_signal_panel()
|
|
1003
|
+
self.imageview = self.__add_image_panel()
|
|
1004
|
+
sdock = self.__add_dockwidget(self.signalview, title=_("Signal View"))
|
|
1005
|
+
idock = self.__add_dockwidget(self.imageview, title=_("Image View"))
|
|
1006
|
+
self.tabifyDockWidget(sdock, idock)
|
|
1007
|
+
self.docks = {self.signalpanel: sdock, self.imagepanel: idock}
|
|
1008
|
+
self.tabwidget.currentChanged.connect(self.__tab_index_changed)
|
|
1009
|
+
self.signalpanel.SIG_OBJECT_ADDED.connect(
|
|
1010
|
+
lambda: self.set_current_panel("signal")
|
|
1011
|
+
)
|
|
1012
|
+
self.imagepanel.SIG_OBJECT_ADDED.connect(
|
|
1013
|
+
lambda: self.set_current_panel("image")
|
|
1014
|
+
)
|
|
1015
|
+
for panel in (self.signalpanel, self.imagepanel):
|
|
1016
|
+
panel.setup_panel()
|
|
1017
|
+
|
|
1018
|
+
def __setup_central_widget(self) -> None:
|
|
1019
|
+
"""Setup central widget (main panel)"""
|
|
1020
|
+
self.tabwidget.setMaximumWidth(600)
|
|
1021
|
+
s_idx = self.tabwidget.addTab(
|
|
1022
|
+
self.signalpanel, get_icon("signal.svg"), _("Signal Panel")
|
|
1023
|
+
)
|
|
1024
|
+
i_idx = self.tabwidget.addTab(
|
|
1025
|
+
self.imagepanel, get_icon("image.svg"), _("Image Panel")
|
|
1026
|
+
)
|
|
1027
|
+
self.tabwidget.setTabToolTip(
|
|
1028
|
+
s_idx, _("1D Signals: Manage and process one-dimensional data")
|
|
1029
|
+
)
|
|
1030
|
+
self.tabwidget.setTabToolTip(
|
|
1031
|
+
i_idx, _("2D Images: Manage and process two-dimensional data")
|
|
1032
|
+
)
|
|
1033
|
+
|
|
1034
|
+
# Apply enhanced tab bar styling
|
|
1035
|
+
tab_bar = self.tabwidget.tabBar()
|
|
1036
|
+
font = tab_bar.font()
|
|
1037
|
+
font.setPointSize(10)
|
|
1038
|
+
tab_bar.setFont(font)
|
|
1039
|
+
# Use QTimer to ensure tab bar is properly sized first
|
|
1040
|
+
QC.QTimer.singleShot(0, self.__update_tab_icon_size)
|
|
1041
|
+
|
|
1042
|
+
self.setCentralWidget(self.tabwidget)
|
|
1043
|
+
|
|
1044
|
+
def __update_tab_icon_size(self) -> None:
|
|
1045
|
+
"""Update tab icon size based on tab bar height"""
|
|
1046
|
+
tab_bar = self.tabwidget.tabBar()
|
|
1047
|
+
if tab_bar.height() > 0:
|
|
1048
|
+
# Use approximately 80% of tab height for icon size
|
|
1049
|
+
icon_size = int(tab_bar.height() * 0.8)
|
|
1050
|
+
self.tabwidget.setIconSize(QC.QSize(icon_size, icon_size))
|
|
1051
|
+
|
|
1052
|
+
@staticmethod
|
|
1053
|
+
def __get_local_doc_path() -> str | None:
|
|
1054
|
+
"""Return local documentation path, if it exists"""
|
|
1055
|
+
locale = QC.QLocale.system().name()
|
|
1056
|
+
for suffix in ("_" + locale[:2], "_en"):
|
|
1057
|
+
path = osp.join(DATAPATH, "doc", f"{APP_NAME}{suffix}.pdf")
|
|
1058
|
+
if osp.isfile(path):
|
|
1059
|
+
return path
|
|
1060
|
+
return None
|
|
1061
|
+
|
|
1062
|
+
def __add_menus(self) -> None:
|
|
1063
|
+
"""Adding menus"""
|
|
1064
|
+
self.file_menu = self.menuBar().addMenu(_("&File"))
|
|
1065
|
+
configure_menu_about_to_show(self.file_menu, self.__update_file_menu)
|
|
1066
|
+
self.create_menu = self.menuBar().addMenu(_("&Create"))
|
|
1067
|
+
self.edit_menu = self.menuBar().addMenu(_("&Edit"))
|
|
1068
|
+
self.roi_menu = self.menuBar().addMenu(_("ROI"))
|
|
1069
|
+
self.operation_menu = self.menuBar().addMenu(_("Operations"))
|
|
1070
|
+
self.processing_menu = self.menuBar().addMenu(_("Processing"))
|
|
1071
|
+
self.analysis_menu = self.menuBar().addMenu(_("Analysis"))
|
|
1072
|
+
self.plugins_menu = self.menuBar().addMenu(_("Plugins"))
|
|
1073
|
+
self.view_menu = self.menuBar().addMenu(_("&View"))
|
|
1074
|
+
configure_menu_about_to_show(self.view_menu, self.__update_view_menu)
|
|
1075
|
+
self.help_menu = self.menuBar().addMenu("?")
|
|
1076
|
+
for menu in (
|
|
1077
|
+
self.create_menu,
|
|
1078
|
+
self.edit_menu,
|
|
1079
|
+
self.roi_menu,
|
|
1080
|
+
self.operation_menu,
|
|
1081
|
+
self.processing_menu,
|
|
1082
|
+
self.analysis_menu,
|
|
1083
|
+
self.plugins_menu,
|
|
1084
|
+
):
|
|
1085
|
+
configure_menu_about_to_show(menu, self.__update_generic_menu)
|
|
1086
|
+
help_menu_actions = [
|
|
1087
|
+
create_action(
|
|
1088
|
+
self,
|
|
1089
|
+
_("Online documentation"),
|
|
1090
|
+
icon=get_icon("libre-gui-help.svg"),
|
|
1091
|
+
triggered=lambda: webbrowser.open(__docurl__),
|
|
1092
|
+
),
|
|
1093
|
+
]
|
|
1094
|
+
localdocpath = self.__get_local_doc_path()
|
|
1095
|
+
if localdocpath is not None:
|
|
1096
|
+
help_menu_actions += [
|
|
1097
|
+
create_action(
|
|
1098
|
+
self,
|
|
1099
|
+
_("PDF documentation"),
|
|
1100
|
+
icon=get_icon("help_pdf.svg"),
|
|
1101
|
+
triggered=lambda: webbrowser.open(localdocpath),
|
|
1102
|
+
),
|
|
1103
|
+
]
|
|
1104
|
+
help_menu_actions += [
|
|
1105
|
+
create_action(
|
|
1106
|
+
self,
|
|
1107
|
+
_("Tour") + "...",
|
|
1108
|
+
icon=get_icon("tour.svg"),
|
|
1109
|
+
triggered=self.show_tour,
|
|
1110
|
+
),
|
|
1111
|
+
create_action(
|
|
1112
|
+
self,
|
|
1113
|
+
_("Demo") + "...",
|
|
1114
|
+
icon=get_icon("play_demo.svg"),
|
|
1115
|
+
triggered=self.play_demo,
|
|
1116
|
+
),
|
|
1117
|
+
None,
|
|
1118
|
+
]
|
|
1119
|
+
if TEST_SEGFAULT_ERROR:
|
|
1120
|
+
help_menu_actions += [
|
|
1121
|
+
create_action(
|
|
1122
|
+
self,
|
|
1123
|
+
_("Test segfault/Python error"),
|
|
1124
|
+
triggered=self.test_segfault_error,
|
|
1125
|
+
)
|
|
1126
|
+
]
|
|
1127
|
+
help_menu_actions += [
|
|
1128
|
+
create_action(
|
|
1129
|
+
self,
|
|
1130
|
+
_("Log files") + "...",
|
|
1131
|
+
icon=get_icon("logs.svg"),
|
|
1132
|
+
triggered=self.__show_logviewer,
|
|
1133
|
+
),
|
|
1134
|
+
create_action(
|
|
1135
|
+
self,
|
|
1136
|
+
_("Installation and configuration") + "...",
|
|
1137
|
+
icon=get_icon("libre-toolbox.svg"),
|
|
1138
|
+
triggered=lambda: instconfviewer.exec_datalab_installconfig_dialog(
|
|
1139
|
+
self
|
|
1140
|
+
),
|
|
1141
|
+
),
|
|
1142
|
+
None,
|
|
1143
|
+
create_action(
|
|
1144
|
+
self,
|
|
1145
|
+
_("Project home page"),
|
|
1146
|
+
icon=get_icon("libre-gui-globe.svg"),
|
|
1147
|
+
triggered=lambda: webbrowser.open(__homeurl__),
|
|
1148
|
+
),
|
|
1149
|
+
create_action(
|
|
1150
|
+
self,
|
|
1151
|
+
_("Bug report or feature request"),
|
|
1152
|
+
icon=get_icon("libre-gui-globe.svg"),
|
|
1153
|
+
triggered=lambda: webbrowser.open(__supporturl__),
|
|
1154
|
+
),
|
|
1155
|
+
create_action(
|
|
1156
|
+
self,
|
|
1157
|
+
_("About..."),
|
|
1158
|
+
icon=get_icon("libre-gui-about.svg"),
|
|
1159
|
+
triggered=self.__about,
|
|
1160
|
+
),
|
|
1161
|
+
]
|
|
1162
|
+
add_actions(self.help_menu, help_menu_actions)
|
|
1163
|
+
|
|
1164
|
+
def __update_console_show_mode(self) -> None:
|
|
1165
|
+
"""Update console show mode from configuration option
|
|
1166
|
+
|
|
1167
|
+
Console show mode is whether the console is shown or not when an error occurs.
|
|
1168
|
+
"""
|
|
1169
|
+
if self.console is not None:
|
|
1170
|
+
state = Conf.console.show_console_on_error.get()
|
|
1171
|
+
cdock = self.docks[self.console]
|
|
1172
|
+
if not state and cdock.isVisible():
|
|
1173
|
+
cdock.hide()
|
|
1174
|
+
if state:
|
|
1175
|
+
self.console.exception_occurred.connect(self.console.show_console)
|
|
1176
|
+
else:
|
|
1177
|
+
self.console.exception_occurred.disconnect(self.console.show_console)
|
|
1178
|
+
|
|
1179
|
+
def __setup_console(self) -> None:
|
|
1180
|
+
"""Add an internal console"""
|
|
1181
|
+
ns = {
|
|
1182
|
+
"dl": self,
|
|
1183
|
+
"np": np,
|
|
1184
|
+
"sps": sps,
|
|
1185
|
+
"spi": spi,
|
|
1186
|
+
"os": os,
|
|
1187
|
+
"sys": sys,
|
|
1188
|
+
"osp": osp,
|
|
1189
|
+
"time": time,
|
|
1190
|
+
}
|
|
1191
|
+
msg = _(
|
|
1192
|
+
"Welcome to DataLab console!\n"
|
|
1193
|
+
"---------------------------\n"
|
|
1194
|
+
"You can access the main window with the 'dl' variable.\n"
|
|
1195
|
+
"Example:\n"
|
|
1196
|
+
" o = dl.get_object() # returns currently selected object\n"
|
|
1197
|
+
" o = dl[1] # returns object number 1\n"
|
|
1198
|
+
" o = dl['My image'] # returns object which title is 'My image'\n"
|
|
1199
|
+
" o.data # returns object data\n"
|
|
1200
|
+
"Modules imported at startup: "
|
|
1201
|
+
"os, sys, os.path as osp, time, "
|
|
1202
|
+
"numpy as np, scipy.signal as sps, scipy.ndimage as spi"
|
|
1203
|
+
)
|
|
1204
|
+
self.console = DockableConsole(self, namespace=ns, message=msg, debug=DEBUG)
|
|
1205
|
+
self.console.setMaximumBlockCount(Conf.console.max_line_count.get(5000))
|
|
1206
|
+
self.console.go_to_error.connect(go_to_error)
|
|
1207
|
+
cdock = self.__add_dockwidget(self.console, _("Console"))
|
|
1208
|
+
self.docks[self.console] = cdock
|
|
1209
|
+
cdock.hide()
|
|
1210
|
+
self.console.interpreter.widget_proxy.sig_new_prompt.connect(
|
|
1211
|
+
lambda txt: self.repopulate_panel_trees()
|
|
1212
|
+
)
|
|
1213
|
+
self.__update_console_show_mode()
|
|
1214
|
+
self.console.exception_occurred.connect(self.consolestatus.exception_occurred)
|
|
1215
|
+
cdock.visibilityChanged.connect(self.consolestatus.console_visibility_changed)
|
|
1216
|
+
self.consolestatus.SIG_SHOW_CONSOLE.connect(self.console.show_console)
|
|
1217
|
+
|
|
1218
|
+
def __add_macro_panel(self) -> None:
|
|
1219
|
+
"""Add macro panel"""
|
|
1220
|
+
self.macropanel = macro.MacroPanel(self)
|
|
1221
|
+
mdock = self.__add_dockwidget(self.macropanel, _("Macro Panel"))
|
|
1222
|
+
self.docks[self.macropanel] = mdock
|
|
1223
|
+
self.tabifyDockWidget(self.docks[self.imagepanel], mdock)
|
|
1224
|
+
self.docks[self.signalpanel].raise_()
|
|
1225
|
+
|
|
1226
|
+
def __configure_panels(self) -> None:
|
|
1227
|
+
"""Configure panels"""
|
|
1228
|
+
# Connectings signals
|
|
1229
|
+
for panel in self.panels:
|
|
1230
|
+
panel.SIG_OBJECT_ADDED.connect(self.set_modified)
|
|
1231
|
+
panel.SIG_OBJECT_REMOVED.connect(self.set_modified)
|
|
1232
|
+
self.macropanel.SIG_OBJECT_MODIFIED.connect(self.set_modified)
|
|
1233
|
+
# Initializing common panel actions
|
|
1234
|
+
self.autorefresh_action.setChecked(Conf.view.auto_refresh.get(True))
|
|
1235
|
+
self.showfirstonly_action.setChecked(Conf.view.show_first_only.get(False))
|
|
1236
|
+
self.showlabel_action.setChecked(Conf.view.show_label.get(False))
|
|
1237
|
+
# Restoring current tab from last session
|
|
1238
|
+
tab_idx = Conf.main.current_tab.get(None)
|
|
1239
|
+
if tab_idx is not None:
|
|
1240
|
+
self.tabwidget.setCurrentIndex(tab_idx)
|
|
1241
|
+
# Set focus on current panel, so that keyboard shortcuts work (Fixes #10)
|
|
1242
|
+
self.tabwidget.currentWidget().setFocus()
|
|
1243
|
+
|
|
1244
|
+
def set_process_isolation_enabled(self, state: bool) -> None:
|
|
1245
|
+
"""Enable/disable process isolation
|
|
1246
|
+
|
|
1247
|
+
Args:
|
|
1248
|
+
state: True to enable process isolation
|
|
1249
|
+
"""
|
|
1250
|
+
for processor in (self.imagepanel.processor, self.signalpanel.processor):
|
|
1251
|
+
processor.set_process_isolation_enabled(state)
|
|
1252
|
+
|
|
1253
|
+
# ------Remote control
|
|
1254
|
+
@remote_controlled
|
|
1255
|
+
def get_current_panel(self) -> str:
|
|
1256
|
+
"""Return current panel name
|
|
1257
|
+
|
|
1258
|
+
Returns:
|
|
1259
|
+
Panel name (valid values: "signal", "image", "macro")
|
|
1260
|
+
"""
|
|
1261
|
+
panel = self.tabwidget.currentWidget()
|
|
1262
|
+
dock = self.docks[panel]
|
|
1263
|
+
if panel is self.signalpanel and dock.isVisible():
|
|
1264
|
+
return "signal"
|
|
1265
|
+
if panel is self.imagepanel and dock.isVisible():
|
|
1266
|
+
return "image"
|
|
1267
|
+
return "macro"
|
|
1268
|
+
|
|
1269
|
+
@remote_controlled
|
|
1270
|
+
def set_current_panel(
|
|
1271
|
+
self, panel: Literal["signal", "image", "macro"] | BaseDataPanel
|
|
1272
|
+
) -> None:
|
|
1273
|
+
"""Switch to panel.
|
|
1274
|
+
|
|
1275
|
+
Args:
|
|
1276
|
+
panel: panel name or panel instance
|
|
1277
|
+
|
|
1278
|
+
Raises:
|
|
1279
|
+
ValueError: unknown panel
|
|
1280
|
+
"""
|
|
1281
|
+
if not isinstance(panel, str):
|
|
1282
|
+
if panel not in self.panels:
|
|
1283
|
+
raise ValueError(f"Unknown panel {panel}")
|
|
1284
|
+
panel = (
|
|
1285
|
+
"signal"
|
|
1286
|
+
if panel is self.signalpanel
|
|
1287
|
+
else "image"
|
|
1288
|
+
if panel is self.imagepanel
|
|
1289
|
+
else "macro"
|
|
1290
|
+
)
|
|
1291
|
+
if self.get_current_panel() == panel:
|
|
1292
|
+
if panel in ("signal", "image"):
|
|
1293
|
+
# Force tab index changed event to be sure that the dock associated
|
|
1294
|
+
# to the current panel is raised
|
|
1295
|
+
self.__tab_index_changed(self.tabwidget.currentIndex())
|
|
1296
|
+
return
|
|
1297
|
+
if panel == "signal":
|
|
1298
|
+
self.tabwidget.setCurrentWidget(self.signalpanel)
|
|
1299
|
+
elif panel == "image":
|
|
1300
|
+
self.tabwidget.setCurrentWidget(self.imagepanel)
|
|
1301
|
+
elif panel == "macro":
|
|
1302
|
+
self.docks[self.macropanel].raise_()
|
|
1303
|
+
else:
|
|
1304
|
+
raise ValueError(f"Unknown panel {panel}")
|
|
1305
|
+
|
|
1306
|
+
@remote_controlled
|
|
1307
|
+
def calc(self, name: str, param: gds.DataSet | None = None) -> None:
|
|
1308
|
+
"""Call computation feature ``name``
|
|
1309
|
+
|
|
1310
|
+
.. note::
|
|
1311
|
+
|
|
1312
|
+
This calls either the processor's ``compute_<name>`` method (if it exists),
|
|
1313
|
+
or the processor's ``<name>`` computation feature (if it is registered,
|
|
1314
|
+
using the ``run_feature`` method).
|
|
1315
|
+
It looks for the function in all panels, starting with the current one.
|
|
1316
|
+
|
|
1317
|
+
Args:
|
|
1318
|
+
name: Compute function name
|
|
1319
|
+
param: Compute function parameter. Defaults to None.
|
|
1320
|
+
|
|
1321
|
+
Raises:
|
|
1322
|
+
ValueError: unknown function
|
|
1323
|
+
"""
|
|
1324
|
+
panels = [self.tabwidget.currentWidget()]
|
|
1325
|
+
panels.extend(self.panels)
|
|
1326
|
+
for panel in panels:
|
|
1327
|
+
if isinstance(panel, base.BaseDataPanel):
|
|
1328
|
+
name = name.removeprefix("compute_")
|
|
1329
|
+
panel: base.BaseDataPanel
|
|
1330
|
+
# Some computation features are wrapped in a method with a
|
|
1331
|
+
# "compute_" prefix, so we check for this first:
|
|
1332
|
+
func = getattr(panel.processor, f"compute_{name}", None)
|
|
1333
|
+
if func is not None:
|
|
1334
|
+
if param is None:
|
|
1335
|
+
func()
|
|
1336
|
+
else:
|
|
1337
|
+
func(param)
|
|
1338
|
+
return
|
|
1339
|
+
# If the function is not wrapped, we check if it is a
|
|
1340
|
+
# registered feature:
|
|
1341
|
+
try:
|
|
1342
|
+
feature = panel.processor.get_feature(name)
|
|
1343
|
+
panel.processor.run_feature(feature, param)
|
|
1344
|
+
return
|
|
1345
|
+
except ValueError:
|
|
1346
|
+
continue
|
|
1347
|
+
raise ValueError(f"Unknown computation function {name}")
|
|
1348
|
+
|
|
1349
|
+
# ------GUI refresh
|
|
1350
|
+
def has_objects(self) -> bool:
|
|
1351
|
+
"""Return True if sig/ima panels have any object"""
|
|
1352
|
+
return sum(len(panel) for panel in self.panels) > 0
|
|
1353
|
+
|
|
1354
|
+
def set_modified(self, state: bool = True) -> None:
|
|
1355
|
+
"""Set mainwindow modified state"""
|
|
1356
|
+
state = state and self.has_objects()
|
|
1357
|
+
self.__is_modified = state
|
|
1358
|
+
title = APP_NAME + ("*" if state else "")
|
|
1359
|
+
if not datalab.__version__.replace(".", "").isdigit():
|
|
1360
|
+
title += f" [{datalab.__version__}]"
|
|
1361
|
+
self.setWindowTitle(title)
|
|
1362
|
+
|
|
1363
|
+
def __add_dockwidget(self, child, title: str) -> QW.QDockWidget:
|
|
1364
|
+
"""Add QDockWidget and toggleViewAction"""
|
|
1365
|
+
dockwidget, location = child.create_dockwidget(title)
|
|
1366
|
+
dockwidget.setObjectName(title)
|
|
1367
|
+
self.addDockWidget(location, dockwidget)
|
|
1368
|
+
return dockwidget
|
|
1369
|
+
|
|
1370
|
+
def repopulate_panel_trees(self) -> None:
|
|
1371
|
+
"""Repopulate all panel trees"""
|
|
1372
|
+
for panel in self.panels:
|
|
1373
|
+
if isinstance(panel, base.BaseDataPanel):
|
|
1374
|
+
panel.objview.populate_tree()
|
|
1375
|
+
|
|
1376
|
+
def __update_actions(self, update_other_data_panel: bool = False) -> None:
|
|
1377
|
+
"""Update selection dependent actions
|
|
1378
|
+
|
|
1379
|
+
Args:
|
|
1380
|
+
update_other_data_panel: True to update other data panel actions
|
|
1381
|
+
(i.e. if the current panel is the signal panel, also update the image
|
|
1382
|
+
panel actions, and vice-versa)
|
|
1383
|
+
"""
|
|
1384
|
+
is_signal = self.tabwidget.currentWidget() is self.signalpanel
|
|
1385
|
+
panel = self.signalpanel if is_signal else self.imagepanel
|
|
1386
|
+
other_panel = self.imagepanel if is_signal else self.signalpanel
|
|
1387
|
+
if update_other_data_panel:
|
|
1388
|
+
other_panel.selection_changed()
|
|
1389
|
+
panel.selection_changed()
|
|
1390
|
+
self.signalpanel_toolbar.setVisible(is_signal)
|
|
1391
|
+
self.imagepanel_toolbar.setVisible(not is_signal)
|
|
1392
|
+
if self.plugins_menu is not None:
|
|
1393
|
+
plugin_actions = panel.get_category_actions(ActionCategory.PLUGINS)
|
|
1394
|
+
self.plugins_menu.setEnabled(len(plugin_actions) > 0)
|
|
1395
|
+
|
|
1396
|
+
def __tab_index_changed(self, index: int) -> None:
|
|
1397
|
+
"""Switch from signal to image mode, or vice-versa"""
|
|
1398
|
+
dock = self.docks[self.tabwidget.widget(index)]
|
|
1399
|
+
dock.raise_()
|
|
1400
|
+
self.__update_actions()
|
|
1401
|
+
|
|
1402
|
+
def __update_generic_menu(self, menu: QW.QMenu | None = None) -> None:
|
|
1403
|
+
"""Update menu before showing up -- Generic method"""
|
|
1404
|
+
if menu is None:
|
|
1405
|
+
menu = self.sender()
|
|
1406
|
+
menu.clear()
|
|
1407
|
+
panel = self.tabwidget.currentWidget()
|
|
1408
|
+
category = {
|
|
1409
|
+
self.file_menu: ActionCategory.FILE,
|
|
1410
|
+
self.create_menu: ActionCategory.CREATE,
|
|
1411
|
+
self.edit_menu: ActionCategory.EDIT,
|
|
1412
|
+
self.roi_menu: ActionCategory.ROI,
|
|
1413
|
+
self.view_menu: ActionCategory.VIEW,
|
|
1414
|
+
self.operation_menu: ActionCategory.OPERATION,
|
|
1415
|
+
self.processing_menu: ActionCategory.PROCESSING,
|
|
1416
|
+
self.analysis_menu: ActionCategory.ANALYSIS,
|
|
1417
|
+
self.plugins_menu: ActionCategory.PLUGINS,
|
|
1418
|
+
}[menu]
|
|
1419
|
+
actions = panel.get_category_actions(category)
|
|
1420
|
+
add_actions(menu, actions)
|
|
1421
|
+
|
|
1422
|
+
def __update_file_menu(self) -> None:
|
|
1423
|
+
"""Update file menu before showing up"""
|
|
1424
|
+
self.saveh5_action.setEnabled(self.has_objects())
|
|
1425
|
+
self.__update_generic_menu(self.file_menu)
|
|
1426
|
+
add_actions(
|
|
1427
|
+
self.file_menu,
|
|
1428
|
+
[
|
|
1429
|
+
None,
|
|
1430
|
+
self.openh5_action,
|
|
1431
|
+
self.saveh5_action,
|
|
1432
|
+
self.browseh5_action,
|
|
1433
|
+
None,
|
|
1434
|
+
self.settings_action,
|
|
1435
|
+
],
|
|
1436
|
+
)
|
|
1437
|
+
if self.quit_action is not None:
|
|
1438
|
+
add_actions(self.file_menu, [self.quit_action])
|
|
1439
|
+
|
|
1440
|
+
def __update_view_menu(self) -> None:
|
|
1441
|
+
"""Update view menu before showing up"""
|
|
1442
|
+
self.__update_generic_menu(self.view_menu)
|
|
1443
|
+
add_actions(self.view_menu, [None] + self.createPopupMenu().actions())
|
|
1444
|
+
|
|
1445
|
+
@remote_controlled
|
|
1446
|
+
def toggle_show_titles(self, state: bool) -> None:
|
|
1447
|
+
"""Toggle show annotations option
|
|
1448
|
+
|
|
1449
|
+
Args:
|
|
1450
|
+
state: state
|
|
1451
|
+
"""
|
|
1452
|
+
Conf.view.show_label.set(state)
|
|
1453
|
+
for datapanel in (self.signalpanel, self.imagepanel):
|
|
1454
|
+
for obj in datapanel.objmodel:
|
|
1455
|
+
obj.set_metadata_option("showlabel", state)
|
|
1456
|
+
datapanel.refresh_plot("selected", True, False)
|
|
1457
|
+
|
|
1458
|
+
def handle_autorefresh_action(self, state: bool) -> None:
|
|
1459
|
+
"""Handle auto-refresh action from UI (with confirmation dialog)
|
|
1460
|
+
|
|
1461
|
+
Args:
|
|
1462
|
+
state: desired state
|
|
1463
|
+
"""
|
|
1464
|
+
# If disabling auto-refresh, show confirmation dialog
|
|
1465
|
+
if not state:
|
|
1466
|
+
txtlist = [
|
|
1467
|
+
"<b>" + _("Disable auto-refresh?") + "</b>",
|
|
1468
|
+
"",
|
|
1469
|
+
_(
|
|
1470
|
+
"When auto-refresh is disabled, the plot view will not "
|
|
1471
|
+
"automatically update when objects are modified, added or removed."
|
|
1472
|
+
),
|
|
1473
|
+
"",
|
|
1474
|
+
_(
|
|
1475
|
+
"You will need to manually click the refresh button to update "
|
|
1476
|
+
"the view."
|
|
1477
|
+
),
|
|
1478
|
+
"",
|
|
1479
|
+
_("Are you sure you want to disable auto-refresh?"),
|
|
1480
|
+
]
|
|
1481
|
+
|
|
1482
|
+
answer = QW.QMessageBox.question(
|
|
1483
|
+
self,
|
|
1484
|
+
APP_NAME,
|
|
1485
|
+
"<br>".join(txtlist),
|
|
1486
|
+
QW.QMessageBox.Yes | QW.QMessageBox.No,
|
|
1487
|
+
QW.QMessageBox.No,
|
|
1488
|
+
)
|
|
1489
|
+
|
|
1490
|
+
if answer == QW.QMessageBox.No:
|
|
1491
|
+
# User cancelled, restore the action's checked state
|
|
1492
|
+
self.autorefresh_action.blockSignals(True)
|
|
1493
|
+
self.autorefresh_action.setChecked(True)
|
|
1494
|
+
self.autorefresh_action.blockSignals(False)
|
|
1495
|
+
return
|
|
1496
|
+
|
|
1497
|
+
# Apply the change
|
|
1498
|
+
self.toggle_auto_refresh(state)
|
|
1499
|
+
|
|
1500
|
+
@remote_controlled
|
|
1501
|
+
def toggle_auto_refresh(self, state: bool) -> None:
|
|
1502
|
+
"""Toggle auto refresh option
|
|
1503
|
+
|
|
1504
|
+
Args:
|
|
1505
|
+
state: state
|
|
1506
|
+
"""
|
|
1507
|
+
Conf.view.auto_refresh.set(state)
|
|
1508
|
+
for datapanel in (self.signalpanel, self.imagepanel):
|
|
1509
|
+
datapanel.plothandler.set_auto_refresh(state)
|
|
1510
|
+
|
|
1511
|
+
@remote_controlled
|
|
1512
|
+
def toggle_show_first_only(self, state: bool) -> None:
|
|
1513
|
+
"""Toggle show first only option
|
|
1514
|
+
|
|
1515
|
+
Args:
|
|
1516
|
+
state: state
|
|
1517
|
+
"""
|
|
1518
|
+
Conf.view.show_first_only.set(state)
|
|
1519
|
+
for datapanel in (self.signalpanel, self.imagepanel):
|
|
1520
|
+
datapanel.plothandler.set_show_first_only(state)
|
|
1521
|
+
|
|
1522
|
+
# ------Common features
|
|
1523
|
+
@remote_controlled
|
|
1524
|
+
def reset_all(self) -> None:
|
|
1525
|
+
"""Reset all application data"""
|
|
1526
|
+
for panel in self.panels:
|
|
1527
|
+
if panel is not None:
|
|
1528
|
+
panel.remove_all_objects()
|
|
1529
|
+
|
|
1530
|
+
@staticmethod
|
|
1531
|
+
def __check_h5file(filename: str, operation: str) -> str:
|
|
1532
|
+
"""Check HDF5 filename"""
|
|
1533
|
+
filename = osp.abspath(osp.normpath(filename))
|
|
1534
|
+
bname = osp.basename(filename)
|
|
1535
|
+
if operation == "load" and not osp.isfile(filename):
|
|
1536
|
+
raise IOError(f'File not found "{bname}"')
|
|
1537
|
+
Conf.main.base_dir.set(filename)
|
|
1538
|
+
return filename
|
|
1539
|
+
|
|
1540
|
+
@remote_controlled
|
|
1541
|
+
def save_to_h5_file(self, filename=None) -> None:
|
|
1542
|
+
"""Save to a DataLab HDF5 file
|
|
1543
|
+
|
|
1544
|
+
Args:
|
|
1545
|
+
filename: HDF5 filename. If None, a file dialog is opened.
|
|
1546
|
+
|
|
1547
|
+
Raises:
|
|
1548
|
+
IOError: if filename is invalid or file cannot be saved.
|
|
1549
|
+
"""
|
|
1550
|
+
if filename is None:
|
|
1551
|
+
basedir = Conf.main.base_dir.get()
|
|
1552
|
+
with qth.save_restore_stds():
|
|
1553
|
+
filename, _fl = getsavefilename(
|
|
1554
|
+
self,
|
|
1555
|
+
_("Save"),
|
|
1556
|
+
basedir,
|
|
1557
|
+
"HDF5 (*.h5 *.hdf5 *.hdf *.he5);;All files (*)",
|
|
1558
|
+
)
|
|
1559
|
+
if not filename:
|
|
1560
|
+
return
|
|
1561
|
+
with qth.qt_try_loadsave_file(self, filename, "save"):
|
|
1562
|
+
filename = self.__check_h5file(filename, "save")
|
|
1563
|
+
self.h5inputoutput.save_file(filename)
|
|
1564
|
+
self.set_modified(False)
|
|
1565
|
+
|
|
1566
|
+
@remote_controlled
|
|
1567
|
+
def open_h5_files(
|
|
1568
|
+
self,
|
|
1569
|
+
h5files: list[str] | None = None,
|
|
1570
|
+
import_all: bool | None = None,
|
|
1571
|
+
reset_all: bool | None = None,
|
|
1572
|
+
) -> None:
|
|
1573
|
+
"""Open a DataLab HDF5 file or import from any other HDF5 file.
|
|
1574
|
+
|
|
1575
|
+
Args:
|
|
1576
|
+
h5files: HDF5 filenames (optionally with dataset name, separated by ":")
|
|
1577
|
+
import_all: Import all datasets from HDF5 files
|
|
1578
|
+
reset_all: Reset all application data before importing
|
|
1579
|
+
"""
|
|
1580
|
+
if not self.confirm_memory_state():
|
|
1581
|
+
return
|
|
1582
|
+
if reset_all is None:
|
|
1583
|
+
# When workspace is empty, always preserve UUIDs (reset_all=True)
|
|
1584
|
+
# since there's no risk of conflicts
|
|
1585
|
+
if not self.has_objects():
|
|
1586
|
+
reset_all = True
|
|
1587
|
+
else:
|
|
1588
|
+
reset_all = Conf.io.h5_clear_workspace.get()
|
|
1589
|
+
if Conf.io.h5_clear_workspace_ask.get():
|
|
1590
|
+
# Build message with optional note for native workspace import
|
|
1591
|
+
msg = _(
|
|
1592
|
+
"Do you want to clear current workspace "
|
|
1593
|
+
"(signals and images) before importing data from "
|
|
1594
|
+
"HDF5 files?"
|
|
1595
|
+
)
|
|
1596
|
+
# Only show the UUID conflict note when importing native DataLab
|
|
1597
|
+
# workspace files (import_all=True), not when using HDF5 browser
|
|
1598
|
+
if import_all:
|
|
1599
|
+
msg += "<br><br>" + _(
|
|
1600
|
+
"<u>Note:</u> If you choose <i>No</i>, when importing "
|
|
1601
|
+
"DataLab workspace files, objects with conflicting "
|
|
1602
|
+
"identifiers will have their processing history lost "
|
|
1603
|
+
"(features like 'Show source' and 'Recompute' will not "
|
|
1604
|
+
"work for those objects). Non-conflicting objects will "
|
|
1605
|
+
"preserve their processing history."
|
|
1606
|
+
)
|
|
1607
|
+
msg += "<br><br>" + _(
|
|
1608
|
+
"Choosing to ignore this message will prevent it "
|
|
1609
|
+
"from being displayed again, and will use the "
|
|
1610
|
+
"current setting (%s)."
|
|
1611
|
+
) % (_("Yes") if reset_all else _("No"))
|
|
1612
|
+
answer = QW.QMessageBox.question(
|
|
1613
|
+
self,
|
|
1614
|
+
_("Warning"),
|
|
1615
|
+
msg,
|
|
1616
|
+
QW.QMessageBox.Yes | QW.QMessageBox.No | QW.QMessageBox.Ignore,
|
|
1617
|
+
)
|
|
1618
|
+
if answer == QW.QMessageBox.Yes:
|
|
1619
|
+
reset_all = True
|
|
1620
|
+
elif answer == QW.QMessageBox.Ignore:
|
|
1621
|
+
Conf.io.h5_clear_workspace_ask.set(False)
|
|
1622
|
+
if h5files is None:
|
|
1623
|
+
basedir = Conf.main.base_dir.get()
|
|
1624
|
+
with qth.save_restore_stds():
|
|
1625
|
+
h5files, _fl = getopenfilenames(
|
|
1626
|
+
self,
|
|
1627
|
+
_("Open"),
|
|
1628
|
+
basedir,
|
|
1629
|
+
_("HDF5 files (*.h5 *.hdf5 *.hdf *.he5);;All files (*)"),
|
|
1630
|
+
)
|
|
1631
|
+
if not h5files:
|
|
1632
|
+
return
|
|
1633
|
+
filenames, dsetnames = [], []
|
|
1634
|
+
for fname_with_dset in h5files:
|
|
1635
|
+
if "," in fname_with_dset:
|
|
1636
|
+
filename, dsetname = fname_with_dset.split(",")
|
|
1637
|
+
dsetnames.append(dsetname)
|
|
1638
|
+
else:
|
|
1639
|
+
filename = fname_with_dset
|
|
1640
|
+
dsetnames.append(None)
|
|
1641
|
+
filenames.append(filename)
|
|
1642
|
+
if import_all is None and all(dsetname is None for dsetname in dsetnames):
|
|
1643
|
+
self.browse_h5_files(filenames, reset_all)
|
|
1644
|
+
return
|
|
1645
|
+
for filename, dsetname in zip(filenames, dsetnames):
|
|
1646
|
+
if import_all is None and dsetname is None:
|
|
1647
|
+
self.import_h5_file(filename, reset_all)
|
|
1648
|
+
else:
|
|
1649
|
+
with qth.qt_try_loadsave_file(self, filename, "load"):
|
|
1650
|
+
filename = self.__check_h5file(filename, "load")
|
|
1651
|
+
if dsetname is None:
|
|
1652
|
+
self.h5inputoutput.open_file(filename, import_all, reset_all)
|
|
1653
|
+
else:
|
|
1654
|
+
self.h5inputoutput.import_dataset_from_file(filename, dsetname)
|
|
1655
|
+
reset_all = False
|
|
1656
|
+
|
|
1657
|
+
def browse_h5_files(self, filenames: list[str], reset_all: bool) -> None:
|
|
1658
|
+
"""Browse HDF5 files
|
|
1659
|
+
|
|
1660
|
+
Args:
|
|
1661
|
+
filenames: HDF5 filenames
|
|
1662
|
+
reset_all: Reset all application data before importing
|
|
1663
|
+
"""
|
|
1664
|
+
for filename in filenames:
|
|
1665
|
+
self.__check_h5file(filename, "load")
|
|
1666
|
+
self.h5inputoutput.import_files(filenames, False, reset_all)
|
|
1667
|
+
|
|
1668
|
+
@remote_controlled
|
|
1669
|
+
def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
|
|
1670
|
+
"""Import HDF5 file into DataLab
|
|
1671
|
+
|
|
1672
|
+
Args:
|
|
1673
|
+
filename: HDF5 filename (optionally with dataset name,
|
|
1674
|
+
separated by ":")
|
|
1675
|
+
reset_all: Delete all DataLab signals/images before importing data
|
|
1676
|
+
"""
|
|
1677
|
+
with qth.qt_try_loadsave_file(self, filename, "load"):
|
|
1678
|
+
filename = self.__check_h5file(filename, "load")
|
|
1679
|
+
self.h5inputoutput.import_files([filename], False, reset_all)
|
|
1680
|
+
|
|
1681
|
+
# This method is intentionally *not* remote controlled
|
|
1682
|
+
# (see TODO regarding RemoteClient.add_object method)
|
|
1683
|
+
# @remote_controlled
|
|
1684
|
+
def add_object(
|
|
1685
|
+
self, obj: SignalObj | ImageObj, group_id: str = "", set_current=True
|
|
1686
|
+
) -> None:
|
|
1687
|
+
"""Add object - signal or image
|
|
1688
|
+
|
|
1689
|
+
Args:
|
|
1690
|
+
obj: object to add (signal or image)
|
|
1691
|
+
group_id: group ID (optional)
|
|
1692
|
+
set_current: True to set the object as current object
|
|
1693
|
+
"""
|
|
1694
|
+
if self.confirm_memory_state():
|
|
1695
|
+
if isinstance(obj, SignalObj):
|
|
1696
|
+
self.signalpanel.add_object(obj, group_id, set_current)
|
|
1697
|
+
elif isinstance(obj, ImageObj):
|
|
1698
|
+
self.imagepanel.add_object(obj, group_id, set_current)
|
|
1699
|
+
else:
|
|
1700
|
+
raise TypeError(f"Unsupported object type {type(obj)}")
|
|
1701
|
+
|
|
1702
|
+
@remote_controlled
|
|
1703
|
+
def load_from_files(self, filenames: list[str]) -> None:
|
|
1704
|
+
"""Open objects from files in current panel (signals/images)
|
|
1705
|
+
|
|
1706
|
+
Args:
|
|
1707
|
+
filenames: list of filenames
|
|
1708
|
+
"""
|
|
1709
|
+
panel = self.__get_current_basedatapanel()
|
|
1710
|
+
panel.load_from_files(filenames)
|
|
1711
|
+
|
|
1712
|
+
@remote_controlled
|
|
1713
|
+
def load_from_directory(self, path: str) -> None:
|
|
1714
|
+
"""Open objects from directory in current panel (signals/images).
|
|
1715
|
+
|
|
1716
|
+
Args:
|
|
1717
|
+
path: directory path
|
|
1718
|
+
"""
|
|
1719
|
+
panel = self.__get_current_basedatapanel()
|
|
1720
|
+
panel.load_from_directory(path)
|
|
1721
|
+
|
|
1722
|
+
# ------Other methods related to AbstractDLControl interface
|
|
1723
|
+
def get_version(self) -> str:
|
|
1724
|
+
"""Return DataLab public version.
|
|
1725
|
+
|
|
1726
|
+
Returns:
|
|
1727
|
+
DataLab version
|
|
1728
|
+
"""
|
|
1729
|
+
return datalab.__version__
|
|
1730
|
+
|
|
1731
|
+
def close_application(self) -> None: # Implementing AbstractDLControl interface
|
|
1732
|
+
"""Close DataLab application"""
|
|
1733
|
+
self.close()
|
|
1734
|
+
|
|
1735
|
+
def raise_window(self) -> None: # Implementing AbstractDLControl interface
|
|
1736
|
+
"""Raise DataLab window"""
|
|
1737
|
+
bring_to_front(self)
|
|
1738
|
+
|
|
1739
|
+
def add_signal(
|
|
1740
|
+
self,
|
|
1741
|
+
title: str,
|
|
1742
|
+
xdata: np.ndarray,
|
|
1743
|
+
ydata: np.ndarray,
|
|
1744
|
+
xunit: str = "",
|
|
1745
|
+
yunit: str = "",
|
|
1746
|
+
xlabel: str = "",
|
|
1747
|
+
ylabel: str = "",
|
|
1748
|
+
group_id: str = "",
|
|
1749
|
+
set_current: bool = True,
|
|
1750
|
+
) -> bool: # pylint: disable=too-many-arguments
|
|
1751
|
+
"""Add signal data to DataLab.
|
|
1752
|
+
|
|
1753
|
+
Args:
|
|
1754
|
+
title: Signal title
|
|
1755
|
+
xdata: X data
|
|
1756
|
+
ydata: Y data
|
|
1757
|
+
xunit: X unit. Defaults to ""
|
|
1758
|
+
yunit: Y unit. Defaults to ""
|
|
1759
|
+
xlabel: X label. Defaults to ""
|
|
1760
|
+
ylabel: Y label. Defaults to ""
|
|
1761
|
+
group_id: group id in which to add the signal. Defaults to ""
|
|
1762
|
+
set_current: if True, set the added signal as current
|
|
1763
|
+
|
|
1764
|
+
Returns:
|
|
1765
|
+
True if signal was added successfully, False otherwise
|
|
1766
|
+
|
|
1767
|
+
Raises:
|
|
1768
|
+
ValueError: Invalid xdata dtype
|
|
1769
|
+
ValueError: Invalid ydata dtype
|
|
1770
|
+
"""
|
|
1771
|
+
obj = create_signal(
|
|
1772
|
+
title,
|
|
1773
|
+
xdata,
|
|
1774
|
+
ydata,
|
|
1775
|
+
units=(xunit, yunit),
|
|
1776
|
+
labels=(xlabel, ylabel),
|
|
1777
|
+
)
|
|
1778
|
+
self.add_object(obj, group_id, set_current)
|
|
1779
|
+
return True
|
|
1780
|
+
|
|
1781
|
+
def add_image(
|
|
1782
|
+
self,
|
|
1783
|
+
title: str,
|
|
1784
|
+
data: np.ndarray,
|
|
1785
|
+
xunit: str = "",
|
|
1786
|
+
yunit: str = "",
|
|
1787
|
+
zunit: str = "",
|
|
1788
|
+
xlabel: str = "",
|
|
1789
|
+
ylabel: str = "",
|
|
1790
|
+
zlabel: str = "",
|
|
1791
|
+
group_id: str = "",
|
|
1792
|
+
set_current: bool = True,
|
|
1793
|
+
) -> bool: # pylint: disable=too-many-arguments
|
|
1794
|
+
"""Add image data to DataLab.
|
|
1795
|
+
|
|
1796
|
+
Args:
|
|
1797
|
+
title: Image title
|
|
1798
|
+
data: Image data
|
|
1799
|
+
xunit: X unit. Defaults to ""
|
|
1800
|
+
yunit: Y unit. Defaults to ""
|
|
1801
|
+
zunit: Z unit. Defaults to ""
|
|
1802
|
+
xlabel: X label. Defaults to ""
|
|
1803
|
+
ylabel: Y label. Defaults to ""
|
|
1804
|
+
zlabel: Z label. Defaults to ""
|
|
1805
|
+
group_id: group id in which to add the image. Defaults to ""
|
|
1806
|
+
set_current: if True, set the added image as current
|
|
1807
|
+
|
|
1808
|
+
Returns:
|
|
1809
|
+
True if image was added successfully, False otherwise
|
|
1810
|
+
|
|
1811
|
+
Raises:
|
|
1812
|
+
ValueError: Invalid data dtype
|
|
1813
|
+
"""
|
|
1814
|
+
obj = create_image(
|
|
1815
|
+
title,
|
|
1816
|
+
data,
|
|
1817
|
+
units=(xunit, yunit, zunit),
|
|
1818
|
+
labels=(xlabel, ylabel, zlabel),
|
|
1819
|
+
)
|
|
1820
|
+
self.add_object(obj, group_id, set_current)
|
|
1821
|
+
return True
|
|
1822
|
+
|
|
1823
|
+
# ------?
|
|
1824
|
+
def __about(self) -> None: # pragma: no cover
|
|
1825
|
+
"""About dialog box"""
|
|
1826
|
+
self.check_stable_release()
|
|
1827
|
+
if self.remote_server.port is None:
|
|
1828
|
+
xrpcstate = '<font color="red">' + _("not started") + "</font>"
|
|
1829
|
+
else:
|
|
1830
|
+
xrpcstate = _("started (port %s)") % self.remote_server.port
|
|
1831
|
+
xrpcstate = f"<font color='green'>{xrpcstate}</font>"
|
|
1832
|
+
if Conf.main.process_isolation_enabled.get():
|
|
1833
|
+
pistate = "<font color='green'>" + _("enabled") + "</font>"
|
|
1834
|
+
else:
|
|
1835
|
+
pistate = "<font color='red'>" + _("disabled") + "</font>"
|
|
1836
|
+
adv_conf = "<br>".join(
|
|
1837
|
+
[
|
|
1838
|
+
"<i>" + _("Advanced configuration:") + "</i>",
|
|
1839
|
+
"• " + _("XML-RPC server:") + " " + xrpcstate,
|
|
1840
|
+
"• " + _("Process isolation:") + " " + pistate,
|
|
1841
|
+
]
|
|
1842
|
+
)
|
|
1843
|
+
created_by = _("Created by")
|
|
1844
|
+
dev_by = _("Developed and maintained by %s open-source project team") % APP_NAME
|
|
1845
|
+
cprght = "2023 DataLab Platform Developers"
|
|
1846
|
+
QW.QMessageBox.about(
|
|
1847
|
+
self,
|
|
1848
|
+
_("About") + " " + APP_NAME,
|
|
1849
|
+
f"""<b>{APP_NAME}</b> v{datalab.__version__}<br>{APP_DESC}
|
|
1850
|
+
<p>{created_by} Pierre Raybaut<br>{dev_by}<br>Copyright © {cprght}
|
|
1851
|
+
<p>{adv_conf}""",
|
|
1852
|
+
)
|
|
1853
|
+
|
|
1854
|
+
def __update_color_mode(self, startup: bool = False) -> None:
|
|
1855
|
+
"""Update color mode
|
|
1856
|
+
|
|
1857
|
+
Args:
|
|
1858
|
+
startup: True if method is called during application startup (in that case,
|
|
1859
|
+
color theme is applied only if mode != "auto")
|
|
1860
|
+
"""
|
|
1861
|
+
mode = Conf.main.color_mode.get()
|
|
1862
|
+
if startup and mode == "auto":
|
|
1863
|
+
guidata_qth.win32_fix_title_bar_background(self)
|
|
1864
|
+
return
|
|
1865
|
+
|
|
1866
|
+
# Prevent Qt from refreshing the window when changing the color mode:
|
|
1867
|
+
self.setUpdatesEnabled(False)
|
|
1868
|
+
|
|
1869
|
+
plotpy_config.set_plotpy_color_mode(mode)
|
|
1870
|
+
|
|
1871
|
+
if self.console is not None:
|
|
1872
|
+
self.console.update_color_mode()
|
|
1873
|
+
if self.macropanel is not None:
|
|
1874
|
+
self.macropanel.update_color_mode()
|
|
1875
|
+
if self.docks is not None:
|
|
1876
|
+
for dock in self.docks.values():
|
|
1877
|
+
widget = dock.widget()
|
|
1878
|
+
if isinstance(widget, DockablePlotWidget):
|
|
1879
|
+
widget.update_color_mode()
|
|
1880
|
+
|
|
1881
|
+
# Allow Qt to refresh the window:
|
|
1882
|
+
self.setUpdatesEnabled(True)
|
|
1883
|
+
|
|
1884
|
+
def __edit_settings(self) -> None:
|
|
1885
|
+
"""Edit settings"""
|
|
1886
|
+
changed_options = edit_settings(self)
|
|
1887
|
+
sigima_options.fft_shift_enabled.set(Conf.proc.fft_shift_enabled.get())
|
|
1888
|
+
sigima_options.auto_normalize_kernel.set(Conf.proc.auto_normalize_kernel.get())
|
|
1889
|
+
refresh_signal_panel = refresh_image_panel = False
|
|
1890
|
+
|
|
1891
|
+
# Handling changes to shape/marker parameters
|
|
1892
|
+
s_view_result_param = (
|
|
1893
|
+
"sig_shape_param" in changed_options
|
|
1894
|
+
or "sig_marker_param" in changed_options
|
|
1895
|
+
) and have_geometry_results(self.signalpanel.objview.get_sel_objects(True))
|
|
1896
|
+
i_view_result_param = (
|
|
1897
|
+
"ima_shape_param" in changed_options
|
|
1898
|
+
or "ima_marker_param" in changed_options
|
|
1899
|
+
) and have_geometry_results(self.imagepanel.objview.get_sel_objects(True))
|
|
1900
|
+
if (s_view_result_param or i_view_result_param) and (
|
|
1901
|
+
QW.QMessageBox.question(
|
|
1902
|
+
self,
|
|
1903
|
+
_("Apply settings to existing results?"),
|
|
1904
|
+
_(
|
|
1905
|
+
"Visualization settings for annotated shapes and "
|
|
1906
|
+
"markers have been modified.\n\n"
|
|
1907
|
+
"Do you want to apply these settings to existing results "
|
|
1908
|
+
"in the workspace?"
|
|
1909
|
+
),
|
|
1910
|
+
QW.QMessageBox.Yes | QW.QMessageBox.No,
|
|
1911
|
+
QW.QMessageBox.No,
|
|
1912
|
+
)
|
|
1913
|
+
== QW.QMessageBox.Yes
|
|
1914
|
+
):
|
|
1915
|
+
if s_view_result_param:
|
|
1916
|
+
self.signalpanel.plothandler.refresh_all_shape_items()
|
|
1917
|
+
if i_view_result_param:
|
|
1918
|
+
self.imagepanel.plothandler.refresh_all_shape_items()
|
|
1919
|
+
|
|
1920
|
+
for option in changed_options:
|
|
1921
|
+
if option in (
|
|
1922
|
+
"max_shapes_to_draw",
|
|
1923
|
+
"max_cells_in_label",
|
|
1924
|
+
"max_cols_in_label",
|
|
1925
|
+
):
|
|
1926
|
+
refresh_signal_panel = refresh_image_panel = True
|
|
1927
|
+
if option == "show_result_label":
|
|
1928
|
+
for panel in (self.signalpanel, self.imagepanel):
|
|
1929
|
+
panel.acthandler.show_label_action.setChecked(
|
|
1930
|
+
Conf.view.show_result_label.get()
|
|
1931
|
+
)
|
|
1932
|
+
if option == "color_mode":
|
|
1933
|
+
self.__update_color_mode()
|
|
1934
|
+
if option == "show_console_on_error":
|
|
1935
|
+
self.__update_console_show_mode()
|
|
1936
|
+
if option == "plot_toolbar_position":
|
|
1937
|
+
for dock in self.docks.values():
|
|
1938
|
+
widget = dock.widget()
|
|
1939
|
+
if isinstance(widget, DockablePlotWidget):
|
|
1940
|
+
widget.update_toolbar_position()
|
|
1941
|
+
if option.startswith(("sig_autodownsampling", "sig_linewidth")):
|
|
1942
|
+
refresh_signal_panel = True
|
|
1943
|
+
if option == "sig_autoscale_margin_percent":
|
|
1944
|
+
# Update signal plot widget autoscale margin
|
|
1945
|
+
sig_margin = Conf.view.sig_autoscale_margin_percent.get()
|
|
1946
|
+
for dock in self.docks.values():
|
|
1947
|
+
widget: DockablePlotWidget | QW.QWidget = dock.widget()
|
|
1948
|
+
if isinstance(widget, DockablePlotWidget):
|
|
1949
|
+
plot = widget.get_plot()
|
|
1950
|
+
if (
|
|
1951
|
+
hasattr(plot, "options")
|
|
1952
|
+
and plot.options.type == PlotType.CURVE
|
|
1953
|
+
):
|
|
1954
|
+
plot.set_autoscale_margin_percent(sig_margin)
|
|
1955
|
+
if option == "ima_autoscale_margin_percent":
|
|
1956
|
+
# Update image plot widget autoscale margin
|
|
1957
|
+
ima_margin = Conf.view.ima_autoscale_margin_percent.get()
|
|
1958
|
+
for dock in self.docks.values():
|
|
1959
|
+
widget: DockablePlotWidget | QW.QWidget = dock.widget()
|
|
1960
|
+
if isinstance(widget, DockablePlotWidget):
|
|
1961
|
+
plot = widget.get_plot()
|
|
1962
|
+
if (
|
|
1963
|
+
hasattr(plot, "options")
|
|
1964
|
+
and plot.options.type == PlotType.IMAGE
|
|
1965
|
+
):
|
|
1966
|
+
plot.set_autoscale_margin_percent(ima_margin)
|
|
1967
|
+
if option == "ima_defaults" and len(self.imagepanel) > 0:
|
|
1968
|
+
answer = QW.QMessageBox.question(
|
|
1969
|
+
self,
|
|
1970
|
+
_("Visualization settings"),
|
|
1971
|
+
_(
|
|
1972
|
+
"Default visualization settings have changed.<br><br>"
|
|
1973
|
+
"Do you want to update all active %s objects?"
|
|
1974
|
+
)
|
|
1975
|
+
% _("image"),
|
|
1976
|
+
QW.QMessageBox.Yes | QW.QMessageBox.No,
|
|
1977
|
+
)
|
|
1978
|
+
if answer == QW.QMessageBox.Yes:
|
|
1979
|
+
self.imagepanel.update_metadata_view_settings()
|
|
1980
|
+
if option == "ima_aspect_ratio_1_1":
|
|
1981
|
+
refresh_image_panel = True
|
|
1982
|
+
if refresh_signal_panel:
|
|
1983
|
+
self.signalpanel.manual_refresh()
|
|
1984
|
+
if refresh_image_panel:
|
|
1985
|
+
self.imagepanel.manual_refresh()
|
|
1986
|
+
|
|
1987
|
+
def __show_logviewer(self) -> None:
|
|
1988
|
+
"""Show error logs"""
|
|
1989
|
+
logviewer.exec_datalab_logviewer_dialog(self)
|
|
1990
|
+
|
|
1991
|
+
def play_demo(self) -> None:
|
|
1992
|
+
"""Play demo"""
|
|
1993
|
+
# pylint: disable=import-outside-toplevel
|
|
1994
|
+
# pylint: disable=cyclic-import
|
|
1995
|
+
from datalab.tests.scenarios import demo
|
|
1996
|
+
|
|
1997
|
+
demo.play_demo(self)
|
|
1998
|
+
|
|
1999
|
+
def show_tour(self) -> None:
|
|
2000
|
+
"""Show tour"""
|
|
2001
|
+
# pylint: disable=import-outside-toplevel
|
|
2002
|
+
# pylint: disable=cyclic-import
|
|
2003
|
+
from datalab.gui import tour
|
|
2004
|
+
|
|
2005
|
+
tour.start(self)
|
|
2006
|
+
|
|
2007
|
+
@staticmethod
|
|
2008
|
+
def test_segfault_error() -> None:
|
|
2009
|
+
"""Generate errors (both fault and traceback)"""
|
|
2010
|
+
import ctypes # pylint: disable=import-outside-toplevel
|
|
2011
|
+
|
|
2012
|
+
ctypes.string_at(0)
|
|
2013
|
+
raise RuntimeError("!!! Testing RuntimeError !!!")
|
|
2014
|
+
|
|
2015
|
+
def show(self) -> None:
|
|
2016
|
+
"""Reimplement QMainWindow method"""
|
|
2017
|
+
super().show()
|
|
2018
|
+
if self.__old_size is not None:
|
|
2019
|
+
self.resize(self.__old_size)
|
|
2020
|
+
|
|
2021
|
+
# ------Close window
|
|
2022
|
+
def close_properly(self) -> bool:
|
|
2023
|
+
"""Close properly
|
|
2024
|
+
|
|
2025
|
+
Returns:
|
|
2026
|
+
True if closed properly, False otherwise
|
|
2027
|
+
"""
|
|
2028
|
+
if not env.execenv.unattended and self.__is_modified:
|
|
2029
|
+
answer = QW.QMessageBox.warning(
|
|
2030
|
+
self,
|
|
2031
|
+
_("Quit"),
|
|
2032
|
+
_(
|
|
2033
|
+
"Do you want to save all signals and images "
|
|
2034
|
+
"to an HDF5 file before quitting DataLab?"
|
|
2035
|
+
),
|
|
2036
|
+
QW.QMessageBox.Yes | QW.QMessageBox.No | QW.QMessageBox.Cancel,
|
|
2037
|
+
)
|
|
2038
|
+
if answer == QW.QMessageBox.Yes:
|
|
2039
|
+
self.save_to_h5_file()
|
|
2040
|
+
if self.__is_modified:
|
|
2041
|
+
return False
|
|
2042
|
+
elif answer == QW.QMessageBox.Cancel:
|
|
2043
|
+
return False
|
|
2044
|
+
self.hide() # Avoid showing individual widgets closing one after the other
|
|
2045
|
+
for panel in self.panels:
|
|
2046
|
+
if panel is not None:
|
|
2047
|
+
panel.close()
|
|
2048
|
+
if self.console is not None:
|
|
2049
|
+
try:
|
|
2050
|
+
self.console.close()
|
|
2051
|
+
except RuntimeError:
|
|
2052
|
+
# TODO: [P3] Investigate further why the following error occurs when
|
|
2053
|
+
# restarting the mainwindow (this is *not* a production case):
|
|
2054
|
+
# "RuntimeError: wrapped C/C++ object of type DockableConsole
|
|
2055
|
+
# has been deleted".
|
|
2056
|
+
# Another solution to avoid this error would be to really restart
|
|
2057
|
+
# the application (run each unit test in a separate process), but
|
|
2058
|
+
# it would represent too much effort for an error occuring in test
|
|
2059
|
+
# configurations only.
|
|
2060
|
+
pass
|
|
2061
|
+
self.reset_all()
|
|
2062
|
+
self.__save_pos_size_and_state()
|
|
2063
|
+
self.__unregister_plugins()
|
|
2064
|
+
|
|
2065
|
+
# Saving current tab for next session
|
|
2066
|
+
Conf.main.current_tab.set(self.tabwidget.currentIndex())
|
|
2067
|
+
|
|
2068
|
+
execenv.log(self, "closed properly")
|
|
2069
|
+
return True
|
|
2070
|
+
|
|
2071
|
+
def closeEvent(self, event: QG.QCloseEvent) -> None:
|
|
2072
|
+
"""Reimplement QMainWindow method"""
|
|
2073
|
+
if self.hide_on_close:
|
|
2074
|
+
self.__old_size = self.size()
|
|
2075
|
+
self.hide()
|
|
2076
|
+
else:
|
|
2077
|
+
if self.close_properly():
|
|
2078
|
+
self.SIG_CLOSING.emit()
|
|
2079
|
+
event.accept()
|
|
2080
|
+
else:
|
|
2081
|
+
event.ignore()
|