summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.rst224
-rw-r--r--PKG-INFO13
-rw-r--r--README.rst9
-rwxr-xr-xbuild-deb.sh100
-rw-r--r--debian/changelog53
-rw-r--r--debian/control107
-rw-r--r--debian/gitlab-ci.yml20
-rw-r--r--debian/gitlab-ci.yml.tpl3
-rw-r--r--debian/patches/0002-use-the-system-mathjax-privacy-breach.patch4
-rw-r--r--debian/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch4
-rw-r--r--debian/patches/0003-fix-unit-test.patch29
-rw-r--r--debian/patches/0004-fix-FTBFS-with-numpy-0.16.patch24
-rw-r--r--debian/patches/0006-Tests-if-openCL-can-be-used.patch21
-rw-r--r--debian/patches/series3
-rw-r--r--debian/pydist-overrides3
-rw-r--r--debian/python-silx-doc.doc-base6
-rwxr-xr-xdebian/rules12
-rw-r--r--debian/tests/control15
-rw-r--r--doc/source/conf.py3
-rw-r--r--doc/source/ext/snapshotqt_directive.py234
-rw-r--r--doc/source/install.rst6
-rw-r--r--doc/source/modules/gui/data/img/ArrayTableWidget.pngbin29088 -> 21097 bytes
-rw-r--r--doc/source/modules/gui/data/img/DataViewer.pngbin37627 -> 20225 bytes
-rw-r--r--doc/source/modules/gui/gallery.rst115
-rw-r--r--doc/source/modules/gui/icons.rst27
-rw-r--r--doc/source/modules/gui/plot/dev.rst1
-rw-r--r--doc/source/modules/gui/plot/img/BasicGridStatsWidget.pngbin0 -> 5702 bytes
-rw-r--r--doc/source/modules/gui/plot/img/BasicStatsWidget.pngbin0 -> 6575 bytes
-rw-r--r--doc/source/modules/gui/plot/img/LimitsToolBar.pngbin2331 -> 21499 bytes
-rw-r--r--doc/source/modules/gui/plot/img/logColorbar.pngbin8575 -> 5240 bytes
-rw-r--r--doc/source/modules/gui/plot/index.rst7
-rw-r--r--doc/source/modules/gui/plot/roi.rst31
-rw-r--r--doc/source/modules/gui/plot/statswidget.rst16
-rw-r--r--doc/source/modules/gui/plot/utils.rst12
-rw-r--r--doc/source/modules/gui/plot3d/glutils.rst10
-rw-r--r--doc/source/modules/gui/plot3d/img/SceneWidget.pngbin349485 -> 65009 bytes
-rw-r--r--doc/source/modules/gui/plot3d/items.rst4
-rw-r--r--doc/source/modules/gui/widgets/img/FrameBrowser.pngbin3731 -> 2161 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/HorizontalSliderWithBrowser.pngbin4215 -> 2278 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/PeriodicCombo.pngbin3464 -> 1878 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/PeriodicList.pngbin12035 -> 17621 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/PeriodicTable.pngbin35124 -> 25540 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/RangeSlider.pngbin2686 -> 1024 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/TableWidget.pngbin4058 -> 3156 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/ThreadPoolPushButton.pngbin2729 -> 1577 bytes
-rw-r--r--doc/source/modules/gui/widgets/img/WaitingPushButton.pngbin1962 -> 941 bytes
-rw-r--r--doc/source/modules/image/index.rst2
-rw-r--r--doc/source/modules/image/shapes.rst4
-rw-r--r--doc/source/modules/opencl/convolution.rst10
-rw-r--r--doc/source/modules/opencl/index.rst4
-rw-r--r--doc/source/modules/opencl/processing.rst10
-rw-r--r--doc/source/modules/opencl/sinofilter.rst9
-rw-r--r--doc/source/modules/opencl/statistics.rst10
-rw-r--r--doc/source/sample_code/img/compareImages.pngbin0 -> 33398 bytes
-rw-r--r--doc/source/sample_code/img/compositeline.pngbin0 -> 23150 bytes
-rw-r--r--doc/source/sample_code/img/dropZones.pngbin0 -> 8182 bytes
-rw-r--r--doc/source/sample_code/img/exampleBaseline.pngbin0 -> 79266 bytes
-rw-r--r--doc/source/sample_code/img/findContours.pngbin0 -> 390630 bytes
-rw-r--r--doc/source/sample_code/img/plot3dUpdateScatterFromThread.pngbin0 -> 181775 bytes
-rw-r--r--doc/source/sample_code/img/plotCurveLegendWidget.pngbin0 -> 105797 bytes
-rw-r--r--doc/source/sample_code/img/plotStats.pngbin0 -> 52815 bytes
-rw-r--r--doc/source/sample_code/img/scatterview.pngbin0 -> 73884 bytes
-rw-r--r--doc/source/sample_code/img/syncPlotLocation.pngbin0 -> 178464 bytes
-rw-r--r--doc/source/sample_code/index.rst285
-rw-r--r--examples/compareImages.py110
-rw-r--r--examples/compositeline.py80
-rw-r--r--examples/customDataView.py4
-rw-r--r--examples/dropZones.py134
-rw-r--r--examples/exampleBaseline.py164
-rw-r--r--examples/fileDialog.py6
-rw-r--r--examples/findContours.py4
-rwxr-xr-xexamples/hdf5widget.py49
-rwxr-xr-xexamples/imageview.py12
-rw-r--r--examples/plot3dSceneWindow.py15
-rw-r--r--examples/plot3dUpdateScatterFromThread.py176
-rw-r--r--examples/plotInteractiveImageROI.py25
-rw-r--r--examples/plotLimits.py6
-rw-r--r--examples/plotStats.py68
-rw-r--r--examples/plotWidget.py72
-rwxr-xr-xexamples/printPreview.py24
-rwxr-xr-xexamples/scatterview.py99
-rwxr-xr-xexamples/simplewidget.py64
-rw-r--r--examples/syncPlotLocation.py105
-rw-r--r--examples/viewer3DVolume.py10
-rw-r--r--package/debian10/changelog141
-rw-r--r--package/debian10/compat (renamed from debian/compat)0
-rw-r--r--package/debian10/control172
-rw-r--r--package/debian10/gbp.conf (renamed from package/debian8/gbp.conf)0
-rw-r--r--package/debian10/patches/0002-use-the-system-mathjax-privacy-breach.patch25
-rw-r--r--package/debian10/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch22
-rw-r--r--package/debian10/patches/series2
-rw-r--r--package/debian10/py3dist-overrides1
-rw-r--r--package/debian10/python-silx-doc.doc-base (renamed from package/debian8/python-silx-doc.doc-base)0
-rwxr-xr-xpackage/debian10/rules85
-rw-r--r--package/debian10/source/format (renamed from package/debian8/source/format)0
-rw-r--r--package/debian10/source/options (renamed from package/debian8/source/options)0
-rw-r--r--package/debian10/tests/control (renamed from debian/tests/control.autodep8)8
-rw-r--r--package/debian10/watch7
-rw-r--r--package/debian8/changelog22
-rw-r--r--package/debian8/clean1
-rw-r--r--package/debian8/compat1
-rw-r--r--package/debian8/control205
-rwxr-xr-xpackage/debian8/rules56
-rw-r--r--package/debian8/watch5
-rw-r--r--package/debian9/control10
-rw-r--r--requirements-dev.txt3
-rw-r--r--requirements.txt18
-rwxr-xr-xrun_tests.py9
-rw-r--r--setup.py207
-rw-r--r--silx.egg-info/PKG-INFO13
-rw-r--r--silx.egg-info/SOURCES.txt141
-rw-r--r--silx.egg-info/requires.txt3
-rw-r--r--silx/_config.py46
-rw-r--r--silx/app/convert.py48
-rw-r--r--silx/app/test/test_convert.py16
-rw-r--r--silx/app/view/About.py5
-rw-r--r--silx/app/view/Viewer.py108
-rw-r--r--silx/app/view/main.py90
-rw-r--r--silx/app/view/test/test_view.py38
-rw-r--r--silx/gui/_glutils/Context.py42
-rw-r--r--silx/gui/_glutils/OpenGLWidget.py11
-rw-r--r--silx/gui/_glutils/Program.py12
-rw-r--r--silx/gui/_glutils/Texture.py10
-rw-r--r--silx/gui/_glutils/__init__.py5
-rw-r--r--silx/gui/_glutils/utils.py73
-rwxr-xr-x[-rw-r--r--]silx/gui/colors.py481
-rw-r--r--silx/gui/console.py32
-rw-r--r--silx/gui/data/ArrayTableModel.py5
-rw-r--r--silx/gui/data/DataViewer.py145
-rw-r--r--silx/gui/data/DataViewerFrame.py5
-rw-r--r--silx/gui/data/DataViewerSelector.py6
-rw-r--r--silx/gui/data/DataViews.py548
-rw-r--r--silx/gui/data/Hdf5TableView.py13
-rw-r--r--silx/gui/data/HexaTableView.py8
-rw-r--r--silx/gui/data/NXdataWidgets.py401
-rw-r--r--silx/gui/data/NumpyAxesSelector.py236
-rw-r--r--silx/gui/data/TextFormatter.py50
-rw-r--r--silx/gui/data/_VolumeWindow.py148
-rw-r--r--silx/gui/data/test/test_arraywidget.py6
-rw-r--r--silx/gui/data/test/test_dataviewer.py20
-rw-r--r--silx/gui/data/test/test_numpyaxesselector.py11
-rw-r--r--silx/gui/data/test/test_textformatter.py14
-rw-r--r--silx/gui/dialog/AbstractDataFileDialog.py88
-rw-r--r--silx/gui/dialog/ColormapDialog.py346
-rw-r--r--silx/gui/dialog/DataFileDialog.py10
-rw-r--r--silx/gui/dialog/FileTypeComboBox.py39
-rw-r--r--silx/gui/dialog/ImageFileDialog.py28
-rw-r--r--silx/gui/dialog/SafeFileSystemModel.py6
-rw-r--r--silx/gui/dialog/test/test_colormapdialog.py42
-rw-r--r--silx/gui/dialog/test/test_datafiledialog.py130
-rw-r--r--silx/gui/dialog/test/test_imagefiledialog.py135
-rw-r--r--silx/gui/dialog/utils.py6
-rw-r--r--silx/gui/fit/FitWidget.py10
-rw-r--r--silx/gui/hdf5/Hdf5Formatter.py17
-rwxr-xr-x[-rw-r--r--]silx/gui/hdf5/Hdf5Item.py188
-rw-r--r--silx/gui/hdf5/Hdf5TreeModel.py41
-rw-r--r--silx/gui/hdf5/NexusSortFilterProxyModel.py4
-rw-r--r--silx/gui/hdf5/_utils.py74
-rwxr-xr-x[-rw-r--r--]silx/gui/hdf5/test/test_hdf5.py71
-rw-r--r--silx/gui/icons.py8
-rw-r--r--silx/gui/plot/ColorBar.py21
-rw-r--r--silx/gui/plot/CompareImages.py79
-rw-r--r--silx/gui/plot/ComplexImageView.py90
-rw-r--r--silx/gui/plot/CurvesROIWidget.py1847
-rwxr-xr-x[-rw-r--r--]silx/gui/plot/LegendSelector.py207
-rw-r--r--silx/gui/plot/MaskToolsWidget.py108
-rw-r--r--silx/gui/plot/PlotInteraction.py356
-rw-r--r--silx/gui/plot/PlotToolButtons.py133
-rwxr-xr-x[-rw-r--r--]silx/gui/plot/PlotWidget.py724
-rw-r--r--silx/gui/plot/PlotWindow.py186
-rw-r--r--silx/gui/plot/PrintPreviewToolButton.py61
-rw-r--r--silx/gui/plot/Profile.py138
-rw-r--r--silx/gui/plot/ProfileMainWindow.py17
-rw-r--r--silx/gui/plot/ScatterMaskToolsWidget.py72
-rw-r--r--silx/gui/plot/ScatterView.py117
-rw-r--r--silx/gui/plot/StackView.py42
-rw-r--r--silx/gui/plot/StatsWidget.py1936
-rw-r--r--silx/gui/plot/_BaseMaskToolsWidget.py197
-rw-r--r--silx/gui/plot/_utils/delaunay.py (renamed from silx/third_party/concurrent_futures.py)53
-rw-r--r--silx/gui/plot/_utils/dtime_ticklayout.py4
-rwxr-xr-x[-rw-r--r--]silx/gui/plot/actions/control.py23
-rw-r--r--silx/gui/plot/actions/fit.py22
-rw-r--r--silx/gui/plot/actions/histogram.py1
-rw-r--r--silx/gui/plot/actions/io.py78
-rwxr-xr-x[-rw-r--r--]silx/gui/plot/backends/BackendBase.py129
-rwxr-xr-x[-rw-r--r--]silx/gui/plot/backends/BackendMatplotlib.py716
-rwxr-xr-x[-rw-r--r--]silx/gui/plot/backends/BackendOpenGL.py1294
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py284
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py124
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotImage.py4
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotTriangles.py194
-rw-r--r--silx/gui/plot/backends/glutils/GLSupport.py63
-rw-r--r--silx/gui/plot/backends/glutils/GLText.py23
-rw-r--r--silx/gui/plot/backends/glutils/GLTexture.py3
-rw-r--r--silx/gui/plot/backends/glutils/__init__.py3
-rw-r--r--silx/gui/plot/items/__init__.py13
-rw-r--r--silx/gui/plot/items/_pick.py70
-rw-r--r--silx/gui/plot/items/axis.py6
-rw-r--r--silx/gui/plot/items/complex.py138
-rw-r--r--silx/gui/plot/items/core.py396
-rw-r--r--silx/gui/plot/items/curve.py49
-rw-r--r--silx/gui/plot/items/histogram.py50
-rw-r--r--silx/gui/plot/items/image.py98
-rwxr-xr-x[-rw-r--r--]silx/gui/plot/items/marker.py32
-rw-r--r--silx/gui/plot/items/roi.py282
-rw-r--r--silx/gui/plot/items/scatter.py627
-rw-r--r--silx/gui/plot/items/shape.py109
-rw-r--r--silx/gui/plot/matplotlib/Colormap.py16
-rw-r--r--silx/gui/plot/matplotlib/__init__.py76
-rw-r--r--silx/gui/plot/stats/stats.py400
-rw-r--r--silx/gui/plot/stats/statshandler.py124
-rw-r--r--silx/gui/plot/test/testAlphaSlider.py5
-rw-r--r--silx/gui/plot/test/testComplexImageView.py6
-rw-r--r--silx/gui/plot/test/testCurvesROIWidget.py349
-rw-r--r--silx/gui/plot/test/testItem.py13
-rw-r--r--silx/gui/plot/test/testMaskToolsWidget.py7
-rwxr-xr-x[-rw-r--r--]silx/gui/plot/test/testPlotWidget.py330
-rw-r--r--silx/gui/plot/test/testPlotWindow.py29
-rw-r--r--silx/gui/plot/test/testProfile.py6
-rw-r--r--silx/gui/plot/test/testSaveAction.py20
-rw-r--r--silx/gui/plot/test/testScatterMaskToolsWidget.py5
-rw-r--r--silx/gui/plot/test/testStackView.py6
-rw-r--r--silx/gui/plot/test/testStats.py496
-rw-r--r--silx/gui/plot/test/testUtilsAxis.py49
-rw-r--r--silx/gui/plot/tools/PositionInfo.py2
-rw-r--r--silx/gui/plot/tools/profile/ScatterProfileToolBar.py362
-rw-r--r--silx/gui/plot/tools/profile/_BaseProfileToolBar.py6
-rw-r--r--silx/gui/plot/tools/roi.py61
-rw-r--r--silx/gui/plot/tools/test/testScatterProfileToolBar.py3
-rw-r--r--silx/gui/plot/tools/test/testTools.py30
-rw-r--r--silx/gui/plot/tools/toolbars.py28
-rw-r--r--silx/gui/plot/utils/axis.py288
-rw-r--r--silx/gui/plot3d/ParamTreeView.py2
-rw-r--r--silx/gui/plot3d/Plot3DWidget.py52
-rw-r--r--silx/gui/plot3d/Plot3DWindow.py21
-rw-r--r--silx/gui/plot3d/ScalarFieldView.py21
-rw-r--r--silx/gui/plot3d/SceneWidget.py55
-rw-r--r--silx/gui/plot3d/SceneWindow.py22
-rw-r--r--silx/gui/plot3d/_model/items.py665
-rw-r--r--silx/gui/plot3d/actions/mode.py61
-rw-r--r--silx/gui/plot3d/items/__init__.py8
-rw-r--r--silx/gui/plot3d/items/_pick.py39
-rw-r--r--silx/gui/plot3d/items/core.py4
-rw-r--r--silx/gui/plot3d/items/mesh.py288
-rw-r--r--silx/gui/plot3d/items/mixins.py39
-rw-r--r--silx/gui/plot3d/items/scatter.py146
-rw-r--r--silx/gui/plot3d/items/volume.py457
-rw-r--r--silx/gui/plot3d/scene/camera.py2
-rw-r--r--silx/gui/plot3d/scene/core.py11
-rw-r--r--silx/gui/plot3d/scene/cutplane.py18
-rw-r--r--silx/gui/plot3d/scene/function.py87
-rw-r--r--silx/gui/plot3d/scene/interaction.py60
-rw-r--r--silx/gui/plot3d/scene/primitives.py147
-rw-r--r--silx/gui/plot3d/scene/utils.py73
-rw-r--r--silx/gui/plot3d/scene/viewport.py75
-rw-r--r--silx/gui/plot3d/test/__init__.py8
-rw-r--r--silx/gui/plot3d/test/testSceneWidget.py84
-rw-r--r--silx/gui/plot3d/test/testSceneWidgetPicking.py149
-rw-r--r--silx/gui/plot3d/test/testSceneWindow.py209
-rw-r--r--silx/gui/plot3d/test/testStatsWidget.py213
-rw-r--r--silx/gui/plot3d/tools/PositionInfoWidget.py42
-rw-r--r--silx/gui/qt/_pyside_dynamic.py54
-rw-r--r--silx/gui/qt/_qt.py30
-rw-r--r--silx/gui/qt/_utils.py15
-rw-r--r--silx/gui/qt/inspect.py13
-rwxr-xr-x[-rw-r--r--]silx/gui/test/test_colors.py151
-rwxr-xr-x[-rw-r--r--]silx/gui/utils/__init__.py32
-rw-r--r--silx/gui/utils/concurrent.py4
-rw-r--r--silx/gui/utils/projecturl.py77
-rwxr-xr-xsilx/gui/utils/qtutils.py170
-rwxr-xr-x[-rw-r--r--]silx/gui/utils/test/__init__.py12
-rw-r--r--silx/gui/utils/test/test.py76
-rw-r--r--silx/gui/utils/test/test_async.py4
-rwxr-xr-xsilx/gui/utils/test/test_qtutils.py75
-rw-r--r--silx/gui/utils/test/test_testutils.py (renamed from silx/third_party/enum.py)44
-rw-r--r--silx/gui/utils/testutils.py36
-rw-r--r--silx/gui/widgets/ColormapNameComboBox.py166
-rw-r--r--silx/gui/widgets/FrameBrowser.py2
-rwxr-xr-xsilx/gui/widgets/LegendIconWidget.py513
-rw-r--r--silx/gui/widgets/PrintPreview.py74
-rw-r--r--silx/gui/widgets/RangeSlider.py202
-rw-r--r--silx/gui/widgets/UrlSelectionTable.py164
-rw-r--r--silx/image/bilinear.c24832
-rw-r--r--silx/image/marchingsquares/_mergeimpl.cpp33571
-rw-r--r--silx/image/shapes.c24893
-rw-r--r--silx/image/shapes.pyx28
-rw-r--r--silx/image/test/test_shapes.py44
-rw-r--r--silx/image/tomography.py170
-rw-r--r--silx/image/utils.py53
-rw-r--r--silx/io/commonh5.py13
-rw-r--r--silx/io/convert.py29
-rw-r--r--silx/io/dictdump.py36
-rwxr-xr-x[-rw-r--r--]silx/io/fabioh5.py83
-rw-r--r--silx/io/nxdata/__init__.py4
-rw-r--r--silx/io/nxdata/_utils.py5
-rw-r--r--silx/io/nxdata/parse.py93
-rw-r--r--silx/io/nxdata/write.py5
-rw-r--r--silx/io/octaveh5.py13
-rw-r--r--silx/io/specfile.c38400
-rw-r--r--silx/io/specfile.pyx10
-rw-r--r--silx/io/specfile/src/locale_management.c3
-rw-r--r--silx/io/specfile/src/sfdata.c27
-rw-r--r--silx/io/specfile/src/sflabel.c4
-rw-r--r--silx/io/specfile/src/sftools.c3
-rw-r--r--silx/io/spech5.py5
-rw-r--r--silx/io/test/test_commonh5.py61
-rw-r--r--silx/io/test/test_dictdump.py25
-rwxr-xr-x[-rw-r--r--]silx/io/test/test_fabioh5.py102
-rw-r--r--silx/io/test/test_nxdata.py13
-rw-r--r--silx/io/test/test_specfile.py13
-rw-r--r--silx/io/test/test_spech5.py20
-rw-r--r--silx/io/test/test_spectoh5.py17
-rw-r--r--silx/io/test/test_utils.py267
-rw-r--r--silx/io/url.py9
-rw-r--r--silx/io/utils.py141
-rw-r--r--silx/math/chistogramnd.c34202
-rw-r--r--silx/math/chistogramnd.pyx2
-rw-r--r--silx/math/chistogramnd_lut.c57039
-rw-r--r--silx/math/colormap.c49978
-rw-r--r--silx/math/colormap.pyx6
-rw-r--r--silx/math/combo.c43112
-rw-r--r--silx/math/combo.pyx4
-rw-r--r--silx/math/fft/__init__.py8
-rw-r--r--silx/math/fft/basefft.py146
-rw-r--r--silx/math/fft/clfft.py286
-rw-r--r--silx/math/fft/cufft.py253
-rw-r--r--silx/math/fft/fft.py96
-rw-r--r--silx/math/fft/fftw.py214
-rw-r--r--silx/math/fft/npfft.py124
-rw-r--r--silx/math/fft/setup.py (renamed from silx/third_party/six.py)36
-rw-r--r--silx/math/fft/test/__init__.py (renamed from silx/sx/test/__init__.py)17
-rw-r--r--silx/math/fft/test/test_fft.py266
-rw-r--r--silx/math/fit/bgtheories.py4
-rw-r--r--silx/math/fit/filters.c24847
-rw-r--r--silx/math/fit/filters.pyx35
-rw-r--r--silx/math/fit/filters/include/filters.h2
-rw-r--r--silx/math/fit/fitmanager.py36
-rw-r--r--silx/math/fit/functions.c31769
-rw-r--r--silx/math/fit/functions.pyx4
-rw-r--r--silx/math/fit/peaks.c22408
-rw-r--r--silx/math/fit/peaks.pyx4
-rw-r--r--silx/math/fit/test/test_fitmanager.py87
-rw-r--r--silx/math/interpolate.pyx165
-rw-r--r--silx/math/marchingcubes.cpp26975
-rw-r--r--silx/math/marchingcubes.pyx2
-rw-r--r--silx/math/medianfilter/include/median_filter.hpp20
-rw-r--r--silx/math/medianfilter/medianfilter.cpp30500
-rw-r--r--silx/math/medianfilter/medianfilter.pyx2
-rw-r--r--silx/math/medianfilter/test/benchmark.py4
-rw-r--r--silx/math/setup.py10
-rw-r--r--silx/math/test/__init__.py6
-rw-r--r--silx/math/test/test_HistogramndLut_nominal.py3
-rw-r--r--silx/math/test/test_colormap.py8
-rw-r--r--silx/math/test/test_combo.py21
-rw-r--r--silx/math/test/test_histogramnd_nominal.py27
-rw-r--r--silx/math/test/test_interpolate.py136
-rw-r--r--silx/opencl/backprojection.py470
-rw-r--r--silx/opencl/codec/test/test_byte_offset.py9
-rw-r--r--silx/opencl/common.py95
-rw-r--r--silx/opencl/convolution.py443
-rw-r--r--silx/opencl/linalg.py8
-rw-r--r--silx/opencl/processing.py94
-rw-r--r--silx/opencl/projection.py11
-rw-r--r--silx/opencl/reconstruction.py33
-rw-r--r--silx/opencl/sinofilter.py435
-rw-r--r--silx/opencl/sparse.py377
-rw-r--r--silx/opencl/statistics.py224
-rw-r--r--silx/opencl/test/__init__.py11
-rw-r--r--silx/opencl/test/test_addition.py5
-rw-r--r--silx/opencl/test/test_backprojection.py97
-rw-r--r--silx/opencl/test/test_convolution.py265
-rw-r--r--silx/opencl/test/test_kahan.py271
-rw-r--r--silx/opencl/test/test_linalg.py5
-rw-r--r--silx/opencl/test/test_sparse.py203
-rw-r--r--silx/opencl/test/test_stats.py114
-rw-r--r--silx/opencl/utils.py93
-rw-r--r--silx/resources/__init__.py178
-rw-r--r--silx/resources/gui/colormaps/cividis.npybin0 -> 3200 bytes
-rw-r--r--silx/resources/gui/icons/compare-mode-a-minus-b.pngbin0 -> 3862 bytes
-rw-r--r--silx/resources/gui/icons/compare-mode-a-minus-b.svg25
-rw-r--r--silx/resources/gui/icons/description-description.pngbin0 -> 756 bytes
-rw-r--r--silx/resources/gui/icons/description-description.svg13
-rw-r--r--silx/resources/gui/icons/description-error.pngbin0 -> 952 bytes
-rw-r--r--silx/resources/gui/icons/description-error.svg13
-rw-r--r--silx/resources/gui/icons/description-name.pngbin0 -> 822 bytes
-rw-r--r--silx/resources/gui/icons/description-name.svg13
-rw-r--r--silx/resources/gui/icons/description-program.pngbin0 -> 767 bytes
-rw-r--r--silx/resources/gui/icons/description-program.svg13
-rw-r--r--silx/resources/gui/icons/description-title.pngbin0 -> 707 bytes
-rw-r--r--silx/resources/gui/icons/description-title.svg13
-rw-r--r--silx/resources/gui/icons/description-value.pngbin0 -> 833 bytes
-rw-r--r--silx/resources/gui/icons/description-value.svg13
-rw-r--r--silx/resources/gui/icons/eye.pngbin0 -> 755 bytes
-rw-r--r--silx/resources/gui/icons/eye.svg23
-rw-r--r--silx/resources/gui/icons/pointing-hand.pngbin0 -> 680 bytes
-rw-r--r--silx/resources/gui/icons/pointing-hand.svg2
-rw-r--r--[-rwxr-xr-x]silx/resources/gui/icons/shape-ellipse.pngbin743 -> 643 bytes
-rw-r--r--silx/resources/gui/icons/shape-ellipse.svg7
-rw-r--r--silx/resources/opencl/array_utils.cl66
-rw-r--r--silx/resources/opencl/convolution.cl312
-rw-r--r--silx/resources/opencl/convolution_textures.cl374
-rw-r--r--silx/resources/opencl/kahan.cl143
-rw-r--r--silx/resources/opencl/sparse.cl94
-rw-r--r--silx/resources/opencl/statistics.cl208
-rw-r--r--silx/setup.py3
-rw-r--r--silx/sx/__init__.py15
-rw-r--r--silx/sx/_plot.py100
-rw-r--r--silx/sx/_plot3d.py14
-rw-r--r--silx/test/__init__.py7
-rw-r--r--silx/test/test_resources.py58
-rw-r--r--silx/test/test_sx.py (renamed from silx/sx/test/test_sx.py)23
-rw-r--r--silx/test/utils.py35
-rw-r--r--silx/third_party/modest_image.py322
-rw-r--r--silx/third_party/setup.py1
-rw-r--r--silx/utils/ExternalResources.py321
-rw-r--r--silx/utils/array_like.py6
-rw-r--r--silx/utils/debug.py3
-rw-r--r--silx/utils/enum.py79
-rw-r--r--silx/utils/files.py56
-rwxr-xr-x[-rw-r--r--]silx/utils/number.py6
-rw-r--r--silx/utils/proxy.py41
-rwxr-xr-x[-rw-r--r--]silx/utils/test/__init__.py10
-rw-r--r--silx/utils/test/test_array_like.py10
-rw-r--r--silx/utils/test/test_enum.py96
-rw-r--r--silx/utils/test/test_external_resources.py99
-rw-r--r--silx/utils/test/test_number.py7
-rw-r--r--silx/utils/test/test_proxy.py53
-rwxr-xr-xsilx/utils/test/test_testutils.py96
-rw-r--r--silx/utils/test/test_weakref.py6
-rwxr-xr-x[-rw-r--r--]silx/utils/testutils.py73
-rw-r--r--version.py4
431 files changed, 26794 insertions, 451997 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 073e1ac..0777568 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -1,6 +1,196 @@
Change Log
==========
+0.12.0: 2020/01/09
+------------------
+
+Python 2.7 is no longer officially supported (even if tests pass and most of the library should work).
+
+* silx view application:
+
+ * Added: keep the same axes selection when changing dataset except for the stack view (PR #2701, #2780)
+ * Added a Description column in the browsing tree to display NeXus title or name (PR #2804)
+ * Added support of URL as filename (PR #2750)
+ * Behavior changed: no longer lock HDF5 files by default, can be changed with `--hdf5-file-locking` option (PR #2861)
+
+* `silx.gui`:
+
+ * `silx.gui.plot`:
+
+ * Added scatter plot regular and irregular grid visualization mode (PR #2810, #2815, #2820, #2824, #2831)
+ * Added `baseline` argument to `PlotWidget` `addCurve` and `addHistogram` methods (PR #2715)
+ * Added right axis support to `PlotWidget` marker items (PR #2744)
+ * Added `BoundingRect` `PlotWidget` item (PR #2823)
+ * Added more markers to `PlotWidget` items using symbols (PR #2792)
+ * Improved and fixed `PlotWidget` and backends rendering and picking to guarantee rendering order of items (PR #2602, #2694, #2726, #2728, #2730, #2731, #2732, #2734, #2746, #2800, #2822, #2829, #2851, #2853)
+ * Improved `RegionOfInterest`: Added `sigItemChanged` signal, renamed `get|setLabel` to `get|setName` (PR #2684, #2729, #2794, #2803, #2860)
+ * Improved `StackView`: Allow to save dataset to HDF5 (PR #2813)
+
+ * `silx.gui.plot3d`:
+
+ * Added colormapped isosurface display to `ComplexField3D` (PR #2675)
+
+ * Miscellaneous:
+
+ * Added `cividis` colormap (PR #2763)
+ * Added `silx.gui.widgets.ColormapNameComboBox` widget (PR #2814)
+ * Added `silx.gui.widgets.LegendIconWidget` widget (PR #2783)
+ * Added `silx.gui.utils.blockSignals` context manager (PR #2697, #2702)
+ * Added `silx.gui.utils.qtutils.getQEventName` function (PR #2725)
+ * Added `silx.gui.colors.asQColor` function (PR #2753)
+ * Minor fixes (PR #2662, #2667, #2674, #2719, #2724, #2747, #2757, #2760, #2766, #2789, #2798, #2799, #2805, #2811, #2832, #2834, #2839, #2849, #2852, #2857, #2864, #2867)
+
+* `silx.opencl`:
+
+ * Added `silx.opencl.sparse.CSR` with support of different data types (PR #2671)
+ * Improved support of different platforms like PoCL (PR #2669, #2698, #2806)
+ * Moved non-OpenCL related utilities to `silx.opencl.utils` module (PR #2782)
+ * Fixed `silx.opencl.sinofilter.SinoFilter` to avoid importing scikit-cuda (PR #2721)
+ * Fixed kernel garbage collection (PR #2708)
+ * Fixed `silx.opencl.convolution.Convolution` (PR #2781)
+
+* `silx.math`/`silx.image`:
+
+ * Added trilinear interpolator: `silx.math.interpolate.interp3d` (PR #2678)
+ * Added `silx.image.utils.gaussian_kernel` function (PR #2782)
+ * Improved `silx.image.shapes.Polygon` argument check (PR #2761)
+ * Fixed and improved `silx.math.fft` with FFTW backend (PR #2751)
+ * Fixed support of not finite data in fit manager (PR #2868)
+
+* `silx.io`:
+
+ * Added `asarray=True` argument to `silx.io.dictdump.h5todict` function (PR #2692, #2767)
+ * Improved `silx.io.utils.DataUrl` (PR #2790)
+ * Increased max number of motors in `specfile` (PR #2817)
+ * Fixed data conversion when reading images with `fabio` (PR #2735)
+
+* Build, documentation and tests:
+
+ * Added `Cython` as a build dependency (PR #2795, #2807, #2808)
+ * Added Debian 10 packaging (PR #2670, #2672, #2666, #2686, #2706)
+ * Improved documentation (PR #2673, #2680, #2679, #2772, #2759, #2779, #2801, #2802, #2833, #2857, #2869)
+ * Improved testing tools (PR #2704, #2796, #2818)
+ * Improved `bootstrap.py` script (PR #2727, #2733)
+
+
+0.11.0: 2019/07/03
+------------------
+
+ * Graphical user interface:
+
+ * Plot:
+
+ * Add sample code on how to update a plot3d widget from a thread
+ * ScatterPlot: add the possibility to plot as a surface using Delaunay triangulation
+ * ScatterView: add a tool button to change scatter visualization mode (ex. Solid)
+ * (OpenGL backend) Fix memory leak when creating/deleting widgets in a loop
+
+
+ * Plot3D:
+
+ * Add an action to toggle plot3d's `PositionInfoWidget` picking.
+ * Add a 3D complex field visualization: Complex3DField (also available from silx view)
+ * Add a PositionInfoWidget and a tool button to toggle the picking mode to SceneWindow
+ * Add the possibility to render the scene with linear fog.
+
+ * `silx.gui.widgets`:
+
+ * Fix ImageFileDialog selection for a cube with shape like `1,y,x`.
+
+ * Miscellaneous:
+
+ * Requires numpy version >= 1.12
+ * HDF5 creator script
+ * Support of Python 3.4 is dropped. Please upgrade to at least Python 3.5.
+ * This is the last version to officially support Python 2.7.
+ * The source code is Python 3.8 ready.
+ * Improve PySide2 support. PyQt4 and PySide are deprecated.
+
+
+
+0.10.0: 2019/02/19
+------------------
+
+ * Graphical user interface:
+
+ * Plot:
+
+ * Add support of foreground color
+ * Fix plot background colors
+ * Add tool to mask ellipse
+ * StatsWidget:
+
+ * Add support for plot3D widgets
+ * Add a PyMca like widget
+
+ * `Colormap`: Phase colormap is now editable
+ * `ImageView`: Add ColorBarWidget
+ * `PrintPreview`:
+
+ * Add API to define 'comment' and 'title'
+ * Fix resizing in PyQt5
+
+ * Selection: Allow style definition
+ * `ColormapDialog`: display 'values' plot in log if colormap uses log
+ * Synchronize ColorBar with plot background colors
+ * `CurvesROIWidget`: ROI is now an object.
+
+ * Plot3D:
+
+ * `SceneWidget`: add ColormapMesh item
+ * Add compatibility with the StatsWidget to display statistic on 3D volumes.
+ * Add `ScalarFieldView.get|setOuterScale`
+ * Fix label update in param tree
+ * Add `ColormapMesh` item to the `SceneWidget`
+
+ * HDF5 tree:
+
+ * Allow URI drop
+ * Robustness of hdf5 tree with corrupted files
+
+ * `silx.gui.widgets`:
+
+ * Add URL selection table
+
+ * Input/output:
+
+ * Support compressed Fabio extensions
+ * Add a function to create external dataset for .vol file
+
+ * `silx view`:
+
+ * Support 2D view for 3D NXData
+ * Add a NXdata for complex images
+ * Add a 3d scalar field view to the NXdata views zoo
+ * Improve shortcuts, view loading
+ * Improve silx view loading, shortcuts and sliders ergonomy
+ * Support default attribute pointing to an NXdata at any group level
+
+ * `silx convert`
+
+ * Allow to use a filter id for compression
+
+ * Math:
+
+ * fft: multibackend fft
+
+ * OpenCL:
+
+ * Compute statistics on a numpy.ndarray
+ * Backprojection:
+
+ * Add sinogram filters (SinoFilter)
+ * Input and/or output can be device arrays.
+
+ * Miscellaneous:
+
+ * End of PySide support (use PyQt5)
+ * Last version supporting numpy 1.8.0. Next version will drop support for numpy < 1.12
+ * Python 2.7 support will be dropped before end 2019. From version 0.11, a deprecation warning will be issued.
+ * Remove some old deprecated methods/arguments
+ * Set Cython language_level to 3
+
+
0.9.0: 2018/10/23
-----------------
@@ -67,7 +257,7 @@ Change Log
* Graphical user interface:
* Plot:
-
+
* Adds support of x-axis date/time ticks for time series display (see `silx.gui.plot.items.XAxis.setTickMode`)
* Adds support of interactive authoring of regions of interest (see `silx.gui.plot.items.roi` and `silx.gui.plot.tools.roi`)
* Adds `StatsWidget` widget for displaying statistics on data displayed in a `PlotWidget`
@@ -201,7 +391,7 @@ Change Log
* OpenCl. Tomography. Implement a filtered back projection.
* Add a *PrintPreview* widget and a *PrintPreviewToolButton* for *PlotWidget*.
* Plot:
-
+
* Add a context menu on right click.
* Add a *ComplexImageView* widget.
* Merged abstract *Plot* class with *PlotWidget* class.
@@ -212,14 +402,14 @@ Change Log
* Refactor plot actions, new sub-package *silx.gui.plot.actions*.
* Add signals on *PlotWidget* items notifying updates.
* Mask. Support loading of TIFF images.
-
+
* Plot3d:
-
+
* Rework toolbar and interaction to use only the left mouse button.
* Support any colormap.
-
+
* Hdf5TreeView:
-
+
* Add an API to select a single tree node item (*setSelectedH5Node*)
* Better support and display of types.
* New column for displaying the kind of links.
@@ -229,25 +419,25 @@ Change Log
* Median filter. Add new modes (*reflect, mirror, shrink*) in addition to *nearest*.
* IO:
-
+
* Rename module *spectoh5* to *convert*. Add support for conversion of *fabio* formats.
* Support NPZ format.
* Support opening an URI (*silx.io.open(filename::path)*).
* *Group* methods *.keys*, *.value* and *.items* now return lists in Python 2
and iterators in Python 3.
-
+
* Image. Add tomography utils: *phantomgenerator* to produce Shepp-Logan phantom, function to compute center of rotation (*calc_center_corr*, *calc_center_centroid*) and rescale the intensity of an image (*rescale_intensity*).
-
+
* Commands:
-
+
* *silx view*:
-
+
* Add command line option *--use-opengl-plot*.
* Add command line option *--debug*, to print dataset reading errors.
* Support opening URI (*silx view filename::path*).
-
+
* *silx convert*. New command line application to convert supported data files to HDF5.
-
+
* Enable usage of *silx.resources* for other projects.
* The *silx* license is now fully MIT.
@@ -256,7 +446,7 @@ Change Log
-----------------
* Adds OpenGL backend to 1D and 2D graphics
- * Adds Object Oriented plot API with Curve, Histogram, Image, ImageRgba and Scatter items.
+ * Adds Object Oriented plot API with Curve, Histogram, Image, ImageRgba and Scatter items.
* Implements generic launcher (``silx view``)
* NXdataViewer. Module providing NeXus NXdata support
* Math/OpenCL. Implementation of median filter.
@@ -269,7 +459,7 @@ Change Log
* ROIs. Simplification of API: setRois, getRois, calculateRois.
* ROIs. Correction of calculation bug when the X-axis values were not ordered.
* Sift. Moves package from ``silx.image`` to ``silx.opencl``.
-
+
0.4.0: 2017/02/01
-----------------
@@ -283,7 +473,7 @@ Change Log
* Adds pixel intensity histogram action
* Adds histogram parameter to addCurve
* Refactoring. Create silx.gui.data (include widgets for data)
- * Refactoring. Rename utils.load as silx.io.open
+ * Refactoring. Rename utils.load as silx.io.open
* Changes active curve behavior in Plot. No default active curve is set by default
* Fit Action. Add polynomial functions and background customization
* PlotWindow. Provide API to access toolbar actions
@@ -305,7 +495,7 @@ Change Log
* Adds HDF5 load API (supporting Spec files) to silx.io.utils module
* Adds SpecFile support for multiple MCA headers
* Adds HDF5 TreeView
- * Adds FitManager to silx.math.fit and FitWidget to silx.gui.fit
+ * Adds FitManager to silx.math.fit and FitWidget to silx.gui.fit
* Adds ThreadPoolPushButton to silx.gui.widgets
* Adds getDataRange function to plot widget
* Adds loadUi, Slot and Property to qt.py
diff --git a/PKG-INFO b/PKG-INFO
index 52f365a..1c225b2 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: silx
-Version: 0.9.0
+Version: 0.12.0
Summary: Software library for X-ray data analysis
Home-page: http://www.silx.org/
Author: data analysis unit
@@ -24,7 +24,8 @@ Description:
images file formats.
* OpenCL-based data processing: image alignment (SIFT),
image processing (median filter, histogram),
- filtered backprojection for tomography
+ filtered backprojection for tomography,
+ convolution
* Data reduction: histogramming, fitting, median filter
* A set of Qt widgets, including:
@@ -55,13 +56,13 @@ Description:
Or using Anaconda on Linux and MacOS:
- .. code-block:: bash
-
+ .. code-block:: bash
+
conda install silx -c conda-forge
Unofficial packages for different distributions are available:
- - Unofficial Debian8 packages are available at http://www.silx.org/pub/debian/
+ - Unofficial Debian9 packages are available at http://www.silx.org/pub/debian/
- CentOS 7 rpm packages are provided by Max IV at: http://pubrepo.maxiv.lu.se/rpm/el7/x86_64/
- Fedora 23 rpm packages are provided by Max IV at http://pubrepo.maxiv.lu.se/rpm/fc23/x86_64/
- Arch Linux (AUR) packages are also available: https://aur.archlinux.org/packages/python-silx
@@ -129,10 +130,10 @@ Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Cython
Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
diff --git a/README.rst b/README.rst
index bd91773..6b30551 100644
--- a/README.rst
+++ b/README.rst
@@ -16,7 +16,8 @@ The current version features:
images file formats.
* OpenCL-based data processing: image alignment (SIFT),
image processing (median filter, histogram),
- filtered backprojection for tomography
+ filtered backprojection for tomography,
+ convolution
* Data reduction: histogramming, fitting, median filter
* A set of Qt widgets, including:
@@ -47,13 +48,13 @@ To install silx with a minimal set of dependencies, run:
Or using Anaconda on Linux and MacOS:
-.. code-block:: bash
-
+.. code-block:: bash
+
conda install silx -c conda-forge
Unofficial packages for different distributions are available:
-- Unofficial Debian8 packages are available at http://www.silx.org/pub/debian/
+- Unofficial Debian9 packages are available at http://www.silx.org/pub/debian/
- CentOS 7 rpm packages are provided by Max IV at: http://pubrepo.maxiv.lu.se/rpm/el7/x86_64/
- Fedora 23 rpm packages are provided by Max IV at http://pubrepo.maxiv.lu.se/rpm/fc23/x86_64/
- Arch Linux (AUR) packages are also available: https://aur.archlinux.org/packages/python-silx
diff --git a/build-deb.sh b/build-deb.sh
index 208b706..7ca6d7e 100755
--- a/build-deb.sh
+++ b/build-deb.sh
@@ -3,7 +3,7 @@
# Project: Silx
# https://github.com/silx-kit/silx
#
-# Copyright (C) 2015-2017 European Synchrotron Radiation Facility, Grenoble, France
+# Copyright (C) 2015-2019 European Synchrotron Radiation Facility, Grenoble, France
#
# Principal author: Jérôme Kieffer (Jerome.Kieffer@ESRF.eu)
#
@@ -29,9 +29,9 @@
project=silx
source_project=silx
-version=$(python -c"import version; print(version.version)")
-strictversion=$(python -c"import version; print(version.strictversion)")
-debianversion=$(python -c"import version; print(version.debianversion)")
+version=$(python3 -c"import version; print(version.version)")
+strictversion=$(python3 -c"import version; print(version.strictversion)")
+debianversion=$(python3 -c"import version; print(version.debianversion)")
deb_name=$(echo "$source_project" | tr '[:upper:]' '[:lower:]')
@@ -44,18 +44,12 @@ then
#we are probably on a ubuntu platform
debian_version=$(cat /etc/debian_version | cut -d/ -f1)
case $debian_version in
- squeeze)
- debian_version=6
- ;;
- wheezy)
- debian_version=7
- ;;
- jessie)
- debian_version=8
- ;;
stretch)
debian_version=9
;;
+ buster)
+ debian_version=10
+ ;;
esac
fi
@@ -85,13 +79,11 @@ optional arguments:
--help show this help text
--install install the packages generated at the end of
the process using 'sudo dpkg'
- --debian7 Simulate a debian7 system (fail-safe)
- --debian8 Simulate a debian 8 Jessie system
--debian9 Simulate a debian 9 Stretch system
+ --debian10 Simulate a debian 10 Buster system
"
install=0
-use_python3=0 #used only for stdeb
while :
do
@@ -104,24 +96,6 @@ do
install=1
shift
;;
- --python3)
- use_python3=1
- shift
- ;;
- --debian7)
- debian_version=7
- target_system=debian${debian_version}
- dist_directory=${project_directory}/dist/${target_system}
- build_directory=${project_directory}/build/${target_system}
- shift
- ;;
- --debian8)
- debian_version=8
- target_system=debian${debian_version}
- dist_directory=${project_directory}/dist/${target_system}
- build_directory=${project_directory}/build/${target_system}
- shift
- ;;
--debian9)
debian_version=9
target_system=debian${debian_version}
@@ -149,11 +123,11 @@ clean_up()
mkdir -p ${build_directory}
}
-build_deb_8_plus () {
+build_deb () {
echo "Build for debian 8 or newer using actual packaging"
tarname=${project}_${debianversion}.orig.tar.gz
clean_up
- python setup.py debian_src
+ python3 setup.py debian_src
cp -f dist/${tarname} ${build_directory}
if [ -f dist/${project}-testimages.tar.gz ]
then
@@ -205,9 +179,19 @@ build_deb_8_plus () {
#export PYBUILD_DISABLE_python3=test
#export DEB_BUILD_OPTIONS=nocheck
fi
-
+
+ case $debian_version in
+ 9)
+ debian_name=stretch
+ ;;
+ 10)
+ debian_name=buster
+ ;;
+ esac
+
dch -v ${debianversion}-1 "upstream development build of ${project} ${version}"
- dch --bpo "${project} snapshot ${version} built for ${target_system}"
+ dch -D ${debian_name}-backports -l~bpo${debian_version}+ "${project} snapshot ${version} built for ${target_system}"
+ #dch --bpo "${project} snapshot ${version} built for ${target_system}"
dpkg-buildpackage -r
rc=$?
@@ -227,47 +211,11 @@ build_deb_8_plus () {
fi
}
-build_deb_7_minus () {
- echo "Build for debian 7 or older using stdeb"
- tarname=${project}-${strictversion}.tar.gz
- clean_up
-
- python setup.py sdist
- cp -f dist/${tarname} ${build_directory}
- cd ${build_directory}
- tar -xzf ${tarname}
- cd ${project}-${strictversion}
-
- if [ $use_python3 = 1 ]
- then
- echo Using Python 2+3
- python3 setup.py --command-packages=stdeb.command sdist_dsc --with-python2=True --with-python3=True --no-python3-scripts=True build --no-cython bdist_deb
- rc=$?
- else
- echo Using Python 2
- # bdist_deb feed /usr/bin using setup.py entry-points
- python setup.py --command-packages=stdeb.command build --no-cython bdist_deb
- rc=$?
- fi
-
- # move packages to dist directory
- rm -rf ${dist_directory}
- mkdir -p ${dist_directory}
- mv -f deb_dist/*.deb ${dist_directory}
-
- # back to the root
- cd ../../..
-}
-if [ $debian_version -ge 8 ]
-then
- build_deb_8_plus
-else
- build_deb_7_minus
-fi
+build_deb
if [ $install -eq 1 ]; then
- sudo -v su -c "dpkg -i ${dist_directory}/*.deb"
+ sudo su -c "dpkg -i ${dist_directory}/*.deb"
fi
exit "$rc"
diff --git a/debian/changelog b/debian/changelog
index 2ac45dd..aef7cef 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,56 @@
+silx (0.12.0+dfsg-1) unstable; urgency=medium
+
+ * New upstream version 0.12.0+dfsg
+
+ -- Alexandre Marie <alexandre.marie@synchrotron-soleil.fr> Thu, 30 Jan 2020 11:48:55 +0100
+
+silx (0.12.0~b0+dfsg-1) UNRELEASED; urgency=medium
+
+ * New upstream version 0.12.0~b0+dfsg
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Mon, 23 Dec 2019 13:49:12 +0100
+
+silx (0.11.0+dfsg-3) unstable; urgency=medium
+
+ * d/control: Build-Depends on python3-qtconsole(Closes: #946571).
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Wed, 11 Dec 2019 09:10:31 +0100
+
+silx (0.11.0+dfsg-2) unstable; urgency=medium
+
+ * Use debhelper-compat instead of debian/compat.
+ * remove Python2 modules (Closes: #938481).
+ * Switched to compat level 12
+ - d/rules: Used --doc-main-package python3-silx.
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Sat, 19 Oct 2019 13:19:44 +0200
+
+silx (0.11.0+dfsg-1) unstable; urgency=medium
+
+ [ Alexandre Marie ]
+ * Added test on openCL's use
+ * New upstream version 0.11.0+dfsg
+ * d/patches
+ - 0004-fix-missing-import.patches (Removed)
+ - 0005-fix-problem-with-sift-import (Removed)
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Tue, 09 Jul 2019 15:26:55 +0200
+
+silx (0.10.1+dfsg-1~exp2) experimental; urgency=medium
+
+ * d/patchs
+ + 0005-fix-problem-with-sift-import.patch (Added)
+
+ -- Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> Tue, 28 May 2019 11:17:46 +0200
+
+silx (0.10.1+dfsg-1~exp1) experimental; urgency=medium
+
+ * New upstream version 0.10.1+dfsg
+ * d/patches
+ + 0004-fix-missing-import.patch (Added)
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Tue, 28 May 2019 08:20:44 +0200
+
silx (0.9.0+dfsg-3) unstable; urgency=medium
* d/rules: Do not run Qt test for now.
diff --git a/debian/control b/debian/control
index 0d43d0b..fa42f3d 100644
--- a/debian/control
+++ b/debian/control
@@ -5,47 +5,13 @@ Uploaders: Jerome Kieffer <jerome.kieffer@esrf.fr>,
Alexandre Marie <alexandre.marie@synchrotron-soleil.fr>
Section: science
Priority: optional
-Build-Depends: cython (>= 0.23.2),
- cython-dbg (>= 0.23.2),
- cython3 (>= 0.23.2),
+Build-Depends: cython3 (>= 0.23.2),
cython3-dbg (>= 0.23.2),
- debhelper (>= 10),
+ debhelper-compat (= 12),
dh-python,
help2man,
- ipython,
- ipython-qtconsole,
ipython3,
- ipython3-qtconsole,
- pandoc <!nodoc>,
- python-all-dbg,
- python-all-dev,
- python-concurrent.futures,
- python-fabio,
- python-fabio-dbg,
- python-h5py,
- python-h5py-dbg,
- python-mako,
- python-matplotlib,
- python-matplotlib-dbg,
- python-nbsphinx <!nodoc>,
- python-numpy,
- python-numpy-dbg,
- python-opengl,
- python-pil,
- python-pil-dbg,
- python-pyopencl,
- python-pyopencl-dbg,
- python-pyqt5,
- python-pyqt5-dbg,
- python-pyqt5.qtopengl,
- python-pyqt5.qtopengl-dbg,
- python-pyqt5.qtsvg,
- python-pyqt5.qtsvg-dbg,
- python-scipy,
- python-scipy-dbg,
- python-setuptools,
- python-sphinx,
- python-sphinxcontrib.programoutput,
+ pandoc <!nodoc>,
python3-all-dbg,
python3-all-dev,
python3-fabio,
@@ -68,6 +34,7 @@ Build-Depends: cython (>= 0.23.2),
python3-pyqt5.qtopengl-dbg,
python3-pyqt5.qtsvg,
python3-pyqt5.qtsvg-dbg,
+ python3-qtconsole,
python3-scipy,
python3-scipy-dbg,
python3-setuptools,
@@ -108,72 +75,6 @@ Description: Toolbox for X-Ray data analysis - Executables
.
This uses the Python 3 version of the package.
-Package: python-silx
-Architecture: any
-Section: python
-Depends: ${misc:Depends}, ${python:Depends}, ${shlibs:Depends}
-Description: Toolbox for X-Ray data analysis - Python2 library
- The silx project aims at providing a collection of Python packages to
- support the development of data assessment, reduction and analysis
- applications at synchrotron radiation facilities. It aims at
- providing reading/writing different file formats, data reduction
- routines and a set of Qt widgets to browse and visualize data.
- .
- The current version provides :
- .
- * reading HDF5 file format (with support of SPEC file format)
- * histogramming
- * fitting
- * 1D and 2D visualization using multiple backends (matplotlib or OpenGL)
- * image plot widget with a set of associated tools (See changelog file).
- * Unified browser for HDF5, SPEC and image file formats supporting inspection
- and visualization of n-dimensional datasets.
- * Unified viewer (silx view filename) for HDF5, SPEC and image file formats
- * OpenGL-based widget to display 3D scalar field with
- isosurface and cutting plane.
- .
- This is the Python 2 version of the package.
-
-Package: python-silx-dbg
-Architecture: any
-Section: debug
-Depends: python-fabio-dbg,
- python-h5py-dbg,
- python-lxml-dbg,
- python-matplotlib-dbg,
- python-numpy-dbg,
- python-pil-dbg,
- python-pyopencl-dbg,
- python-pyqt5-dbg,
- python-pyqt5.qtopengl-dbg,
- python-pyqt5.qtsvg-dbg,
- python-scipy-dbg,
- python-silx (= ${binary:Version}),
- ${misc:Depends},
- ${python:Depends},
- ${shlibs:Depends}
-Description: Toolbox for X-Ray data analysis - Python2 debug
- The silx project aims at providing a collection of Python packages to
- support the development of data assessment, reduction and analysis
- applications at synchrotron radiation facilities. It aims at
- providing reading/writing different file formats, data reduction
- routines and a set of Qt widgets to browse and visualize data.
- .
- The current version provides :
- .
- * reading HDF5 file format (with support of SPEC file format)
- * histogramming
- * fitting
- * 1D and 2D visualization using multiple backends (matplotlib or OpenGL)
- * image plot widget with a set of associated tools (See changelog file).
- * Unified browser for HDF5, SPEC and image file formats supporting inspection
- and visualization of n-dimensional datasets.
- * Unified viewer (silx view filename) for HDF5, SPEC and image file formats
- * OpenGL-based widget to display 3D scalar field with
- isosurface and cutting plane.
- .
- This is the Python 2 debug version of the package.
-
Package: python3-silx
Architecture: any
Section: python
diff --git a/debian/gitlab-ci.yml b/debian/gitlab-ci.yml
index b7dc52a..33c3a64 100644
--- a/debian/gitlab-ci.yml
+++ b/debian/gitlab-ci.yml
@@ -1,16 +1,4 @@
-include: https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
-
-build:
- extends: .build-unstable
-
-reprotest:
- extends: .test-reprotest
-
-lintian:
- extends: .test-lintian
-
-autopkgtest:
- extends: .test-autopkgtest
-
-piuparts:
- extends: .test-piuparts
+---
+include:
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml
diff --git a/debian/gitlab-ci.yml.tpl b/debian/gitlab-ci.yml.tpl
deleted file mode 100644
index eeb89b6..0000000
--- a/debian/gitlab-ci.yml.tpl
+++ /dev/null
@@ -1,3 +0,0 @@
-include: https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
-
-# end of salsa pipeline bot parser
diff --git a/debian/patches/0002-use-the-system-mathjax-privacy-breach.patch b/debian/patches/0002-use-the-system-mathjax-privacy-breach.patch
index 641d90a..04deea7 100644
--- a/debian/patches/0002-use-the-system-mathjax-privacy-breach.patch
+++ b/debian/patches/0002-use-the-system-mathjax-privacy-breach.patch
@@ -8,10 +8,10 @@ Subject: use the system mathjax (privacy breach)
1 file changed, 5 insertions(+)
diff --git a/doc/source/conf.py b/doc/source/conf.py
-index 23efd15..532f6bf 100644
+index 86dbccf..18bfce2 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
-@@ -142,6 +142,11 @@ pygments_style = 'sphinx'
+@@ -143,6 +143,11 @@ pygments_style = 'sphinx'
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
diff --git a/debian/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch b/debian/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch
index f4bbf44..78446f3 100644
--- a/debian/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch
+++ b/debian/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch
@@ -7,10 +7,10 @@ Subject: do not modify PYTHONPATH from setup.py
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
-index 5ce0435..40b2e8e 100644
+index 86ae5bb..a7f3f2a 100644
--- a/setup.py
+++ b/setup.py
-@@ -257,7 +257,8 @@ class BuildMan(Command):
+@@ -261,7 +261,8 @@ class BuildMan(Command):
path.insert(0, os.path.abspath(build.build_lib))
env = dict((str(k), str(v)) for k, v in os.environ.items())
diff --git a/debian/patches/0003-fix-unit-test.patch b/debian/patches/0003-fix-unit-test.patch
deleted file mode 100644
index 07523e7..0000000
--- a/debian/patches/0003-fix-unit-test.patch
+++ /dev/null
@@ -1,29 +0,0 @@
-From: =?utf-8?q?Picca_Fr=C3=A9d=C3=A9ric-Emmanuel?=
- <picca@synchrotron-soleil.fr>
-Date: Fri, 11 Aug 2017 10:15:26 +0200
-Subject: fix unit test
-
----
- silx/opencl/common.py | 9 ++++++++-
- 1 file changed, 8 insertions(+), 1 deletion(-)
-
-diff --git a/silx/opencl/common.py b/silx/opencl/common.py
-index 9a04035..17c67d1 100644
---- a/silx/opencl/common.py
-+++ b/silx/opencl/common.py
-@@ -61,7 +61,14 @@ else:
- pyopencl = None
- else:
- import pyopencl.array as array
-- mf = pyopencl.mem_flags
-+
-+if pyopencl is None:
-+ class mf(object):
-+ WRITE_ONLY = 1
-+ READ_ONLY = 1
-+ READ_WRITE = 1
-+else:
-+ mf = pyopencl.mem_flags
-
- if pyopencl is None:
- # Define default mem flags
diff --git a/debian/patches/0004-fix-FTBFS-with-numpy-0.16.patch b/debian/patches/0004-fix-FTBFS-with-numpy-0.16.patch
deleted file mode 100644
index bcc32ad..0000000
--- a/debian/patches/0004-fix-FTBFS-with-numpy-0.16.patch
+++ /dev/null
@@ -1,24 +0,0 @@
-From: =?utf-8?q?Picca_Fr=C3=A9d=C3=A9ric-Emmanuel?=
- <picca@synchrotron-soleil.fr>
-Date: Thu, 21 Feb 2019 11:04:02 +0100
-Subject: fix FTBFS with numpy 0.16
-
----
- setup.py | 4 ++--
- 1 file changed, 2 insertions(+), 2 deletions(-)
-
-diff --git a/setup.py b/setup.py
-index 40b2e8e..9f6ae13 100644
---- a/setup.py
-+++ b/setup.py
-@@ -614,8 +614,8 @@ class BuildExt(build_ext):
- extern = 'extern "C" ' if ext.language == 'c++' else ''
- return_type = 'void' if sys.version_info[0] <= 2 else 'PyObject*'
-
-- ext.extra_compile_args.append(
-- '''-fvisibility=hidden -D'PyMODINIT_FUNC=%s__attribute__((visibility("default"))) %s ' ''' % (extern, return_type))
-+ # ext.extra_compile_args.append(
-+ # '''-fvisibility=hidden -D'PyMODINIT_FUNC=%s__attribute__((visibility("default"))) %s ' ''' % (extern, return_type))
-
- def is_debug_interpreter(self):
- """
diff --git a/debian/patches/0006-Tests-if-openCL-can-be-used.patch b/debian/patches/0006-Tests-if-openCL-can-be-used.patch
new file mode 100644
index 0000000..78a3bf8
--- /dev/null
+++ b/debian/patches/0006-Tests-if-openCL-can-be-used.patch
@@ -0,0 +1,21 @@
+From: Alexandre Marie <alexandre.marie@synchrotron-soleil.fr>
+Date: Fri, 5 Jul 2019 16:52:20 +0200
+Subject: Tests if openCL can be used
+
+---
+ silx/opencl/common.py | 2 ++
+ 1 file changed, 2 insertions(+)
+
+diff --git a/silx/opencl/common.py b/silx/opencl/common.py
+index 110d941..3525bf4 100644
+--- a/silx/opencl/common.py
++++ b/silx/opencl/common.py
+@@ -75,6 +75,8 @@ if pyopencl is None:
+ WRITE_ONLY = 1
+ READ_ONLY = 1
+ READ_WRITE = 1
++else:
++ mf = pyopencl.mem_flags
+
+
+ FLOP_PER_CORE = {"GPU": 64, # GPU, Fermi at least perform 64 flops per cycle/multicore, G80 were at 24 or 48 ...
diff --git a/debian/patches/series b/debian/patches/series
index 57660df..efd273e 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,4 +1,3 @@
0002-use-the-system-mathjax-privacy-breach.patch
-0003-fix-unit-test.patch
0003-do-not-modify-PYTHONPATH-from-setup.py.patch
-0004-fix-FTBFS-with-numpy-0.16.patch
+0006-Tests-if-openCL-can-be-used.patch
diff --git a/debian/pydist-overrides b/debian/pydist-overrides
deleted file mode 100644
index 4a84372..0000000
--- a/debian/pydist-overrides
+++ /dev/null
@@ -1,3 +0,0 @@
-pyqt5 python-pyqt5,python-pyqt5.qtopengl,python-pyqt5.qtsvg
-enum34_python_version python-enum34
-futures_python_version python-concurrent.futures
diff --git a/debian/python-silx-doc.doc-base b/debian/python-silx-doc.doc-base
index b290d8a..c8efa7f 100644
--- a/debian/python-silx-doc.doc-base
+++ b/debian/python-silx-doc.doc-base
@@ -1,9 +1,9 @@
Document: silx-manual
Title: silx documentation manual
Author: Jérôme Kieffer <jerome.kieffer@esrf.eu>
-Abstract: Toolbox for X-Ray data analysis
+Abstract: Toolbox for X-Ray data analysis
Section: Science/Data Analysis
Format: HTML
-Index: /usr/share/doc/python-silx-doc/html/index.html
-Files: /usr/share/doc/python-silx-doc/html/*
+Index: /usr/share/doc/python3-silx/html/index.html
+Files: /usr/share/doc/python3-silx/html/*
diff --git a/debian/rules b/debian/rules
index 501910c..d086f63 100755
--- a/debian/rules
+++ b/debian/rules
@@ -9,6 +9,7 @@ export DEB_BUILD_MAINT_OPTIONS = hardening=+all
export PYBUILD_AFTER_INSTALL=rm -rf {destdir}/usr/bin/
export PYBUILD_NAME=silx
export SPECFILE_USE_GNU_SOURCE=1
+export SILX_FULL_INSTALL_REQUIRES=1
DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH)
@@ -23,7 +24,7 @@ ALL_PYX := $(call rwildcard,silx/,*.pyx)
PY3VER := $(shell py3versions -dv)
%:
- dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild
+ dh $@ --with python3,sphinxdoc --buildsystem=pybuild
override_dh_clean:
dh_clean
@@ -41,7 +42,6 @@ override_dh_auto_build:
dh_auto_build -- -s custom --build-args="env PYTHONPATH={build_dir} {interpreter} setup.py build_man"
override_dh_install:
- dh_numpy
dh_numpy3
# install scripts into silx
@@ -65,11 +65,9 @@ override_dh_python3:
# SILX_TEST_LOW_MEM=True to disable tests taking large amount of memory
# GPU=False to disable the use of a GPU with OpenCL test
# WITH_GL_TEST=False to disable tests using OpenGL
-# UNACTIVATED UNTIL dh_python from UNSTABLE IS FIXED
-# https://lists.debian.org/debian-python/2017/08/msg00095.html
override_dh_auto_test:
-# mkdir -p $(POCL_CACHE_DIR) # create POCL cachedir in order to avoid an FTBFS in sbuild
-# dh_auto_test -- -s custom --test-args="env PYTHONPATH={build_dir} GPU=False SILX_OPENCL=False SILX_TEST_LAW_MEM=True xvfb-run -a --server-args=\"-screen 0 1024x768x24\" {interpreter} run_tests.py -vv --installed"
+ mkdir -p $(POCL_CACHE_DIR) # create POCL cachedir in order to avoid an FTBFS in sbuild
+ dh_auto_test -- -s custom --test-args="env PYTHONPATH={build_dir} GPU=False WITH_QT_TEST=False SILX_OPENCL=False SILX_TEST_LAW_MEM=True xvfb-run -a --server-args=\"-screen 0 1024x768x24\" {interpreter} run_tests.py -vv --installed"
override_dh_installman:
dh_installman -p silx build/man/*.1
@@ -79,6 +77,6 @@ ifeq (,$(findstring nodocs, $(DEB_BUILD_OPTIONS)))
#mkdir -p $(POCL_CACHE_DIR) # create POCL cachedir in order to avoid an FTBFS in sbuild
mkdir -p -m 700 $(XDG_RUNTIME_DIR)
pybuild --build -s custom -p $(PY3VER) --build-args="cd doc && env PYTHONPATH={build_dir} http_proxy='127.0.0.1:9' xvfb-run -a --server-args=\"-screen 0 1024x768x24\" {interpreter} -m sphinx -N -bhtml source build/html"
- dh_installdocs "doc/build/html" -p python-silx-doc
+ dh_installdocs "doc/build/html" -p python-silx-doc --doc-main-package=python3-silx
dh_sphinxdoc -O--buildsystem=pybuild
endif
diff --git a/debian/tests/control b/debian/tests/control
new file mode 100644
index 0000000..deb174c
--- /dev/null
+++ b/debian/tests/control
@@ -0,0 +1,15 @@
+Test-Command: set -efu
+ ; for py in $(py3versions -r 2>/dev/null)
+ ; do cd "$AUTOPKGTEST_TMP"
+ ; echo "Testing with $py:"
+ ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py -c "import silx.test; silx.test.run_tests()" 2>&1
+ ; done
+Depends: python3-all, python3-silx, xauth, xvfb
+
+Test-Command: set -efu
+ ; for py in $(py3versions -r 2>/dev/null)
+ ; do cd "$AUTOPKGTEST_TMP"
+ ; echo "Testing with $py-dbg:"
+ ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py-dbg -c "import silx.test; silx.test.run_tests()" 2>&1
+ ; done
+Depends: python3-all-dbg, python3-silx-dbg, xauth, xvfb
diff --git a/doc/source/conf.py b/doc/source/conf.py
index 532f6bf..18bfce2 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
# /*##########################################################################
-# Copyright (C) 2015-2017 European Synchrotron Radiation Facility
+# Copyright (C) 2015-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -76,6 +76,7 @@ extensions = [
'sphinx.ext.viewcode',
'sphinx.ext.doctest',
'sphinxext-archive',
+ 'snapshotqt_directive',
'nbsphinx'
]
diff --git a/doc/source/ext/snapshotqt_directive.py b/doc/source/ext/snapshotqt_directive.py
new file mode 100644
index 0000000..582b934
--- /dev/null
+++ b/doc/source/ext/snapshotqt_directive.py
@@ -0,0 +1,234 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""RST directive to include snapshot of a Qt application in Sphinx doc.
+
+Configuration variable in conf.py:
+
+- snapshotqt_image_type: image file extension (default 'png').
+- snapshotqt_script_dir: relative path of the root directory for scripts from
+ the documentation source directory (i.e., the directory of conf.py)
+ (default: '..').
+"""
+from __future__ import absolute_import
+
+__authors__ = ["H. Payno", "T. Vincent"]
+__license__ = "MIT"
+__date__ = "07/12/2018"
+
+import os
+import logging
+import sys
+from docutils.parsers.rst.directives.images import Image
+from docutils.parsers.rst import directives
+
+# from docutils.par
+# note: conf.py is patching the PATH so this will be the 'current' qt version
+
+home = os.path.abspath(os.path.join(__file__, "..", "..", "..", '..'))
+
+
+if not os.environ.get('DIRECTIVE_SNAPSHOT_QT') == 'True':
+ """
+ In case we don't wan't to regenerate screenshot, simply apply Figure
+ directive
+ """
+ class SnapshotQtDirective(Image):
+ option_spec = Image.option_spec.copy()
+ option_spec['script'] = directives.unchanged
+ has_content = True
+
+ def run(self):
+ self.options['figwidth'] = 'image'
+ self.content = []
+
+ # Create an image filename from arguments
+ return Image.run(self)
+
+ def makescreenshot(*args, **kwargs):
+ raise RuntimeError('not defined without env variable SILX_GENERATE_SCREENSHOT set to True')
+
+ def setup(app):
+ app.add_config_value('snapshotqt_image_type', 'png', 'env')
+ app.add_config_value('snapshotqt_script_dir', '..', 'env')
+ app.add_directive('snapshotqt', SnapshotQtDirective)
+ return {'version': '0.1'}
+
+else:
+ from silx.gui import qt
+
+ logging.basicConfig()
+ _logger = logging.getLogger(__name__)
+
+ # RST directive ###############################################################
+
+ class SnapshotQtDirective(Image):
+ """Image of a Qt application snapshot.
+
+ Directive Type: "snapshotqt"
+ Doctree Elements: As for figure
+ Directive Arguments: One or more, required (script URI + script arguments).
+ Directive Options: Possible.
+ Directive Content: Interpreted as the figure caption and optional legend.
+
+ A "snapshotqt" is a rst `figure
+ <http://docutils.sourceforge.net/docs/ref/rst/directives.html#figure>`_
+ that is generated from a Python script that uses Qt.
+
+ The path of the script to take a snapshot is relative to
+ the path given in conf.py 'snapshotqt_script_dir' value.
+
+ ::
+
+ .. snapshotqt: img/demo.py
+ :align: center
+ :height: 5cm
+
+ source code
+
+
+ you can also define a snapshot from a script, using the :script: option
+ .. note:: on this path are given from the project root level
+
+ ::
+ .. snapshotqt: img/demo.py
+ :align: center
+ :height: 5cm
+ :script: myscript.py
+ """
+ option_spec = Image.option_spec.copy()
+ option_spec['script'] = directives.unchanged
+ has_content = True
+
+ def run(self):
+ assert len(self.arguments) > 0
+ # Run script stored in arguments and replace by snapshot filename
+ script = self.options.pop('script', None)
+ env = self.state.document.settings.env
+
+ image_ext = env.config.snapshotqt_image_type.lower()
+ script_name = self.arguments[0].replace(image_ext, 'py')
+ output_script = os.path.join(env.app.outdir, script_name)
+
+ image_file_source_path = env.relfn2path(self.arguments[0])[0]
+ image_file_source_path = os.path.join(home, env.srcdir, image_file_source_path)
+
+ def createNeededDirs(_dir):
+ parentDir = os.path.dirname(_dir)
+ if parentDir not in ('', os.sep):
+ createNeededDirs(parentDir)
+ if os.path.exists(_dir) is False:
+ os.mkdir(_dir)
+
+ createNeededDirs(os.path.dirname(output_script))
+
+ has_source_code = not (self.content is None or len(self.content) is 0)
+ if has_source_code:
+ with open(output_script, 'w') as _file:
+ _file.write("# from silx.gui import qt\n")
+ _file.write("# app = qt.QApplication([])\n")
+ for _line in self.content:
+ _towrite = _line.lstrip(' ')
+ if not _towrite.startswith(':'):
+ _file.write(_towrite + '\n')
+ _file.write("app.exec_()")
+ self.content = []
+ if script is not None:
+ _logger.warning('Cannot specify a script if source code (content) is given.'
+ 'Ignore script option')
+ makescreenshot(script_or_module=output_script,
+ filename=image_file_source_path)
+ else:
+ # script
+ if script is None:
+ _logger.warning('no source code or script defined in the snapshot'
+ 'directive, fail to generate a screenshot')
+ else:
+ script_path = os.path.join(home, script)
+ makescreenshot(script_or_module=script_path,
+ filename=image_file_source_path)
+
+ #
+ # Use created image as in Figure
+ return super(SnapshotQtDirective, self).run()
+
+ def setup(app):
+ app.add_config_value('snapshotqt_image_type', 'png', 'env')
+ app.add_config_value('snapshotqt_script_dir', '..', 'env')
+ app.add_directive('snapshotqt', SnapshotQtDirective)
+ return {'version': '0.1'}
+
+ # screensImageFileDialogH5.hot function ########################################################
+
+ def makescreenshot(script_or_module, filename):
+ _logger.info('generate screenshot for %s from %s, binding is %s'
+ '' % (filename, script_or_module, qt.BINDING))
+
+ def grabWindow(winID):
+ screen = qt.QApplication.primaryScreen()
+ return screen.grabWindow(winID)
+
+ global _count
+ _count = 15
+ global _TIMEOUT
+ _TIMEOUT = 1000. # in ms
+ app = qt.QApplication.instance() or qt.QApplication([])
+ _logger.debug('Using Qt bindings: %s', qt)
+
+ def _grabActiveWindowAndClose():
+ global _count
+ activeWindow = qt.QApplication.activeWindow()
+ if activeWindow is not None:
+ if activeWindow.isVisible():
+ # hot fix since issue with pySide2 API
+ if qt.BINDING == 'PySide2':
+ pixmap = activeWindow.grab()
+ else:
+ pixmap = grabWindow(activeWindow.winId())
+ saveOK = pixmap.save(filename)
+ if not saveOK:
+ _logger.error(
+ 'Cannot save snapshot to %s', filename)
+ else:
+ _logger.error('activeWindow is not visible.')
+ app.quit()
+ else:
+ _count -= 1
+ if _count > 0:
+ # Only restart a timer if everything is OK
+ qt.QTimer.singleShot(_TIMEOUT,
+ _grabActiveWindowAndClose)
+ else:
+ app.quit()
+ raise TimeoutError(
+ 'Aborted: It took too long to have an active window.')
+ script_or_module = os.path.abspath(script_or_module)
+
+ sys.argv = [script_or_module]
+ sys.path.append(
+ os.path.abspath(os.path.dirname(script_or_module)))
+ qt.QTimer.singleShot(_TIMEOUT, _grabActiveWindowAndClose)
+ with open(script_or_module) as f:
+ code = compile(f.read(), script_or_module, 'exec')
+ exec(code, globals(), locals())
diff --git a/doc/source/install.rst b/doc/source/install.rst
index 7941a7b..8e5220b 100644
--- a/doc/source/install.rst
+++ b/doc/source/install.rst
@@ -110,7 +110,7 @@ Linux
Packages are available for a few distributions:
-- Debian 8: see `Installing a Debian package`_.
+- Debian 9: see `Installing a Debian package`_.
- `CentOS 7 RPM packages <http://pubrepo.maxiv.lu.se/rpm/el7/x86_64/>`_ provided by the Max IV institute at Lund, Sweden.
- `Fedora 23 rpm packages <http://pubrepo.maxiv.lu.se/rpm/fc23/x86_64/>`_ provided by the Max IV institute at Lund, Sweden.
- `Arch Linux (AUR) package <https://aur.archlinux.org/packages/python-silx>`_ provided by Leonid Bloch.
@@ -125,14 +125,14 @@ You can also follow one of those installation procedures:
Installing a Debian package
+++++++++++++++++++++++++++
-Debian 8 (Jessie) packages are available on http://www.silx.org/pub/debian/ for amd64 computers.
+Debian 9 (Stretch) packages are available on http://www.silx.org/pub/debian/ for amd64 computers.
To install it, you need to download this file :
.. code-block:: bash
http://www.silx.org/pub/debian/silx.list
-and copy it into the /etc/apt/source.list.d folder.
+and copy it into the /etc/apt/sources.list.d folder.
Then run ``apt-get update`` and ``apt-get install python-silx``
.. code-block:: bash
diff --git a/doc/source/modules/gui/data/img/ArrayTableWidget.png b/doc/source/modules/gui/data/img/ArrayTableWidget.png
index 7c81d02..6ae9114 100644
--- a/doc/source/modules/gui/data/img/ArrayTableWidget.png
+++ b/doc/source/modules/gui/data/img/ArrayTableWidget.png
Binary files differ
diff --git a/doc/source/modules/gui/data/img/DataViewer.png b/doc/source/modules/gui/data/img/DataViewer.png
index 719c822..c2185d3 100644
--- a/doc/source/modules/gui/data/img/DataViewer.png
+++ b/doc/source/modules/gui/data/img/DataViewer.png
Binary files differ
diff --git a/doc/source/modules/gui/gallery.rst b/doc/source/modules/gui/gallery.rst
index fffcfd8..9923c0b 100644
--- a/doc/source/modules/gui/gallery.rst
+++ b/doc/source/modules/gui/gallery.rst
@@ -39,14 +39,27 @@ Widgets gallery
* - Widget
- Description
- * - .. image:: data/img/ArrayTableWidget.png
+ * - .. snapshotqt:: data/img/ArrayTableWidget.png
:height: 150px
:align: center
+
+ from silx.gui.data.ArrayTableWidget import ArrayTableWidget
+ import numpy.random
+ table = ArrayTableWidget()
+ table.setArrayData(numpy.random.random((100, 100, 100)))
+ table.resize(500, 300)
+ table.show()
- :class:`ArrayTableWidget` is a table widget with browsers designed to
display the content of multi-dimensional data arrays.
- * - .. image:: data/img/DataViewer.png
+ * - .. snapshotqt:: data/img/DataViewer.png
:height: 150px
:align: center
+
+ import numpy.random
+ from silx.gui.data.DataViewer import DataViewer
+ viewer = DataViewer()
+ viewer.setData(numpy.random.random((100, 100, 100)))
+ viewer.show()
- :class:`DataViewer` is a widget designed to display data using the most
adapted view.
* - .. image:: data/img/DataViewerFrame.png
@@ -208,14 +221,33 @@ Additional widgets:
:align: center
- :class:`.PlotTools.PositionInfo` is a widget displaying mouse position and
information of a :class:`PlotWidget` associated to the mouse position.
- * - .. image:: plot/img/LimitsToolBar.png
+ * - .. snapshotqt:: plot/img/LimitsToolBar.png
:width: 300px
:align: center
+
+ from silx.gui.plot import Plot2D
+ from silx.gui.plot.tools.LimitsToolBar import LimitsToolBar
+ plot = Plot2D()
+ toolbar = LimitsToolBar(plot=plot)
+ toolbar.resize(400, 30)
+ plot.show()
+ toolbar.show()
+ app.processEvents()
- :class:`.PlotTools.LimitsToolBar` is a QToolBar displaying and
controlling the limits of a :class:`PlotWidget`.
- * - .. image:: plot/img/logColorbar.png
+ * - .. snapshotqt:: plot/img/logColorbar.png
:height: 150px
:align: center
+
+ from silx.gui.plot import Plot2D
+ from silx.gui.plot.ColorBar import ColorBarWidget
+ from silx.gui.plot.Colors import Colormap
+ import numpy
+ plot = Plot2D()
+ colorbar = ColorBarWidget(plot=plot, legend='Colormap Log scale')
+ colorbar.setColormap(Colormap(name='jet', normalization='log', vmin=1.0, vmax=10e3))
+ colorbar.show()
+ colorbar.resize(20, 500)
- :class:`.ColorBar.ColorBarWidget` display colormap gradient and can be linked with a plot
to display the colormap
* - .. image:: plot/img/statsWidget.png
@@ -243,9 +275,10 @@ Additional widgets:
and associated toolbars.
It can display 2D images, 2D scatter data, 3D scatter data and 3D volumes with different visualizations.
See ``plot3dSceneWindow.py`` in :ref:`plot3d-sample-code`.
- * - .. image:: plot3d/img/SceneWidget.png
+ * - .. snapshotqt:: plot3d/img/SceneWidget.png
:height: 150px
:align: center
+ :script: examples/plot3dSceneWindow.py
- :class:`SceneWidget` is a :class:`Plot3DWidget` providing a 3D scene for visualizing different kind of data.
It can display 2D images, 2D scatter data, 3D scatter data and 3D volumes with different visualizations.
See ``plot3dSceneWindow.py`` in :ref:`plot3d-sample-code`.
@@ -303,49 +336,103 @@ Additional widgets:
* - Widget
- Description
- * - .. image:: widgets/img/FrameBrowser.png
+ * - .. snapshotqt:: widgets/img/FrameBrowser.png
:width: 110px
:align: center
+
+ from silx.gui.widgets.FrameBrowser import FrameBrowser
+ widget = FrameBrowser()
+ widget.setRange(0, 10)
+ widget.show()
- :class:`FrameBrowser.FrameBrowser` is a browser widget designed to
browse through a sequence of integers (e.g. the indices of an array)
- * - .. image:: widgets/img/HorizontalSliderWithBrowser.png
+ * - .. snapshotqt:: widgets/img/HorizontalSliderWithBrowser.png
:width: 150px
:align: center
+
+ from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
+ slider = HorizontalSliderWithBrowser()
+ slider.show()
- :class:`FrameBrowser.HorizontalSliderWithBrowser` is a :class:`FrameBrowser`
with an additional slider.
- * - .. image:: widgets/img/RangeSlider.png
+ * - .. snapshotqt:: widgets/img/RangeSlider.png
:width: 150px
:align: center
+
+ from silx.gui.widgets.RangeSlider import RangeSlider
+ from silx.gui.plot.Colors import Colormap
+ import numpy
+ widget = RangeSlider()
+ widget.setRange(0, 500)
+ widget.setValues(100, 400)
+ background = numpy.sin(numpy.arange(250) / 250.0)
+ background[0], background[-1] = background[-1], background[0]
+ colormap = Colormap("viridis")
+ widget.setGroovePixmapFromProfile(background, colormap)
+ widget.show()
- :class:`~silx.gui.widgets.RangeSlider.RangeSlider` is a slider with 2 thumbs dedicated
to the interactive selection of an interval.
- * - .. image:: widgets/img/PeriodicCombo.png
+ * - .. snapshotqt:: widgets/img/PeriodicCombo.png
:width: 150px
:align: center
+
+ from silx.gui.widgets.PeriodicTable import PeriodicCombo
+ widget = PeriodicCombo()
+ widget.setSelection('Yb')
+ widget.show()
- :class:`PeriodicTable.PeriodicCombo` is a :class:`QComboBox` widget designed to
select a single atomic element.
- * - .. image:: widgets/img/PeriodicList.png
+ * - .. snapshotqt:: widgets/img/PeriodicList.png
:height: 150px
:align: center
+
+ from silx.gui.widgets.PeriodicTable import PeriodicList
+ widget = PeriodicList()
+ widget.setSelectedElements(('S', 'Cl'))
+ widget.resize(200, 400)
+ widget.show()
- :class:`PeriodicTable.PeriodicList` is a :class:`QTreeWidget` designed to select one
or more atomic elements.
- * - .. image:: widgets/img/PeriodicTable.png
+ * - .. snapshotqt:: widgets/img/PeriodicTable.png
:height: 150px
:align: center
+
+ from silx.gui.widgets.PeriodicTable import PeriodicTable
+ widget = PeriodicTable()
+ widget.setSelection(('S', 'H', 'Zr'))
+ widget.show()
- :class:`PeriodicTable.PeriodicTable` is a periodic table widget designed to select one
or more atomic elements.
- * - .. image:: widgets/img/TableWidget.png
+ * - .. snapshotqt:: widgets/img/TableWidget.png
:height: 150px
:align: center
+
+ from silx.gui.widgets.TableWidget import TableWidget
+ widget = TableWidget()
+ widget.setRowCount(8)
+ widget.setColumnCount(4)
+ widget.resize(300, 200)
+ widget.show()
- :class:`TableWidget.TableWidget` and :class:`TableWidget.TableView` inherit respectively
:class:`QTableWidget` and :class:`QTableView`, and add a context menu with *cut/copy/paste*
actions.
- * - .. image:: widgets/img/WaitingPushButton.png
+ * - .. snapshotqt:: widgets/img/WaitingPushButton.png
:width: 60px
:align: center
+
+ from silx.gui.widgets.WaitingPushButton import WaitingPushButton
+ from silx.gui import icons
+ animated_icon = icons.getWaitIcon()
+ button = WaitingPushButton(icon=animated_icon.currentIcon(), text='Run')
+ button.show()
- :class:`WaitingPushButton` is a :class:`QPushButton` that can be graphically disabled,
for example to wait for a callback function to finish computing.
- * - .. image:: widgets/img/ThreadPoolPushButton.png
+ * - .. snapshotqt:: widgets/img/ThreadPoolPushButton.png
:width: 100px
:align: center
+
+ from silx.gui.widgets.ThreadPoolPushButton import ThreadPoolPushButton
+ button = ThreadPoolPushButton(text="Compute 2^16")
+ button.show()
- :class:`ThreadPoolPushButton` is a :class:`WaitingPushButton` that executes a
callback in a thread.
diff --git a/doc/source/modules/gui/icons.rst b/doc/source/modules/gui/icons.rst
index 659bc4c..ba7d89b 100644
--- a/doc/source/modules/gui/icons.rst
+++ b/doc/source/modules/gui/icons.rst
@@ -75,6 +75,8 @@ Available icons
- compare-align-stretch
* - |compare-keypoints|
- compare-keypoints
+ * - |compare-mode-a-minus-b|
+ - compare-mode-a-minus-b
* - |compare-mode-a|
- compare-mode-a
* - |compare-mode-b|
@@ -107,6 +109,18 @@ Available icons
- cube-top
* - |cube|
- cube
+ * - |description-description|
+ - description-description
+ * - |description-error|
+ - description-error
+ * - |description-name|
+ - description-name
+ * - |description-program|
+ - description-program
+ * - |description-title|
+ - description-title
+ * - |description-value|
+ - description-value
* - |document-open|
- document-open
* - |document-print|
@@ -121,6 +135,8 @@ Available icons
- draw-rubber
* - |edit-copy|
- edit-copy
+ * - |eye|
+ - eye
* - |first|
- first
* - |folder|
@@ -251,6 +267,8 @@ Available icons
- plot-ylog
* - |plot-yup|
- plot-yup
+ * - |pointing-hand|
+ - pointing-hand
* - |previous|
- previous
* - |profile-clear|
@@ -370,6 +388,7 @@ Available icons
.. |compare-align-origin| image:: ../../../../silx/resources/gui/icons/compare-align-origin.png
.. |compare-align-stretch| image:: ../../../../silx/resources/gui/icons/compare-align-stretch.png
.. |compare-keypoints| image:: ../../../../silx/resources/gui/icons/compare-keypoints.png
+.. |compare-mode-a-minus-b| image:: ../../../../silx/resources/gui/icons/compare-mode-a-minus-b.png
.. |compare-mode-a| image:: ../../../../silx/resources/gui/icons/compare-mode-a.png
.. |compare-mode-b| image:: ../../../../silx/resources/gui/icons/compare-mode-b.png
.. |compare-mode-hline| image:: ../../../../silx/resources/gui/icons/compare-mode-hline.png
@@ -386,6 +405,12 @@ Available icons
.. |cube-rotate| image:: ../../../../silx/resources/gui/icons/cube-rotate.png
.. |cube-top| image:: ../../../../silx/resources/gui/icons/cube-top.png
.. |cube| image:: ../../../../silx/resources/gui/icons/cube.png
+.. |description-description| image:: ../../../../silx/resources/gui/icons/description-description.png
+.. |description-error| image:: ../../../../silx/resources/gui/icons/description-error.png
+.. |description-name| image:: ../../../../silx/resources/gui/icons/description-name.png
+.. |description-program| image:: ../../../../silx/resources/gui/icons/description-program.png
+.. |description-title| image:: ../../../../silx/resources/gui/icons/description-title.png
+.. |description-value| image:: ../../../../silx/resources/gui/icons/description-value.png
.. |document-open| image:: ../../../../silx/resources/gui/icons/document-open.png
.. |document-print| image:: ../../../../silx/resources/gui/icons/document-print.png
.. |document-save| image:: ../../../../silx/resources/gui/icons/document-save.png
@@ -393,6 +418,7 @@ Available icons
.. |draw-pencil| image:: ../../../../silx/resources/gui/icons/draw-pencil.png
.. |draw-rubber| image:: ../../../../silx/resources/gui/icons/draw-rubber.png
.. |edit-copy| image:: ../../../../silx/resources/gui/icons/edit-copy.png
+.. |eye| image:: ../../../../silx/resources/gui/icons/eye.png
.. |first| image:: ../../../../silx/resources/gui/icons/first.png
.. |folder| image:: ../../../../silx/resources/gui/icons/folder.png
.. |image-mask| image:: ../../../../silx/resources/gui/icons/image-mask.png
@@ -458,6 +484,7 @@ Available icons
.. |plot-ydown| image:: ../../../../silx/resources/gui/icons/plot-ydown.png
.. |plot-ylog| image:: ../../../../silx/resources/gui/icons/plot-ylog.png
.. |plot-yup| image:: ../../../../silx/resources/gui/icons/plot-yup.png
+.. |pointing-hand| image:: ../../../../silx/resources/gui/icons/pointing-hand.png
.. |previous| image:: ../../../../silx/resources/gui/icons/previous.png
.. |profile-clear| image:: ../../../../silx/resources/gui/icons/profile-clear.png
.. |profile1D| image:: ../../../../silx/resources/gui/icons/profile1D.png
diff --git a/doc/source/modules/gui/plot/dev.rst b/doc/source/modules/gui/plot/dev.rst
index 8966487..0c848e9 100644
--- a/doc/source/modules/gui/plot/dev.rst
+++ b/doc/source/modules/gui/plot/dev.rst
@@ -92,6 +92,7 @@ The following modules are the modules used internally by the plot package.
.. automodule:: silx.gui.plot.CurvesROIWidget
:members:
+ :noindex:
:mod:`Interaction`
++++++++++++++++++
diff --git a/doc/source/modules/gui/plot/img/BasicGridStatsWidget.png b/doc/source/modules/gui/plot/img/BasicGridStatsWidget.png
new file mode 100644
index 0000000..53ddc0e
--- /dev/null
+++ b/doc/source/modules/gui/plot/img/BasicGridStatsWidget.png
Binary files differ
diff --git a/doc/source/modules/gui/plot/img/BasicStatsWidget.png b/doc/source/modules/gui/plot/img/BasicStatsWidget.png
new file mode 100644
index 0000000..c9ed2cd
--- /dev/null
+++ b/doc/source/modules/gui/plot/img/BasicStatsWidget.png
Binary files differ
diff --git a/doc/source/modules/gui/plot/img/LimitsToolBar.png b/doc/source/modules/gui/plot/img/LimitsToolBar.png
index 99af8bd..54b6c2b 100644
--- a/doc/source/modules/gui/plot/img/LimitsToolBar.png
+++ b/doc/source/modules/gui/plot/img/LimitsToolBar.png
Binary files differ
diff --git a/doc/source/modules/gui/plot/img/logColorbar.png b/doc/source/modules/gui/plot/img/logColorbar.png
index cdd247c..c677a9b 100644
--- a/doc/source/modules/gui/plot/img/logColorbar.png
+++ b/doc/source/modules/gui/plot/img/logColorbar.png
Binary files differ
diff --git a/doc/source/modules/gui/plot/index.rst b/doc/source/modules/gui/plot/index.rst
index 6f06830..01cb29b 100644
--- a/doc/source/modules/gui/plot/index.rst
+++ b/doc/source/modules/gui/plot/index.rst
@@ -61,6 +61,13 @@ Additionnal plot tool widgets:
statswidget.rst
stats/index.rst
+Utilities
+
+.. toctree::
+ :maxdepth: 2
+
+ utils.rst
+
Internals
---------
diff --git a/doc/source/modules/gui/plot/roi.rst b/doc/source/modules/gui/plot/roi.rst
index 77b5c2a..efe41a7 100644
--- a/doc/source/modules/gui/plot/roi.rst
+++ b/doc/source/modules/gui/plot/roi.rst
@@ -1,12 +1,15 @@
-.. currentmodule:: silx.gui.plot
+.. currentmodule:: silx.gui.plot.CurvesROIWidget
:mod:`CurvesROIWidget`: ROI from curves
=======================================
+
.. |roiWidgetImage| image:: img/roiwidget.png
:height: 400px
:align: middle
+.. automodule:: silx.gui.plot.CurvesROIWidget
+
You can access to the ROIWidget from a Plot window by :
- using the tool button 'ROI'
@@ -14,4 +17,28 @@ You can access to the ROIWidget from a Plot window by :
|roiWidgetImage|
-.. automodule:: silx.gui.plot.CurvesROIWidget
+
+
+
+:class:`ROI` class
+------------------
+
+.. autoclass:: ROI
+ :show-inheritance:
+ :members:
+
+
+:class:`CurvesROIWidget` class
+-------------------------------
+
+.. autoclass:: CurvesROIWidget
+ :show-inheritance:
+ :members:
+
+
+:class:`ROITable` class
+-----------------------
+
+.. autoclass:: ROITable
+ :show-inheritance:
+ :members:
diff --git a/doc/source/modules/gui/plot/statswidget.rst b/doc/source/modules/gui/plot/statswidget.rst
index f534921..1574abc 100644
--- a/doc/source/modules/gui/plot/statswidget.rst
+++ b/doc/source/modules/gui/plot/statswidget.rst
@@ -31,3 +31,19 @@
:show-inheritance:
:members:
+
+:class:`BasicLineStatsWidget` class
+-----------------------------------
+
+.. autoclass:: BasicLineStatsWidget
+ :show-inheritance:
+ :members:
+
+
+:class:`BasicGridStatsWidget` class
+-----------------------------------
+
+.. autoclass:: BasicGridStatsWidget
+ :show-inheritance:
+ :members:
+
diff --git a/doc/source/modules/gui/plot/utils.rst b/doc/source/modules/gui/plot/utils.rst
new file mode 100644
index 0000000..e930208
--- /dev/null
+++ b/doc/source/modules/gui/plot/utils.rst
@@ -0,0 +1,12 @@
+.. currentmodule:: silx.gui.plot.utils
+
+
+:mod:`axis`: utilities for plots
+================================
+
+SyncAxes
+--------
+
+.. autoclass:: silx.gui.plot.utils.axis.SyncAxes
+ :members:
+
diff --git a/doc/source/modules/gui/plot3d/glutils.rst b/doc/source/modules/gui/plot3d/glutils.rst
index 2c36e83..21781d9 100644
--- a/doc/source/modules/gui/plot3d/glutils.rst
+++ b/doc/source/modules/gui/plot3d/glutils.rst
@@ -13,13 +13,17 @@
Utility functions
-----------------
-.. currentmodule:: silx.gui._glutils
+.. currentmodule:: silx.gui._glutils.Context
For OpenGL context management:
-.. autofunction:: getGLContext
+.. autofunction:: getCurrent
+
+.. autofunction:: setCurrent
-.. autofunction:: setGLContextGetter
+.. autofunction:: current
+
+.. currentmodule:: silx.gui._glutils
For type checking and conversion:
diff --git a/doc/source/modules/gui/plot3d/img/SceneWidget.png b/doc/source/modules/gui/plot3d/img/SceneWidget.png
index 610c41a..dbe7791 100644
--- a/doc/source/modules/gui/plot3d/img/SceneWidget.png
+++ b/doc/source/modules/gui/plot3d/img/SceneWidget.png
Binary files differ
diff --git a/doc/source/modules/gui/plot3d/items.rst b/doc/source/modules/gui/plot3d/items.rst
index 1162cb9..5c4884f 100644
--- a/doc/source/modules/gui/plot3d/items.rst
+++ b/doc/source/modules/gui/plot3d/items.rst
@@ -53,7 +53,7 @@ The following classes are items that describes the content of a :class:`SceneWid
:class:`Scatter2D` inherits from :class:`.DataItem3D` and also provides its API.
.. autoclass:: Scatter2D
- :members: getData, setData, getXData, getYData, getValues,
+ :members: getData, setData, getXData, getYData, getValueData,
supportedVisualizations, isPropertyEnabled,
getVisualization, setVisualization,
isHeightMap, setHeightMap,
@@ -67,7 +67,7 @@ The following classes are items that describes the content of a :class:`SceneWid
:class:`Scatter3D` inherits from :class:`.DataItem3D` and also provides its API.
.. autoclass:: Scatter3D
- :members: getData, setData, getXData, getYData, getZData, getValues,
+ :members: getData, setData, getXData, getYData, getZData, getValueData,
getColormap, setColormap,
getSupportedSymbols, getSymbol, setSymbol
diff --git a/doc/source/modules/gui/widgets/img/FrameBrowser.png b/doc/source/modules/gui/widgets/img/FrameBrowser.png
index c5624f7..3843e70 100644
--- a/doc/source/modules/gui/widgets/img/FrameBrowser.png
+++ b/doc/source/modules/gui/widgets/img/FrameBrowser.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/HorizontalSliderWithBrowser.png b/doc/source/modules/gui/widgets/img/HorizontalSliderWithBrowser.png
index d64b2df..8cfdb25 100644
--- a/doc/source/modules/gui/widgets/img/HorizontalSliderWithBrowser.png
+++ b/doc/source/modules/gui/widgets/img/HorizontalSliderWithBrowser.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/PeriodicCombo.png b/doc/source/modules/gui/widgets/img/PeriodicCombo.png
index e0b40c2..4a93e86 100644
--- a/doc/source/modules/gui/widgets/img/PeriodicCombo.png
+++ b/doc/source/modules/gui/widgets/img/PeriodicCombo.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/PeriodicList.png b/doc/source/modules/gui/widgets/img/PeriodicList.png
index d1e540b..42f432e 100644
--- a/doc/source/modules/gui/widgets/img/PeriodicList.png
+++ b/doc/source/modules/gui/widgets/img/PeriodicList.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/PeriodicTable.png b/doc/source/modules/gui/widgets/img/PeriodicTable.png
index c06dded..ce52262 100644
--- a/doc/source/modules/gui/widgets/img/PeriodicTable.png
+++ b/doc/source/modules/gui/widgets/img/PeriodicTable.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/RangeSlider.png b/doc/source/modules/gui/widgets/img/RangeSlider.png
index b068c6f..f552fb3 100644
--- a/doc/source/modules/gui/widgets/img/RangeSlider.png
+++ b/doc/source/modules/gui/widgets/img/RangeSlider.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/TableWidget.png b/doc/source/modules/gui/widgets/img/TableWidget.png
index a0f819f..b6a4965 100644
--- a/doc/source/modules/gui/widgets/img/TableWidget.png
+++ b/doc/source/modules/gui/widgets/img/TableWidget.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/ThreadPoolPushButton.png b/doc/source/modules/gui/widgets/img/ThreadPoolPushButton.png
index 4710d16..18a7416 100644
--- a/doc/source/modules/gui/widgets/img/ThreadPoolPushButton.png
+++ b/doc/source/modules/gui/widgets/img/ThreadPoolPushButton.png
Binary files differ
diff --git a/doc/source/modules/gui/widgets/img/WaitingPushButton.png b/doc/source/modules/gui/widgets/img/WaitingPushButton.png
index fa1d51a..4fd3e3c 100644
--- a/doc/source/modules/gui/widgets/img/WaitingPushButton.png
+++ b/doc/source/modules/gui/widgets/img/WaitingPushButton.png
Binary files differ
diff --git a/doc/source/modules/image/index.rst b/doc/source/modules/image/index.rst
index 477cc9f..cf4867b 100644
--- a/doc/source/modules/image/index.rst
+++ b/doc/source/modules/image/index.rst
@@ -12,4 +12,6 @@
marchingsquares.rst
shapes.rst
sift.rst
+ projection.rst
backprojection.rst
+ reconstruction.rst
diff --git a/doc/source/modules/image/shapes.rst b/doc/source/modules/image/shapes.rst
index a20b0cd..be51975 100644
--- a/doc/source/modules/image/shapes.rst
+++ b/doc/source/modules/image/shapes.rst
@@ -2,7 +2,7 @@
.. currentmodule:: silx.image
:mod:`shapes`: 2D shapes drawing
----------------------------------
+--------------------------------
.. automodule:: silx.image.shapes
- :members: circle_fill, draw_line, polygon_fill_mask, Polygon
+ :members: circle_fill, ellipse_fill, draw_line, polygon_fill_mask, Polygon
diff --git a/doc/source/modules/opencl/convolution.rst b/doc/source/modules/opencl/convolution.rst
new file mode 100644
index 0000000..f33cc5b
--- /dev/null
+++ b/doc/source/modules/opencl/convolution.rst
@@ -0,0 +1,10 @@
+
+.. currentmodule:: silx.opencl
+
+:mod:`convolution`: Convolution
+-------------------------------
+
+.. automodule:: silx.opencl.convolution
+ :members: Convolution
+ :show-inheritance:
+ :undoc-members:
diff --git a/doc/source/modules/opencl/index.rst b/doc/source/modules/opencl/index.rst
index e17eecb..ef8b9e6 100644
--- a/doc/source/modules/opencl/index.rst
+++ b/doc/source/modules/opencl/index.rst
@@ -10,6 +10,10 @@
sift/index.rst
fbp.rst
+ sinofilter.rst
+ processing.rst
+ convolution.rst
+ statistics.rst
medfilt.rst
codec_cbf.rst
diff --git a/doc/source/modules/opencl/processing.rst b/doc/source/modules/opencl/processing.rst
new file mode 100644
index 0000000..a246cc6
--- /dev/null
+++ b/doc/source/modules/opencl/processing.rst
@@ -0,0 +1,10 @@
+
+.. currentmodule:: silx.opencl
+
+:mod:`processing`: Processing
+-------------------------------
+
+.. automodule:: silx.opencl.processing
+ :members: OpenclProcessing, KernelContainer
+ :show-inheritance:
+ :undoc-members:
diff --git a/doc/source/modules/opencl/sinofilter.rst b/doc/source/modules/opencl/sinofilter.rst
new file mode 100644
index 0000000..81fe100
--- /dev/null
+++ b/doc/source/modules/opencl/sinofilter.rst
@@ -0,0 +1,9 @@
+
+.. currentmodule:: silx.opencl
+
+:mod:`sinofilter`: Sinogram filtering.
+--------------------------------------------------
+
+.. automodule:: silx.opencl.sinofilter
+ :members:
+ :show-inheritance:
diff --git a/doc/source/modules/opencl/statistics.rst b/doc/source/modules/opencl/statistics.rst
new file mode 100644
index 0000000..0db7566
--- /dev/null
+++ b/doc/source/modules/opencl/statistics.rst
@@ -0,0 +1,10 @@
+
+.. currentmodule:: silx.opencl
+
+:mod:`statistics`: Statistics
+-------------------------------
+
+.. automodule:: silx.opencl.statistics
+ :members: Statistics
+ :show-inheritance:
+ :undoc-members:
diff --git a/doc/source/sample_code/img/compareImages.png b/doc/source/sample_code/img/compareImages.png
new file mode 100644
index 0000000..49618b5
--- /dev/null
+++ b/doc/source/sample_code/img/compareImages.png
Binary files differ
diff --git a/doc/source/sample_code/img/compositeline.png b/doc/source/sample_code/img/compositeline.png
new file mode 100644
index 0000000..8950286
--- /dev/null
+++ b/doc/source/sample_code/img/compositeline.png
Binary files differ
diff --git a/doc/source/sample_code/img/dropZones.png b/doc/source/sample_code/img/dropZones.png
new file mode 100644
index 0000000..3196c6b
--- /dev/null
+++ b/doc/source/sample_code/img/dropZones.png
Binary files differ
diff --git a/doc/source/sample_code/img/exampleBaseline.png b/doc/source/sample_code/img/exampleBaseline.png
new file mode 100644
index 0000000..fcd5778
--- /dev/null
+++ b/doc/source/sample_code/img/exampleBaseline.png
Binary files differ
diff --git a/doc/source/sample_code/img/findContours.png b/doc/source/sample_code/img/findContours.png
new file mode 100644
index 0000000..06dcf01
--- /dev/null
+++ b/doc/source/sample_code/img/findContours.png
Binary files differ
diff --git a/doc/source/sample_code/img/plot3dUpdateScatterFromThread.png b/doc/source/sample_code/img/plot3dUpdateScatterFromThread.png
new file mode 100644
index 0000000..acd1c58
--- /dev/null
+++ b/doc/source/sample_code/img/plot3dUpdateScatterFromThread.png
Binary files differ
diff --git a/doc/source/sample_code/img/plotCurveLegendWidget.png b/doc/source/sample_code/img/plotCurveLegendWidget.png
new file mode 100644
index 0000000..d857950
--- /dev/null
+++ b/doc/source/sample_code/img/plotCurveLegendWidget.png
Binary files differ
diff --git a/doc/source/sample_code/img/plotStats.png b/doc/source/sample_code/img/plotStats.png
new file mode 100644
index 0000000..e4fc781
--- /dev/null
+++ b/doc/source/sample_code/img/plotStats.png
Binary files differ
diff --git a/doc/source/sample_code/img/scatterview.png b/doc/source/sample_code/img/scatterview.png
new file mode 100644
index 0000000..32f0235
--- /dev/null
+++ b/doc/source/sample_code/img/scatterview.png
Binary files differ
diff --git a/doc/source/sample_code/img/syncPlotLocation.png b/doc/source/sample_code/img/syncPlotLocation.png
new file mode 100644
index 0000000..0b86f3e
--- /dev/null
+++ b/doc/source/sample_code/img/syncPlotLocation.png
Binary files differ
diff --git a/doc/source/sample_code/index.rst b/doc/source/sample_code/index.rst
index a5cbf11..082579b 100644
--- a/doc/source/sample_code/index.rst
+++ b/doc/source/sample_code/index.rst
@@ -25,8 +25,7 @@ All sample codes can be downloaded as a zip file: |sample_code_archive|.
- Description
* - :download:`icons.py <../../../examples/icons.py>`
- .. image:: img/icons.png
- :height: 150px
- :align: center
+ :width: 150px
- Display icons and animated icons provided by silx.
:mod:`silx.gui.data` and :mod:`silx.gui.hdf5`
@@ -41,24 +40,17 @@ All sample codes can be downloaded as a zip file: |sample_code_archive|.
- Description
* - :download:`customHdf5TreeModel.py <../../../examples/customHdf5TreeModel.py>`
- .. image:: img/customHdf5TreeModel.png
- :height: 150px
- :align: center
+ :width: 150px
- Qt Hdf5 widget examples
* - :download:`customDataView.py <../../../examples/customDataView.py>`
- .. image:: img/customDataView.png
- :height: 150px
- :align: center
+ :width: 150px
- Qt data view example
* - :download:`hdf5widget.py <../../../examples/hdf5widget.py>`
- .. image:: img/hdf5widget.png
- :height: 150px
- :align: center
+ :width: 150px
- Qt Hdf5 widget examples
- .. note:: This module has a dependency on the `h5py <http://www.h5py.org/>`_
- library, which is not a mandatory dependency for `silx`. You might need
- to install it if you don't already have it.
-
:mod:`silx.gui.dialog`
......................
@@ -71,9 +63,12 @@ All sample codes can be downloaded as a zip file: |sample_code_archive|.
- Description
* - :download:`fileDialog.py <../../../examples/fileDialog.py>`
- .. image:: img/fileDialog.png
- :height: 150px
- :align: center
+ :width: 150px
- Example for the use of the ImageFileDialog.
+ * - :download:`colormapDialog.py <../../../examples/colormapDialog.py>`
+ - .. image:: img/colormapDialog.png
+ :width: 150px
+ - This script shows the features of a :mod:`~silx.gui.dialog.ColormapDialog`.
:mod:`silx.gui.widgets`
.......................
@@ -87,14 +82,13 @@ All sample codes can be downloaded as a zip file: |sample_code_archive|.
- Description
* - :download:`periodicTable.py <../../../examples/periodicTable.py>`
- .. image:: img/periodicTable.png
- :height: 150px
+ :width: 150px
:align: center
- This script is a simple example of how to use the periodic table widgets,
select elements and connect signals.
* - :download:`simplewidget.py <../../../examples/simplewidget.py>`
- .. image:: img/simplewidget.png
- :height: 150px
- :align: center
+ :width: 150px
- This script shows a gallery of simple widgets provided by silx.
It shows the following widgets:
@@ -117,8 +111,7 @@ Widgets
- Description
* - :download:`imageview.py <../../../examples/imageview.py>`
- .. image:: img/imageview.png
- :height: 150px
- :align: center
+ :width: 150px
- Example to show the use of :mod:`~silx.gui.plot.ImageView` widget.
It can be used to open an EDF or TIFF file from the shell command line.
@@ -127,22 +120,38 @@ Widgets
``python examples/imageview.py <file to open>``
To get help:
``python examples/imageview.py -h``
-
- For developers with a git clone you can use it with the bootstrap
- To view an image file with the current installed silx library:
-
- ``./bootstrap.py python examples/imageview.py <file to open>``
* - :download:`stackView.py <../../../examples/stackView.py>`
- .. image:: img/stackView.png
- :height: 150px
- :align: center
+ :width: 150px
- This script is a simple example to illustrate how to use the
:mod:`~silx.gui.plot.StackView` widget.
- * - :download:`colormapDialog.py <../../../examples/colormapDialog.py>`
- - .. image:: img/colormapDialog.png
- :height: 150px
- :align: center
- - This script shows the features of a :mod:`~silx.gui.dialog.ColormapDialog`.
+ * - :download:`scatterview.py <../../../examples/scatterview.py>`
+ - .. image:: img/scatterview.png
+ :width: 150px
+ - Example to show the use of :class:`~silx.gui.plot.ScatterView.ScatterView` widget
+ * - :download:`compareImages.py <../../../examples/compareImages.py>`
+ - .. image:: img/compareImages.png
+ :width: 150px
+ - usage: compareImages.py [-h] [--debug] [--testdata] [--use-opengl-plot]
+ [files [files ...]]
+
+ Example demonstrating the use of the widget CompareImages
+
+ positional arguments:
+ files Image data to compare (HDF5 file with path, EDF files,
+ JPEG/PNG image files). Data from HDF5 files can be
+ accessed using dataset path and slicing as an URL:
+ silx:../my_file.h5?path=/entry/data&slice=10 EDF file
+ frames also can can be accessed using URL:
+ fabio:../my_file.edf?slice=10 Using URL in command like
+ usually have to be quoted: "URL".
+
+ optional arguments:
+ -h, --help show this help message and exit
+ --debug Set logging system in debug mode
+ --testdata Use synthetic images to test the application
+ --use-opengl-plot Use OpenGL for plots (instead of matplotlib)
+
:class:`silx.gui.plot.actions.PlotAction`
.........................................
@@ -158,16 +167,14 @@ Sample code that adds buttons to the toolbar of a silx plot widget.
- Description
* - :download:`plotClearAction.py <../../../examples/plotClearAction.py>`
- .. image:: img/plotClearAction.png
- :height: 150px
- :align: center
+ :width: 150px
- This script shows how to create a minimalistic
:class:`~silx.gui.plot.actions.PlotAction` that clear the plot.
This illustrates how to add more buttons in a plot widget toolbar.
* - :download:`shiftPlotAction.py <../../../examples/shiftPlotAction.py>`
- .. image:: img/shiftPlotAction.png
- :height: 150px
- :align: center
+ :width: 150px
- This script is a simple (trivial) example of how to create a :class:`~silx.gui.plot.PlotWindow`,
create a custom :class:`~silx.gui.plot.actions.PlotAction` and add it to the toolbar.
@@ -176,8 +183,7 @@ Sample code that adds buttons to the toolbar of a silx plot widget.
* - :download:`fftPlotAction.py <../../../examples/fftPlotAction.py>`,
:download:`fft.png <../../../examples/fft.png>`
- .. image:: img/fftPlotAction.png
- :height: 150px
- :align: center
+ :width: 150px
- This script is a simple example of how to create a :class:`~silx.gui.plot.PlotWindow`
with a custom :class:`~silx.gui.plot.actions.PlotAction` added to the toolbar.
@@ -195,7 +201,7 @@ Sample code that adds buttons to the toolbar of a silx plot widget.
Add features to :class:`~silx.gui.plot.PlotWidget`
..................................................
-Sample code that adds specific tools or functions to plot widgets.
+Sample code that adds specific tools or functions to :class:`~silx.gui.plot.PlotWidget`.
.. list-table::
:widths: 1 1 4
@@ -206,8 +212,7 @@ Sample code that adds specific tools or functions to plot widgets.
- Description
* - :download:`plotWidget.py <../../../examples/plotWidget.py>`
- .. image:: img/plotWidget.png
- :height: 150px
- :align: center
+ :width: 150px
- This script shows how to create a custom window around a PlotWidget.
It subclasses :class:`QMainWindow`, uses a :class:`~silx.gui.plot.PlotWidget`
@@ -218,10 +223,73 @@ Sample code that adds specific tools or functions to plot widgets.
- QAction from :mod:`silx.gui.plot.actions`
- QToolButton from :mod:`silx.gui.plot.PlotToolButtons`
- :class:`silx.gui.plot.ColorBar.ColorBarWidget`
+ * - :download:`plotItemsSelector.py <../../../examples/plotItemsSelector.py>`
+ - .. image:: img/plotItemsSelector.png
+ :width: 150px
+ - This example illustrates how to use a :class:`ItemsSelectionDialog` widget
+ associated with a :class:`~silx.gui.plot.PlotWidget`
+ * - :download:`plotInteractiveImageROI.py <../../../examples/plotInteractiveImageROI.py>`
+ - .. image:: img/plotInteractiveImageROI.png
+ :width: 150px
+ - This script illustrates image ROI selection in a :class:`~silx.gui.plot.PlotWidget`
+
+ It uses :class:`~silx.gui.plot.tools.roi.RegionOfInterestManager` and
+ :class:`~silx.gui.plot.tools.roi.RegionOfInterestTableWidget` to handle the
+ interactive selection and to display the list of selected ROIs.
+ * - :download:`printPreview.py <../../../examples/printPreview.py>`
+ - .. image:: img/printPreview.png
+ :width: 150px
+ - This script illustrates how to add a print preview tool button to any plot
+ widget inheriting :class:`~silx.gui.plot.PlotWidget`.
+
+ Three plot widgets are instantiated. One of them uses a standalone
+ :class:`~silx.gui.plot.PrintPreviewToolButton.PrintPreviewToolButton`,
+ while the other two use a
+ :class:`~silx.gui.plot.PrintPreviewToolButton.SingletonPrintPreviewToolButton`
+ which allows them to send their content to the same print preview page.
+ * - :download:`scatterMask.py <../../../examples/scatterMask.py>`
+ - .. image:: img/scatterMask.png
+ :width: 150px
+ - This example demonstrates how to use ScatterMaskToolsWidget
+ and NamedScatterAlphaSlider with a PlotWidget.
+ * - :download:`plotCurveLegendWidget.py <../../../examples/plotCurveLegendWidget.py>`
+ - .. image:: img/plotCurveLegendWidget.png
+ :width: 150px
+ - This example illustrates the use of :class:`CurveLegendsWidget`.
+
+ :class:`CurveLegendsWidget` display curves style and legend currently visible
+ in a :class:`~silx.gui.plot.PlotWidget`
+ * - :download:`plotStats.py <../../../examples/plotStats.py>`
+ - .. image:: img/plotStats.png
+ :width: 150px
+ - This script is a simple example of how to add your own statistic to a
+ :class:`~silx.gui.plot.statsWidget.StatsWidget` from customs
+ :class:`~silx.gui.plot.stats.Stats` and display it.
+
+ On this example we will:
+
+ - show sum of values for each type
+ - compute curve integrals (only for 'curve').
+ - compute center of mass for all possible items
+
+ .. note:: for now the possible types manged by the Stats are ('curve', 'image',
+ 'scatter' and 'histogram')
+
+:class:`~silx.gui.plot.PlotWidget` features
+...........................................
+
+Sample code that illustrates some functionalities of :class:`~silx.gui.plot.PlotWidget`.
+
+.. list-table::
+ :widths: 1 1 4
+ :header-rows: 1
+
+ * - Source
+ - Screenshot
+ - Description
* - :download:`plotContextMenu.py <../../../examples/plotContextMenu.py>`
- .. image:: img/plotContextMenu.png
- :height: 150px
- :align: center
+ :width: 150px
- This script illustrates the addition of a context menu to a
:class:`~silx.gui.plot.PlotWidget`.
@@ -235,22 +303,14 @@ Sample code that adds specific tools or functions to plot widgets.
inherit from :class:`~silx.gui.plot.PlotWidget`.
For more information on context menus, see Qt documentation.
- * - :download:`plotItemsSelector.py <../../../examples/plotItemsSelector.py>`
- - .. image:: img/plotItemsSelector.png
- :height: 150px
- :align: center
- - This example illustrates how to use a :class:`ItemsSelectionDialog` widget
- associated with a :class:`~silx.gui.plot.PlotWidget`
* - :download:`plotLimits.py <../../../examples/plotLimits.py>`
- .. image:: img/plotLimits.png
- :height: 150px
- :align: center
+ :width: 150px
- This script is an example to illustrate how to use axis synchronization
tool.
* - :download:`plotUpdateCurveFromThread.py <../../../examples/plotUpdateCurveFromThread.py>`
- .. image:: img/plotUpdateCurveFromThread.png
- :height: 150px
- :align: center
+ :width: 150px
- This script illustrates the update of a :mod:`silx.gui.plot` widget from a thread.
The problem is that plot and GUI methods should be called from the main thread.
@@ -263,8 +323,7 @@ Sample code that adds specific tools or functions to plot widgets.
of a plot.
* - :download:`plotUpdateImageFromThread.py <../../../examples/plotUpdateImageFromThread.py>`
- .. image:: img/plotUpdateImageFromThread.png
- :height: 150px
- :align: center
+ :width: 150px
- This script illustrates the update of a :mod:`silx.gui.plot` widget from a thread.
The problem is that plot and GUI methods should be called from the main thread.
@@ -275,39 +334,29 @@ Sample code that adds specific tools or functions to plot widgets.
In this example a thread calls submitToQtMainThread to update the curve
of a plot.
- * - :download:`plotInteractiveImageROI.py <../../../examples/plotInteractiveImageROI.py>`
- - .. image:: img/plotInteractiveImageROI.png
- :height: 150px
- :align: center
- - This script illustrates image ROI selection in a :class:`~silx.gui.plot.PlotWidget`
-
- It uses :class:`~silx.gui.plot.tools.roi.RegionOfInterestManager` and
- :class:`~silx.gui.plot.tools.roi.RegionOfInterestTableWidget` to handle the
- interactive selection and to display the list of selected ROIs.
- * - :download:`printPreview.py <../../../examples/printPreview.py>`
- - .. image:: img/printPreview.png
- :height: 150px
- :align: center
- - This script illustrates how to add a print preview tool button to any plot
- widget inheriting :class:`~silx.gui.plot.PlotWidget`.
-
- Three plot widgets are instantiated. One of them uses a standalone
- :class:`~silx.gui.plot.PrintPreviewToolButton.PrintPreviewToolButton`,
- while the other two use a
- :class:`~silx.gui.plot.PrintPreviewToolButton.SingletonPrintPreviewToolButton`
- which allows them to send their content to the same print preview page.
- * - :download:`scatterMask.py <../../../examples/scatterMask.py>`
- - .. image:: img/scatterMask.png
- :height: 150px
- :align: center
- - This example demonstrates how to use ScatterMaskToolsWidget
- and NamedScatterAlphaSlider with a PlotWidget.
* - :download:`syncaxis.py <../../../examples/syncaxis.py>`
- .. image:: img/syncaxis.png
- :height: 150px
- :align: center
+ :width: 150px
- This script is an example to illustrate how to use axis synchronization
tool.
+ * - :download:`compositeline.py <../../../examples/compositeline.py>`
+ - .. image:: img/compositeline.png
+ :width: 150px
+ - Example to show the use of markers to draw head and tail of lines.
+ * - :download:`dropZones.py <../../../examples/dropZones.py>`
+ - .. image:: img/dropZones.png
+ :width: 150px
+ - Example of drop zone supporting application/x-silx-uri
+ * - :download:`exampleBaseline.py <../../../examples/exampleBaseline.py>`
+ - .. image:: img/exampleBaseline.png
+ :width: 150px
+ - This example illustrates some usage possible with the baseline parameter
+ * - :download:`syncPlotLocation.py <../../../examples/syncPlotLocation.py>`
+ - .. image:: img/syncPlotLocation.png
+ :width: 150px
+ - This script is an example to illustrate how to use axis synchronization
+ tool.
+
.. _plot3d-sample-code:
@@ -321,10 +370,38 @@ Sample code that adds specific tools or functions to plot widgets.
* - Source
- Screenshot
- Description
+ * - :download:`plot3dSceneWindow.py <../../../examples/plot3dSceneWindow.py>`
+ - .. image:: img/plot3dSceneWindow.png
+ :width: 150px
+ - This script displays the different items of :class:`~silx.gui.plot3d.SceneWindow`.
+
+ It shows the different visualizations of :class:`~silx.gui.plot3d.SceneWindow`
+ and :class:`~silx.gui.plot3d.SceneWidget`.
+ It illustrates the API to set those items.
+
+ It features:
+
+ - 2D images: data and RGBA images
+ - 2D scatter data, displayed either as markers, wireframe or surface.
+ - 3D scatter plot
+ - 3D scalar field with iso-surface and cutting plane.
+ - A clipping plane.
+ * - :download:`plot3dUpdateScatterFromThread.py <../../../examples/plot3dUpdateScatterFromThread.py>`
+ - .. image:: img/plot3dUpdateScatterFromThread.png
+ :width: 150px
+ - This script illustrates the update of a
+ :class:`~silx.gui.plot3d.SceneWindow.SceneWindow` widget from a thread.
+
+ The problem is that GUI methods should be called from the main thread.
+ To safely update the scene from another thread, one need to execute the update
+ asynchronously in the main thread.
+ In this example, this is achieved with
+ :func:`~silx.gui.utils.concurrent.submitToQtMainThread`.
+
+ In this example a thread calls submitToQtMainThread to append data to a 3D scatter.
* - :download:`plot3dContextMenu.py <../../../examples/plot3dContextMenu.py>`
- .. image:: img/plot3dContextMenu.png
- :height: 150px
- :align: center
+ :width: 150px
- This script adds a context menu to a :class:`silx.gui.plot3d.ScalarFieldView`.
This is done by adding a custom context menu to the :class:`Plot3DWidget`:
@@ -335,30 +412,13 @@ Sample code that adds specific tools or functions to plot widgets.
For more information on context menus, see Qt documentation.
* - :download:`viewer3DVolume.py <../../../examples/viewer3DVolume.py>`
- .. image:: img/viewer3DVolume.png
- :height: 150px
- :align: center
+ :width: 150px
- This script illustrates the use of :class:`silx.gui.plot3d.ScalarFieldView`.
It loads a 3D scalar data set from a file and displays iso-surfaces and
an interactive cutting plane.
It can also be started without providing a file.
- * - :download:`plot3dSceneWindow.py <../../../examples/plot3dSceneWindow.py>`
- - .. image:: img/plot3dSceneWindow.png
- :height: 150px
- :align: center
- - This script displays the different items of :class:`~silx.gui.plot3d.SceneWindow`.
-
- It shows the different visualizations of :class:`~silx.gui.plot3d.SceneWindow`
- and :class:`~silx.gui.plot3d.SceneWidget`.
- It illustrates the API to set those items.
- It features:
-
- - 2D images: data and RGBA images
- - 2D scatter data, displayed either as markers, wireframe or surface.
- - 3D scatter plot
- - 3D scalar field with iso-surface and cutting plane.
- - A clipping plane.
:mod:`silx.io` sample code
++++++++++++++++++++++++++
@@ -374,3 +434,22 @@ Sample code that adds specific tools or functions to plot widgets.
-
- This script is an example of how to use the :mod:`silx.io.convert` module.
See the following tutorial for more information: :doc:`../Tutorials/convert`
+
+
+:mod:`silx.image` sample code
++++++++++++++++++++++++++++++
+
+.. list-table::
+ :widths: 1 1 4
+ :header-rows: 1
+
+ * - Source
+ - Screenshot
+ - Description
+ * - :download:`findContours.py <../../../examples/findContours.py>`
+ - .. image:: img/findContours.png
+ :width: 150px
+ - Find contours examples
+
+ .. note:: This module has an optional dependency with sci-kit image library.
+ You might need to install it if you don't already have it.
diff --git a/examples/compareImages.py b/examples/compareImages.py
index 94f68a0..623216a 100644
--- a/examples/compareImages.py
+++ b/examples/compareImages.py
@@ -30,19 +30,18 @@ import sys
import logging
import numpy
import argparse
+import os
import silx.io
from silx.gui import qt
import silx.test.utils
+from silx.io.url import DataUrl
from silx.gui.plot.CompareImages import CompareImages
+from silx.gui.widgets.UrlSelectionTable import UrlSelectionTable
_logger = logging.getLogger(__name__)
-try:
- import fabio
-except ImportError:
- _logger.debug("Backtrace", exc_info=True)
- fabio = None
+import fabio
try:
import PIL
@@ -51,6 +50,70 @@ except ImportError:
PIL = None
+class CompareImagesSelection(qt.QMainWindow):
+ def __init__(self, backend):
+ qt.QMainWindow.__init__(self, parent=None)
+ self._plot = CompareImages(parent=self, backend=backend)
+
+ self._selectionTable = UrlSelectionTable(parent=self)
+ self._dockWidgetMenu = qt.QDockWidget(parent=self)
+ self._dockWidgetMenu.layout().setContentsMargins(0, 0, 0, 0)
+ self._dockWidgetMenu.setFeatures(qt.QDockWidget.DockWidgetMovable)
+ self._dockWidgetMenu.setWidget(self._selectionTable)
+ self.addDockWidget(qt.Qt.LeftDockWidgetArea, self._dockWidgetMenu)
+
+ self.setCentralWidget(self._plot)
+
+ self._selectionTable.sigImageAChanged.connect(self._updateImageA)
+ self._selectionTable.sigImageBChanged.connect(self._updateImageB)
+
+ def setUrls(self, urls):
+ for url in urls:
+ self._selectionTable.addUrl(url)
+
+ def setFiles(self, files):
+ urls = list()
+ for _file in files:
+ if os.path.isfile(_file):
+ urls.append(DataUrl(file_path=_file, scheme=None))
+ urls.sort(key=lambda url: url.path())
+ window.setUrls(urls)
+ window._selectionTable.setSelection(url_img_a=urls[0].path(),
+ url_img_b=urls[1].path())
+
+ def clear(self):
+ self._plot.clear()
+ self._selectionTable.clear()
+
+ def _updateImageA(self, urlpath):
+ self._updateImage(urlpath, self._plot.setImage1)
+
+ def _updateImage(self, urlpath, fctptr):
+ def getData():
+ _url = silx.io.url.DataUrl(path=urlpath)
+ for scheme in ('silx', 'fabio'):
+ try:
+ dataImg = silx.io.utils.get_data(
+ silx.io.url.DataUrl(file_path=_url.file_path(),
+ data_slice=_url.data_slice(),
+ data_path=_url.data_path(),
+ scheme=scheme))
+ except:
+ _logger.debug("Error while loading image with %s" % scheme,
+ exc_info=True)
+ else:
+ # TODO: check is an image
+ return dataImg
+ return None
+
+ data = getData()
+ if data is not None:
+ fctptr(data)
+
+ def _updateImageB(self, urlpath):
+ self._updateImage(urlpath, self._plot.setImage2)
+
+
def createTestData():
data = numpy.arange(100 * 100)
data = (data % 100) / 5.0
@@ -68,14 +131,10 @@ def loadImage(filename):
except Exception:
_logger.debug("Error while loading image with silx.io", exc_info=True)
- if fabio is None and PIL is None:
- raise ImportError("fabio nor PIL are available")
-
- if fabio is not None:
- try:
- return fabio.open(filename).data
- except Exception:
- _logger.debug("Error while loading image with fabio", exc_info=True)
+ try:
+ return fabio.open(filename).data
+ except Exception:
+ _logger.debug("Error while loading image with fabio", exc_info=True)
if PIL is not None:
try:
@@ -128,22 +187,25 @@ if __name__ == "__main__":
if options.debug:
logging.root.setLevel(logging.DEBUG)
- if options.testdata:
- _logger.info("Generate test data")
- data1, data2 = createTestData()
- else:
- if len(options.files) != 2:
- raise Exception("Expected 2 images to compare them")
- data1 = loadImage(options.files[0])
- data2 = loadImage(options.files[1])
-
if options.use_opengl_plot:
backend = "gl"
else:
backend = "mpl"
app = qt.QApplication([])
- window = CompareImages(backend=backend)
- window.setData(data1, data2)
+ if options.testdata or len(options.files) == 2:
+ if options.testdata:
+ _logger.info("Generate test data")
+ data1, data2 = createTestData()
+ else:
+ data1 = loadImage(options.files[0])
+ data2 = loadImage(options.files[1])
+ window = CompareImages(backend=backend)
+ window.setData(data1, data2)
+ else:
+ data = options.files
+ window = CompareImagesSelection(backend=backend)
+ window.setFiles(options.files)
+
window.setVisible(True)
app.exec_()
diff --git a/examples/compositeline.py b/examples/compositeline.py
new file mode 100644
index 0000000..892ecf3
--- /dev/null
+++ b/examples/compositeline.py
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""
+Example to show the use of markers to draw head and tail of lines.
+"""
+from __future__ import division
+
+__license__ = "MIT"
+
+import logging
+from silx.gui.plot import Plot1D
+from silx.gui import qt
+import numpy
+
+
+logging.basicConfig()
+logger = logging.getLogger(__name__)
+
+
+def main(argv=None):
+ """Display few lines with markers.
+ """
+ global app # QApplication must be global to avoid seg fault on quit
+ app = qt.QApplication([])
+ sys.excepthook = qt.exceptionHandler
+
+ mainWindow = Plot1D(backend="gl")
+ mainWindow.setAttribute(qt.Qt.WA_DeleteOnClose)
+ plot = mainWindow
+ plot.setDataMargins(0.1, 0.1, 0.1, 0.1)
+
+ plot.addCurve(x=[-10,0,0,-10,-10], y=[90,90,10,10,90], legend="box1", color="gray")
+ plot.addCurve(x=[110,100,100,110,110], y=[90,90,10,10,90], legend="box2", color="gray")
+ plot.addCurve(y=[-10,0,0,-10,-10], x=[90,90,10,10,90], legend="box3", color="gray")
+ plot.addCurve(y=[110,100,100,110,110], x=[90,90,10,10,90], legend="box4", color="gray")
+
+ def addLine(source, destination, symbolSource, symbolDestination, legend, color):
+ line = numpy.array([source, destination]).T
+ plot.addCurve(x=line[0,:], y=line[1,:], color=color, legend=legend)
+ plot.addMarker(x=source[0], y=source[1], symbol=symbolSource, color=color)
+ plot.addMarker(x=destination[0], y=destination[1], symbol=symbolDestination, color=color)
+
+ addLine([0, 50], [100, 50], "caretleft", "caretright", "l1", "red")
+ addLine([0, 30], [100, 30], "tickup", "tickdown", "l2", "blue")
+ addLine([0, 70], [100, 70], "|", "|", "l3", "black")
+
+ addLine([50, 0], [50, 100], "caretdown", "caretup", "l4", "red")
+ addLine([30, 0], [30, 100], "tickleft", "tickright", "l5", "blue")
+ addLine([70, 0], [70, 100], "_", "_", "l6", "black")
+
+ mainWindow.setVisible(True)
+ return app.exec_()
+
+
+if __name__ == "__main__":
+ import sys
+ sys.exit(main(argv=sys.argv[1:]))
diff --git a/examples/customDataView.py b/examples/customDataView.py
index 6db5c3e..33662e8 100644
--- a/examples/customDataView.py
+++ b/examples/customDataView.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,6 +26,7 @@
"""Qt data view example
"""
+import enum
import logging
import sys
@@ -36,7 +37,6 @@ _logger = logging.getLogger("customDataView")
from silx.gui import qt
from silx.gui.data.DataViewerFrame import DataViewerFrame
from silx.gui.data.DataViews import DataView
-from silx.third_party import enum
class Color(enum.Enum):
diff --git a/examples/dropZones.py b/examples/dropZones.py
new file mode 100644
index 0000000..27d9df8
--- /dev/null
+++ b/examples/dropZones.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""
+Example of drop zone supporting application/x-silx-uri
+"""
+
+from __future__ import absolute_import
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "25/01/2019"
+
+import logging
+import silx.io
+from silx.gui import qt
+from silx.gui.plot.PlotWidget import PlotWidget
+
+_logger = logging.getLogger(__name__)
+logging.basicConfig()
+
+
+class DropPlotWidget(PlotWidget):
+
+ def __init__(self, parent=None, backend=None):
+ PlotWidget.__init__(self, parent=parent, backend=backend)
+ self.setAcceptDrops(True)
+
+ def dragEnterEvent(self, event):
+ if event.mimeData().hasFormat("application/x-silx-uri"):
+ event.acceptProposedAction()
+
+ def dropEvent(self, event):
+ byteString = event.mimeData().data("application/x-silx-uri")
+ silxUrl = byteString.data().decode("utf-8")
+ with silx.io.open(silxUrl) as h5:
+ if silx.io.is_dataset(h5):
+ dataset = h5[...]
+ else:
+ _logger.error("Unsupported URI")
+ dataset = None
+
+ if dataset is not None:
+ if dataset.ndim == 1:
+ self.clear()
+ self.addCurve(y=dataset, x=range(dataset.size))
+ event.acceptProposedAction()
+ elif dataset.ndim == 2:
+ self.clear()
+ self.addImage(data=dataset)
+ event.acceptProposedAction()
+ else:
+ _logger.error("Unsupported dataset")
+
+
+class DropLabel(qt.QLabel):
+
+ def __init__(self, parent=None, backend=None):
+ qt.QLabel.__init__(self)
+ self.setAcceptDrops(True)
+ self.setText("Drop something here")
+
+ def dragEnterEvent(self, event):
+ if event.mimeData().hasFormat("application/x-silx-uri"):
+ event.acceptProposedAction()
+
+ def dropEvent(self, event):
+ byteString = event.mimeData().data("application/x-silx-uri")
+ silxUrl = byteString.data().decode("utf-8")
+ url = silx.io.url.DataUrl(silxUrl)
+ self.setText(url.path())
+
+ toolTipTemplate = ("<html><ul>"
+ "<li><b>file_path</b>: {file_path}</li>"
+ "<li><b>data_path</b>: {data_path}</li>"
+ "<li><b>data_slice</b>: {data_slice}</li>"
+ "<li><b>scheme</b>: {scheme}</li>"
+ "</html>"
+ "</ul></html>"
+ )
+
+ toolTip = toolTipTemplate.format(
+ file_path=url.file_path(),
+ data_path=url.data_path(),
+ data_slice=url.data_slice(),
+ scheme=url.scheme())
+
+ self.setToolTip(toolTip)
+ event.acceptProposedAction()
+
+
+class DropExample(qt.QMainWindow):
+
+ def __init__(self, parent=None):
+ super(DropExample, self).__init__(parent)
+ centralWidget = qt.QWidget(self)
+ layout = qt.QVBoxLayout()
+ centralWidget.setLayout(layout)
+ layout.addWidget(DropPlotWidget(parent=self))
+ layout.addWidget(DropLabel(parent=self))
+ self.setCentralWidget(centralWidget)
+
+
+def main():
+ app = qt.QApplication([])
+ example = DropExample()
+ example.show()
+ app.exec_()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/exampleBaseline.py b/examples/exampleBaseline.py
new file mode 100644
index 0000000..edd0fc3
--- /dev/null
+++ b/examples/exampleBaseline.py
@@ -0,0 +1,164 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This example illustrates some usage possible with the baseline parameter
+"""
+
+__authors__ = ["H. Payno"]
+__license__ = "MIT"
+__date__ = "12/09/2019"
+
+
+from silx.gui import qt
+from silx.gui.plot import Plot1D
+import numpy
+import sys
+import argparse
+
+
+def stacked_histogran(plot, edges, histograms, colors, legend):
+ # check that we have the same number of histogram, color and baseline
+ current_baseline = numpy.zeros_like(edges)
+
+ for histogram, color, layer_index in zip(histograms, colors, range(len(colors))):
+ stacked_histo = histogram + current_baseline
+ plot.addHistogram(histogram=stacked_histo,
+ edges=edges,
+ legend='_'.join((legend, str(layer_index))),
+ color=color,
+ baseline=current_baseline,
+ z=len(histograms)-layer_index,
+ fill=True)
+ current_baseline = stacked_histo
+
+
+def get_plot_std(backend):
+ x = numpy.arange(0, 10, step=0.1)
+ my_sin = numpy.sin(x)
+ y = numpy.arange(-4, 6, step=0.1) + my_sin
+ mean = numpy.arange(-5, 5, step=0.1) + my_sin
+ baseline = numpy.arange(-6, 4, step=0.1) + my_sin
+ edges = x[y >= 3.0]
+ histo = mean[y >= 3.0] - 1.8
+
+ plot = Plot1D(backend=backend)
+ plot.addCurve(x=x, y=y, baseline=baseline, color='grey',
+ legend='std-curve', fill=True)
+ plot.addCurve(x=x, y=mean, color='red', legend='mean')
+ plot.addHistogram(histogram=histo, edges=edges, color='red',
+ legend='mean2', fill=True)
+ return plot
+
+
+def get_plot_stacked_histogram(backend):
+ plot = Plot1D(backend=backend)
+ # first histogram
+ edges = numpy.arange(-6, 6, step=0.5)
+ histo_1 = numpy.random.random(len(edges))
+ histo_2 = numpy.random.random(len(edges))
+ histo_3 = numpy.random.random(len(edges))
+ histo_4 = numpy.random.random(len(edges))
+ stacked_histogran(plot=plot,
+ edges=edges,
+ histograms=(histo_1, histo_2, histo_3, histo_4),
+ colors=('blue', 'green', 'red', 'yellow'),
+ legend='first_stacked_histo')
+
+ # second histogram
+ edges = numpy.arange(10, 25, step=1.0)
+ histo_1 = -numpy.random.random(len(edges))
+ histo_2 = -numpy.random.random(len(edges))
+ stacked_histogran(plot=plot, histograms=(histo_1, histo_2),
+ edges=edges,
+ colors=('gray', 'black'),
+ legend='second_stacked_histo')
+
+ # last histogram
+ edges = [30, 40]
+ histograms = [
+ [0.2, 0.3],
+ [0.0, 1.0],
+ [0.1, 0.4],
+ [0.2, 0.0],
+ [0.6, 0.4],
+ ]
+ stacked_histogran(plot=plot,
+ histograms=histograms,
+ edges=edges,
+ colors=('blue', 'green', 'red', 'yellow', 'cyan'),
+ legend='third_stacked_histo')
+
+ return plot
+
+
+def get_plot_mean_baseline(backend):
+ plot = Plot1D(backend=backend)
+ x = numpy.arange(0, 10, step=0.1)
+ y = numpy.sin(x)
+ plot.addCurve(x=x, y=y, baseline=0, fill=True)
+ plot.setYAxisLogarithmic(True)
+ return plot
+
+
+def get_plot_log(backend):
+ plot = Plot1D(backend=backend)
+ x = numpy.arange(0, 10, step=0.01)
+ y = numpy.exp2(x)
+ baseline = numpy.exp(x)
+ plot.addCurve(x=x, y=y, baseline=baseline, fill=True)
+ plot.setYAxisLogarithmic(True)
+ return plot
+
+
+def main(argv):
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--backend',
+ dest="backend",
+ action="store",
+ default=None,
+ help='Set plot backend. Should be "matplotlib" (default) or "opengl"')
+
+ options = parser.parse_args(argv[1:])
+ assert options.backend in (None, 'matplotlib', 'opengl')
+ qapp = qt.QApplication([])
+
+ plot_std = get_plot_std(backend=options.backend)
+ plot_std.show()
+
+ plot_mean = get_plot_mean_baseline(backend=options.backend)
+ plot_mean.show()
+
+ plot_stacked_histo = get_plot_stacked_histogram(backend=options.backend)
+ plot_stacked_histo.show()
+
+ plot_log = get_plot_log(backend=options.backend)
+ plot_log.show()
+
+ qapp.exec_()
+
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/examples/fileDialog.py b/examples/fileDialog.py
index 9730b9a..40191bb 100644
--- a/examples/fileDialog.py
+++ b/examples/fileDialog.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,15 +33,15 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "14/02/2018"
+import enum
import logging
from silx.gui import qt
from silx.gui.dialog.ImageFileDialog import ImageFileDialog
from silx.gui.dialog.DataFileDialog import DataFileDialog
import silx.io
-from silx.third_party import enum
-logging.basicConfig(level=logging.DEBUG)
+logging.basicConfig()
class Mode(enum.Enum):
diff --git a/examples/findContours.py b/examples/findContours.py
index a5bb663..a7b5ac4 100644
--- a/examples/findContours.py
+++ b/examples/findContours.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -25,7 +25,7 @@
# ###########################################################################*/
"""Find contours examples
-.. note:: This module has an optional dependancy with sci-kit image library.
+.. note:: This module has an optional dependency with sci-kit image library.
You might need to install it if you don't already have it.
"""
diff --git a/examples/hdf5widget.py b/examples/hdf5widget.py
index bf92d4e..217eb7f 100755
--- a/examples/hdf5widget.py
+++ b/examples/hdf5widget.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -23,17 +23,14 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""Qt Hdf5 widget examples
-
-.. note:: This module has a dependency on the `h5py <http://www.h5py.org/>`_
- library, which is not a mandatory dependency for `silx`. You might need
- to install it if you don't already have it.
-"""
+"""Qt Hdf5 widget examples"""
import logging
import sys
import tempfile
+
import numpy
+import six
logging.basicConfig()
_logger = logging.getLogger("hdf5widget")
@@ -50,15 +47,12 @@ import h5py
import silx.gui.hdf5
import silx.utils.html
-from silx.third_party import six
from silx.gui import qt
from silx.gui.data.DataViewerFrame import DataViewerFrame
from silx.gui.widgets.ThreadPoolPushButton import ThreadPoolPushButton
-try:
- import fabio
-except ImportError:
- fabio = None
+import fabio
+
_file_cache = {}
@@ -713,26 +707,25 @@ class Hdf5TreeViewExample(qt.QMainWindow):
content.layout().addStretch(1)
- if fabio is not None:
- content = qt.QGroupBox("Create EDF", panel)
- content.setLayout(qt.QVBoxLayout())
- panel.layout().addWidget(content)
+ content = qt.QGroupBox("Create EDF", panel)
+ content.setLayout(qt.QVBoxLayout())
+ panel.layout().addWidget(content)
- combo = qt.QComboBox()
- combo.addItem("Containing all types", get_edf_with_all_types)
- combo.addItem("Containing 100000 datasets", get_edf_with_100000_frames)
- combo.activated.connect(self.__edfComboChanged)
- content.layout().addWidget(combo)
+ combo = qt.QComboBox()
+ combo.addItem("Containing all types", get_edf_with_all_types)
+ combo.addItem("Containing 100000 datasets", get_edf_with_100000_frames)
+ combo.activated.connect(self.__edfComboChanged)
+ content.layout().addWidget(combo)
- button = ThreadPoolPushButton(content, text="Create")
- button.setCallable(combo.itemData(combo.currentIndex()))
- button.succeeded.connect(self.__fileCreated)
- content.layout().addWidget(button)
+ button = ThreadPoolPushButton(content, text="Create")
+ button.setCallable(combo.itemData(combo.currentIndex()))
+ button.succeeded.connect(self.__fileCreated)
+ content.layout().addWidget(button)
- self.__edfCombo = combo
- self.__createEdfButton = button
+ self.__edfCombo = combo
+ self.__createEdfButton = button
- content.layout().addStretch(1)
+ content.layout().addStretch(1)
option = qt.QGroupBox("Tree options", panel)
option.setLayout(qt.QVBoxLayout())
diff --git a/examples/imageview.py b/examples/imageview.py
index 37d1857..5c7eddb 100755
--- a/examples/imageview.py
+++ b/examples/imageview.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -32,17 +32,12 @@ To view an image file with the current installed silx library:
``python examples/imageview.py <file to open>``
To get help:
``python examples/imageview.py -h``
-
-For developers with a git clone you can use it with the bootstrap
-To view an image file with the current installed silx library:
-
-``./bootstrap.py python examples/imageview.py <file to open>``
"""
from __future__ import division
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "18/10/2016"
+__date__ = "08/11/2018"
import logging
from silx.gui.plot.ImageView import ImageViewMainWindow
@@ -118,8 +113,7 @@ def main(argv=None):
if args.log: # Use log normalization by default
colormap = mainWindow.getDefaultColormap()
- colormap['normalization'] = 'log'
- mainWindow.setColormap(colormap)
+ colormap.setNormalization(colormap.LOGARITHM)
mainWindow.setImage(data,
origin=args.origin,
diff --git a/examples/plot3dSceneWindow.py b/examples/plot3dSceneWindow.py
index cf6f209..1b2f808 100644
--- a/examples/plot3dSceneWindow.py
+++ b/examples/plot3dSceneWindow.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -51,13 +51,11 @@ import numpy
from silx.gui import qt
from silx.gui.plot3d.SceneWindow import SceneWindow, items
-from silx.gui.plot3d.tools.PositionInfoWidget import PositionInfoWidget
-from silx.gui.widgets.BoxLayoutDockWidget import BoxLayoutDockWidget
SIZE = 1024
# Create QApplication
-qapp = qt.QApplication([])
+qapp = qt.QApplication.instance() or qt.QApplication([])
# Create a SceneWindow widget
window = SceneWindow()
@@ -69,20 +67,13 @@ sceneWidget.setForegroundColor((1., 1., 1., 1.))
sceneWidget.setTextColor((0.1, 0.1, 0.1, 1.))
-# Add PositionInfoWidget to display picking info
-positionInfo = PositionInfoWidget()
-positionInfo.setSceneWidget(sceneWidget)
-dock = BoxLayoutDockWidget()
-dock.setWindowTitle("Selection Info")
-dock.setWidget(positionInfo)
-window.addDockWidget(qt.Qt.BottomDockWidgetArea, dock)
-
# 2D Image ###
# Add a dummy RGBA image
img = numpy.random.random(3 * SIZE ** 2).reshape(SIZE, SIZE, 3) # Dummy image
imageRgba = sceneWidget.addImage(img) # Add ImageRgba item to the scene
+imageRgba.setLabel('Random RGBA image') # Set name displayed in parameter tree
# Set imageRgba transform
imageRgba.setTranslation(SIZE*.15, SIZE*.15, 0.) # Translate the image
diff --git a/examples/plot3dUpdateScatterFromThread.py b/examples/plot3dUpdateScatterFromThread.py
new file mode 100644
index 0000000..9c2213f
--- /dev/null
+++ b/examples/plot3dUpdateScatterFromThread.py
@@ -0,0 +1,176 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This script illustrates the update of a
+:class:`~silx.gui.plot3d.SceneWindow.SceneWindow` widget from a thread.
+
+The problem is that GUI methods should be called from the main thread.
+To safely update the scene from another thread, one need to execute the update
+asynchronously in the main thread.
+In this example, this is achieved with
+:func:`~silx.gui.utils.concurrent.submitToQtMainThread`.
+
+In this example a thread calls submitToQtMainThread to append data to a 3D scatter.
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "08/03/2019"
+
+
+import threading
+import time
+
+import numpy
+
+from silx.gui import qt
+from silx.gui.utils import concurrent
+from silx.gui.plot3d.SceneWindow import SceneWindow
+from silx.gui.plot3d import items
+
+
+MAX_NUMBER_OF_POINTS = 10**6
+
+
+class UpdateScatterThread(threading.Thread):
+ """Thread updating the scatter 3D item data
+
+ :param ~silx.gui.plot3d.items.Scatter3D scatter3d: 3D scatter to update.
+ """
+
+ def __init__(self, scatter3d):
+ self.scatter3d = scatter3d
+ self.running = False
+ self.future_result = None
+ super(UpdateScatterThread, self).__init__()
+
+ def start(self):
+ """Start the update thread"""
+ self.running = True
+ super(UpdateScatterThread, self).start()
+
+ def _appendScatterData(self, x, y, z, value):
+ """Add some data points to the Scatter3D item.
+
+ This method MUST be called in the Qt main thread.
+
+ :param numpy.ndarray x:
+ :param numpy.ndarray y:
+ :param numpy.ndarray z:
+ :param numpy.ndarray value:
+ """
+ # use copy=False to avoid useless copy of numpy arrays
+ curX, curY, curZ, curValue = self.scatter3d.getData(copy=False)
+
+ x = numpy.append(curX, x)
+ y = numpy.append(curY, y)
+ z = numpy.append(curZ, z)
+ value = numpy.append(curValue, value)
+
+ # Update data
+ self.scatter3d.setData(x, y, z, value, copy=False)
+
+ def run(self):
+ """Method implementing thread loop that updates the scatter data
+
+ It produces adds scatter points every 10 ms or so, up to 1 million.
+ """
+ count = 0 # Number of data points currently rendered
+
+ # Init arrays that accumulate scatter points
+ x = numpy.array((), dtype=numpy.float32)
+ y = numpy.array((), dtype=numpy.float32)
+ z = numpy.array((), dtype=numpy.float32)
+ value = numpy.array((), dtype=numpy.float32)
+
+ while self.running:
+ time.sleep(0.01)
+
+ # Generate new data points
+ inclination = numpy.random.random(1000).astype(numpy.float32) * numpy.pi
+ azimuth = numpy.random.random(1000).astype(numpy.float32) * 2. * numpy.pi
+ radius = numpy.random.normal(loc=10., scale=.5, size=1000)
+ newX = radius * numpy.sin(inclination) * numpy.cos(azimuth)
+ newY = radius * numpy.sin(inclination) * numpy.sin(azimuth)
+ newZ = radius * numpy.cos(inclination)
+ newValue = numpy.random.random(1000).astype(numpy.float32)
+
+ # Accumulate data points
+ x = numpy.append(x, newX)
+ y = numpy.append(y, newY)
+ z = numpy.append(z, newZ)
+ value = numpy.append(value, newValue)
+
+ # Only append data if the previous one has been added
+ if self.future_result is None or self.future_result.done():
+ if count > MAX_NUMBER_OF_POINTS:
+ # Restart a new scatter plot asyn
+ self.future_result = concurrent.submitToQtMainThread(
+ self.scatter3d.setData, x, y, z, value)
+
+ count = len(x)
+ else:
+ # Append data asynchronously
+ self.future_result = concurrent.submitToQtMainThread(
+ self._appendScatterData, x, y, z, value)
+
+ count += len(x)
+
+ # Reset accumulators
+ x = numpy.array((), dtype=numpy.float32)
+ y = numpy.array((), dtype=numpy.float32)
+ z = numpy.array((), dtype=numpy.float32)
+ value = numpy.array((), dtype=numpy.float32)
+
+ def stop(self):
+ """Stop the update thread"""
+ self.running = False
+ self.join(2)
+
+
+def main():
+ global app
+ app = qt.QApplication([])
+
+ # Create a SceneWindow
+ window = SceneWindow()
+ window.show()
+
+ sceneWidget = window.getSceneWidget()
+ scatter = items.Scatter3D()
+ scatter.setSymbol(',')
+ scatter.getColormap().setName('magma')
+ sceneWidget.addItem(scatter)
+
+ # Create the thread that calls submitToQtMainThread
+ updateThread = UpdateScatterThread(scatter)
+ updateThread.start() # Start updating the plot
+
+ app.exec_()
+
+ updateThread.stop() # Stop updating the plot
+
+
+if __name__ == '__main__':
+ main()
diff --git a/examples/plotInteractiveImageROI.py b/examples/plotInteractiveImageROI.py
index d45bdf5..8a4019f 100644
--- a/examples/plotInteractiveImageROI.py
+++ b/examples/plotInteractiveImageROI.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -39,14 +39,14 @@ from silx.gui.plot import Plot2D
from silx.gui.plot.tools.roi import RegionOfInterestManager
from silx.gui.plot.tools.roi import RegionOfInterestTableWidget
from silx.gui.plot.items.roi import RectangleROI
+from silx.gui.plot.items import LineMixIn, SymbolMixIn
def dummy_image():
"""Create a dummy image"""
x = numpy.linspace(-1.5, 1.5, 1024)
xv, yv = numpy.meshgrid(x, x)
- signal = numpy.exp(- (xv ** 2 / 0.15 ** 2
- + yv ** 2 / 0.25 ** 2))
+ signal = numpy.exp(- (xv ** 2 / 0.15 ** 2 + yv ** 2 / 0.25 ** 2))
# add noise
signal += 0.3 * numpy.random.random(size=signal.shape)
return signal
@@ -54,8 +54,12 @@ def dummy_image():
app = qt.QApplication([]) # Start QApplication
+backend = "matplotlib"
+if "--opengl" in sys.argv:
+ backend = "opengl"
+
# Create the plot widget and add an image
-plot = Plot2D()
+plot = Plot2D(backend=backend)
plot.getDefaultColormap().setName('viridis')
plot.addImage(dummy_image())
@@ -67,8 +71,13 @@ roiManager.setColor('pink') # Set the color of ROI
# Set the name of each created region of interest
def updateAddedRegionOfInterest(roi):
"""Called for each added region of interest: set the name"""
- if roi.getLabel() == '':
- roi.setLabel('ROI %d' % len(roiManager.getRois()))
+ if roi.getName() == '':
+ roi.setName('ROI %d' % len(roiManager.getRois()))
+ if isinstance(roi, LineMixIn):
+ roi.setLineWidth(2)
+ roi.setLineStyle('--')
+ if isinstance(roi, SymbolMixIn):
+ roi.setSymbolSize(5)
roiManager.sigRoiAdded.connect(updateAddedRegionOfInterest)
@@ -76,7 +85,7 @@ roiManager.sigRoiAdded.connect(updateAddedRegionOfInterest)
# Add a rectangular region of interest
roi = RectangleROI()
roi.setGeometry(origin=(50, 50), size=(200, 200))
-roi.setLabel('Initial ROI')
+roi.setName('Initial ROI')
roiManager.addRoi(roi)
# Create the table widget displaying
@@ -99,6 +108,7 @@ widget.setLayout(layout)
layout.addWidget(roiToolbar)
layout.addWidget(roiTable)
+
def roiDockVisibilityChanged(visible):
"""Handle change of visibility of the roi dock widget
@@ -107,6 +117,7 @@ def roiDockVisibilityChanged(visible):
if not visible:
roiManager.stop()
+
dock = qt.QDockWidget('Image ROI')
dock.setWidget(widget)
dock.visibilityChanged.connect(roiDockVisibilityChanged)
diff --git a/examples/plotLimits.py b/examples/plotLimits.py
index 0a39bc6..c7cc7f5 100644
--- a/examples/plotLimits.py
+++ b/examples/plotLimits.py
@@ -23,8 +23,8 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""This script is an example to illustrate how to use axis synchronization
-tool.
+"""This script is an example to illustrate how to set range constraints on
+plot axes.
"""
from silx.gui import qt
@@ -37,7 +37,7 @@ class ConstrainedViewPlot(qt.QMainWindow):
def __init__(self):
qt.QMainWindow.__init__(self)
- self.setWindowTitle("Plot with synchronized axes")
+ self.setWindowTitle("Plot with constrained axes")
widget = qt.QWidget(self)
self.setCentralWidget(widget)
diff --git a/examples/plotStats.py b/examples/plotStats.py
index fff7585..5f6e768 100644
--- a/examples/plotStats.py
+++ b/examples/plotStats.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -43,9 +43,49 @@ __date__ = "24/07/2018"
from silx.gui import qt
+from silx.gui.colors import Colormap
from silx.gui.plot import Plot1D
from silx.gui.plot.stats.stats import StatBase
+from silx.gui.utils import concurrent
+import random
+import threading
+import argparse
import numpy
+import time
+
+
+class UpdateThread(threading.Thread):
+ """Thread updating the curve of a :class:`~silx.gui.plot.Plot1D`
+
+ :param plot1d: The Plot1D to update."""
+
+ def __init__(self, plot1d):
+ self.plot1d = plot1d
+ self.running = False
+ super(UpdateThread, self).__init__()
+
+ def start(self):
+ """Start the update thread"""
+ self.running = True
+ super(UpdateThread, self).start()
+
+ def run(self):
+ """Method implementing thread loop that updates the plot"""
+ while self.running:
+ time.sleep(1)
+ # Run plot update asynchronously
+ concurrent.submitToQtMainThread(
+ self.plot1d.addCurve,
+ numpy.arange(1000),
+ numpy.random.random(1000),
+ resetzoom=False,
+ legend=random.choice(('mycurve0', 'mycurve1'))
+ )
+
+ def stop(self):
+ """Stop the update thread"""
+ self.running = False
+ self.join(2)
class Integral(StatBase):
@@ -87,21 +127,27 @@ class COM(StatBase):
return comX, comY
-def main():
+def main(argv):
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ '--update-mode',
+ default='auto',
+ help='update mode to display (manual or auto)')
+
+ options = parser.parse_args(argv[1:])
+
app = qt.QApplication([])
plot = Plot1D()
- x = numpy.arange(21)
- y = numpy.arange(21)
- plot.addCurve(x=x, y=y, legend='myCurve')
- plot.addCurve(x=x, y=(y + 5), legend='myCurve2')
-
- plot.setActiveCurve('myCurve')
+ # Create the thread that calls submitToQtMainThread
+ updateThread = UpdateThread(plot)
+ updateThread.start() # Start updating the plot
plot.addScatter(x=[0, 2, 5, 5, 12, 20],
y=[2, 3, 4, 20, 15, 6],
value=[5, 6, 7, 10, 90, 20],
+ colormap=Colormap('viridis'),
legend='myScatter')
stats = [
@@ -111,11 +157,15 @@ def main():
]
plot.getStatsWidget().setStats(stats)
+ plot.getStatsWidget().setUpdateMode(options.update_mode)
+ plot.getStatsWidget().setDisplayOnlyActiveItem(False)
plot.getStatsWidget().parent().setVisible(True)
plot.show()
app.exec_()
+ updateThread.stop() # Stop updating the plot
if __name__ == '__main__':
- main()
+ import sys
+ main(sys.argv)
diff --git a/examples/plotWidget.py b/examples/plotWidget.py
index c8a90a5..af64afb 100644
--- a/examples/plotWidget.py
+++ b/examples/plotWidget.py
@@ -36,7 +36,7 @@ as its central widget and adds toolbars and a colorbar by using pluggable widget
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "05/09/2017"
+__date__ = "06/05/2019"
import numpy
@@ -68,15 +68,29 @@ class MyPlotWindow(qt.QMainWindow):
palette.setColor(qt.QPalette.Window, qt.Qt.white)
colorBar.setPalette(palette)
+ options = qt.QWidget(self)
+ layout = qt.QVBoxLayout(options)
+ button = qt.QPushButton("Show image", self)
+ button.clicked.connect(self.showImage)
+ layout.addWidget(button)
+ button = qt.QPushButton("Show scatter", self)
+ button.clicked.connect(self.showScatter)
+ layout.addWidget(button)
+ button = qt.QPushButton("Show delaunay", self)
+ button.clicked.connect(self.showDelaunay)
+ layout.addWidget(button)
+ layout.addStretch()
+
# Combine the ColorBarWidget and the PlotWidget as
# this QMainWindow's central widget
gridLayout = qt.QGridLayout()
gridLayout.setSpacing(0)
gridLayout.setContentsMargins(0, 0, 0, 0)
- gridLayout.addWidget(self._plot, 0, 0)
- gridLayout.addWidget(colorBar, 0, 1)
+ gridLayout.addWidget(options, 0, 0)
+ gridLayout.addWidget(self._plot, 0, 1)
+ gridLayout.addWidget(colorBar, 0, 2)
gridLayout.setRowStretch(0, 1)
- gridLayout.setColumnStretch(0, 1)
+ gridLayout.setColumnStretch(1, 1)
centralWidget = qt.QWidget(self)
centralWidget.setLayout(gridLayout)
self.setCentralWidget(centralWidget)
@@ -116,6 +130,44 @@ class MyPlotWindow(qt.QMainWindow):
"""Returns the PlotWidget contains in this window"""
return self._plot
+ def showImage(self):
+ plot = self.getPlotWidget()
+ plot.clear()
+ plot.getDefaultColormap().setName('viridis')
+
+ # Add an image to the plot
+ x = numpy.outer(
+ numpy.linspace(-10, 10, 200), numpy.linspace(-10, 5, 150))
+ image = numpy.sin(x) / x
+ plot.addImage(image)
+ plot.resetZoom()
+
+ def showScatter(self):
+ plot = self.getPlotWidget()
+ plot.clear()
+ plot.getDefaultColormap().setName('viridis')
+
+ nbPoints = 50
+ x = numpy.random.rand(nbPoints)
+ y = numpy.random.rand(nbPoints)
+ value = numpy.random.rand(nbPoints)
+ plot.addScatter(x=x, y=y, value=value)
+ plot.resetZoom()
+
+ def showDelaunay(self):
+ plot = self.getPlotWidget()
+ plot.clear()
+ plot.getDefaultColormap().setName('viridis')
+
+ nbPoints = 50
+ x = numpy.random.rand(nbPoints)
+ y = numpy.random.rand(nbPoints)
+ value = numpy.random.rand(nbPoints)
+ legend = plot.addScatter(x=x, y=y, value=value)
+ scatter = plot.getScatter(legend)
+ scatter.setVisualization("solid")
+ plot.resetZoom()
+
def main():
global app
@@ -125,17 +177,7 @@ def main():
window = MyPlotWindow()
window.setAttribute(qt.Qt.WA_DeleteOnClose)
window.show()
-
- # Change the default colormap
- plot = window.getPlotWidget()
- plot.getDefaultColormap().setName('viridis')
-
- # Add an image to the plot
- x = numpy.outer(
- numpy.linspace(-10, 10, 200), numpy.linspace(-10, 5, 150))
- image = numpy.sin(x) / x
- plot.addImage(image)
-
+ window.showImage()
app.exec_()
diff --git a/examples/printPreview.py b/examples/printPreview.py
index 7567adb..6de8209 100755
--- a/examples/printPreview.py
+++ b/examples/printPreview.py
@@ -42,22 +42,38 @@ from silx.gui import qt
from silx.gui.plot import PlotWidget
from silx.gui.plot import PrintPreviewToolButton
+
+class MyPrintPreviewButton(PrintPreviewToolButton.PrintPreviewToolButton):
+ """This class illustrates how to subclass PrintPreviewToolButton
+ to add a title and a comment."""
+ def getTitle(self):
+ return "Widget 1's plot"
+
+ def getCommentAndPosition(self):
+ legends = self.getPlot().getAllCurves(just_legend=True)
+ comment = "Curves displayed in widget 1:\n\t"
+ if legends:
+ comment += ", ".join(legends)
+ else:
+ comment += "none"
+ return comment, "CENTER"
+
+
app = qt.QApplication([])
x = numpy.arange(1000)
-# first widget has a standalone print preview action
+# first widget has a standalone preview action with custom title and comment
pw1 = PlotWidget()
pw1.setWindowTitle("Widget 1 with standalone print preview")
toolbar1 = qt.QToolBar(pw1)
-toolbutton1 = PrintPreviewToolButton.PrintPreviewToolButton(parent=toolbar1,
- plot=pw1)
+toolbutton1 = MyPrintPreviewButton(parent=toolbar1, plot=pw1)
pw1.addToolBar(toolbar1)
toolbar1.addWidget(toolbutton1)
pw1.show()
pw1.addCurve(x, numpy.tan(x * 2 * numpy.pi / 1000))
-# next two plots share a common print preview
+# next two plots share a common standard print preview
pw2 = PlotWidget()
pw2.setWindowTitle("Widget 2 with shared print preview")
toolbar2 = qt.QToolBar(pw2)
diff --git a/examples/scatterview.py b/examples/scatterview.py
new file mode 100755
index 0000000..cab32c0
--- /dev/null
+++ b/examples/scatterview.py
@@ -0,0 +1,99 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""
+Example to show the use of :class:`~silx.gui.plot.ScatterView.ScatterView` widget.
+"""
+from __future__ import division
+
+__license__ = "MIT"
+
+import logging
+from silx.gui.plot.ScatterView import ScatterView
+from silx.gui import qt
+import numpy
+import scipy.signal
+
+
+logging.basicConfig()
+logger = logging.getLogger(__name__)
+
+
+def createData():
+ nbPoints = 200
+ nbX = int(numpy.sqrt(nbPoints))
+ nbY = nbPoints // nbX + 1
+
+ # Motor position
+ yy = numpy.atleast_2d(numpy.ones(nbY)).T
+ xx = numpy.atleast_2d(numpy.ones(nbX))
+
+ positionX = numpy.linspace(10, 50, nbX) * yy
+ positionX = positionX.reshape(nbX * nbY)
+ positionX = positionX + numpy.random.rand(len(positionX)) - 0.5
+
+ positionY = numpy.atleast_2d(numpy.linspace(20, 60, nbY)).T * xx
+ positionY = positionY.reshape(nbX * nbY)
+ positionY = positionY + numpy.random.rand(len(positionY)) - 0.5
+
+ # Diodes position
+ lut = scipy.signal.gaussian(max(nbX, nbY), std=8) * 10
+ yy, xx = numpy.ogrid[:nbY, :nbX]
+ signal = lut[yy] * lut[xx]
+ diode1 = numpy.random.poisson(signal * 10)
+ diode1 = diode1.reshape(nbX * nbY)
+ return positionX, positionY, diode1
+
+
+def main(argv=None):
+ """Display an image from a file in an :class:`ImageView` widget.
+
+ :param argv: list of command line arguments or None (the default)
+ to use sys.argv.
+ :type argv: list of str
+ :return: Exit status code
+ :rtype: int
+ :raises IOError: if no image can be loaded from the file
+ """
+ import argparse
+ import os.path
+
+ global app # QApplication must be global to avoid seg fault on quit
+ app = qt.QApplication([])
+ sys.excepthook = qt.exceptionHandler
+
+ mainWindow = ScatterView()
+ mainWindow.setAttribute(qt.Qt.WA_DeleteOnClose)
+ xx, yy, value = createData()
+ mainWindow.setData(x=xx, y=yy, value=value)
+ mainWindow.show()
+ mainWindow.setFocus(qt.Qt.OtherFocusReason)
+
+ return app.exec_()
+
+
+if __name__ == "__main__":
+ import sys
+ sys.exit(main(argv=sys.argv[1:]))
diff --git a/examples/simplewidget.py b/examples/simplewidget.py
index e952dc6..88977b7 100755
--- a/examples/simplewidget.py
+++ b/examples/simplewidget.py
@@ -44,6 +44,7 @@ from silx.gui.colors import Colormap
from silx.gui.widgets.WaitingPushButton import WaitingPushButton
from silx.gui.widgets.ThreadPoolPushButton import ThreadPoolPushButton
from silx.gui.widgets.RangeSlider import RangeSlider
+from silx.gui.widgets.LegendIconWidget import LegendIconWidget
class SimpleWidgetExample(qt.QMainWindow):
@@ -69,6 +70,10 @@ class SimpleWidgetExample(qt.QMainWindow):
layout.addWidget(self.createRangeSlider())
layout.addWidget(self.createRangeSliderWithBackground())
+ panel = self.createLegendIconPanel(self)
+ layout.addWidget(qt.QLabel("LegendIconWidget"))
+ layout.addWidget(panel)
+
self.setCentralWidget(main_panel)
def createWaitingPushButton(self):
@@ -122,6 +127,65 @@ class SimpleWidgetExample(qt.QMainWindow):
widget.setGroovePixmapFromProfile(background, colormap)
return widget
+ def createLegendIconPanel(self, parent):
+ panel = qt.QWidget(parent)
+ layout = qt.QVBoxLayout(panel)
+
+ # Empty
+ legend = LegendIconWidget(panel)
+ layout.addWidget(legend)
+
+ # Line
+ legend = LegendIconWidget(panel)
+ legend.setLineStyle("-")
+ legend.setLineColor("blue")
+ legend.setLineWidth(2)
+ layout.addWidget(legend)
+
+ # Symbol
+ legend = LegendIconWidget(panel)
+ legend.setSymbol("o")
+ legend.setSymbolColor("red")
+ layout.addWidget(legend)
+
+ # Line and symbol
+ legend = LegendIconWidget(panel)
+ legend.setLineStyle(":")
+ legend.setLineColor("green")
+ legend.setLineWidth(2)
+ legend.setSymbol("x")
+ legend.setSymbolColor("violet")
+ layout.addWidget(legend)
+
+ # Colormap
+ legend = LegendIconWidget(panel)
+ legend.setColormap("viridis")
+ layout.addWidget(legend)
+
+ # Symbol and colormap
+ legend = LegendIconWidget(panel)
+ legend.setSymbol("o")
+ legend.setSymbolColormap("viridis")
+ layout.addWidget(legend)
+
+ # Symbol (without surface) and colormap
+ legend = LegendIconWidget(panel)
+ legend.setSymbol("+")
+ legend.setSymbolColormap("plasma")
+ layout.addWidget(legend)
+
+ # Colormap + Line + Symbol
+ legend = LegendIconWidget(panel)
+ legend.setColormap("gray")
+ legend.setLineStyle("-")
+ legend.setLineColor("white")
+ legend.setLineWidth(3)
+ legend.setSymbol(".")
+ legend.setSymbolColormap("red")
+ layout.addWidget(legend)
+
+ return panel
+
def main():
"""
diff --git a/examples/syncPlotLocation.py b/examples/syncPlotLocation.py
new file mode 100644
index 0000000..55332bc
--- /dev/null
+++ b/examples/syncPlotLocation.py
@@ -0,0 +1,105 @@
+#!/usr/bin/env python
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This script is an example to illustrate how to use axis synchronization
+tool.
+"""
+
+from silx.gui import qt
+from silx.gui.plot import Plot2D
+import numpy
+import silx.test.utils
+from silx.gui.plot.utils.axis import SyncAxes
+from silx.gui.colors import Colormap
+
+
+class SyncPlot(qt.QMainWindow):
+
+ def __init__(self):
+ qt.QMainWindow.__init__(self)
+ self.setWindowTitle("Plot with synchronized axes")
+ widget = qt.QWidget(self)
+ self.setCentralWidget(widget)
+
+ layout = qt.QGridLayout()
+ widget.setLayout(layout)
+
+ backend = "gl"
+ plots = []
+
+ data = numpy.arange(100 * 100)
+ data = (data % 100) / 5.0
+ data = numpy.sin(data)
+ data.shape = 100, 100
+
+ colormaps = ["gray", "red", "green", "blue"]
+ for i in range(2 * 2):
+ plot = Plot2D(parent=widget, backend=backend)
+ plot.setInteractiveMode('pan')
+ plot.setDefaultColormap(Colormap(colormaps[i]))
+ noisyData = silx.test.utils.add_gaussian_noise(data, mean=i / 10.0)
+ plot.addImage(noisyData)
+ plots.append(plot)
+
+ xAxis = [p.getXAxis() for p in plots]
+ yAxis = [p.getYAxis() for p in plots]
+
+ self.constraint1 = SyncAxes(xAxis,
+ syncLimits=False,
+ syncScale=True,
+ syncDirection=True,
+ syncCenter=True,
+ syncZoom=True)
+ self.constraint2 = SyncAxes(yAxis,
+ syncLimits=False,
+ syncScale=True,
+ syncDirection=True,
+ syncCenter=True,
+ syncZoom=True)
+
+ for i, plot in enumerate(plots):
+ if i % 2 == 0:
+ plot.setFixedWidth(400)
+ else:
+ plot.setFixedWidth(500)
+ if i // 2 == 0:
+ plot.setFixedHeight(400)
+ else:
+ plot.setFixedHeight(500)
+ layout.addWidget(plot, i // 2, i % 2)
+
+ def createCenteredLabel(self, text):
+ label = qt.QLabel(self)
+ label.setAlignment(qt.Qt.AlignCenter)
+ label.setText(text)
+ return label
+
+
+if __name__ == "__main__":
+ app = qt.QApplication([])
+ window = SyncPlot()
+ window.setAttribute(qt.Qt.WA_DeleteOnClose, True)
+ window.setVisible(True)
+ app.exec_()
diff --git a/examples/viewer3DVolume.py b/examples/viewer3DVolume.py
index d030fba..4de04f6 100644
--- a/examples/viewer3DVolume.py
+++ b/examples/viewer3DVolume.py
@@ -52,13 +52,7 @@ logging.basicConfig()
_logger = logging.getLogger(__name__)
-
-try:
- import h5py
-except ImportError:
- _logger.warning('h5py is not installed: HDF5 not supported')
- h5py = None
-
+import h5py
def load(filename):
"""Load 3D scalar field from file.
@@ -72,7 +66,7 @@ def load(filename):
if not os.path.isfile(filename.split('::')[0]):
raise IOError('No input file: %s' % filename)
- if h5py is not None and h5py.is_hdf5(filename.split('::')[0]):
+ if h5py.is_hdf5(filename.split('::')[0]):
if '::' not in filename:
raise ValueError(
'HDF5 path not provided: Use <filename>::<path> format')
diff --git a/package/debian10/changelog b/package/debian10/changelog
new file mode 100644
index 0000000..3b5427f
--- /dev/null
+++ b/package/debian10/changelog
@@ -0,0 +1,141 @@
+silx (0.11.0+dfsg-1) unstable; urgency=medium
+
+ [ Alexandre Marie ]
+ * Added test on openCL's use
+ * New upstream version 0.11.0+dfsg
+ * d/patches
+ - 0004-fix-missing-import.patches (Removed)
+ - 0005-fix-problem-with-sift-import (Removed)
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Tue, 09 Jul 2019 15:26:55 +0200
+
+silx (0.10.1+dfsg-1~exp2) experimental; urgency=medium
+
+ * d/patchs
+ + 0005-fix-problem-with-sift-import.patch (Added)
+
+ -- Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> Tue, 28 May 2019 11:17:46 +0200
+
+silx (0.10.1+dfsg-1~exp1) experimental; urgency=medium
+
+ * New upstream version 0.10.1+dfsg
+ * d/patches
+ + 0004-fix-missing-import.patch (Added)
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Tue, 28 May 2019 08:20:44 +0200
+
+silx (0.9.0+dfsg-3) unstable; urgency=medium
+
+ * d/rules: Do not run Qt test for now.
+ * d/t/control.autodep8: Fixed to run test for real.
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Thu, 21 Feb 2019 11:22:03 +0100
+
+silx (0.9.0+dfsg-2) unstable; urgency=medium
+
+ * d/patches:
+ + 0004-fix-FTBFS-with-numpy-0.16.patch (Added)
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Thu, 20 Dec 2018 16:21:18 +0100
+
+silx (0.9.0+dfsg-1) unstable; urgency=medium
+
+ [ Picca Frédéric-Emmanuel ]
+ * Fixed autopkgtests and use control.autodep8
+ * Used salsa-ci for continuous integration.
+ * Run autopkgtests via xvfb-run
+ * d/control: Removed Build-Depends: python-lxml[-dbg], python-enum34.
+
+ [ Alexandre Marie ]
+ * New upstream version 0.9.0+dfsg
+ * d/watch: uversionmangling to deal with rc|alpha|beta versions.
+
+ -- Alexandre Marie <alexandre.marie@synchrotron-soleil.fr> Mon, 17 Dec 2018 13:25:52 +0100
+
+silx (0.8.0+dfsg-1) unstable; urgency=medium
+
+ * New upstream version 0.8.0+dfsg
+ * Added the pub 4096R/26F8E116 key to keyring
+ 2016-04-11 Thomas Vincent <thomas.vincent@esrf.fr>
+ * d/control
+ - Build-Depends
+ + Added pandoc
+ - Removed obsolete X-Python[3]-Version
+ * d/rules
+ - Installed the QtDesgigner files only for Qt5.
+ - Override dh_python3 to deal with qtdesigner files.
+ - Run sphinx with xvfb in order to have the right silx.sx documentation.
+ - Avoid QT warnings by setting XDG_RUNTIME_DIR
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Tue, 31 Jul 2018 16:24:57 +0200
+
+silx (0.7.0+dfsg-2) unstable; urgency=medium
+
+ * d/rules
+ - use py3versions to get the python3 default interpreter version.
+ This makes the package backportable.
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Tue, 13 Mar 2018 20:04:20 +0100
+
+silx (0.7.0+dfsg-1) unstable; urgency=medium
+
+ * New upstream version 0.7.0+dfsg
+ * Bumped Strandards-Versions to 4.1.3 (nothing to do)
+ * d/control
+ - Build-Depends
+ + Added python[3]-nbsphinx, python-concurrent.futures
+ * d/copyright
+ remove the third_party _local files.
+ * d/patches
+ + 0003-do-not-modify-PYTHONPATH-from-setup.py.patch (added)
+ - 0005-slocale.h-is-removed-in-GLIBC-2.26.patch (obsolete)
+ - 0006-prefer-pyqt5-over-pyside.patch (obsolete)
+ * d/rules
+ - removed the jessie backports specific code
+ - compile extensions only once per interpreter.
+ - unactive for now the build time tests.
+ - build the doc only with python3.
+ * d/watch
+ - check the pgp signature
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Tue, 13 Mar 2018 07:32:00 +0100
+
+silx (0.6.1+dfsg-2) unstable; urgency=medium
+
+ * d/control
+ - Bump Standrad-Version 4.1.1 (nothing to do)
+ * fixed glibc 2.26 FTBFS with upstream patch glib2.26 (Closes: #882881)
+ * d/patches
+ + 0005-slocale.h-is-removed-in-GLIBC-2.26.patch (Added)
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Sun, 21 Jan 2018 09:32:38 +0100
+
+silx (0.6.1+dfsg-1) unstable; urgency=medium
+
+ * New upstream version 0.6.1+dfsg
+ * update watch file
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Sat, 25 Nov 2017 17:02:19 +0100
+
+silx (0.6.0+dfsg-1) unstable; urgency=medium
+
+ * New upstream version 0.6.0+dfsg
+ * d/patches
+ - 0001-fix-the-build_man-target.patch (deleted)
+ - 0004-test-unactive-ressource-for-now.patch (deleted)
+ - 0005-fix-the-sift-removal.patch (deleted)
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Sat, 07 Oct 2017 08:08:56 +0200
+
+silx (0.5.0+dfsg-2) unstable; urgency=medium
+
+ * d/control
+ - Added all the -dbg dependencies for the -dbg packages.
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Fri, 01 Sep 2017 15:10:44 +0200
+
+silx (0.5.0+dfsg-1) unstable; urgency=medium
+
+ * Initial release (Closes: #871637)
+
+ -- Picca Frédéric-Emmanuel <picca@debian.org> Wed, 02 Aug 2017 11:00:20 +0100
diff --git a/debian/compat b/package/debian10/compat
index f599e28..f599e28 100644
--- a/debian/compat
+++ b/package/debian10/compat
diff --git a/package/debian10/control b/package/debian10/control
new file mode 100644
index 0000000..f16ccea
--- /dev/null
+++ b/package/debian10/control
@@ -0,0 +1,172 @@
+Source: silx
+Maintainer: Debian Science Maintainers <debian-science-maintainers@lists.alioth.debian.org>
+Uploaders: Jerome Kieffer <jerome.kieffer@esrf.fr>,
+ Picca Frédéric-Emmanuel <picca@debian.org>,
+ Alexandre Marie <alexandre.marie@synchrotron-soleil.fr>
+Section: science
+Priority: optional
+Build-Depends: cython (>= 0.23.2),
+ cython3 (>= 0.23.2),
+ debhelper (>= 10),
+ dh-python,
+ help2man,
+ ipython,
+ ipython-qtconsole,
+ ipython3,
+ ipython3-qtconsole,
+ pandoc <!nodoc>,
+ python-all-dev,
+ python-concurrent.futures,
+ python-fabio,
+ python-h5py,
+ python-mako,
+ python-matplotlib,
+ python-nbsphinx <!nodoc>,
+ python-numpy,
+ python-opengl,
+ python-pil,
+ python-pyopencl,
+ python-pyqt5,
+ python-pyqt5.qtopengl,
+ python-pyqt5.qtsvg,
+ python-scipy,
+ python-setuptools,
+ python-sphinx,
+ python-sphinxcontrib.programoutput,
+ python3-all-dev,
+ python3-dateutil,
+ python3-qtconsole,
+ python3-six,
+ python3-fabio,
+ python3-h5py,
+ python3-mako,
+ python3-matplotlib,
+ python3-nbsphinx <!nodoc>,
+ python3-numpy,
+ python3-opengl,
+ python3-pil,
+ python3-pyopencl,
+ python3-pyqt5,
+ python3-pyqt5.qtopengl,
+ python3-pyqt5.qtsvg,
+ python3-scipy,
+ python3-setuptools,
+ python3-sphinx,
+ python3-sphinxcontrib.programoutput,
+ xauth,
+ xvfb,
+ openstack-pkg-tools,
+ locales,
+ devscripts
+Standards-Version: 4.1.3
+Vcs-Browser: https://salsa.debian.org/science-team/silx
+Vcs-Git: https://salsa.debian.org/science-team/silx.git
+Homepage: https://github.com/silx-kit/silx
+
+Package: silx
+Architecture: all
+Depends: python3-silx (>= ${source:Version}),
+ ${misc:Depends},
+ ${python3:Depends},
+ ${shlibs:Depends}
+Description: Toolbox for X-Ray data analysis - Executables
+ The silx project aims at providing a collection of Python packages to
+ support the development of data assessment, reduction and analysis
+ applications at synchrotron radiation facilities. It aims at
+ providing reading/writing different file formats, data reduction
+ routines and a set of Qt widgets to browse and visualize data.
+ .
+ The current version provides :
+ .
+ * reading HDF5 file format (with support of SPEC file format)
+ * histogramming
+ * fitting
+ * 1D and 2D visualization using multiple backends (matplotlib or OpenGL)
+ * image plot widget with a set of associated tools (See changelog file).
+ * Unified browser for HDF5, SPEC and image file formats supporting inspection
+ and visualization of n-dimensional datasets.
+ * Unified viewer (silx view filename) for HDF5, SPEC and image file formats
+ * OpenGL-based widget to display 3D scalar field with
+ isosurface and cutting plane.
+ .
+ This uses the Python 3 version of the package.
+
+Package: python-silx
+Architecture: any
+Section: python
+Depends: ${misc:Depends}, ${python:Depends}, ${shlibs:Depends}
+Description: Toolbox for X-Ray data analysis - Python2 library
+ The silx project aims at providing a collection of Python packages to
+ support the development of data assessment, reduction and analysis
+ applications at synchrotron radiation facilities. It aims at
+ providing reading/writing different file formats, data reduction
+ routines and a set of Qt widgets to browse and visualize data.
+ .
+ The current version provides :
+ .
+ * reading HDF5 file format (with support of SPEC file format)
+ * histogramming
+ * fitting
+ * 1D and 2D visualization using multiple backends (matplotlib or OpenGL)
+ * image plot widget with a set of associated tools (See changelog file).
+ * Unified browser for HDF5, SPEC and image file formats supporting inspection
+ and visualization of n-dimensional datasets.
+ * Unified viewer (silx view filename) for HDF5, SPEC and image file formats
+ * OpenGL-based widget to display 3D scalar field with
+ isosurface and cutting plane.
+ .
+ This is the Python 2 version of the package.
+
+
+Package: python3-silx
+Architecture: any
+Section: python
+Depends: ${misc:Depends}, ${python3:Depends}, ${shlibs:Depends}
+Description: Toolbox for X-Ray data analysis - Python3
+ The silx project aims at providing a collection of Python packages to
+ support the development of data assessment, reduction and analysis
+ applications at synchrotron radiation facilities. It aims at
+ providing reading/writing different file formats, data reduction
+ routines and a set of Qt widgets to browse and visualize data.
+ .
+ The current version provides :
+ .
+ * reading HDF5 file format (with support of SPEC file format)
+ * histogramming
+ * fitting
+ * 1D and 2D visualization using multiple backends (matplotlib or OpenGL)
+ * image plot widget with a set of associated tools (See changelog file).
+ * Unified browser for HDF5, SPEC and image file formats supporting inspection
+ and visualization of n-dimensional datasets.
+ * Unified viewer (silx view filename) for HDF5, SPEC and image file formats
+ * OpenGL-based widget to display 3D scalar field with
+ isosurface and cutting plane.
+ .
+ This is the Python 3 version of the package.
+
+
+Package: python-silx-doc
+Architecture: all
+Section: doc
+Depends: libjs-mathjax, ${misc:Depends}, ${sphinxdoc:Depends}
+Description: Toolbox for X-Ray data analysis - Documentation
+ The silx project aims at providing a collection of Python packages to
+ support the development of data assessment, reduction and analysis
+ applications at synchrotron radiation facilities. It aims at
+ providing reading/writing different file formats, data reduction
+ routines and a set of Qt widgets to browse and visualize data.
+ .
+ The current version provides :
+ .
+ * reading HDF5 file format (with support of SPEC file format)
+ * histogramming
+ * fitting
+ * 1D and 2D visualization using multiple backends (matplotlib or OpenGL)
+ * image plot widget with a set of associated tools (See changelog file).
+ * Unified browser for HDF5, SPEC and image file formats supporting inspection
+ and visualization of n-dimensional datasets.
+ * Unified viewer (silx view filename) for HDF5, SPEC and image file formats
+ * OpenGL-based widget to display 3D scalar field with
+ isosurface and cutting plane.
+ .
+ This is the common documentation package.
diff --git a/package/debian8/gbp.conf b/package/debian10/gbp.conf
index f68d262..f68d262 100644
--- a/package/debian8/gbp.conf
+++ b/package/debian10/gbp.conf
diff --git a/package/debian10/patches/0002-use-the-system-mathjax-privacy-breach.patch b/package/debian10/patches/0002-use-the-system-mathjax-privacy-breach.patch
new file mode 100644
index 0000000..04deea7
--- /dev/null
+++ b/package/debian10/patches/0002-use-the-system-mathjax-privacy-breach.patch
@@ -0,0 +1,25 @@
+From: =?utf-8?q?Picca_Fr=C3=A9d=C3=A9ric-Emmanuel?=
+ <picca@synchrotron-soleil.fr>
+Date: Thu, 10 Aug 2017 10:19:39 +0200
+Subject: use the system mathjax (privacy breach)
+
+---
+ doc/source/conf.py | 5 +++++
+ 1 file changed, 5 insertions(+)
+
+diff --git a/doc/source/conf.py b/doc/source/conf.py
+index 86dbccf..18bfce2 100644
+--- a/doc/source/conf.py
++++ b/doc/source/conf.py
+@@ -143,6 +143,11 @@ pygments_style = 'sphinx'
+ # A list of ignored prefixes for module index sorting.
+ # modindex_common_prefix = []
+
++# -- Option for MathJax extension ----------------------------------------------
++
++# Override required in order to use Debian's system mathjax
++mathjax_path = 'file:///usr/share/javascript/mathjax/MathJax.js?config=TeX-AMS-MML_HTMLorMML'
++
+
+ # -- Options for HTML output ---------------------------------------------------
+
diff --git a/package/debian10/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch b/package/debian10/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch
new file mode 100644
index 0000000..e58e985
--- /dev/null
+++ b/package/debian10/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch
@@ -0,0 +1,22 @@
+From: =?utf-8?q?Picca_Fr=C3=A9d=C3=A9ric-Emmanuel?= <picca@debian.org>
+Date: Sun, 4 Mar 2018 16:36:35 +0100
+Subject: do not modify PYTHONPATH from setup.py
+
+---
+ setup.py | 3 ++-
+ 1 file changed, 2 insertions(+), 1 deletion(-)
+
+diff --git a/setup.py b/setup.py
+index b63d0eb..334f7a8 100644
+--- a/setup.py
++++ b/setup.py
+@@ -260,7 +260,8 @@ class BuildMan(Command):
+ path.insert(0, os.path.abspath(build.build_lib))
+
+ env = dict((str(k), str(v)) for k, v in os.environ.items())
+- env["PYTHONPATH"] = os.pathsep.join(path)
++
++ # env["PYTHONPATH"] = os.pathsep.join(path)
+ if not os.path.isdir("build/man"):
+ os.makedirs("build/man")
+ import subprocess
diff --git a/package/debian10/patches/series b/package/debian10/patches/series
new file mode 100644
index 0000000..e3795b3
--- /dev/null
+++ b/package/debian10/patches/series
@@ -0,0 +1,2 @@
+0002-use-the-system-mathjax-privacy-breach.patch
+0003-do-not-modify-PYTHONPATH-from-setup.py.patch
diff --git a/package/debian10/py3dist-overrides b/package/debian10/py3dist-overrides
new file mode 100644
index 0000000..2c4ce13
--- /dev/null
+++ b/package/debian10/py3dist-overrides
@@ -0,0 +1 @@
+pyqt5 python3-pyqt5,python3-pyqt5.qtopengl,python3-pyqt5.qtsvg \ No newline at end of file
diff --git a/package/debian8/python-silx-doc.doc-base b/package/debian10/python-silx-doc.doc-base
index b290d8a..b290d8a 100644
--- a/package/debian8/python-silx-doc.doc-base
+++ b/package/debian10/python-silx-doc.doc-base
diff --git a/package/debian10/rules b/package/debian10/rules
new file mode 100755
index 0000000..b75711c
--- /dev/null
+++ b/package/debian10/rules
@@ -0,0 +1,85 @@
+#!/usr/bin/make -f
+
+# avoir bbuild FTBFS
+export HOME=$(CURDIR)/debian/tmp-home
+export XDG_RUNTIME_DIR=$(HOME)/runtime
+export POCL_CACHE_DIR=$(HOME)/.cache/
+
+export DEB_BUILD_MAINT_OPTIONS = hardening=+all
+export PYBUILD_AFTER_INSTALL=rm -rf {destdir}/usr/bin/
+export PYBUILD_NAME=silx
+export SPECFILE_USE_GNU_SOURCE=1
+export SILX_FULL_INSTALL_REQUIRES=1
+
+DEB_HOST_MULTIARCH ?= $(shell dpkg-architecture -qDEB_HOST_MULTIARCH)
+
+# Make does not offer a recursive wildcard function, so here's one:
+rwildcard=$(wildcard $1$2) $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2))
+
+# How to recursively find all files with the same name in a given folder
+ALL_PYX := $(call rwildcard,silx/,*.pyx)
+#NOTA: No space before *
+
+# get the default python3 interpreter version
+PY3VER := $(shell py3versions -dv)
+
+%:
+ dh $@ --with python2,python3,sphinxdoc --buildsystem=pybuild
+
+override_dh_clean:
+ dh_clean
+ # remove the cython generated file to force rebuild
+ rm -f $(patsubst %.pyx,%.cpp,${ALL_PYX})
+ rm -f $(patsubst %.pyx,%.c,${ALL_PYX})
+ rm -f $(patsubst %.pyx,%.html,${ALL_PYX})
+ rm -rf doc/build/html
+ rm -rf build/man
+ rm -rf *.egg-info
+
+override_dh_auto_build:
+ dh_auto_build
+ # build man pages
+ dh_auto_build -- -s custom --build-args="env PYTHONPATH={build_dir} {interpreter} setup.py build_man"
+
+override_dh_install:
+ dh_numpy
+ dh_numpy3
+
+ # install scripts into silx
+ python3 setup.py install_scripts -d debian/silx/usr/bin
+ dh_install -p silx package/desktop/*.desktop usr/share/applications
+ dh_install -p silx package/desktop/silx.png usr/share/icons/hicolor/48x48/apps
+ dh_install -p silx package/desktop/silx.svg usr/share/icons/hicolor/scalable/apps
+ dh_install -p silx package/desktop/silx.xml usr/share/mime/packages
+
+ # install the qtdesigner files only for the python3 package
+ dh_install -p python3-silx qtdesigner_plugins/*.py /usr/lib/$(DEB_HOST_MULTIARCH)/qt5/plugins/designer/python
+
+ dh_install
+
+override_dh_python3:
+ dh_python3
+ dh_python3 -p python3-silx /usr/lib/$(DEB_HOST_MULTIARCH)/qt5/plugins/designer/python
+
+# WITH_QT_TEST=False to disable graphical tests
+# SILX_OPENCL=False to disable OpenCL tests
+# SILX_TEST_LOW_MEM=True to disable tests taking large amount of memory
+# GPU=False to disable the use of a GPU with OpenCL test
+# WITH_GL_TEST=False to disable tests using OpenGL
+# UNACTIVATED UNTIL dh_python from UNSTABLE IS FIXED
+# https://lists.debian.org/debian-python/2017/08/msg00095.html
+override_dh_auto_test:
+ mkdir -p $(POCL_CACHE_DIR) # create POCL cachedir in order to avoid an FTBFS in sbuild
+ dh_auto_test -- -s custom --test-args="env PYTHONPATH={build_dir} GPU=False WITH_QT_TEST=False SILX_OPENCL=False SILX_TEST_LAW_MEM=True xvfb-run -a --server-args=\"-screen 0 1024x768x24\" {interpreter} run_tests.py -vv --installed"
+
+override_dh_installman:
+ dh_installman -p silx build/man/*.1
+
+override_dh_sphinxdoc:
+ifeq (,$(findstring nodocs, $(DEB_BUILD_OPTIONS)))
+ #mkdir -p $(POCL_CACHE_DIR) # create POCL cachedir in order to avoid an FTBFS in sbuild
+ mkdir -p -m 700 $(XDG_RUNTIME_DIR)
+ pybuild --build -s custom -p $(PY3VER) --build-args="cd doc && env PYTHONPATH={build_dir} http_proxy='127.0.0.1:9' xvfb-run -a --server-args=\"-screen 0 1024x768x24\" {interpreter} -m sphinx -N -bhtml source build/html"
+ dh_installdocs "doc/build/html" -p python-silx-doc
+ dh_sphinxdoc -O--buildsystem=pybuild
+endif
diff --git a/package/debian8/source/format b/package/debian10/source/format
index 163aaf8..163aaf8 100644
--- a/package/debian8/source/format
+++ b/package/debian10/source/format
diff --git a/package/debian8/source/options b/package/debian10/source/options
index 6e88e49..6e88e49 100644
--- a/package/debian8/source/options
+++ b/package/debian10/source/options
diff --git a/debian/tests/control.autodep8 b/package/debian10/tests/control
index 5ffa42b..1e5cddf 100644
--- a/debian/tests/control.autodep8
+++ b/package/debian10/tests/control
@@ -2,7 +2,7 @@ Test-Command: set -efu
; for py in $(pyversions -r 2>/dev/null)
; do cd "$AUTOPKGTEST_TMP"
; echo "Testing with $py:"
- ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py -m unittest discover silx 2>&1
+ ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py -c "import silx.test; silx.test.run_tests()" 2>&1
; done
Depends: python-all, python-silx, xauth, xvfb
@@ -10,7 +10,7 @@ Test-Command: set -efu
; for py in $(pyversions -r 2>/dev/null)
; do cd "$AUTOPKGTEST_TMP"
; echo "Testing with $py-dbg:"
- ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py-dbg -m unittest discover silx 2>&1
+ ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py-dbg -c "import silx.test; silx.test.run_tests()" 2>&1
; done
Depends: python-all-dbg, python-silx-dbg, xauth, xvfb
@@ -18,7 +18,7 @@ Test-Command: set -efu
; for py in $(py3versions -r 2>/dev/null)
; do cd "$AUTOPKGTEST_TMP"
; echo "Testing with $py:"
- ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py -m unittest discover silx 2>&1
+ ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py -c "import silx.test; silx.test.run_tests()" 2>&1
; done
Depends: python3-all, python3-silx, xauth, xvfb
@@ -26,6 +26,6 @@ Test-Command: set -efu
; for py in $(py3versions -r 2>/dev/null)
; do cd "$AUTOPKGTEST_TMP"
; echo "Testing with $py-dbg:"
- ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py-dbg -m unittest discover silx 2>&1
+ ; xvfb-run -a --server-args="-screen 0 1024x768x24" $py-dbg -c "import silx.test; silx.test.run_tests()" 2>&1
; done
Depends: python3-all-dbg, python3-silx-dbg, xauth, xvfb
diff --git a/package/debian10/watch b/package/debian10/watch
new file mode 100644
index 0000000..99444f9
--- /dev/null
+++ b/package/debian10/watch
@@ -0,0 +1,7 @@
+version=4
+opts=repacksuffix=+dfsg,\
+pgpsigurlmangle=s/$/.asc/,\
+dversionmangle=s/\+dfsg//,\
+uversionmangle=s/(rc|a|b|c)/~$1/ \
+https://pypi.python.org/packages/source/s/@PACKAGE@/ \
+ @PACKAGE@-@ANY_VERSION@@ARCHIVE_EXT@ debian uupdate
diff --git a/package/debian8/changelog b/package/debian8/changelog
deleted file mode 100644
index 763d069..0000000
--- a/package/debian8/changelog
+++ /dev/null
@@ -1,22 +0,0 @@
-silx (0.3.0-1) unstable; urgency=low
-
- * debian/control
- - Add missing dependencies
- - Add -dbg packages
- * Upstream version 0.4.0a
-
- -- Thomas Vincent <tvincent@edna-site.org> Mon, 23 Jan 2017 17:09:20 +0100
-
-silx (0.1.0-1) unstable; urgency=low
-
- * debian/control
- - Comment -dbg packages
- * Upstream version 0.1.0
-
- -- Thomas Vincent <thomas.vincent@esrf.fr> Thu, 14 Apr 2016 09:35:28 +0200
-
-silx (0.0.0-1) unstable; urgency=low
-
- * Initial release (Closes: #??????)
-
- -- Jerome Kieffer <jerome.kieffer@esrf.fr> Tue, 15 Mar 2016 11:00:20 +0100
diff --git a/package/debian8/clean b/package/debian8/clean
deleted file mode 100644
index 668a363..0000000
--- a/package/debian8/clean
+++ /dev/null
@@ -1 +0,0 @@
-*.egg-info/* \ No newline at end of file
diff --git a/package/debian8/compat b/package/debian8/compat
deleted file mode 100644
index ec63514..0000000
--- a/package/debian8/compat
+++ /dev/null
@@ -1 +0,0 @@
-9
diff --git a/package/debian8/control b/package/debian8/control
deleted file mode 100644
index d84a339..0000000
--- a/package/debian8/control
+++ /dev/null
@@ -1,205 +0,0 @@
-Source: silx
-Maintainer: Debian Science Maintainers <debian-science-maintainers@lists.alioth.debian.org>
-Uploaders: Jerome Kieffer <jerome.kieffer@esrf.fr>,
- Picca Frédéric-Emmanuel <picca@debian.org>
-Section: science
-Priority: extra
-Build-Depends: cython,
- cython-dbg,
- cython3,
- cython3-dbg,
- libstdc++6-4.9-dbg,
- libstdc++-4.9-dev,
- libstdc++6,
- debhelper (>=9.20150101+deb8u2),
- dh-python,
- python-all-dev,
- python-all-dbg,
- python-numpy,
- python-numpy-dbg,
- python-fabio,
- python-fabio-dbg,
- python-h5py,
- python-h5py-dbg,
- python-pyopencl,
- python-pyopencl-dbg,
- python-mako,
- python-matplotlib,
- python-matplotlib-dbg,
- python-dateutil,
- python-opengl,
- python-pyqt5,
- python-pyqt5-dbg,
- python-pyqt5.qtsvg,
- python-pyqt5.qtsvg-dbg,
- python-pyqt5.qtopengl,
- python-pyqt5.qtopengl-dbg,
- python-scipy,
- python-scipy-dbg,
- python-sphinx,
- python-sphinxcontrib.programoutput,
- python-enum34,
- python-concurrent.futures,
- python3-all-dev,
- python3-all-dbg,
- python3-numpy,
- python3-numpy-dbg,
- python3-fabio,
- python3-fabio-dbg,
- python3-h5py,
- python3-h5py-dbg,
- python3-pyopencl,
- python3-pyopencl-dbg,
- python3-mako,
- python3-matplotlib,
- python3-matplotlib-dbg,
- python3-dateutil,
- python3-opengl,
- python3-pyqt5,
- python3-pyqt5-dbg,
- python3-pyqt5.qtsvg,
- python3-pyqt5.qtsvg-dbg,
- python3-pyqt5.qtopengl,
- python3-pyqt5.qtopengl-dbg,
- python3-scipy,
- python3-scipy-dbg,
- python3-sphinx,
- python3-sphinxcontrib.programoutput,
- help2man,
- devscripts
-Standards-Version: 3.9.6
-Vcs-Browser: https://anonscm.debian.org/cgit/debian-science/packages/silx.git
-Vcs-Git: git://anonscm.debian.org/debian-science/packages/silx.git
-Homepage: https://github.com/silx-kit/silx
-X-Python-Version: >= 2.7
-X-Python3-Version: >= 3.4
-
-Package: silx
-Architecture: all
-Depends: ${misc:Depends},
- ${python:Depends},
- ${shlibs:Depends},
- python3-pkg-resources,
- python3-silx (>= ${source:Version})
-Description: Toolbox for X-Ray data analysis - Executables
- .
- This uses the Python 3 version of the package.
-
-Package: python-silx
-Architecture: any
-Section: python
-Depends: ${misc:Depends},
- ${python:Depends},
- ${shlibs:Depends},
- libstdc++6,
- python-numpy,
- python-fabio,
- python-h5py,
- python-pyopencl,
- python-mako,
- python-matplotlib,
- python-dateutil,
- python-opengl,
- python-pyqt5,
- python-pyqt5.qtsvg,
- python-pyqt5.qtopengl,
- python-scipy,
- python-six,
- python-enum34,
- python-concurrent.futures,
-# Recommends:
-Suggests: python-rfoo
-Description: Toolbox for X-Ray data analysis - Python2 library
- .
- This is the Python 2 version of the package.
-
-Package: python-silx-dbg
-Architecture: any
-Section: debug
-Depends: ${misc:Depends},
- ${python:Depends},
- ${shlibs:Depends},
- python-silx (= ${binary:Version}),
- libstdc++6-4.9-dbg,
- python-dbg,
- python-numpy-dbg,
- python-fabio-dbg,
- python-h5py-dbg,
- python-pyopencl-dbg,
- python-mako,
- python-matplotlib-dbg,
- python-dateutil,
- python-opengl,
- python-pyqt5-dbg,
- python-pyqt5.qtsvg-dbg,
- python-pyqt5.qtopengl-dbg,
- python-scipy-dbg,
- python-six,
- python-enum34,
- python-concurrent.futures,
-Description: Toolbox for X-Ray data analysis - python2 debug
- .
- This package contains the extension built for the Python 2 debug
- interpreter.
-
-Package: python3-silx
-Architecture: any
-Section: python
-Depends: ${misc:Depends},
- ${python3:Depends},
- ${shlibs:Depends},
- libstdc++6,
- python3-numpy,
- python3-fabio,
- python3-h5py,
- python3-pyopencl,
- python3-mako,
- python3-matplotlib,
- python3-dateutil,
- python3-opengl,
- python3-pyqt5,
- python3-pyqt5.qtsvg,
- python3-pyqt5.qtopengl,
- python3-scipy,
- python3-six,
-# Recommends:
-# Suggests: python3-rfoo
-Description: Toolbox for X-Ray data analysis - Python3
- .
- This is the Python 3 version of the package.
-
-Package: python3-silx-dbg
-Architecture: any
-Section: debug
-Depends: ${misc:Depends},
- ${python3:Depends},
- ${shlibs:Depends},
- python3-silx (= ${binary:Version}),
- libstdc++6-4.9-dbg,
- python3-dbg,
- python3-numpy-dbg,
- python3-fabio-dbg,
- python3-h5py-dbg,
- python3-pyopencl-dbg,
- python3-mako,
- python3-matplotlib-dbg,
- python3-dateutil,
- python3-opengl,
- python3-pyqt5-dbg,
- python3-pyqt5.qtsvg-dbg,
- python3-pyqt5.qtopengl-dbg,
- python3-scipy-dbg,
- python3-six,
-Description: Toolbox for X-Ray data analysis - Python3 debug
- .
- This package contains the extension built for the Python 3 debug
- interpreter.
-
-Package: python-silx-doc
-Architecture: all
-Section: doc
-Depends: ${misc:Depends},
- ${sphinxdoc:Depends}
-Description: Toolbox for X-Ray data analysis - Documentation
- .
- This is the common documentation package.
diff --git a/package/debian8/rules b/package/debian8/rules
deleted file mode 100755
index f72ed66..0000000
--- a/package/debian8/rules
+++ /dev/null
@@ -1,56 +0,0 @@
-#!/usr/bin/make -f
-
-export PYBUILD_NAME=silx
-export SPECFILE_USE_GNU_SOURCE=1
-
-# Make does not offer a recursive wildcard function, so here's one:
-rwildcard=$(wildcard $1$2) $(foreach d,$(wildcard $1*),$(call rwildcard,$d/,$2))
-
-# How to recursively find all files with the same name in a given folder
-ALL_PYX := $(call rwildcard,silx/,*.pyx)
-#NOTA: No space before *
-
-%:
- dh $@ --with python2,python3 --buildsystem=pybuild
-
-override_dh_clean:
- dh_clean
- # remove the cython generated file to force rebuild
- rm -f $(patsubst %.pyx,%.cpp,${ALL_PYX})
- rm -f $(patsubst %.pyx,%.c,${ALL_PYX})
- rm -f $(patsubst %.pyx,%.html,${ALL_PYX})
- rm -rf build/html
- rm -rf *.egg-info
-
-override_dh_auto_build:
- dh_auto_build
- python setup.py build build_man build_doc
-
-override_dh_install:
- dh_numpy
- dh_numpy3
-
- # move the scripts to right package
- dh_install -p silx debian/python3-silx/usr/bin/* usr/bin
- dh_install -p silx package/desktop/*.desktop usr/share/applications
- dh_install -p silx package/desktop/silx.png usr/share/icons/hicolor/48x48/apps
- dh_install -p silx package/desktop/silx.svg usr/share/icons/hicolor/scalable/apps
- dh_install -p silx package/desktop/silx.xml usr/share/mime/packages
- rm -rf debian/python-silx/usr/bin
- rm -rf debian/python3-silx/usr/bin
-
- # remove all py/pyc/egg-info files from dbg packages
- find debian/python-silx-dbg/usr -type f \( -not -name "*.so" \) -delete
- find debian/python3-silx-dbg/usr -type f \( -not -name "*.so" \) -delete
-
- dh_install
-
-override_dh_auto_test:
- dh_auto_test -- -s custom --test-args="env PYTHONPATH={build_dir} WITH_QT_TEST=False {interpreter} run_tests.py -v"
-
-override_dh_installman:
- dh_installman -p silx build/man/*.1
-
-override_dh_installdocs:
- dh_installdocs "build/sphinx/html" -p python-silx-doc
- dh_installdocs
diff --git a/package/debian8/watch b/package/debian8/watch
deleted file mode 100644
index 8972716..0000000
--- a/package/debian8/watch
+++ /dev/null
@@ -1,5 +0,0 @@
-version=3
-opts=repacksuffix=+dfsg,\
-uversionmangle=s/(rc|a|b|c)/~$1/,\
-dversionmangle=s/\+dfsg// \
-http://pypi.debian.net/silx/silx-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
diff --git a/package/debian9/control b/package/debian9/control
index fee1b1a..4abe0fe 100644
--- a/package/debian9/control
+++ b/package/debian9/control
@@ -17,14 +17,17 @@ Build-Depends: cython,
python-mako,
python-qtconsole,
python-matplotlib,
+ python-nbsphinx,
python-dateutil,
python-opengl,
python-pyqt5,
python-pyqt5.qtsvg,
python-pyqt5.qtopengl,
python-scipy,
+ python-setuptools,
python-sphinx,
python-sphinxcontrib.programoutput,
+ python-six,
python-enum34,
python-concurrent.futures,
python3-all-dev,
@@ -35,12 +38,15 @@ Build-Depends: cython,
python3-mako,
python3-qtconsole,
python3-matplotlib,
+ python3-nbsphinx,
python3-dateutil,
python3-opengl,
python3-pyqt5,
python3-pyqt5.qtsvg,
python3-pyqt5.qtopengl,
python3-scipy,
+ python3-setuptools,
+ python3-six,
python3-sphinx,
python3-sphinxcontrib.programoutput,
openstack-pkg-tools,
@@ -57,7 +63,7 @@ X-Python3-Version: >= 3.4
Package: silx
Architecture: all
Depends: ${misc:Depends},
- ${python:Depends},
+ ${python3:Depends},
${shlibs:Depends},
python3-pkg-resources,
python3-silx (>= ${source:Version})
@@ -85,6 +91,7 @@ Depends: ${misc:Depends},
python-pyqt5.qtsvg,
python-pyqt5.qtopengl,
python-scipy,
+ python-setuptools,
python-six,
python-enum34,
python-concurrent.futures,
@@ -115,6 +122,7 @@ Depends: ${misc:Depends},
python3-pyqt5.qtsvg,
python3-pyqt5.qtopengl,
python3-scipy,
+ python3-setuptools,
python3-six,
# Recommends:
# Suggests: python3-rfoo
diff --git a/requirements-dev.txt b/requirements-dev.txt
index cac1f35..62ce91e 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,7 +1,7 @@
# List of silx development dependencies
# Those ARE NOT required for installation, at runtime or to build from source (except for the doc)
-numpy >= 1.8
+-r requirements.txt
setuptools # Advanced packaging tools
wheel # To build wheels
Cython >= 0.21.1 # To regenerate .c/.cpp files from .pyx
@@ -10,6 +10,7 @@ lxml # For test coverage in run_test.py
coverage # For test coverage in run_test.py
pillow # For loading images in documentation generation
nbsphinx # For converting ipynb in documentation
+pandoc # For documentation Qt snapshot updates
# Use dev version of PyInstaller to keep hooks up-to-date
https://github.com/pyinstaller/pyinstaller/archive/develop.zip; sys_platform == "win32"
diff --git a/requirements.txt b/requirements.txt
index 90dc020..989e5b3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,17 +5,25 @@
--find-links http://www.silx.org/pub/wheelhouse/
--only-binary numpy,h5py,scipy,PyQt4,PyQt5
-numpy >= 1.8
-fabio >= 0.7
+# Required dependencies (from setup.py install_requires)
+numpy >= 1.12
+setuptools
h5py
-scipy # For silx.math.fit demo, silx.image.sift demo, silx.image.sift.test
-pyopencl; platform_machine in "i386, x86_64" # For silx.opencl
+fabio >= 0.7
+six
+enum34; python_version == '2.7'
+futures; python_version == '2.7'
+
+# Extra dependencies (from setup.py extra_requires 'full' target)
+pyopencl; platform_machine in "i386, x86_64, AMD64" # For silx.opencl
Mako # For pyopencl reduction
qtconsole # For silx.gui.console
matplotlib >= 1.2.0 # For silx.gui.plot
PyOpenGL # For silx.gui.plot3d
-Pillow # For silx.opencl.image.test
python-dateutil # For silx.gui.plot
+scipy # For silx.math.fit demo, silx.image.sift demo, silx.image.sift.test
+Pillow # For silx.opencl.image.test
+Cython >= 0.21.1 # For silx.math, silx.io, silx.image
# PyQt5, PySide2 or PyQt4 # For silx.gui
# Try to install a Qt binding from a wheel
diff --git a/run_tests.py b/run_tests.py
index bea6625..6007344 100755
--- a/run_tests.py
+++ b/run_tests.py
@@ -115,7 +115,6 @@ except Exception as error:
else:
logger.info("Numpy %s", numpy.version.version)
-
try:
import h5py
except Exception as error:
@@ -314,10 +313,10 @@ def import_project_module(project_name, project_dir):
if "--installed" in sys.argv:
try:
module = importer(project_name)
- except ImportError:
- raise ImportError(
- "%s not installed: Cannot run tests on installed version" %
- PROJECT_NAME)
+ except Exception:
+ logger.error("Cannot run tests on installed version: %s not installed or raising error.",
+ project_name)
+ raise
else: # Use built source
build_dir = build_project(project_name, project_dir)
if build_dir is None:
diff --git a/setup.py b/setup.py
index 9f6ae13..a7f3f2a 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
# coding: utf8
# /*##########################################################################
#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -25,7 +25,7 @@
# ###########################################################################*/
__authors__ = ["Jérôme Kieffer", "Thomas Vincent"]
-__date__ = "23/04/2018"
+__date__ = "12/02/2019"
__license__ = "MIT"
@@ -40,6 +40,7 @@ import glob
# The silx.io module seems to be loaded instead.
import io
+
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("silx.setup")
@@ -84,7 +85,10 @@ export LC_ALL=en_US.utf-8
def get_version():
"""Returns current version number from version.py file"""
+ dirname = os.path.dirname(os.path.abspath(__file__))
+ sys.path.insert(0, dirname)
import version
+ sys.path = sys.path[1:]
return version.strictversion
@@ -111,10 +115,10 @@ classifiers = ["Development Status :: 4 - Beta",
"Operating System :: POSIX",
"Programming Language :: Cython",
"Programming Language :: Python :: 2.7",
- "Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
+ "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Scientific/Engineering :: Physics",
"Topic :: Software Development :: Libraries :: Python Modules",
@@ -346,8 +350,20 @@ if sphinx is not None:
self.mkpath(self.builder_target_dir)
BuildDoc.run(self)
sys.path.pop(0)
+
+ class BuildDocAndGenerateScreenshotCommand(BuildDocCommand):
+ def run(self):
+ old = os.environ.get('DIRECTIVE_SNAPSHOT_QT')
+ os.environ['DIRECTIVE_SNAPSHOT_QT'] = 'True'
+ BuildDocCommand.run(self)
+ if old is not None:
+ os.environ['DIRECTIVE_SNAPSHOT_QT'] = old
+ else:
+ del os.environ['DIRECTIVE_SNAPSHOT_QT']
+
else:
BuildDocCommand = SphinxExpectedCommand
+ BuildDocAndGenerateScreenshotCommand = SphinxExpectedCommand
# ################### #
@@ -417,26 +433,24 @@ class Build(_build):
"do not use OpenMP for compiled extension modules"),
('openmp', None,
"use OpenMP for the compiled extension modules"),
- ('no-cython', None,
- "do not compile Cython extension modules (use default compiled c-files)"),
('force-cython', None,
"recompile all Cython extension modules"),
]
user_options.extend(_build.user_options)
- boolean_options = ['no-openmp', 'openmp', 'no-cython', 'force-cython']
+ boolean_options = ['no-openmp', 'openmp', 'force-cython']
boolean_options.extend(_build.boolean_options)
def initialize_options(self):
_build.initialize_options(self)
self.no_openmp = None
self.openmp = None
- self.no_cython = None
self.force_cython = None
def finalize_options(self):
_build.finalize_options(self)
- self.finalize_cython_options(min_version='0.21.1')
+ if not self.force_cython:
+ self.force_cython = self._parse_env_as_bool("FORCE_CYTHON") is True
self.finalize_openmp_options()
def _parse_env_as_bool(self, key):
@@ -463,9 +477,9 @@ class Build(_build):
elif self.no_openmp:
use_openmp = False
else:
- env_force_cython = self._parse_env_as_bool("WITH_OPENMP")
- if env_force_cython is not None:
- use_openmp = env_force_cython
+ env_with_openmp = self._parse_env_as_bool("WITH_OPENMP")
+ if env_with_openmp is not None:
+ use_openmp = env_with_openmp
else:
# Use it by default
use_openmp = True
@@ -475,7 +489,7 @@ class Build(_build):
# By default Xcode5 & XCode6 do not support OpenMP, Xcode4 is OK.
osx = tuple([int(i) for i in platform.mac_ver()[0].split(".")])
if osx >= (10, 8):
- logger.warning("OpenMP support ignored. Your platform do not support it")
+ logger.warning("OpenMP support ignored. Your platform does not support it.")
use_openmp = False
# Remove attributes used by distutils parsing
@@ -484,49 +498,6 @@ class Build(_build):
del self.openmp
self.use_openmp = use_openmp
- def finalize_cython_options(self, min_version=None):
- """
- Check if cythonization must be used for the extensions.
-
- The result is stored into the object.
- """
-
- if self.force_cython:
- use_cython = "force"
- elif self.no_cython:
- use_cython = "no"
- else:
- env_force_cython = self._parse_env_as_bool("FORCE_CYTHON")
- env_with_cython = self._parse_env_as_bool("WITH_CYTHON")
- if env_force_cython is True:
- use_cython = "force"
- elif env_with_cython is True:
- use_cython = "yes"
- elif env_with_cython is False:
- use_cython = "no"
- else:
- # Use it by default
- use_cython = "yes"
-
- if use_cython in ["force", "yes"]:
- try:
- import Cython.Compiler.Version
- if min_version and Cython.Compiler.Version.version < min_version:
- msg = "Cython version is too old. At least version is %s \
- expected. Cythonization is skipped."
- logger.warning(msg, str(min_version))
- use_cython = "no"
- except ImportError:
- msg = "Cython is not available. Cythonization is skipped."
- logger.warning(msg)
- use_cython = "no"
-
- # Remove attribute used by distutils parsing
- # use 'use_cython' and 'force_cython' instead
- del self.no_cython
- self.force_cython = use_cython == "force"
- self.use_cython = use_cython in ["force", "yes"]
-
class BuildExt(build_ext):
"""Handle extension compilation.
@@ -542,40 +513,14 @@ class BuildExt(build_ext):
LINK_ARGS_CONVERTER = {'-fopenmp': ''}
- description = 'Build silx extensions'
+ description = 'Build extensions'
def finalize_options(self):
build_ext.finalize_options(self)
build_obj = self.distribution.get_command_obj("build")
self.use_openmp = build_obj.use_openmp
- self.use_cython = build_obj.use_cython
self.force_cython = build_obj.force_cython
- def patch_with_default_cythonized_files(self, ext):
- """Replace cython files by .c or .cpp files in extension's sources.
-
- It replaces the *.pyx and *.py source files of the extensions
- to either *.cpp or *.c source files.
- No compilation is performed.
-
- :param Extension ext: An extension to patch.
- """
- new_sources = []
- for source in ext.sources:
- base, file_ext = os.path.splitext(source)
- if file_ext in ('.pyx', '.py'):
- if ext.language == 'c++':
- cythonized = base + '.cpp'
- else:
- cythonized = base + '.c'
- if not os.path.isfile(cythonized):
- raise RuntimeError("Source file not found: %s. Cython is needed" % cythonized)
- print("Use default cythonized file for %s" % source)
- new_sources.append(cythonized)
- else:
- new_sources.append(source)
- ext.sources = new_sources
-
def patch_extension(self, ext):
"""
Patch an extension according to requested Cython and OpenMP usage.
@@ -583,16 +528,14 @@ class BuildExt(build_ext):
:param Extension ext: An extension
"""
# Cytonize
- if not self.use_cython:
- self.patch_with_default_cythonized_files(ext)
- else:
- from Cython.Build import cythonize
- patched_exts = cythonize(
- [ext],
- compiler_directives={'embedsignature': True},
- force=self.force_cython
- )
- ext.sources = patched_exts[0].sources
+ from Cython.Build import cythonize
+ patched_exts = cythonize(
+ [ext],
+ compiler_directives={'embedsignature': True,
+ 'language_level': 3},
+ force=self.force_cython
+ )
+ ext.sources = patched_exts[0].sources
# Remove OpenMP flags if OpenMP is disabled
if not self.use_openmp:
@@ -603,10 +546,15 @@ class BuildExt(build_ext):
# Convert flags from gcc to MSVC if required
if self.compiler.compiler_type == 'msvc':
- ext.extra_compile_args = [self.COMPILE_ARGS_CONVERTER.get(f, f)
- for f in ext.extra_compile_args]
- ext.extra_link_args = [self.LINK_ARGS_CONVERTER.get(f, f)
- for f in ext.extra_link_args]
+ extra_compile_args = [self.COMPILE_ARGS_CONVERTER.get(f, f)
+ for f in ext.extra_compile_args]
+ # Avoid empty arg
+ ext.extra_compile_args = [arg for arg in extra_compile_args if arg]
+
+ extra_link_args = [self.LINK_ARGS_CONVERTER.get(f, f)
+ for f in ext.extra_link_args]
+ # Avoid empty arg
+ ext.extra_link_args = [arg for arg in extra_link_args if arg]
elif self.compiler.compiler_type == 'unix':
# Avoids runtime symbol collision for manylinux1 platform
@@ -614,8 +562,17 @@ class BuildExt(build_ext):
extern = 'extern "C" ' if ext.language == 'c++' else ''
return_type = 'void' if sys.version_info[0] <= 2 else 'PyObject*'
- # ext.extra_compile_args.append(
- # '''-fvisibility=hidden -D'PyMODINIT_FUNC=%s__attribute__((visibility("default"))) %s ' ''' % (extern, return_type))
+ ext.extra_compile_args.append('-fvisibility=hidden')
+
+ import numpy
+ numpy_version = [int(i) for i in numpy.version.short_version.split(".", 2)[:2]]
+ if numpy_version < [1, 16]:
+ ext.extra_compile_args.append(
+ '''-D'PyMODINIT_FUNC=%s__attribute__((visibility("default"))) %s ' ''' % (extern, return_type))
+ else:
+ ext.define_macros.append(
+ ('PyMODINIT_FUNC',
+ '%s__attribute__((visibility("default"))) %s ' % (extern, return_type)))
def is_debug_interpreter(self):
"""
@@ -678,6 +635,7 @@ class BuildExt(build_ext):
self.patch_extension(ext)
build_ext.build_extensions(self)
+
################################################################################
# Clean command
################################################################################
@@ -743,36 +701,6 @@ class CleanCommand(Clean):
except OSError:
pass
-################################################################################
-# Source tree
-################################################################################
-
-class SourceDistWithCython(sdist):
- """
- Force cythonization of the extensions before generating the source
- distribution.
-
- To provide the widest compatibility the cythonized files are provided
- without suppport of OpenMP.
- """
-
- description = "Create a source distribution including cythonozed files (tarball, zip file, etc.)"
-
- def finalize_options(self):
- sdist.finalize_options(self)
- self.extensions = self.distribution.ext_modules
-
- def run(self):
- self.cythonize_extensions()
- sdist.run(self)
-
- def cythonize_extensions(self):
- from Cython.Build import cythonize
- cythonize(
- self.extensions,
- compiler_directives={'embedsignature': True},
- force=True
- )
################################################################################
# Debian source tree
@@ -860,9 +788,20 @@ def get_project_configuration(dry_run):
"setuptools",
# for io support
"h5py",
- "fabio>=0.7"]
+ "fabio>=0.7",
+ # Python 2/3 compatibility
+ "six",
+ ]
- setup_requires = ["setuptools", "numpy"]
+ # Add Python 2.7 backports
+ # Equivalent to but supported by old setuptools:
+ # "enum34; python_version == '2.7'",
+ # "futures; python_version == '2.7'",
+ if sys.version_info[0] == 2:
+ install_requires.append("enum34")
+ install_requires.append("futures")
+
+ setup_requires = ["setuptools", "numpy>=1.12", "Cython>=0.21.1"]
# extras requirements: target 'full' to install all dependencies at once
full_requires = [
@@ -883,6 +822,12 @@ def get_project_configuration(dry_run):
'full': full_requires,
}
+ # Here for packaging purpose only
+ # Setting the SILX_FULL_INSTALL_REQUIRES environment variable
+ # put all dependencies as install_requires
+ if os.environ.get('SILX_FULL_INSTALL_REQUIRES') is not None:
+ install_requires += full_requires
+
package_data = {
# Resources files for silx
'silx.resources': [
@@ -910,12 +855,12 @@ def get_project_configuration(dry_run):
build=Build,
build_py=build_py,
test=PyTest,
+ build_screenshots=BuildDocAndGenerateScreenshotCommand,
build_doc=BuildDocCommand,
test_doc=TestDocCommand,
build_ext=BuildExt,
build_man=BuildMan,
clean=CleanCommand,
- sdist=SourceDistWithCython,
debian_src=sdist_debian)
if dry_run:
diff --git a/silx.egg-info/PKG-INFO b/silx.egg-info/PKG-INFO
index 52f365a..1c225b2 100644
--- a/silx.egg-info/PKG-INFO
+++ b/silx.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: silx
-Version: 0.9.0
+Version: 0.12.0
Summary: Software library for X-ray data analysis
Home-page: http://www.silx.org/
Author: data analysis unit
@@ -24,7 +24,8 @@ Description:
images file formats.
* OpenCL-based data processing: image alignment (SIFT),
image processing (median filter, histogram),
- filtered backprojection for tomography
+ filtered backprojection for tomography,
+ convolution
* Data reduction: histogramming, fitting, median filter
* A set of Qt widgets, including:
@@ -55,13 +56,13 @@ Description:
Or using Anaconda on Linux and MacOS:
- .. code-block:: bash
-
+ .. code-block:: bash
+
conda install silx -c conda-forge
Unofficial packages for different distributions are available:
- - Unofficial Debian8 packages are available at http://www.silx.org/pub/debian/
+ - Unofficial Debian9 packages are available at http://www.silx.org/pub/debian/
- CentOS 7 rpm packages are provided by Max IV at: http://pubrepo.maxiv.lu.se/rpm/el7/x86_64/
- Fedora 23 rpm packages are provided by Max IV at http://pubrepo.maxiv.lu.se/rpm/fc23/x86_64/
- Arch Linux (AUR) packages are also available: https://aur.archlinux.org/packages/python-silx
@@ -129,10 +130,10 @@ Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX
Classifier: Programming Language :: Cython
Classifier: Programming Language :: Python :: 2.7
-Classifier: Programming Language :: Python :: 3.4
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
+Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
diff --git a/silx.egg-info/SOURCES.txt b/silx.egg-info/SOURCES.txt
index 2a926d4..30434a4 100644
--- a/silx.egg-info/SOURCES.txt
+++ b/silx.egg-info/SOURCES.txt
@@ -62,6 +62,7 @@ doc/source/description/img/sift_frame_ROI.png
doc/source/description/img/sift_match1.png
doc/source/description/img/sift_match2.png
doc/source/description/img/sift_orientation.png
+doc/source/ext/snapshotqt_directive.py
doc/source/ext/sphinxext-archive.py
doc/source/img/silx.ico
doc/source/img/silx_large.png
@@ -137,6 +138,7 @@ doc/source/modules/gui/plot/scatterview.rst
doc/source/modules/gui/plot/stackview.rst
doc/source/modules/gui/plot/statswidget.rst
doc/source/modules/gui/plot/tools.rst
+doc/source/modules/gui/plot/utils.rst
doc/source/modules/gui/plot/actions/control.rst
doc/source/modules/gui/plot/actions/examples.rst
doc/source/modules/gui/plot/actions/fit.rst
@@ -148,6 +150,8 @@ doc/source/modules/gui/plot/actions/img/fftAction0.png
doc/source/modules/gui/plot/actions/img/fftAction1.png
doc/source/modules/gui/plot/actions/img/shiftAction0.png
doc/source/modules/gui/plot/actions/img/shiftAction3.png
+doc/source/modules/gui/plot/img/BasicGridStatsWidget.png
+doc/source/modules/gui/plot/img/BasicStatsWidget.png
doc/source/modules/gui/plot/img/CompareImages.png
doc/source/modules/gui/plot/img/ComplexImageView.png
doc/source/modules/gui/plot/img/CurveLegendsWidget.png
@@ -252,9 +256,13 @@ doc/source/modules/math/fit/index.rst
doc/source/modules/math/fit/leastsq.rst
doc/source/modules/math/fit/peaksearch.rst
doc/source/modules/opencl/codec_cbf.rst
+doc/source/modules/opencl/convolution.rst
doc/source/modules/opencl/fbp.rst
doc/source/modules/opencl/index.rst
doc/source/modules/opencl/medfilt.rst
+doc/source/modules/opencl/processing.rst
+doc/source/modules/opencl/sinofilter.rst
+doc/source/modules/opencl/statistics.rst
doc/source/modules/opencl/sift/align.rst
doc/source/modules/opencl/sift/index.rst
doc/source/modules/opencl/sift/match.rst
@@ -269,36 +277,49 @@ doc/source/modules/utils/weakref.rst
doc/source/sample_code/index.rst
doc/source/sample_code/img/animatedicons.png
doc/source/sample_code/img/colormapDialog.png
+doc/source/sample_code/img/compareImages.png
+doc/source/sample_code/img/compositeline.png
doc/source/sample_code/img/customDataView.png
doc/source/sample_code/img/customHdf5TreeModel.png
+doc/source/sample_code/img/dropZones.png
+doc/source/sample_code/img/exampleBaseline.png
doc/source/sample_code/img/fftPlotAction.png
doc/source/sample_code/img/fileDialog.png
+doc/source/sample_code/img/findContours.png
doc/source/sample_code/img/hdf5widget.png
doc/source/sample_code/img/icons.png
doc/source/sample_code/img/imageview.png
doc/source/sample_code/img/periodicTable.png
doc/source/sample_code/img/plot3dContextMenu.png
doc/source/sample_code/img/plot3dSceneWindow.png
+doc/source/sample_code/img/plot3dUpdateScatterFromThread.png
doc/source/sample_code/img/plotClearAction.png
doc/source/sample_code/img/plotContextMenu.png
+doc/source/sample_code/img/plotCurveLegendWidget.png
doc/source/sample_code/img/plotInteractiveImageROI.png
doc/source/sample_code/img/plotItemsSelector.png
doc/source/sample_code/img/plotLimits.png
+doc/source/sample_code/img/plotStats.png
doc/source/sample_code/img/plotUpdateCurveFromThread.png
doc/source/sample_code/img/plotUpdateImageFromThread.png
doc/source/sample_code/img/plotWidget.png
doc/source/sample_code/img/printPreview.png
doc/source/sample_code/img/scatterMask.png
+doc/source/sample_code/img/scatterview.png
doc/source/sample_code/img/shiftPlotAction.png
doc/source/sample_code/img/simplewidget.png
doc/source/sample_code/img/stackView.png
+doc/source/sample_code/img/syncPlotLocation.png
doc/source/sample_code/img/syncaxis.png
doc/source/sample_code/img/viewer3DVolume.png
examples/__init__.py
examples/colormapDialog.py
examples/compareImages.py
+examples/compositeline.py
examples/customDataView.py
examples/customHdf5TreeModel.py
+examples/dropZones.py
+examples/exampleBaseline.py
examples/fft.png
examples/fftPlotAction.py
examples/fileDialog.py
@@ -309,6 +330,7 @@ examples/imageview.py
examples/periodicTable.py
examples/plot3dContextMenu.py
examples/plot3dSceneWindow.py
+examples/plot3dUpdateScatterFromThread.py
examples/plotClearAction.py
examples/plotContextMenu.py
examples/plotCurveLegendWidget.py
@@ -321,22 +343,28 @@ examples/plotUpdateImageFromThread.py
examples/plotWidget.py
examples/printPreview.py
examples/scatterMask.py
+examples/scatterview.py
examples/shiftPlotAction.py
examples/simplewidget.py
examples/stackView.py
+examples/syncPlotLocation.py
examples/syncaxis.py
examples/viewer3DVolume.py
examples/writetoh5.py
-package/debian8/changelog
-package/debian8/clean
-package/debian8/compat
-package/debian8/control
-package/debian8/gbp.conf
-package/debian8/python-silx-doc.doc-base
-package/debian8/rules
-package/debian8/watch
-package/debian8/source/format
-package/debian8/source/options
+package/debian10/changelog
+package/debian10/compat
+package/debian10/control
+package/debian10/gbp.conf
+package/debian10/py3dist-overrides
+package/debian10/python-silx-doc.doc-base
+package/debian10/rules
+package/debian10/watch
+package/debian10/patches/0002-use-the-system-mathjax-privacy-breach.patch
+package/debian10/patches/0003-do-not-modify-PYTHONPATH-from-setup.py.patch
+package/debian10/patches/series
+package/debian10/source/format
+package/debian10/source/options
+package/debian10/tests/control
package/debian9/changelog
package/debian9/clean
package/debian9/compat
@@ -419,6 +447,7 @@ silx/gui/data/NXdataWidgets.py
silx/gui/data/NumpyAxesSelector.py
silx/gui/data/RecordTableView.py
silx/gui/data/TextFormatter.py
+silx/gui/data/_VolumeWindow.py
silx/gui/data/__init__.py
silx/gui/data/setup.py
silx/gui/data/test/__init__.py
@@ -498,6 +527,7 @@ silx/gui/plot/_BaseMaskToolsWidget.py
silx/gui/plot/__init__.py
silx/gui/plot/setup.py
silx/gui/plot/_utils/__init__.py
+silx/gui/plot/_utils/delaunay.py
silx/gui/plot/_utils/dtime_ticklayout.py
silx/gui/plot/_utils/panzoom.py
silx/gui/plot/_utils/setup.py
@@ -521,12 +551,14 @@ silx/gui/plot/backends/__init__.py
silx/gui/plot/backends/glutils/GLPlotCurve.py
silx/gui/plot/backends/glutils/GLPlotFrame.py
silx/gui/plot/backends/glutils/GLPlotImage.py
+silx/gui/plot/backends/glutils/GLPlotTriangles.py
silx/gui/plot/backends/glutils/GLSupport.py
silx/gui/plot/backends/glutils/GLText.py
silx/gui/plot/backends/glutils/GLTexture.py
silx/gui/plot/backends/glutils/PlotImageFile.py
silx/gui/plot/backends/glutils/__init__.py
silx/gui/plot/items/__init__.py
+silx/gui/plot/items/_pick.py
silx/gui/plot/items/axis.py
silx/gui/plot/items/complex.py
silx/gui/plot/items/core.py
@@ -630,7 +662,10 @@ silx/gui/plot3d/scene/test/test_utils.py
silx/gui/plot3d/test/__init__.py
silx/gui/plot3d/test/testGL.py
silx/gui/plot3d/test/testScalarFieldView.py
+silx/gui/plot3d/test/testSceneWidget.py
silx/gui/plot3d/test/testSceneWidgetPicking.py
+silx/gui/plot3d/test/testSceneWindow.py
+silx/gui/plot3d/test/testStatsWidget.py
silx/gui/plot3d/tools/GroupPropertiesWidget.py
silx/gui/plot3d/tools/PositionInfoWidget.py
silx/gui/plot3d/tools/ViewpointTools.py
@@ -656,15 +691,22 @@ silx/gui/test/utils.py
silx/gui/utils/__init__.py
silx/gui/utils/concurrent.py
silx/gui/utils/image.py
+silx/gui/utils/projecturl.py
+silx/gui/utils/qtutils.py
silx/gui/utils/testutils.py
silx/gui/utils/test/__init__.py
+silx/gui/utils/test/test.py
silx/gui/utils/test/test_async.py
silx/gui/utils/test/test_image.py
+silx/gui/utils/test/test_qtutils.py
+silx/gui/utils/test/test_testutils.py
silx/gui/widgets/BoxLayoutDockWidget.py
+silx/gui/widgets/ColormapNameComboBox.py
silx/gui/widgets/FloatEdit.py
silx/gui/widgets/FlowLayout.py
silx/gui/widgets/FrameBrowser.py
silx/gui/widgets/HierarchicalTableView.py
+silx/gui/widgets/LegendIconWidget.py
silx/gui/widgets/MedianFilterDialog.py
silx/gui/widgets/PeriodicTable.py
silx/gui/widgets/PrintGeometryDialog.py
@@ -672,6 +714,7 @@ silx/gui/widgets/PrintPreview.py
silx/gui/widgets/RangeSlider.py
silx/gui/widgets/TableWidget.py
silx/gui/widgets/ThreadPoolPushButton.py
+silx/gui/widgets/UrlSelectionTable.py
silx/gui/widgets/WaitingPushButton.py
silx/gui/widgets/__init__.py
silx/gui/widgets/setup.py
@@ -687,19 +730,17 @@ silx/gui/widgets/test/test_tablewidget.py
silx/gui/widgets/test/test_threadpoolpushbutton.py
silx/image/__init__.py
silx/image/backprojection.py
-silx/image/bilinear.c
silx/image/bilinear.pyx
silx/image/medianfilter.py
silx/image/phantomgenerator.py
silx/image/projection.py
silx/image/reconstruction.py
silx/image/setup.py
-silx/image/shapes.c
silx/image/shapes.pyx
silx/image/sift.py
silx/image/tomography.py
+silx/image/utils.py
silx/image/marchingsquares/__init__.py
-silx/image/marchingsquares/_mergeimpl.cpp
silx/image/marchingsquares/_mergeimpl.pyx
silx/image/marchingsquares/_skimage.py
silx/image/marchingsquares/setup.py
@@ -721,7 +762,6 @@ silx/io/fabioh5.py
silx/io/octaveh5.py
silx/io/rawh5.py
silx/io/setup.py
-silx/io/specfile.c
silx/io/specfile.pyx
silx/io/specfile_wrapper.pxd
silx/io/specfilewrapper.py
@@ -763,34 +803,37 @@ silx/io/test/test_url.py
silx/io/test/test_utils.py
silx/math/__init__.py
silx/math/calibration.py
-silx/math/chistogramnd.c
silx/math/chistogramnd.pyx
-silx/math/chistogramnd_lut.c
silx/math/chistogramnd_lut.pyx
-silx/math/colormap.c
silx/math/colormap.pyx
-silx/math/combo.c
silx/math/combo.pyx
silx/math/histogram.py
silx/math/histogramnd_c.pxd
-silx/math/marchingcubes.cpp
+silx/math/interpolate.pyx
silx/math/marchingcubes.pyx
silx/math/math_compatibility.pxd
silx/math/mc.pxd
silx/math/setup.py
+silx/math/fft/__init__.py
+silx/math/fft/basefft.py
+silx/math/fft/clfft.py
+silx/math/fft/cufft.py
+silx/math/fft/fft.py
+silx/math/fft/fftw.py
+silx/math/fft/npfft.py
+silx/math/fft/setup.py
+silx/math/fft/test/__init__.py
+silx/math/fft/test/test_fft.py
silx/math/fit/__init__.py
silx/math/fit/bgtheories.py
-silx/math/fit/filters.c
silx/math/fit/filters.pyx
silx/math/fit/filters_wrapper.pxd
silx/math/fit/fitmanager.py
silx/math/fit/fittheories.py
silx/math/fit/fittheory.py
-silx/math/fit/functions.c
silx/math/fit/functions.pyx
silx/math/fit/functions_wrapper.pxd
silx/math/fit/leastsq.py
-silx/math/fit/peaks.c
silx/math/fit/peaks.pyx
silx/math/fit/peaks_wrapper.pxd
silx/math/fit/setup.py
@@ -821,7 +864,6 @@ silx/math/marchingcubes/mc.hpp
silx/math/marchingcubes/mc_lut.cpp
silx/math/medianfilter/__init__.py
silx/math/medianfilter/median_filter.pxd
-silx/math/medianfilter/medianfilter.cpp
silx/math/medianfilter/medianfilter.pyx
silx/math/medianfilter/setup.py
silx/math/medianfilter/include/median_filter.hpp
@@ -838,10 +880,12 @@ silx/math/test/test_combo.py
silx/math/test/test_histogramnd_error.py
silx/math/test/test_histogramnd_nominal.py
silx/math/test/test_histogramnd_vs_np.py
+silx/math/test/test_interpolate.py
silx/math/test/test_marchingcubes.py
silx/opencl/__init__.py
silx/opencl/backprojection.py
silx/opencl/common.py
+silx/opencl/convolution.py
silx/opencl/image.py
silx/opencl/linalg.py
silx/opencl/medfilt.py
@@ -849,6 +893,9 @@ silx/opencl/processing.py
silx/opencl/projection.py
silx/opencl/reconstruction.py
silx/opencl/setup.py
+silx/opencl/sinofilter.py
+silx/opencl/sparse.py
+silx/opencl/statistics.py
silx/opencl/utils.py
silx/opencl/codec/__init__.py
silx/opencl/codec/byte_offset.py
@@ -880,11 +927,16 @@ silx/opencl/test/__init__.py
silx/opencl/test/test_addition.py
silx/opencl/test/test_array_utils.py
silx/opencl/test/test_backprojection.py
+silx/opencl/test/test_convolution.py
silx/opencl/test/test_image.py
+silx/opencl/test/test_kahan.py
silx/opencl/test/test_linalg.py
silx/opencl/test/test_medfilt.py
silx/opencl/test/test_projection.py
+silx/opencl/test/test_sparse.py
+silx/opencl/test/test_stats.py
silx/resources/__init__.py
+silx/resources/gui/colormaps/cividis.npy
silx/resources/gui/colormaps/inferno.npy
silx/resources/gui/colormaps/magma.npy
silx/resources/gui/colormaps/plasma.npy
@@ -945,6 +997,8 @@ silx/resources/gui/icons/compare-align-stretch.png
silx/resources/gui/icons/compare-align-stretch.svg
silx/resources/gui/icons/compare-keypoints.png
silx/resources/gui/icons/compare-keypoints.svg
+silx/resources/gui/icons/compare-mode-a-minus-b.png
+silx/resources/gui/icons/compare-mode-a-minus-b.svg
silx/resources/gui/icons/compare-mode-a.png
silx/resources/gui/icons/compare-mode-a.svg
silx/resources/gui/icons/compare-mode-b.png
@@ -977,6 +1031,18 @@ silx/resources/gui/icons/cube-top.png
silx/resources/gui/icons/cube-top.svg
silx/resources/gui/icons/cube.png
silx/resources/gui/icons/cube.svg
+silx/resources/gui/icons/description-description.png
+silx/resources/gui/icons/description-description.svg
+silx/resources/gui/icons/description-error.png
+silx/resources/gui/icons/description-error.svg
+silx/resources/gui/icons/description-name.png
+silx/resources/gui/icons/description-name.svg
+silx/resources/gui/icons/description-program.png
+silx/resources/gui/icons/description-program.svg
+silx/resources/gui/icons/description-title.png
+silx/resources/gui/icons/description-title.svg
+silx/resources/gui/icons/description-value.png
+silx/resources/gui/icons/description-value.svg
silx/resources/gui/icons/document-open.png
silx/resources/gui/icons/document-open.svg
silx/resources/gui/icons/document-print.png
@@ -991,6 +1057,8 @@ silx/resources/gui/icons/draw-rubber.png
silx/resources/gui/icons/draw-rubber.svg
silx/resources/gui/icons/edit-copy.png
silx/resources/gui/icons/edit-copy.svg
+silx/resources/gui/icons/eye.png
+silx/resources/gui/icons/eye.svg
silx/resources/gui/icons/first.png
silx/resources/gui/icons/first.svg
silx/resources/gui/icons/folder.png
@@ -1121,6 +1189,8 @@ silx/resources/gui/icons/plot-ylog.png
silx/resources/gui/icons/plot-ylog.svg
silx/resources/gui/icons/plot-yup.png
silx/resources/gui/icons/plot-yup.svg
+silx/resources/gui/icons/pointing-hand.png
+silx/resources/gui/icons/pointing-hand.svg
silx/resources/gui/icons/previous.png
silx/resources/gui/icons/previous.svg
silx/resources/gui/icons/process-working.mng
@@ -1250,10 +1320,15 @@ silx/resources/opencl/array_utils.cl
silx/resources/opencl/backproj.cl
silx/resources/opencl/backproj_helper.cl
silx/resources/opencl/bitonic.cl
+silx/resources/opencl/convolution.cl
+silx/resources/opencl/convolution_textures.cl
+silx/resources/opencl/kahan.cl
silx/resources/opencl/linalg.cl
silx/resources/opencl/medfilt.cl
silx/resources/opencl/preprocess.cl
silx/resources/opencl/proj.cl
+silx/resources/opencl/sparse.cl
+silx/resources/opencl/statistics.cl
silx/resources/opencl/codec/byte_offset.cl
silx/resources/opencl/image/cast.cl
silx/resources/opencl/image/histogram.cl
@@ -1280,30 +1355,18 @@ silx/resources/opencl/sift/transform.cl
silx/sx/__init__.py
silx/sx/_plot.py
silx/sx/_plot3d.py
-silx/sx/test/__init__.py
-silx/sx/test/test_sx.py
silx/test/__init__.py
silx/test/test_resources.py
+silx/test/test_sx.py
silx/test/test_version.py
silx/test/utils.py
silx/third_party/EdfFile.py
silx/third_party/TiffIO.py
silx/third_party/__init__.py
-silx/third_party/concurrent_futures.py
-silx/third_party/enum.py
-silx/third_party/modest_image.py
silx/third_party/scipy_spatial.py
silx/third_party/setup.py
-silx/third_party/six.py
silx/third_party/_local/__init__.py
-silx/third_party/_local/enum.py
-silx/third_party/_local/six.py
-silx/third_party/_local/concurrent_futures/__init__.py
-silx/third_party/_local/concurrent_futures/_base.py
-silx/third_party/_local/concurrent_futures/process.py
-silx/third_party/_local/concurrent_futures/thread.py
silx/third_party/_local/scipy_spatial/__init__.py
-silx/third_party/_local/scipy_spatial/qhull.c
silx/third_party/_local/scipy_spatial/qhull.pxd
silx/third_party/_local/scipy_spatial/qhull.pyx
silx/third_party/_local/scipy_spatial/qhull_misc.h
@@ -1338,12 +1401,15 @@ silx/third_party/_local/scipy_spatial/qhull/src/user_r.h
silx/third_party/_local/scipy_spatial/qhull/src/usermem_r.c
silx/third_party/_local/scipy_spatial/qhull/src/userprintf_r.c
silx/third_party/_local/scipy_spatial/qhull/src/userprintf_rbox_r.c
+silx/utils/ExternalResources.py
silx/utils/__init__.py
silx/utils/_have_openmp.pxi
silx/utils/array_like.py
silx/utils/debug.py
silx/utils/deprecation.py
+silx/utils/enum.py
silx/utils/exceptions.py
+silx/utils/files.py
silx/utils/html.py
silx/utils/launcher.py
silx/utils/number.py
@@ -1357,9 +1423,12 @@ silx/utils/test/__init__.py
silx/utils/test/test_array_like.py
silx/utils/test/test_debug.py
silx/utils/test/test_deprecation.py
+silx/utils/test/test_enum.py
+silx/utils/test/test_external_resources.py
silx/utils/test/test_html.py
silx/utils/test/test_launcher.py
silx/utils/test/test_launcher_command.py
silx/utils/test/test_number.py
silx/utils/test/test_proxy.py
+silx/utils/test/test_testutils.py
silx/utils/test/test_weakref.py \ No newline at end of file
diff --git a/silx.egg-info/requires.txt b/silx.egg-info/requires.txt
index e7cfb25..79c4d9c 100644
--- a/silx.egg-info/requires.txt
+++ b/silx.egg-info/requires.txt
@@ -1,7 +1,8 @@
-numpy>=1.15.3
+numpy>=1.12.0
setuptools
h5py
fabio>=0.7
+six
[full]
pyopencl
diff --git a/silx/_config.py b/silx/_config.py
index 02bbf4e..fb0e409 100644
--- a/silx/_config.py
+++ b/silx/_config.py
@@ -2,7 +2,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,7 +28,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "26/04/2018"
+__date__ = "09/11/2018"
class Config(object):
@@ -38,7 +38,7 @@ class Config(object):
.. versionadded:: 0.8
"""
- DEFAULT_PLOT_BACKEND = "matplotlib"
+ DEFAULT_PLOT_BACKEND = "matplotlib", "opengl"
"""Default plot backend.
It will be used as default backend for all the next created PlotWidget.
@@ -51,13 +51,15 @@ class Config(object):
- A :class:`silx.gui.plot.backend.BackendBase.BackendBase` class
- A callable returning backend class or binding name
+ If multiple backends are provided, the first available one is used.
+
.. versionadded:: 0.8
"""
DEFAULT_COLORMAP_NAME = 'gray'
"""Default LUT for the plot widgets.
- The available list of names are availaible in the module
+ The available list of names are available in the module
:module:`silx.gui.colors`.
.. versionadded:: 0.8
@@ -109,4 +111,38 @@ class Config(object):
.. versionadded:: 0.9
"""
-
+
+ DEFAULT_PLOT_CURVE_SYMBOL_MODE = False
+ """Whether to display curves with markers or not by default in PlotWidget.
+
+ It will have an influence on PlotWidget curve items.
+
+ .. versionadded:: 0.10
+ """
+
+ DEFAULT_PLOT_SYMBOL = 'o'
+ """Default marker of the item.
+
+ It will have an influence on PlotWidget items
+
+ Supported symbols:
+
+ - 'o', 'Circle'
+ - 'd', 'Diamond'
+ - 's', 'Square'
+ - '+', 'Plus'
+ - 'x', 'Cross'
+ - '.', 'Point'
+ - ',', 'Pixel'
+ - '', 'None'
+
+ .. versionadded:: 0.10
+ """
+
+ DEFAULT_PLOT_SYMBOL_SIZE = 6.0
+ """Default marker size of the item.
+
+ It will have an influence on PlotWidget items
+
+ .. versionadded:: 0.10
+ """
diff --git a/silx/app/convert.py b/silx/app/convert.py
index a8c2783..7e601ce 100644
--- a/silx/app/convert.py
+++ b/silx/app/convert.py
@@ -23,29 +23,23 @@
# ############################################################################*/
"""Convert silx supported data files into HDF5 files"""
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "05/02/2019"
+
import ast
import os
import argparse
from glob import glob
import logging
-import numpy
import re
import time
+import numpy
+import six
import silx.io
from silx.io.specfile import is_specfile
-from silx.third_party import six
-
-try:
- from silx.io import fabioh5
-except ImportError:
- fabioh5 = None
-
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "12/09/2017"
-
+from silx.io import fabioh5
_logger = logging.getLogger(__name__)
"""Module logger"""
@@ -306,20 +300,14 @@ def main(argv):
_logger.debug("Backtrace", exc_info=True)
hdf5plugin = None
+ import h5py
+
try:
- import h5py
from silx.io.convert import write_to_h5
except ImportError:
_logger.debug("Backtrace", exc_info=True)
- h5py = None
write_to_h5 = None
- if h5py is None:
- message = "Module 'h5py' is not installed but is mandatory."\
- + " You can install it using \"pip install h5py\"."
- _logger.error(message)
- return -1
-
if hdf5plugin is None:
message = "Module 'hdf5plugin' is not installed. It supports additional hdf5"\
+ " compressions. You can install it using \"pip install hdf5plugin\"."
@@ -455,7 +443,11 @@ def main(argv):
create_dataset_args["chunks"] = chunks
if options.compression is not None:
- create_dataset_args["compression"] = options.compression
+ try:
+ compression = int(options.compression)
+ except ValueError:
+ compression = options.compression
+ create_dataset_args["compression"] = compression
if options.compression_opts is not None:
create_dataset_args["compression_opts"] = options.compression_opts
@@ -470,18 +462,6 @@ def main(argv):
not contains_specfile(options.input_files) and
not options.add_root_group) or options.file_pattern is not None:
# File series -> stack of images
- if fabioh5 is None:
- # return a helpful error message if fabio is missing
- try:
- import fabio
- except ImportError:
- _logger.error("The fabio library is required to convert"
- " edf files. Please install it with 'pip "
- "install fabio` and try again.")
- else:
- # unexpected problem in silx.io.fabioh5
- raise
- return -1
input_group = fabioh5.File(file_series=options.input_files)
if hdf5_path != "/":
# we want to append only data and headers to an existing file
diff --git a/silx/app/test/test_convert.py b/silx/app/test/test_convert.py
index 97be3fd..bb1ae99 100644
--- a/silx/app/test/test_convert.py
+++ b/silx/app/test/test_convert.py
@@ -35,11 +35,7 @@ import tempfile
import unittest
import io
import gc
-
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
import silx
from .. import convert
@@ -103,14 +99,6 @@ class TestConvertCommand(unittest.TestCase):
result = e.args[0]
self.assertEqual(result, 0)
- @testutils.test_logging(convert._logger.name, error=1)
- def testH5pyNotInstalled(self):
- with testutils.EnsureImportError("h5py"):
- result = convert.main(["convert", "foo.spec", "bar.edf"])
- # we explicitly return -1 if h5py is not imported
- self.assertNotEqual(result, 0)
-
- @unittest.skipIf(h5py is None, "h5py is required to test convert")
def testWrongOption(self):
# presence of a wrong option must cause a SystemExit or a return
# with a non-zero status
@@ -120,14 +108,12 @@ class TestConvertCommand(unittest.TestCase):
result = e.args[0]
self.assertNotEqual(result, 0)
- @unittest.skipIf(h5py is None, "h5py is required to test convert")
@testutils.test_logging(convert._logger.name, error=3)
# one error log per missing file + one "Aborted" error log
def testWrongFiles(self):
result = convert.main(["convert", "foo.spec", "bar.edf"])
self.assertNotEqual(result, 0)
- @unittest.skipIf(h5py is None, "h5py is required to test convert")
def testFile(self):
# create a writable temp directory
tempdir = tempfile.mkdtemp()
diff --git a/silx/app/view/About.py b/silx/app/view/About.py
index 4b804f2..a2b430f 100644
--- a/silx/app/view/About.py
+++ b/silx/app/view/About.py
@@ -1,6 +1,6 @@
# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -27,6 +27,7 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "05/07/2018"
+import os
import sys
from silx.gui import qt
@@ -208,7 +209,7 @@ class About(qt.QDialog):
pass
# Access to the logo in SVG or PNG
- logo = icons.getQFile("../logo/silx")
+ logo = icons.getQFile("silx:" + os.path.join("gui", "logo", "silx"))
info = dict(
application_name=self.__applicationName,
diff --git a/silx/app/view/Viewer.py b/silx/app/view/Viewer.py
index 88ff989..2daa2df 100644
--- a/silx/app/view/Viewer.py
+++ b/silx/app/view/Viewer.py
@@ -1,6 +1,6 @@
# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "08/10/2018"
+__date__ = "15/01/2019"
import os
@@ -41,6 +41,7 @@ from .ApplicationContext import ApplicationContext
from .CustomNxdataWidget import CustomNxdataWidget
from .CustomNxdataWidget import CustomNxDataToolBar
from . import utils
+from silx.gui.utils import projecturl
from .DataPanel import DataPanel
@@ -61,6 +62,9 @@ class Viewer(qt.QMainWindow):
qt.QMainWindow.__init__(self, parent)
self.setWindowTitle("Silx viewer")
+ silxIcon = icons.getQIcon("silx")
+ self.setWindowIcon(silxIcon)
+
self.__context = ApplicationContext(self, settings)
self.__context.restoreLibrarySettings()
@@ -74,6 +78,7 @@ class Viewer(qt.QMainWindow):
rightPanel.setOrientation(qt.Qt.Vertical)
self.__splitter2 = rightPanel
+ self.__displayIt = None
self.__treeWindow = self.__createTreeWindow(self.__treeview)
# Custom the model to be able to manage the life cycle of the files
@@ -129,8 +134,10 @@ class Viewer(qt.QMainWindow):
treeModel = self.__treeview.findHdf5TreeModel()
columns = list(treeModel.COLUMN_IDS)
- columns.remove(treeModel.DESCRIPTION_COLUMN)
+ columns.remove(treeModel.VALUE_COLUMN)
columns.remove(treeModel.NODE_COLUMN)
+ columns.remove(treeModel.DESCRIPTION_COLUMN)
+ columns.insert(1, treeModel.DESCRIPTION_COLUMN)
self.__treeview.header().setSections(columns)
self._iconUpward = icons.getQIcon('plot-yup')
@@ -150,11 +157,26 @@ class Viewer(qt.QMainWindow):
action.setText("Refresh")
action.setToolTip("Refresh all selected items")
action.triggered.connect(self.__refreshSelected)
- action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_Plus))
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_F5))
toolbar.addAction(action)
treeView.addAction(action)
self.__refreshAction = action
+ # Another shortcut for refresh
+ action = qt.QAction(toolbar)
+ action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_R))
+ treeView.addAction(action)
+ action.triggered.connect(self.__refreshSelected)
+
+ action = qt.QAction(toolbar)
+ # action.setIcon(icons.getQIcon("view-refresh"))
+ action.setText("Close")
+ action.setToolTip("Close selected item")
+ action.triggered.connect(self.__removeSelected)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_Delete))
+ treeView.addAction(action)
+ self.__closeAction = action
+
toolbar.addSeparator()
action = qt.QAction(toolbar)
@@ -185,6 +207,37 @@ class Viewer(qt.QMainWindow):
layout.addWidget(treeView)
return widget
+ def __removeSelected(self):
+ """Close selected items"""
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+
+ selection = self.__treeview.selectionModel()
+ indexes = selection.selectedIndexes()
+ selectedItems = []
+ model = self.__treeview.model()
+ h5files = set([])
+ while len(indexes) > 0:
+ index = indexes.pop(0)
+ if index.column() != 0:
+ continue
+ h5 = model.data(index, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ rootIndex = index
+ # Reach the root of the tree
+ while rootIndex.parent().isValid():
+ rootIndex = rootIndex.parent()
+ rootRow = rootIndex.row()
+ relativePath = self.__getRelativePath(model, rootIndex, index)
+ selectedItems.append((rootRow, relativePath))
+ h5files.add(h5.file)
+
+ if len(h5files) != 0:
+ model = self.__treeview.findHdf5TreeModel()
+ for h5 in h5files:
+ row = model.h5pyObjectRow(h5)
+ model.removeH5pyObject(h5)
+
+ qt.QApplication.restoreOverrideCursor()
+
def __refreshSelected(self):
"""Refresh all selected items
"""
@@ -387,6 +440,9 @@ class Viewer(qt.QMainWindow):
def __h5FileLoaded(self, loadedH5):
self.__context.pushRecentFile(loadedH5.file.filename)
+ if loadedH5.file.filename == self.__displayIt:
+ self.__displayIt = None
+ self.displayData(loadedH5)
def __h5FileRemoved(self, removedH5):
self.__dataPanel.removeDatasetsFrom(removedH5)
@@ -402,10 +458,11 @@ class Viewer(qt.QMainWindow):
self.__context.saveSettings()
# Clean up as much as possible Python objects
- model = self.__customNxdata.model()
- model.clear()
- model = self.__treeview.findHdf5TreeModel()
- model.clear()
+ self.displayData(None)
+ customModel = self.__customNxdata.model()
+ customModel.clear()
+ hdf5Model = self.__treeview.findHdf5TreeModel()
+ hdf5Model.clear()
def saveSettings(self, settings):
"""Save the window settings to this settings object
@@ -502,6 +559,11 @@ class Viewer(qt.QMainWindow):
action.triggered.connect(self.about)
self._aboutAction = action
+ action = qt.QAction("&Documentation", self)
+ action.setStatusTip("Show the Silx library's documentation")
+ action.triggered.connect(self.showDocumentation)
+ self._documentationAction = action
+
# Plot backend
action = qt.QAction("Plot rendering backend", self)
@@ -563,7 +625,7 @@ class Viewer(qt.QMainWindow):
action = qt.QAction("Show custom NXdata selector", self)
action.setStatusTip("Show a widget which allow to create plot by selecting data and axes")
action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_F5))
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_F6))
action.toggled.connect(self.__toggleCustomNxdataWindow)
self._displayCustomNxdataWindow = action
@@ -662,6 +724,7 @@ class Viewer(qt.QMainWindow):
helpMenu = self.menuBar().addMenu("&Help")
helpMenu.addAction(self._aboutAction)
+ helpMenu.addAction(self._documentationAction)
def open(self):
dialog = self.createFileDialog()
@@ -691,18 +754,11 @@ class Viewer(qt.QMainWindow):
for description, ext in silx.io.supported_extensions().items():
extensions[description] = " ".join(sorted(list(ext)))
- try:
- # NOTE: hdf5plugin have to be loaded before
- import fabio
- except Exception:
- _logger.debug("Backtrace while loading fabio", exc_info=True)
- fabio = None
-
- if fabio is not None:
- extensions["NeXus layout from EDF files"] = "*.edf"
- extensions["NeXus layout from TIFF image files"] = "*.tif *.tiff"
- extensions["NeXus layout from CBF files"] = "*.cbf"
- extensions["NeXus layout from MarCCD image files"] = "*.mccd"
+ # Add extensions supported by fabio
+ extensions["NeXus layout from EDF files"] = "*.edf"
+ extensions["NeXus layout from TIFF image files"] = "*.tif *.tiff"
+ extensions["NeXus layout from CBF files"] = "*.cbf"
+ extensions["NeXus layout from MarCCD image files"] = "*.mccd"
all_supported_extensions = set()
for name, exts in extensions.items():
@@ -724,6 +780,11 @@ class Viewer(qt.QMainWindow):
from .About import About
About.about(self, "Silx viewer")
+ def showDocumentation(self):
+ subpath = "index.html"
+ url = projecturl.getDocumentationUrl(subpath)
+ qt.QDesktopServices.openUrl(qt.QUrl(url))
+
def __forcePlotImageDownward(self):
silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = "downward"
@@ -737,6 +798,9 @@ class Viewer(qt.QMainWindow):
silx.config.DEFAULT_PLOT_BACKEND = "opengl"
def appendFile(self, filename):
+ if self.__displayIt is None:
+ # Store the file to display it (loading could be async)
+ self.__displayIt = filename
self.__treeview.findHdf5TreeModel().appendFile(filename)
def displaySelectedData(self):
@@ -823,7 +887,7 @@ class Viewer(qt.QMainWindow):
menu.addAction(action)
if silx.io.is_file(h5):
- action = qt.QAction("Remove %s" % obj.local_filename, event.source())
+ action = qt.QAction("Close %s" % obj.local_filename, event.source())
action.triggered.connect(lambda: self.__treeview.findHdf5TreeModel().removeH5pyObject(h5))
menu.addAction(action)
action = qt.QAction("Synchronize %s" % obj.local_filename, event.source())
diff --git a/silx/app/view/main.py b/silx/app/view/main.py
index fc89a22..8139175 100644
--- a/silx/app/view/main.py
+++ b/silx/app/view/main.py
@@ -1,6 +1,6 @@
# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -25,36 +25,18 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "07/06/2018"
+__date__ = "17/01/2019"
-import sys
import argparse
import logging
+import os
import signal
+import sys
_logger = logging.getLogger(__name__)
"""Module logger"""
-if "silx.gui.qt" not in sys.modules:
- # Try first PyQt5 and not the priority imposed by silx.gui.qt.
- # To avoid problem with unittests we only do it if silx.gui.qt is not
- # yet loaded.
- # TODO: Can be removed for silx 0.8, as it should be the default binding
- # of the silx library.
- try:
- import PyQt5.QtCore
- except ImportError:
- pass
-
-import silx
-from silx.gui import qt
-
-
-def sigintHandler(*args):
- """Handler for the SIGINT signal."""
- qt.QApplication.quit()
-
def createParser():
parser = argparse.ArgumentParser(description=__doc__)
@@ -80,19 +62,17 @@ def createParser():
action="store_true",
default=False,
help='Start the application using new fresh user preferences')
+ parser.add_argument(
+ '--hdf5-file-locking',
+ dest="hdf5_file_locking",
+ action="store_true",
+ default=False,
+ help='Start the application with HDF5 file locking enabled (it is disabled by default)')
return parser
-def main(argv):
- """
- Main function to launch the viewer as an application
-
- :param argv: Command line arguments
- :returns: exit status
- """
- parser = createParser()
- options = parser.parse_args(argv[1:])
-
+def mainQt(options):
+ """Part of the main depending on Qt"""
if options.debug:
logging.root.setLevel(logging.DEBUG)
@@ -100,31 +80,33 @@ def main(argv):
# Import most of the things here to be sure to use the right logging level
#
+ # This needs to be done prior to load HDF5
+ hdf5_file_locking = 'TRUE' if options.hdf5_file_locking else 'FALSE'
+ _logger.info('Set HDF5_USE_FILE_LOCKING=%s', hdf5_file_locking)
+ os.environ['HDF5_USE_FILE_LOCKING'] = hdf5_file_locking
+
try:
# it should be loaded before h5py
import hdf5plugin # noqa
except ImportError:
_logger.debug("Backtrace", exc_info=True)
- try:
- import h5py
- except ImportError:
- _logger.debug("Backtrace", exc_info=True)
- h5py = None
-
- if h5py is None:
- message = "Module 'h5py' is not installed but is mandatory."\
- + " You can install it using \"pip install h5py\"."
- _logger.error(message)
- return -1
+ import h5py
- #
- # Run the application
- #
+ import silx
+ import silx.utils.files
+ from silx.gui import qt
+ # Make sure matplotlib is configured
+ # Needed for Debian 8: compatibility between Qt4/Qt5 and old matplotlib
+ from silx.gui.plot import matplotlib
app = qt.QApplication([])
qt.QLocale.setDefault(qt.QLocale.c())
+ def sigintHandler(*args):
+ """Handler for the SIGINT signal."""
+ qt.QApplication.quit()
+
signal.signal(signal.SIGINT, sigintHandler)
sys.excepthook = qt.exceptionHandler
@@ -150,7 +132,11 @@ def main(argv):
# It have to be done after the settings (after the Viewer creation)
silx.config.DEFAULT_PLOT_BACKEND = "opengl"
+ # NOTE: under Windows, cmd does not convert `*.tif` into existing files
+ options.files = silx.utils.files.expand_filenames(options.files)
+
for filename in options.files:
+ # TODO: Would be nice to add a process widget and a cancel button
try:
window.appendFile(filename)
except IOError as e:
@@ -164,5 +150,17 @@ def main(argv):
return result
+def main(argv):
+ """
+ Main function to launch the viewer as an application
+
+ :param argv: Command line arguments
+ :returns: exit status
+ """
+ parser = createParser()
+ options = parser.parse_args(argv[1:])
+ mainQt(options)
+
+
if __name__ == '__main__':
main(sys.argv)
diff --git a/silx/app/view/test/test_view.py b/silx/app/view/test/test_view.py
index ebcd405..6601dce 100644
--- a/silx/app/view/test/test_view.py
+++ b/silx/app/view/test/test_view.py
@@ -35,10 +35,7 @@ import numpy
import tempfile
import shutil
import os.path
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
from silx.gui import qt
from silx.app.view.Viewer import Viewer
@@ -56,22 +53,21 @@ def setUpModule():
global _tmpDirectory
_tmpDirectory = tempfile.mkdtemp(prefix=__name__)
- if h5py is not None:
- # create h5 data
- filename = _tmpDirectory + "/data.h5"
- f = h5py.File(filename, "w")
- g = f.create_group("arrays")
- g.create_dataset("scalar", data=10)
- g.create_dataset("integers", data=numpy.array([10, 20, 30]))
- f.close()
+ # create h5 data
+ filename = _tmpDirectory + "/data.h5"
+ f = h5py.File(filename, "w")
+ g = f.create_group("arrays")
+ g.create_dataset("scalar", data=10)
+ g.create_dataset("integers", data=numpy.array([10, 20, 30]))
+ f.close()
- # create h5 data
- filename = _tmpDirectory + "/data2.h5"
- f = h5py.File(filename, "w")
- g = f.create_group("arrays")
- g.create_dataset("scalar", data=20)
- g.create_dataset("integers", data=numpy.array([10, 20, 30]))
- f.close()
+ # create h5 data
+ filename = _tmpDirectory + "/data2.h5"
+ f = h5py.File(filename, "w")
+ g = f.create_group("arrays")
+ g.create_dataset("scalar", data=20)
+ g.create_dataset("integers", data=numpy.array([10, 20, 30]))
+ f.close()
def tearDownModule():
@@ -167,7 +163,6 @@ class TestDataPanel(TestCaseQt):
self.assertIs(widget.getData(), None)
self.assertIs(widget.getCustomNxdataItem(), data)
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testRemoveDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
try:
@@ -179,7 +174,6 @@ class TestDataPanel(TestCaseQt):
widget.setData(None)
f.close()
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testReplaceDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
f2 = h5py.File(os.path.join(_tmpDirectory, "data2.h5"))
@@ -248,7 +242,6 @@ class TestCustomNxdataWidget(TestCaseQt):
self.assertIsNotNone(nxdata)
self.assertFalse(item.isValid())
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testRemoveDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
try:
@@ -261,7 +254,6 @@ class TestCustomNxdataWidget(TestCaseQt):
model.clear()
f.close()
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testReplaceDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
f2 = h5py.File(os.path.join(_tmpDirectory, "data2.h5"))
diff --git a/silx/gui/_glutils/Context.py b/silx/gui/_glutils/Context.py
index 7600992..c62dbb9 100644
--- a/silx/gui/_glutils/Context.py
+++ b/silx/gui/_glutils/Context.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -32,32 +32,44 @@ __authors__ = ["T. Vincent"]
__license__ = "MIT"
__date__ = "25/07/2016"
+import contextlib
-# context #####################################################################
+class _DEFAULT_CONTEXT(object):
+ """The default value for OpenGL context"""
+ pass
-def _defaultGLContextGetter():
- return None
+_context = _DEFAULT_CONTEXT
+"""The current OpenGL context"""
-_glContextGetter = _defaultGLContextGetter
-
-def getGLContext():
+def getCurrent():
"""Returns platform dependent object of current OpenGL context.
This is useful to associate OpenGL resources with the context they are
created in.
:return: Platform specific OpenGL context
- :rtype: None by default or a platform dependent object"""
- return _glContextGetter()
+ """
+ return _context
+
+
+def setCurrent(context=_DEFAULT_CONTEXT):
+ """Set a platform dependent OpenGL context
+
+ :param context: Platform dependent GL context
+ """
+ global _context
+ _context = context
-def setGLContextGetter(getter=_defaultGLContextGetter):
- """Set a platform dependent function to retrieve the current OpenGL context
+@contextlib.contextmanager
+def current(context):
+ """Context manager setting the platform-dependent GL context
- :param getter: Platform dependent GL context getter
- :type getter: Function with no args returning the current OpenGL context
+ :param context: Platform dependent GL context
"""
- global _glContextGetter
- _glContextGetter = getter
+ previous_context = getCurrent()
+ setCurrent(context)
+ yield
+ setCurrent(previous_context)
diff --git a/silx/gui/_glutils/OpenGLWidget.py b/silx/gui/_glutils/OpenGLWidget.py
index 7f600a0..c5ece9c 100644
--- a/silx/gui/_glutils/OpenGLWidget.py
+++ b/silx/gui/_glutils/OpenGLWidget.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,7 +30,7 @@ across Qt<=5.3 QtOpenGL.QGLWidget and QOpenGLWidget.
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "26/07/2017"
+__date__ = "22/11/2019"
import logging
@@ -192,7 +192,12 @@ else:
# Check OpenGL version
if self.getOpenGLVersion() >= self.getRequestedOpenGLVersion():
- version = gl.glGetString(gl.GL_VERSION)
+ try:
+ gl.glGetError() # clear any previous error (if any)
+ version = gl.glGetString(gl.GL_VERSION)
+ except:
+ version = None
+
if version:
self.__isValid = True
else:
diff --git a/silx/gui/_glutils/Program.py b/silx/gui/_glutils/Program.py
index 48c12f5..87eec5f 100644
--- a/silx/gui/_glutils/Program.py
+++ b/silx/gui/_glutils/Program.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,11 +30,11 @@ __date__ = "25/07/2016"
import logging
+import weakref
import numpy
-from . import gl
-from .Context import getGLContext
+from . import Context, gl
_logger = logging.getLogger(__name__)
@@ -61,7 +61,7 @@ class Program(object):
self._vertexShader = vertexShader
self._fragmentShader = fragmentShader
self._attrib0 = attrib0
- self._programs = {}
+ self._programs = weakref.WeakKeyDictionary()
@staticmethod
def _compileGL(vertexShader, fragmentShader, attrib0):
@@ -106,7 +106,7 @@ class Program(object):
return program, attributes, uniforms
def _getProgramInfo(self):
- glcontext = getGLContext()
+ glcontext = Context.getCurrent()
if glcontext not in self._programs:
raise RuntimeError(
"Program was not compiled for current OpenGL context.")
@@ -149,7 +149,7 @@ class Program(object):
def use(self):
"""Make use of the program, compiling it if necessary"""
- glcontext = getGLContext()
+ glcontext = Context.getCurrent()
if glcontext not in self._programs:
self._programs[glcontext] = self._compileGL(
diff --git a/silx/gui/_glutils/Texture.py b/silx/gui/_glutils/Texture.py
index 0875ebe..a7fd44b 100644
--- a/silx/gui/_glutils/Texture.py
+++ b/silx/gui/_glutils/Texture.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,7 +29,11 @@ __license__ = "MIT"
__date__ = "04/10/2016"
-import collections
+try:
+ from collections import abc
+except ImportError: # Python2 support
+ import collections as abc
+
from ctypes import c_void_p
import logging
@@ -93,7 +97,7 @@ class Texture(object):
self.magFilter = magFilter if magFilter is not None else gl.GL_LINEAR
if wrap is not None:
- if not isinstance(wrap, collections.Iterable):
+ if not isinstance(wrap, abc.Iterable):
wrap = [wrap] * self.ndim
assert len(wrap) == self.ndim
diff --git a/silx/gui/_glutils/__init__.py b/silx/gui/_glutils/__init__.py
index 15e48e1..e88affd 100644
--- a/silx/gui/_glutils/__init__.py
+++ b/silx/gui/_glutils/__init__.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -34,9 +34,10 @@ __date__ = "25/07/2016"
# OpenGL convenient functions
from .OpenGLWidget import OpenGLWidget # noqa
-from .Context import getGLContext, setGLContextGetter # noqa
+from . import Context # noqa
from .FramebufferTexture import FramebufferTexture # noqa
from .Program import Program # noqa
from .Texture import Texture # noqa
from .VertexBuffer import VertexBuffer, VertexBufferAttrib, vertexBuffer # noqa
from .utils import sizeofGLType, isSupportedGLType, numpyToGLType # noqa
+from .utils import segmentTrianglesIntersection # noqa
diff --git a/silx/gui/_glutils/utils.py b/silx/gui/_glutils/utils.py
index 73af338..35cf819 100644
--- a/silx/gui/_glutils/utils.py
+++ b/silx/gui/_glutils/utils.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2014-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2014-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -68,3 +68,74 @@ def isSupportedGLType(type_):
def numpyToGLType(type_):
"""Returns the GL type corresponding the provided numpy type or dtype."""
return _TYPE_CONVERTER[numpy.dtype(type_)]
+
+
+def segmentTrianglesIntersection(segment, triangles):
+ """Check for segment/triangles intersection.
+
+ This is based on signed tetrahedron volume comparison.
+
+ See A. Kensler, A., Shirley, P.
+ Optimizing Ray-Triangle Intersection via Automated Search.
+ Symposium on Interactive Ray Tracing, vol. 0, p33-38 (2006)
+
+ :param numpy.ndarray segment:
+ Segment end points as a 2x3 array of coordinates
+ :param numpy.ndarray triangles:
+ Nx3x3 array of triangles
+ :return: (triangle indices, segment parameter, barycentric coord)
+ Indices of intersected triangles, "depth" along the segment
+ of the intersection point and barycentric coordinates of intersection
+ point in the triangle.
+ :rtype: List[numpy.ndarray]
+ """
+ # TODO triangles from vertices + indices
+ # TODO early rejection? e.g., check segment bbox vs triangle bbox
+ segment = numpy.asarray(segment)
+ assert segment.ndim == 2
+ assert segment.shape == (2, 3)
+
+ triangles = numpy.asarray(triangles)
+ assert triangles.ndim == 3
+ assert triangles.shape[1] == 3
+
+ # Test line/triangles intersection
+ d = segment[1] - segment[0]
+ t0s0 = segment[0] - triangles[:, 0, :]
+ edge01 = triangles[:, 1, :] - triangles[:, 0, :]
+ edge02 = triangles[:, 2, :] - triangles[:, 0, :]
+
+ dCrossEdge02 = numpy.cross(d, edge02)
+ t0s0CrossEdge01 = numpy.cross(t0s0, edge01)
+ volume = numpy.sum(dCrossEdge02 * edge01, axis=1)
+ del edge01
+ subVolumes = numpy.empty((len(triangles), 3), dtype=triangles.dtype)
+ subVolumes[:, 1] = numpy.sum(dCrossEdge02 * t0s0, axis=1)
+ del dCrossEdge02
+ subVolumes[:, 2] = numpy.sum(t0s0CrossEdge01 * d, axis=1)
+ subVolumes[:, 0] = volume - subVolumes[:, 1] - subVolumes[:, 2]
+ intersect = numpy.logical_or(
+ numpy.all(subVolumes >= 0., axis=1), # All positive
+ numpy.all(subVolumes <= 0., axis=1)) # All negative
+ intersect = numpy.where(intersect)[0] # Indices of intersected triangles
+
+ # Get barycentric coordinates
+ barycentric = subVolumes[intersect] / volume[intersect].reshape(-1, 1)
+ del subVolumes
+
+ # Test segment/triangles intersection
+ volAlpha = numpy.sum(t0s0CrossEdge01[intersect] * edge02[intersect], axis=1)
+ t = volAlpha / volume[intersect] # segment parameter of intersected triangles
+ del t0s0CrossEdge01
+ del edge02
+ del volAlpha
+ del volume
+
+ inSegmentMask = numpy.logical_and(t >= 0., t <= 1.)
+ intersect = intersect[inSegmentMask]
+ t = t[inSegmentMask]
+ barycentric = barycentric[inSegmentMask]
+
+ # Sort intersecting triangles by t
+ indices = numpy.argsort(t)
+ return intersect[indices], t[indices], barycentric[indices]
diff --git a/silx/gui/colors.py b/silx/gui/colors.py
index a51bcdc..365b569 100644..100755
--- a/silx/gui/colors.py
+++ b/silx/gui/colors.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2015-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2015-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,18 +29,28 @@ from __future__ import absolute_import
__authors__ = ["T. Vincent", "H.Payno"]
__license__ = "MIT"
-__date__ = "05/10/2018"
+__date__ = "29/01/2019"
-from silx.gui import qt
-import copy as copy_mdl
import numpy
import logging
+import collections
+from silx.gui import qt
+from silx import config
from silx.math.combo import min_max
from silx.math.colormap import cmap as _cmap
from silx.utils.exceptions import NotEditableError
+from silx.utils import deprecation
+from silx.resources import resource_filename as _resource_filename
+
_logger = logging.getLogger(__file__)
+try:
+ from matplotlib import cm as _matplotlib_cm
+except ImportError:
+ _logger.info("matplotlib not available, only embedded colormaps available")
+ _matplotlib_cm = None
+
_COLORDICT = {}
"""Dictionary of common colors."""
@@ -67,17 +77,51 @@ _COLORDICT['darkBrown'] = '#660000'
_COLORDICT['darkCyan'] = '#008080'
_COLORDICT['darkYellow'] = '#808000'
_COLORDICT['darkMagenta'] = '#800080'
+_COLORDICT['transparent'] = '#00000000'
# FIXME: It could be nice to expose a functional API instead of that attribute
COLORDICT = _COLORDICT
+_LUT_DESCRIPTION = collections.namedtuple("_LUT_DESCRIPTION", ["source", "cursor_color", "preferred"])
+"""Description of a LUT for internal purpose."""
+
+
+_AVAILABLE_LUTS = collections.OrderedDict([
+ ('gray', _LUT_DESCRIPTION('builtin', 'pink', True)),
+ ('reversed gray', _LUT_DESCRIPTION('builtin', 'pink', True)),
+ ('temperature', _LUT_DESCRIPTION('builtin', 'pink', True)),
+ ('red', _LUT_DESCRIPTION('builtin', 'green', True)),
+ ('green', _LUT_DESCRIPTION('builtin', 'pink', True)),
+ ('blue', _LUT_DESCRIPTION('builtin', 'yellow', True)),
+ ('jet', _LUT_DESCRIPTION('matplotlib', 'pink', True)),
+ ('viridis', _LUT_DESCRIPTION('resource', 'pink', True)),
+ ('cividis', _LUT_DESCRIPTION('resource', 'pink', True)),
+ ('magma', _LUT_DESCRIPTION('resource', 'green', True)),
+ ('inferno', _LUT_DESCRIPTION('resource', 'green', True)),
+ ('plasma', _LUT_DESCRIPTION('resource', 'green', True)),
+ ('hsv', _LUT_DESCRIPTION('matplotlib', 'black', True)),
+])
+"""Description for internal porpose of all the default LUT provided by the library."""
+
+
+DEFAULT_MIN_LIN = 0
+"""Default min value if in linear normalization"""
+DEFAULT_MAX_LIN = 1
+"""Default max value if in linear normalization"""
+DEFAULT_MIN_LOG = 1
+"""Default min value if in log normalization"""
+DEFAULT_MAX_LOG = 10
+"""Default max value if in log normalization"""
+
+
def rgba(color, colorDict=None):
- """Convert color code '#RRGGBB' and '#RRGGBBAA' to (R, G, B, A)
+ """Convert color code '#RRGGBB' and '#RRGGBBAA' to a tuple (R, G, B, A)
+ of floats.
- It also convert RGB(A) values from uint8 to float in [0, 1] and
- accept a QColor as color argument.
+ It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and
+ QColor as color argument.
:param str color: The color to convert
:param dict colorDict: A dictionary of color name conversion to color code
@@ -121,19 +165,34 @@ def rgba(color, colorDict=None):
return r, g, b, a
-_COLORMAP_CURSOR_COLORS = {
- 'gray': 'pink',
- 'reversed gray': 'pink',
- 'temperature': 'pink',
- 'red': 'green',
- 'green': 'pink',
- 'blue': 'yellow',
- 'jet': 'pink',
- 'viridis': 'pink',
- 'magma': 'green',
- 'inferno': 'green',
- 'plasma': 'green',
-}
+def greyed(color, colorDict=None):
+ """Convert color code '#RRGGBB' and '#RRGGBBAA' to a grey color
+ (R, G, B, A).
+
+ It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and
+ QColor as color argument.
+
+ :param str color: The color to convert
+ :param dict colorDict: A dictionary of color name conversion to color code
+ :returns: RGBA colors as floats in [0., 1.]
+ :rtype: tuple
+ """
+ r, g, b, a = rgba(color=color, colorDict=colorDict)
+ g = 0.21 * r + 0.72 * g + 0.07 * b
+ return g, g, g, a
+
+
+def asQColor(color):
+ """Convert color code '#RRGGBB' and '#RRGGBBAA' to a `qt.QColor`.
+
+ It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and
+ QColor as color argument.
+
+ :param str color: The color to convert
+ :rtype: qt.QColor
+ """
+ color = rgba(color)
+ return qt.QColor.fromRgbF(*color)
def cursorColorForColormap(colormapName):
@@ -143,26 +202,140 @@ def cursorColorForColormap(colormapName):
:return: Name of the color.
:rtype: str
"""
- return _COLORMAP_CURSOR_COLORS.get(colormapName, 'black')
+ description = _AVAILABLE_LUTS.get(colormapName, None)
+ if description is not None:
+ color = description.cursor_color
+ if color is not None:
+ return color
+ return 'black'
-DEFAULT_COLORMAPS = (
- 'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
-"""Tuple of supported colormap names."""
+# Colormap loader
+
+_COLORMAP_CACHE = {}
+"""Cache already used colormaps as name: color LUT"""
-DEFAULT_MIN_LIN = 0
-"""Default min value if in linear normalization"""
-DEFAULT_MAX_LIN = 1
-"""Default max value if in linear normalization"""
-DEFAULT_MIN_LOG = 1
-"""Default min value if in log normalization"""
-DEFAULT_MAX_LOG = 10
-"""Default max value if in log normalization"""
+
+def _arrayToRgba8888(colors):
+ """Convert colors from a numpy array using float (0..1) int or uint
+ (0..255) to uint8 RGBA.
+
+ :param numpy.ndarray colors: Array of float int or uint colors to convert
+ :return: colors as uint8
+ :rtype: numpy.ndarray
+ """
+ assert len(colors.shape) == 2
+ assert colors.shape[1] in (3, 4)
+
+ if colors.dtype == numpy.uint8:
+ pass
+ elif colors.dtype.kind == 'f':
+ # Each bin is [N, N+1[ except the last one: [255, 256]
+ colors = numpy.clip(colors.astype(numpy.float64) * 256, 0., 255.)
+ colors = colors.astype(numpy.uint8)
+ elif colors.dtype.kind in 'iu':
+ colors = numpy.clip(colors, 0, 255)
+ colors = colors.astype(numpy.uint8)
+
+ if colors.shape[1] == 3:
+ tmp = numpy.empty((len(colors), 4), dtype=numpy.uint8)
+ tmp[:, 0:3] = colors
+ tmp[:, 3] = 255
+ colors = tmp
+
+ return colors
+
+
+def _createColormapLut(name):
+ """Returns the color LUT corresponding to a colormap name
+
+ :param str name: Name of the colormap to load
+ :returns: Corresponding table of colors
+ :rtype: numpy.ndarray
+ :raise ValueError: If no colormap corresponds to name
+ """
+ description = _AVAILABLE_LUTS.get(name)
+ use_mpl = False
+ if description is not None:
+ if description.source == "builtin":
+ # Build colormap LUT
+ lut = numpy.zeros((256, 4), dtype=numpy.uint8)
+ lut[:, 3] = 255
+
+ if name == 'gray':
+ lut[:, :3] = numpy.arange(256, dtype=numpy.uint8).reshape(-1, 1)
+ elif name == 'reversed gray':
+ lut[:, :3] = numpy.arange(255, -1, -1, dtype=numpy.uint8).reshape(-1, 1)
+ elif name == 'red':
+ lut[:, 0] = numpy.arange(256, dtype=numpy.uint8)
+ elif name == 'green':
+ lut[:, 1] = numpy.arange(256, dtype=numpy.uint8)
+ elif name == 'blue':
+ lut[:, 2] = numpy.arange(256, dtype=numpy.uint8)
+ elif name == 'temperature':
+ # Red
+ lut[128:192, 0] = numpy.arange(2, 255, 4, dtype=numpy.uint8)
+ lut[192:, 0] = 255
+ # Green
+ lut[:64, 1] = numpy.arange(0, 255, 4, dtype=numpy.uint8)
+ lut[64:192, 1] = 255
+ lut[192:, 1] = numpy.arange(252, -1, -4, dtype=numpy.uint8)
+ # Blue
+ lut[:64, 2] = 255
+ lut[64:128, 2] = numpy.arange(254, 0, -4, dtype=numpy.uint8)
+ else:
+ raise RuntimeError("Built-in colormap not implemented")
+ return lut
+
+ elif description.source == "resource":
+ # Load colormap LUT
+ colors = numpy.load(_resource_filename("gui/colormaps/%s.npy" % name))
+ # Convert to uint8 and add alpha channel
+ lut = _arrayToRgba8888(colors)
+ return lut
+
+ elif description.source == "matplotlib":
+ use_mpl = True
+
+ else:
+ raise RuntimeError("Internal LUT source '%s' unsupported" % description.source)
+
+ # Here it expect a matplotlib LUTs
+
+ if use_mpl:
+ # matplotlib is mandatory
+ if _matplotlib_cm is None:
+ raise ValueError("The colormap '%s' expect matplotlib, but matplotlib is not installed" % name)
+
+ if _matplotlib_cm is not None: # Try to load with matplotlib
+ colormap = _matplotlib_cm.get_cmap(name)
+ lut = colormap(numpy.linspace(0, 1, colormap.N, endpoint=True))
+ lut = _arrayToRgba8888(lut)
+ return lut
+
+ raise ValueError("Unknown colormap '%s'" % name)
+
+
+def _getColormap(name):
+ """Returns the color LUT corresponding to a colormap name
+
+ :param str name: Name of the colormap to load
+ :returns: Corresponding table of colors
+ :rtype: numpy.ndarray
+ :raise ValueError: If no colormap corresponds to name
+ """
+ name = str(name)
+ if name not in _COLORMAP_CACHE:
+ lut = _createColormapLut(name)
+ _COLORMAP_CACHE[name] = lut
+ return _COLORMAP_CACHE[name]
class Colormap(qt.QObject):
"""Description of a colormap
+ If no `name` nor `colors` are provided, a default gray LUT is used.
+
:param str name: Name of the colormap
:param tuple colors: optional, custom colormap.
Nx3 or Nx4 numpy array of RGB(A) colors,
@@ -187,10 +360,11 @@ class Colormap(qt.QObject):
sigChanged = qt.Signal()
"""Signal emitted when the colormap has changed."""
- def __init__(self, name='gray', colors=None, normalization=LINEAR, vmin=None, vmax=None):
+ def __init__(self, name=None, colors=None, normalization=LINEAR, vmin=None, vmax=None):
qt.QObject.__init__(self)
+ self._editable = True
+
assert normalization in Colormap.NORMALIZATIONS
- assert not (name is None and colors is None)
if normalization is Colormap.LOGARITHM:
if (vmin is not None and vmin < 0) or (vmax is not None and vmax < 0):
m = "Unsuported vmin (%s) and/or vmax (%s) given for a log scale."
@@ -200,78 +374,76 @@ class Colormap(qt.QObject):
vmin = None
vmax = None
- self._name = str(name) if name is not None else None
- self._setColors(colors)
+ self._name = None
+ self._colors = None
+
+ if colors is not None and name is not None:
+ deprecation.deprecated_warning("Argument",
+ name="silx.gui.plot.Colors",
+ reason="name and colors can't be used at the same time",
+ since_version="0.10.0",
+ skip_backtrace_count=1)
+
+ colors = None
+
+ if name is not None:
+ self.setName(name) # And resets colormap LUT
+ elif colors is not None:
+ self.setColormapLUT(colors)
+ else:
+ # Default colormap is grey
+ self.setName("gray")
+
self._normalization = str(normalization)
self._vmin = float(vmin) if vmin is not None else None
self._vmax = float(vmax) if vmax is not None else None
- self._editable = True
- def isAutoscale(self):
- """Return True if both min and max are in autoscale mode"""
- return self._vmin is None and self._vmax is None
+ def setFromColormap(self, other):
+ """Set this colormap using information from the `other` colormap.
- def getName(self):
- """Return the name of the colormap
- :rtype: str
+ :param ~silx.gui.colors.Colormap other: Colormap to use as reference.
"""
- return self._name
-
- @staticmethod
- def _convertColorsFromFloatToUint8(colors):
- """Convert colors from float in [0, 1] to uint8
-
- :param numpy.ndarray colors: Array of float colors to convert
- :return: colors as uint8
- :rtype: numpy.ndarray
- """
- # Each bin is [N, N+1[ except the last one: [255, 256]
- return numpy.clip(
- colors.astype(numpy.float64) * 256, 0., 255.).astype(numpy.uint8)
-
- def _setColors(self, colors):
- if colors is None:
- self._colors = None
+ if not self.isEditable():
+ raise NotEditableError('Colormap is not editable')
+ if self == other:
+ return
+ old = self.blockSignals(True)
+ name = other.getName()
+ if name is not None:
+ self.setName(name)
else:
- colors = numpy.array(colors, copy=False)
- if colors.shape == ():
- raise TypeError("An array is expected for 'colors' argument. '%s' was found." % type(colors))
- colors.shape = -1, colors.shape[-1]
- if colors.dtype.kind == 'f':
- colors = self._convertColorsFromFloatToUint8(colors)
-
- # Makes sure it is RGBA8888
- self._colors = numpy.zeros((len(colors), 4), dtype=numpy.uint8)
- self._colors[:, 3] = 255 # Alpha channel
- self._colors[:, :colors.shape[1]] = colors # Copy colors
+ self.setColormapLUT(other.getColormapLUT())
+ self.setNormalization(other.getNormalization())
+ self.setVRange(other.getVMin(), other.getVMax())
+ self.blockSignals(old)
+ self.sigChanged.emit()
def getNColors(self, nbColors=None):
"""Returns N colors computed by sampling the colormap regularly.
:param nbColors:
The number of colors in the returned array or None for the default value.
- The default value is 256 for colormap with a name (see :meth:`setName`) and
- it is the size of the LUT for colormap defined with :meth:`setColormapLUT`.
+ The default value is the size of the colormap LUT.
:type nbColors: int or None
:return: 2D array of uint8 of shape (nbColors, 4)
:rtype: numpy.ndarray
"""
# Handle default value for nbColors
if nbColors is None:
- lut = self.getColormapLUT()
- if lut is not None: # In this case uses LUT length
- nbColors = len(lut)
- else: # Default to 256
- nbColors = 256
-
- nbColors = int(nbColors)
+ return numpy.array(self._colors, copy=True)
+ else:
+ colormap = self.copy()
+ colormap.setNormalization(Colormap.LINEAR)
+ colormap.setVRange(vmin=None, vmax=None)
+ colors = colormap.applyToData(
+ numpy.arange(int(nbColors), dtype=numpy.int))
+ return colors
- colormap = self.copy()
- colormap.setNormalization(Colormap.LINEAR)
- colormap.setVRange(vmin=None, vmax=None)
- colors = colormap.applyToData(
- numpy.arange(nbColors, dtype=numpy.int))
- return colors
+ def getName(self):
+ """Return the name of the colormap
+ :rtype: str
+ """
+ return self._name
def setName(self, name):
"""Set the name of the colormap to use.
@@ -281,23 +453,31 @@ class Colormap(qt.QObject):
'reversed gray', 'temperature', 'red', 'green', 'blue', 'jet',
'viridis', 'magma', 'inferno', 'plasma'.
"""
+ name = str(name)
+ if self._name == name:
+ return
if self.isEditable() is False:
raise NotEditableError('Colormap is not editable')
- assert name in self.getSupportedColormaps()
- self._name = str(name)
- self._colors = None
+ if name not in self.getSupportedColormaps():
+ raise ValueError("Colormap name '%s' is not supported" % name)
+ self._name = name
+ self._colors = _getColormap(self._name)
self.sigChanged.emit()
- def getColormapLUT(self):
- """Return the list of colors for the colormap or None if not set
+ def getColormapLUT(self, copy=True):
+ """Return the list of colors for the colormap or None if not set.
+ This returns None if the colormap was set with :meth:`setName`.
+ Use :meth:`getNColors` to get the colormap LUT for any colormap.
+
+ :param bool copy: If true a copy of the numpy array is provided
:return: the list of colors for the colormap or None if not set
:rtype: numpy.ndarray or None
"""
- if self._colors is None:
- return None
+ if self._name is None:
+ return numpy.array(self._colors, copy=copy)
else:
- return numpy.array(self._colors, copy=True)
+ return None
def setColormapLUT(self, colors):
"""Set the colors of the colormap.
@@ -310,10 +490,15 @@ class Colormap(qt.QObject):
"""
if self.isEditable() is False:
raise NotEditableError('Colormap is not editable')
- self._setColors(colors)
- if len(colors) is 0:
- self._colors = None
-
+ assert colors is not None
+
+ colors = numpy.array(colors, copy=False)
+ if colors.shape == ():
+ raise TypeError("An array is expected for 'colors' argument. '%s' was found." % type(colors))
+ assert len(colors) != 0
+ assert colors.ndim >= 2
+ colors.shape = -1, colors.shape[-1]
+ self._colors = _arrayToRgba8888(colors)
self._name = None
self.sigChanged.emit()
@@ -335,6 +520,10 @@ class Colormap(qt.QObject):
self._normalization = str(norm)
self.sigChanged.emit()
+ def isAutoscale(self):
+ """Return True if both min and max are in autoscale mode"""
+ return self._vmin is None and self._vmax is None
+
def getVMin(self):
"""Return the lower bound of the colormap
@@ -504,7 +693,7 @@ class Colormap(qt.QObject):
"""
return {
'name': self._name,
- 'colors': copy_mdl.copy(self._colors),
+ 'colors': self.getColormapLUT(),
'vmin': self._vmin,
'vmax': self._vmax,
'autoscale': self.isAutoscale(),
@@ -546,8 +735,10 @@ class Colormap(qt.QObject):
if dic.get('autoscale', False):
vmin, vmax = None, None
- self._name = name
- self._colors = colors
+ if name is not None:
+ self.setName(name)
+ else:
+ self.setColormapLUT(colors)
self._vmin = vmin
self._vmax = vmax
self._autoscale = True if (vmin is None and vmax is None) else False
@@ -557,7 +748,7 @@ class Colormap(qt.QObject):
@staticmethod
def _fromDict(dic):
- colormap = Colormap(name="")
+ colormap = Colormap()
colormap._setFromDict(dic)
return colormap
@@ -567,7 +758,7 @@ class Colormap(qt.QObject):
:rtype: silx.gui.colors.Colormap
"""
return Colormap(name=self._name,
- colors=copy_mdl.copy(self._colors),
+ colors=self.getColormapLUT(),
vmin=self._vmin,
vmax=self._vmax,
normalization=self._normalization)
@@ -577,34 +768,30 @@ class Colormap(qt.QObject):
:param numpy.ndarray data: The data to convert.
"""
- name = self.getName()
- if name is not None: # Get colormap definition from matplotlib
- # FIXME: If possible remove dependency to the plot
- from .plot.matplotlib import Colormap as MPLColormap
- mplColormap = MPLColormap.getColormap(name)
- colors = mplColormap(numpy.linspace(0, 1, 256, endpoint=True))
- colors = self._convertColorsFromFloatToUint8(colors)
-
- else: # Use user defined LUT
- colors = self.getColormapLUT()
-
vmin, vmax = self.getColormapRange(data)
normalization = self.getNormalization()
-
- return _cmap(data, colors, vmin, vmax, normalization)
+ return _cmap(data, self._colors, vmin, vmax, normalization)
@staticmethod
def getSupportedColormaps():
"""Get the supported colormap names as a tuple of str.
The list should at least contain and start by:
- ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
+
+ ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue',
+ 'viridis', 'magma', 'inferno', 'plasma')
+
:rtype: tuple
"""
- # FIXME: If possible remove dependency to the plot
- from .plot.matplotlib import Colormap as MPLColormap
- maps = MPLColormap.getSupportedColormaps()
- return DEFAULT_COLORMAPS + maps
+ colormaps = set()
+ if _matplotlib_cm is not None:
+ colormaps.update(_matplotlib_cm.cmap_d.keys())
+ colormaps.update(_AVAILABLE_LUTS.keys())
+
+ colormaps = tuple(cmap for cmap in sorted(colormaps)
+ if cmap not in _AVAILABLE_LUTS.keys())
+
+ return tuple(_AVAILABLE_LUTS.keys()) + colormaps
def __str__(self):
return str(self._toDict())
@@ -617,6 +804,10 @@ class Colormap(qt.QObject):
def __eq__(self, other):
"""Compare colormap values and not pointers"""
+ if other is None:
+ return False
+ if not isinstance(other, Colormap):
+ return False
return (self.getName() == other.getName() and
self.getNormalization() == other.getNormalization() and
self.getVMin() == other.getVMin() and
@@ -710,13 +901,14 @@ def preferredColormaps():
"""
global _PREFERRED_COLORMAPS
if _PREFERRED_COLORMAPS is None:
- _PREFERRED_COLORMAPS = DEFAULT_COLORMAPS
# Initialize preferred colormaps
- setPreferredColormaps(('gray', 'reversed gray',
- 'temperature', 'red', 'green', 'blue', 'jet',
- 'viridis', 'magma', 'inferno', 'plasma',
- 'hsv'))
- return _PREFERRED_COLORMAPS
+ default_preferred = []
+ for name, info in _AVAILABLE_LUTS.items():
+ if (info.preferred and
+ (info.source != 'matplotlib' or _matplotlib_cm is not None)):
+ default_preferred.append(name)
+ setPreferredColormaps(default_preferred)
+ return tuple(_PREFERRED_COLORMAPS)
def setPreferredColormaps(colormaps):
@@ -730,10 +922,41 @@ def setPreferredColormaps(colormaps):
:raise ValueError: if the list of available preferred colormaps is empty.
"""
supportedColormaps = Colormap.getSupportedColormaps()
- colormaps = tuple(
- cmap for cmap in colormaps if cmap in supportedColormaps)
+ colormaps = [cmap for cmap in colormaps if cmap in supportedColormaps]
if len(colormaps) == 0:
raise ValueError("Cannot set preferred colormaps to an empty list")
global _PREFERRED_COLORMAPS
_PREFERRED_COLORMAPS = colormaps
+
+
+def registerLUT(name, colors, cursor_color='black', preferred=True):
+ """Register a custom LUT to be used with `Colormap` objects.
+
+ It can override existing LUT names.
+
+ :param str name: Name of the LUT as defined to configure colormaps
+ :param numpy.ndarray colors: The custom LUT to register.
+ Nx3 or Nx4 numpy array of RGB(A) colors,
+ either uint8 or float in [0, 1].
+ :param bool preferred: If true, this LUT will be displayed as part of the
+ preferred colormaps in dialogs.
+ :param str cursor_color: Color used to display overlay over images using
+ colormap with this LUT.
+ """
+ description = _LUT_DESCRIPTION('user', cursor_color, preferred=preferred)
+ colors = _arrayToRgba8888(colors)
+ _AVAILABLE_LUTS[name] = description
+
+ if preferred:
+ # Invalidate the preferred cache
+ global _PREFERRED_COLORMAPS
+ if _PREFERRED_COLORMAPS is not None:
+ if name not in _PREFERRED_COLORMAPS:
+ _PREFERRED_COLORMAPS.append(name)
+ else:
+ # The cache is not yet loaded, it's fine
+ pass
+
+ # Register the cache as the LUT was already loaded
+ _COLORMAP_CACHE[name] = colors
diff --git a/silx/gui/console.py b/silx/gui/console.py
index b6341ef..5dc6336 100644
--- a/silx/gui/console.py
+++ b/silx/gui/console.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -87,17 +87,26 @@ else:
msg = "Module " + __name__ + " cannot be used within an IPython shell"
raise ImportError(msg)
-
try:
- from qtconsole.rich_ipython_widget import RichJupyterWidget as \
- RichIPythonWidget
+ from qtconsole.rich_jupyter_widget import RichJupyterWidget as \
+ _RichJupyterWidget
except ImportError:
- from qtconsole.rich_ipython_widget import RichIPythonWidget
+ try:
+ from qtconsole.rich_ipython_widget import RichJupyterWidget as \
+ _RichJupyterWidget
+ except ImportError:
+ from qtconsole.rich_ipython_widget import RichIPythonWidget as \
+ _RichJupyterWidget
from qtconsole.inprocess import QtInProcessKernelManager
+try:
+ from ipykernel import version_info as _ipykernel_version_info
+except ImportError:
+ _ipykernel_version_info = None
+
-class IPythonWidget(RichIPythonWidget):
+class IPythonWidget(_RichJupyterWidget):
"""Live IPython console widget.
.. image:: img/IPythonWidget.png
@@ -115,6 +124,16 @@ class IPythonWidget(RichIPythonWidget):
self.setWindowTitle(self.banner)
self.kernel_manager = kernel_manager = QtInProcessKernelManager()
kernel_manager.start_kernel()
+
+ # Monkey-patch to workaround issue:
+ # https://github.com/ipython/ipykernel/issues/370
+ if (_ipykernel_version_info is not None and
+ _ipykernel_version_info[0] > 4 and
+ _ipykernel_version_info[:3] <= (5, 1, 0)):
+ def _abort_queues(*args, **kwargs):
+ pass
+ kernel_manager.kernel._abort_queues = _abort_queues
+
self.kernel_client = kernel_client = self._kernel_manager.client()
kernel_client.start_channels()
@@ -178,5 +197,6 @@ def main():
widget.show()
app.exec_()
+
if __name__ == '__main__':
main()
diff --git a/silx/gui/data/ArrayTableModel.py b/silx/gui/data/ArrayTableModel.py
index ad4d33a..8805241 100644
--- a/silx/gui/data/ArrayTableModel.py
+++ b/silx/gui/data/ArrayTableModel.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -245,8 +245,7 @@ class ArrayTableModel(qt.QAbstractTableModel):
if index.isValid() and role == qt.Qt.EditRole:
try:
# cast value to same type as array
- v = numpy.asscalar(
- numpy.array(value, dtype=self._array.dtype))
+ v = numpy.array(value, dtype=self._array.dtype).item()
except ValueError:
return False
diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py
index 4db2863..bad4362 100644
--- a/silx/gui/data/DataViewer.py
+++ b/silx/gui/data/DataViewer.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,13 +31,13 @@ from silx.gui.data import DataViews
from silx.gui.data.DataViews import _normalizeData
import logging
from silx.gui import qt
+from silx.gui.utils import blockSignals
from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
-from silx.utils import deprecation
-from silx.utils.property import classproperty
+
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "24/04/2018"
+__date__ = "12/02/2019"
_logger = logging.getLogger(__name__)
@@ -70,66 +70,6 @@ class DataViewer(qt.QFrame):
viewer.setVisible(True)
"""
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.EMPTY_MODE", since_version="0.7", skip_backtrace_count=2)
- def EMPTY_MODE(self):
- return DataViews.EMPTY_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.PLOT1D_MODE", since_version="0.7", skip_backtrace_count=2)
- def PLOT1D_MODE(self):
- return DataViews.PLOT1D_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.PLOT2D_MODE", since_version="0.7", skip_backtrace_count=2)
- def PLOT2D_MODE(self):
- return DataViews.PLOT2D_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.PLOT3D_MODE", since_version="0.7", skip_backtrace_count=2)
- def PLOT3D_MODE(self):
- return DataViews.PLOT3D_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.RAW_MODE", since_version="0.7", skip_backtrace_count=2)
- def RAW_MODE(self):
- return DataViews.RAW_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.RAW_ARRAY_MODE", since_version="0.7", skip_backtrace_count=2)
- def RAW_ARRAY_MODE(self):
- return DataViews.RAW_ARRAY_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.RAW_RECORD_MODE", since_version="0.7", skip_backtrace_count=2)
- def RAW_RECORD_MODE(self):
- return DataViews.RAW_RECORD_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.RAW_SCALAR_MODE", since_version="0.7", skip_backtrace_count=2)
- def RAW_SCALAR_MODE(self):
- return DataViews.RAW_SCALAR_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.STACK_MODE", since_version="0.7", skip_backtrace_count=2)
- def STACK_MODE(self):
- return DataViews.STACK_MODE
-
- # TODO: Can be removed for silx 0.8
- @classproperty
- @deprecation.deprecated(replacement="DataViews.HDF5_MODE", since_version="0.7", skip_backtrace_count=2)
- def HDF5_MODE(self):
- return DataViews.HDF5_MODE
-
displayedViewChanged = qt.Signal(object)
"""Emitted when the displayed view changes"""
@@ -259,25 +199,38 @@ class DataViewer(qt.QFrame):
"""
Update the numpy-selector according to the needed axis names
"""
- previous = self.__numpySelection.blockSignals(True)
- self.__numpySelection.clear()
- info = self._getInfo()
- axisNames = self.__currentView.axesNames(self.__data, info)
- if info.isArray and info.size != 0 and self.__data is not None and axisNames is not None:
- self.__useAxisSelection = True
- self.__numpySelection.setAxisNames(axisNames)
- self.__numpySelection.setCustomAxis(self.__currentView.customAxisNames())
- data = self.normalizeData(self.__data)
- self.__numpySelection.setData(data)
- if hasattr(data, "shape"):
- isVisible = not (len(axisNames) == 1 and len(data.shape) == 1)
+ with blockSignals(self.__numpySelection):
+ previousPermutation = self.__numpySelection.permutation()
+ previousSelection = self.__numpySelection.selection()
+
+ self.__numpySelection.clear()
+
+ info = self._getInfo()
+ axisNames = self.__currentView.axesNames(self.__data, info)
+ if (info.isArray and info.size != 0 and
+ self.__data is not None and axisNames is not None):
+ self.__useAxisSelection = True
+ self.__numpySelection.setAxisNames(axisNames)
+ self.__numpySelection.setCustomAxis(
+ self.__currentView.customAxisNames())
+ data = self.normalizeData(self.__data)
+ self.__numpySelection.setData(data)
+
+ # Try to restore previous permutation and selection
+ try:
+ self.__numpySelection.setSelection(
+ previousSelection, previousPermutation)
+ except ValueError as e:
+ _logger.info("Not restoring selection because: %s", e)
+
+ if hasattr(data, "shape"):
+ isVisible = not (len(axisNames) == 1 and len(data.shape) == 1)
+ else:
+ isVisible = True
+ self.__axisSelection.setVisible(isVisible)
else:
- isVisible = True
- self.__axisSelection.setVisible(isVisible)
- else:
- self.__useAxisSelection = False
- self.__axisSelection.setVisible(False)
- self.__numpySelection.blockSignals(previous)
+ self.__useAxisSelection = False
+ self.__axisSelection.setVisible(False)
def __updateDataInView(self):
"""
@@ -288,6 +241,7 @@ class DataViewer(qt.QFrame):
else:
self.__displayedData = self.__data
+ # TODO: would be good to avoid that, it should be synchonous
qt.QTimer.singleShot(10, self.__setDataInView)
def __setDataInView(self):
@@ -405,18 +359,16 @@ class DataViewer(qt.QFrame):
data = self.__data
info = self._getInfo()
# sort available views according to priority
- priorities = [v.getDataPriority(data, info) for v in self.__views]
- views = zip(priorities, self.__views)
+ views = []
+ for v in self.__views:
+ views.extend(v.getMatchingViews(data, info))
+ views = [(v.getCachedDataPriority(data, info), v) for v in views]
views = filter(lambda t: t[0] > DataViews.DataView.UNSUPPORTED, views)
views = sorted(views, reverse=True)
+ views = [v[1] for v in views]
# store available views
- if len(views) == 0:
- self.__setCurrentAvailableViews([])
- available = []
- else:
- available = [v[1] for v in views]
- self.__setCurrentAvailableViews(available)
+ self.__setCurrentAvailableViews(views)
def __updateView(self):
"""Display the data using the widget which fit the best"""
@@ -447,7 +399,7 @@ class DataViewer(qt.QFrame):
priority to lowest.
:rtype: DataView
"""
- hdf5View = self.getViewFromModeId(DataViewer.HDF5_MODE)
+ hdf5View = self.getViewFromModeId(DataViews.HDF5_MODE)
if hdf5View in available:
return hdf5View
return self.getViewFromModeId(DataViews.EMPTY_MODE)
@@ -487,6 +439,17 @@ class DataViewer(qt.QFrame):
"""
return self.__currentAvailableViews
+ def getReachableViews(self):
+ """Returns the list of reachable views from the registred available
+ views.
+
+ :rtype: List[DataView]
+ """
+ views = []
+ for v in self.availableViews():
+ views.extend(v.getReachableViews())
+ return views
+
def availableViews(self):
"""Returns the list of registered views
diff --git a/silx/gui/data/DataViewerFrame.py b/silx/gui/data/DataViewerFrame.py
index 4e6d2e8..9bfb95b 100644
--- a/silx/gui/data/DataViewerFrame.py
+++ b/silx/gui/data/DataViewerFrame.py
@@ -27,7 +27,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "24/04/2018"
+__date__ = "12/02/2019"
from silx.gui import qt
from .DataViewer import DataViewer
@@ -120,6 +120,9 @@ class DataViewerFrame(qt.QWidget):
"""
self.__dataViewer.setGlobalHooks(hooks)
+ def getReachableViews(self):
+ return self.__dataViewer.getReachableViews()
+
def availableViews(self):
"""Returns the list of registered views
diff --git a/silx/gui/data/DataViewerSelector.py b/silx/gui/data/DataViewerSelector.py
index 35bbe99..a1e9947 100644
--- a/silx/gui/data/DataViewerSelector.py
+++ b/silx/gui/data/DataViewerSelector.py
@@ -29,7 +29,7 @@ from __future__ import division
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "23/01/2018"
+__date__ = "12/02/2019"
import weakref
import functools
@@ -85,7 +85,7 @@ class DataViewerSelector(qt.QWidget):
iconSize = qt.QSize(16, 16)
- for view in self.__dataViewer.availableViews():
+ for view in self.__dataViewer.getReachableViews():
label = view.label()
icon = view.icon()
button = qt.QPushButton(label)
@@ -155,7 +155,7 @@ class DataViewerSelector(qt.QWidget):
self.__dataViewer.setDisplayedView(view)
def __checkAvailableButtons(self):
- views = set(self.__dataViewer.availableViews())
+ views = set(self.__dataViewer.getReachableViews())
if views == set(self.__buttons.keys()):
return
# Recreate all the buttons
diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py
index 2291e87..eb635c4 100644
--- a/silx/gui/data/DataViews.py
+++ b/silx/gui/data/DataViews.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,6 +31,7 @@ import numbers
import numpy
import silx.io
+from silx.utils import deprecation
from silx.gui import qt, icons
from silx.gui.data.TextFormatter import TextFormatter
from silx.io import nxdata
@@ -41,7 +42,7 @@ from silx.gui.dialog.ColormapDialog import ColormapDialog
__authors__ = ["V. Valls", "P. Knobel"]
__license__ = "MIT"
-__date__ = "23/05/2018"
+__date__ = "19/02/2019"
_logger = logging.getLogger(__name__)
@@ -67,6 +68,8 @@ NXDATA_CURVE_MODE = 73
NXDATA_XYVSCATTER_MODE = 74
NXDATA_IMAGE_MODE = 75
NXDATA_STACK_MODE = 76
+NXDATA_VOLUME_MODE = 77
+NXDATA_VOLUME_AS_STACK_MODE = 78
def _normalizeData(data):
@@ -100,6 +103,7 @@ class DataInfo(object):
"""Store extracted information from a data"""
def __init__(self, data):
+ self.__priorities = {}
data = self.normalizeData(data)
self.isArray = False
self.interpretation = None
@@ -131,9 +135,6 @@ class DataInfo(object):
elif nx_class == "NXdata":
# group claiming to be NXdata could not be parsed
self.isInvalidNXdata = True
- elif nx_class == "NXentry" and "default" in data.attrs:
- # entry claiming to have a default NXdata could not be parsed
- self.isInvalidNXdata = True
elif nx_class == "NXroot" or silx.io.is_file(data):
# root claiming to have a default entry
if "default" in data.attrs:
@@ -141,6 +142,9 @@ class DataInfo(object):
if def_entry in data and "default" in data[def_entry].attrs:
# and entry claims to have default NXdata
self.isInvalidNXdata = True
+ elif "default" in data.attrs:
+ # group claiming to have a default NXdata could not be parsed
+ self.isInvalidNXdata = True
if isinstance(data, numpy.ndarray):
self.isArray = True
@@ -201,6 +205,12 @@ class DataInfo(object):
Else returns the data."""
return _normalizeData(data)
+ def cachePriority(self, view, priority):
+ self.__priorities[view] = priority
+
+ def getPriority(self, view):
+ return self.__priorities[view]
+
class DataViewHooks(object):
"""A set of hooks defined to custom the behaviour of the data views."""
@@ -357,6 +367,35 @@ class DataView(object):
"""
return []
+ def getReachableViews(self):
+ """Returns the views that can be returned by `getMatchingViews`.
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: List[DataView]
+ """
+ return [self]
+
+ def getMatchingViews(self, data, info):
+ """Returns the views according to data and info from the data.
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: List[DataView]
+ """
+ priority = self.getCachedDataPriority(data, info)
+ if priority == DataView.UNSUPPORTED:
+ return []
+ return [self]
+
+ def getCachedDataPriority(self, data, info):
+ try:
+ priority = info.getPriority(self)
+ except KeyError:
+ priority = self.getDataPriority(data, info)
+ info.cachePriority(self, priority)
+ return priority
+
def getDataPriority(self, data, info):
"""
Returns the priority of using this view according to a data.
@@ -377,7 +416,53 @@ class DataView(object):
return str(self) < str(other)
-class CompositeDataView(DataView):
+class _CompositeDataView(DataView):
+ """Contains sub views"""
+
+ def getViews(self):
+ """Returns the direct sub views registered in this view.
+
+ :rtype: List[DataView]
+ """
+ raise NotImplementedError()
+
+ def getReachableViews(self):
+ """Returns all views that can be reachable at on point.
+
+ This method return any sub view provided (recursivly).
+
+ :rtype: List[DataView]
+ """
+ raise NotImplementedError()
+
+ def getMatchingViews(self, data, info):
+ """Returns sub views matching this data and info.
+
+ This method return any sub view provided (recursivly).
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: List[DataView]
+ """
+ raise NotImplementedError()
+
+ @deprecation.deprecated(replacement="getReachableViews", since_version="0.10")
+ def availableViews(self):
+ return self.getViews()
+
+ def isSupportedData(self, data, info):
+ """If true, the composite view allow sub views to access to this data.
+ Else this this data is considered as not supported by any of sub views
+ (incliding this composite view).
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ :rtype: bool
+ """
+ return True
+
+
+class SelectOneDataView(_CompositeDataView):
"""Data view which can display a data using different view according to
the kind of the data."""
@@ -386,7 +471,7 @@ class CompositeDataView(DataView):
:param qt.QWidget parent: Parent of the hold widget
"""
- super(CompositeDataView, self).__init__(parent, modeId, icon, label)
+ super(SelectOneDataView, self).__init__(parent, modeId, icon, label)
self.__views = OrderedDict()
self.__currentView = None
@@ -395,7 +480,7 @@ class CompositeDataView(DataView):
:param DataViewHooks hooks: The data view hooks to use
"""
- super(CompositeDataView, self).setHooks(hooks)
+ super(SelectOneDataView, self).setHooks(hooks)
if hooks is not None:
for v in self.__views:
v.setHooks(hooks)
@@ -407,16 +492,40 @@ class CompositeDataView(DataView):
dataView.setHooks(hooks)
self.__views[dataView] = None
- def availableViews(self):
+ def getReachableViews(self):
+ views = []
+ addSelf = False
+ for v in self.__views:
+ if isinstance(v, SelectManyDataView):
+ views.extend(v.getReachableViews())
+ else:
+ addSelf = True
+ if addSelf:
+ # Single views are hidden by this view
+ views.insert(0, self)
+ return views
+
+ def getMatchingViews(self, data, info):
+ if not self.isSupportedData(data, info):
+ return []
+ view = self.__getBestView(data, info)
+ if isinstance(view, SelectManyDataView):
+ return view.getMatchingViews(data, info)
+ else:
+ return [self]
+
+ def getViews(self):
"""Returns the list of registered views
:rtype: List[DataView]
"""
return list(self.__views.keys())
- def getBestView(self, data, info):
+ def __getBestView(self, data, info):
"""Returns the best view according to priorities."""
- views = [(v.getDataPriority(data, info), v) for v in self.__views.keys()]
+ if not self.isSupportedData(data, info):
+ return None
+ views = [(v.getCachedDataPriority(data, info), v) for v in self.__views.keys()]
views = filter(lambda t: t[0] > DataView.UNSUPPORTED, views)
views = sorted(views, key=lambda t: t[0], reverse=True)
@@ -471,17 +580,17 @@ class CompositeDataView(DataView):
self.__currentView.setData(data)
def axesNames(self, data, info):
- view = self.getBestView(data, info)
+ view = self.__getBestView(data, info)
self.__currentView = view
return view.axesNames(data, info)
def getDataPriority(self, data, info):
- view = self.getBestView(data, info)
+ view = self.__getBestView(data, info)
self.__currentView = view
if view is None:
return DataView.UNSUPPORTED
else:
- return view.getDataPriority(data, info)
+ return view.getCachedDataPriority(data, info)
def replaceView(self, modeId, newView):
"""Replace a data view with a custom view.
@@ -502,7 +611,7 @@ class CompositeDataView(DataView):
if view.modeId() == modeId:
oldView = view
break
- elif isinstance(view, CompositeDataView):
+ elif isinstance(view, _CompositeDataView):
# recurse
hooks = self.getHooks()
if hooks is not None:
@@ -519,6 +628,135 @@ class CompositeDataView(DataView):
return True
+# NOTE: SelectOneDataView was introduced with silx 0.10
+CompositeDataView = SelectOneDataView
+
+
+class SelectManyDataView(_CompositeDataView):
+ """Data view which can select a set of sub views according to
+ the kind of the data.
+
+ This view itself is abstract and is not exposed.
+ """
+
+ def __init__(self, parent, views=None):
+ """Constructor
+
+ :param qt.QWidget parent: Parent of the hold widget
+ """
+ super(SelectManyDataView, self).__init__(parent, modeId=None, icon=None, label=None)
+ if views is None:
+ views = []
+ self.__views = views
+
+ def setHooks(self, hooks):
+ """Set the data context to use with this view.
+
+ :param DataViewHooks hooks: The data view hooks to use
+ """
+ super(SelectManyDataView, self).setHooks(hooks)
+ if hooks is not None:
+ for v in self.__views:
+ v.setHooks(hooks)
+
+ def addView(self, dataView):
+ """Add a new dataview to the available list."""
+ hooks = self.getHooks()
+ if hooks is not None:
+ dataView.setHooks(hooks)
+ self.__views.append(dataView)
+
+ def getViews(self):
+ """Returns the list of registered views
+
+ :rtype: List[DataView]
+ """
+ return list(self.__views)
+
+ def getReachableViews(self):
+ views = []
+ for v in self.__views:
+ views.extend(v.getReachableViews())
+ return views
+
+ def getMatchingViews(self, data, info):
+ """Returns the views according to data and info from the data.
+
+ :param object data: Any object to be displayed
+ :param DataInfo info: Information cached about this data
+ """
+ if not self.isSupportedData(data, info):
+ return []
+ views = [v for v in self.__views if v.getCachedDataPriority(data, info) != DataView.UNSUPPORTED]
+ return views
+
+ def customAxisNames(self):
+ raise RuntimeError("Abstract view")
+
+ def setCustomAxisValue(self, name, value):
+ raise RuntimeError("Abstract view")
+
+ def select(self):
+ raise RuntimeError("Abstract view")
+
+ def createWidget(self, parent):
+ raise RuntimeError("Abstract view")
+
+ def clear(self):
+ for v in self.__views:
+ v.clear()
+
+ def setData(self, data):
+ raise RuntimeError("Abstract view")
+
+ def axesNames(self, data, info):
+ raise RuntimeError("Abstract view")
+
+ def getDataPriority(self, data, info):
+ if not self.isSupportedData(data, info):
+ return DataView.UNSUPPORTED
+ priorities = [v.getCachedDataPriority(data, info) for v in self.__views]
+ priorities = [v for v in priorities if v != DataView.UNSUPPORTED]
+ priorities = sorted(priorities)
+ if len(priorities) == 0:
+ return DataView.UNSUPPORTED
+ return priorities[-1]
+
+ def replaceView(self, modeId, newView):
+ """Replace a data view with a custom view.
+ Return True in case of success, False in case of failure.
+
+ .. note::
+
+ This method must be called just after instantiation, before
+ the viewer is used.
+
+ :param int modeId: Unique mode ID identifying the DataView to
+ be replaced.
+ :param DataViews.DataView newView: New data view
+ :return: True if replacement was successful, else False
+ """
+ oldView = None
+ for iview, view in enumerate(self.__views):
+ if view.modeId() == modeId:
+ oldView = view
+ break
+ elif isinstance(view, CompositeDataView):
+ # recurse
+ hooks = self.getHooks()
+ if hooks is not None:
+ newView.setHooks(hooks)
+ if view.replaceView(modeId, newView):
+ return True
+
+ if oldView is None:
+ return False
+
+ # replace oldView with new view in dict
+ self.__views[iview] = newView
+ return True
+
+
class _EmptyView(DataView):
"""Dummy view to display nothing"""
@@ -655,53 +893,27 @@ class _Plot3dView(DataView):
label="Cube",
icon=icons.getQIcon("view-3d"))
try:
- import silx.gui.plot3d #noqa
+ from ._VolumeWindow import VolumeWindow # noqa
except ImportError:
- _logger.warning("Plot3dView is not available")
+ _logger.warning("3D visualization is not available")
_logger.debug("Backtrace", exc_info=True)
raise
self.__resetZoomNextTime = True
def createWidget(self, parent):
- from silx.gui.plot3d import ScalarFieldView
- from silx.gui.plot3d import SFViewParamTree
+ from ._VolumeWindow import VolumeWindow
- plot = ScalarFieldView.ScalarFieldView(parent)
+ plot = VolumeWindow(parent)
plot.setAxesLabels(*reversed(self.axesNames(None, None)))
-
- def computeIsolevel(data):
- data = data[numpy.isfinite(data)]
- if len(data) == 0:
- return 0
- else:
- return numpy.mean(data) + numpy.std(data)
-
- plot.addIsosurface(computeIsolevel, '#FF0000FF')
-
- # Create a parameter tree for the scalar field view
- options = SFViewParamTree.TreeView(plot)
- options.setSfView(plot)
-
- # Add the parameter tree to the main window in a dock widget
- dock = qt.QDockWidget()
- dock.setWidget(options)
- plot.addDockWidget(qt.Qt.RightDockWidgetArea, dock)
-
return plot
def clear(self):
- self.getWidget().setData(None)
+ self.getWidget().clear()
self.__resetZoomNextTime = True
- def normalizeData(self, data):
- data = DataView.normalizeData(self, data)
- data = _normalizeComplex(data)
- return data
-
def setData(self, data):
data = self.normalizeData(data)
- plot = self.getWidget()
- plot.setData(data)
+ self.getWidget().setData(data)
self.__resetZoomNextTime = False
def axesNames(self, data, info):
@@ -735,10 +947,10 @@ class _ComplexImageView(DataView):
def createWidget(self, parent):
from silx.gui.plot.ComplexImageView import ComplexImageView
widget = ComplexImageView(parent=parent)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.ABSOLUTE)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.SQUARE_AMPLITUDE)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.REAL)
- widget.setColormap(self.defaultColormap(), mode=ComplexImageView.Mode.IMAGINARY)
+ widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.ABSOLUTE)
+ widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.SQUARE_AMPLITUDE)
+ widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.REAL)
+ widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.IMAGINARY)
widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
widget.getPlot().getIntensityHistogramAction().setVisible(True)
widget.getPlot().setKeepDataAspectRatio(True)
@@ -1096,17 +1308,6 @@ class _InvalidNXdataView(DataView):
# invalid: could not even be parsed by NXdata
self._msg = "Group has @NX_class = NXdata, but could not be interpreted"
self._msg += " as valid NXdata."
- elif nx_class == "NXentry":
- self._msg = "NXentry group provides a @default attribute,"
- default_nxdata_name = data.attrs["default"]
- if default_nxdata_name not in data:
- self._msg += " but no corresponding NXdata group exists."
- elif get_attr_as_unicode(data[default_nxdata_name], "NX_class") != "NXdata":
- self._msg += " but the corresponding item is not a "
- self._msg += "NXdata group."
- else:
- self._msg += " but the corresponding NXdata seems to be"
- self._msg += " malformed."
elif nx_class == "NXroot" or silx.io.is_file(data):
default_entry = data[data.attrs["default"]]
default_nxdata_name = default_entry.attrs["default"]
@@ -1122,6 +1323,17 @@ class _InvalidNXdataView(DataView):
else:
self._msg += " but the corresponding NXdata seems to be"
self._msg += " malformed."
+ else:
+ self._msg = "Group provides a @default attribute,"
+ default_nxdata_name = data.attrs["default"]
+ if default_nxdata_name not in data:
+ self._msg += " but no corresponding NXdata group exists."
+ elif get_attr_as_unicode(data[default_nxdata_name], "NX_class") != "NXdata":
+ self._msg += " but the corresponding item is not a "
+ self._msg += "NXdata group."
+ else:
+ self._msg += " but the corresponding NXdata seems to be"
+ self._msg += " malformed."
return 100
@@ -1270,14 +1482,15 @@ class _NXdataXYVScatterView(DataView):
data = self.normalizeData(data)
if info.hasNXdata and not info.isInvalidNXdata:
if nxdata.get_default(data, validate=False).is_x_y_value_scatter:
- return 100
+ # It have to be a little more than a NX curve priority
+ return 110
return DataView.UNSUPPORTED
class _NXdataImageView(DataView):
"""DataView using a Plot2D for displaying NXdata images:
- 2-D signal or n-D signals with *@interpretation=spectrum*."""
+ 2-D signal or n-D signals with *@interpretation=image*."""
def __init__(self, parent):
DataView.__init__(self, parent,
modeId=NXDATA_IMAGE_MODE)
@@ -1323,6 +1536,53 @@ class _NXdataImageView(DataView):
return DataView.UNSUPPORTED
+class _NXdataComplexImageView(DataView):
+ """DataView using a ComplexImageView for displaying NXdata complex images:
+ 2-D signal or n-D signals with *@interpretation=image*."""
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ modeId=NXDATA_IMAGE_MODE)
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot
+ widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap())
+ widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ return widget
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+
+ # last two axes are Y & X
+ img_slicing = slice(-2, None)
+ y_axis, x_axis = nxd.axes[img_slicing]
+ y_label, x_label = nxd.axes_names[img_slicing]
+
+ self.getWidget().setImageData(
+ [nxd.signal] + nxd.auxiliary_signals,
+ x_axis=x_axis, y_axis=y_axis,
+ signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names,
+ xlabel=x_label, ylabel=y_label,
+ title=nxd.title)
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+
+ if info.hasNXdata and not info.isInvalidNXdata:
+ nxd = nxdata.get_default(data, validate=False)
+ if nxd.is_image and numpy.iscomplexobj(nxd.signal):
+ return 100
+
+ return DataView.UNSUPPORTED
+
+
class _NXdataStackView(DataView):
def __init__(self, parent):
DataView.__init__(self, parent,
@@ -1368,6 +1628,154 @@ class _NXdataStackView(DataView):
return DataView.UNSUPPORTED
+class _NXdataVolumeView(DataView):
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ label="NXdata (3D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_MODE)
+ try:
+ import silx.gui.plot3d # noqa
+ except ImportError:
+ _logger.warning("Plot3dView is not available")
+ _logger.debug("Backtrace", exc_info=True)
+ raise
+
+ def normalizeData(self, data):
+ data = DataView.normalizeData(self, data)
+ data = _normalizeComplex(data)
+ return data
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayVolumePlot
+ widget = ArrayVolumePlot(parent)
+ return widget
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+ signal_name = nxd.signal_name
+ z_axis, y_axis, x_axis = nxd.axes[-3:]
+ z_label, y_label, x_label = nxd.axes_names[-3:]
+ title = nxd.title or signal_name
+
+ widget = self.getWidget()
+ widget.setData(
+ nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis,
+ signal_name=signal_name,
+ xlabel=x_label, ylabel=y_label, zlabel=z_label,
+ title=title)
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+ if info.hasNXdata and not info.isInvalidNXdata:
+ if nxdata.get_default(data, validate=False).is_volume:
+ return 150
+
+ return DataView.UNSUPPORTED
+
+
+class _NXdataVolumeAsStackView(DataView):
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ label="NXdata (2D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_AS_STACK_MODE)
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayStackPlot
+ widget = ArrayStackPlot(parent)
+ widget.getStackView().setColormap(self.defaultColormap())
+ widget.getStackView().getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ return widget
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+ signal_name = nxd.signal_name
+ z_axis, y_axis, x_axis = nxd.axes[-3:]
+ z_label, y_label, x_label = nxd.axes_names[-3:]
+ title = nxd.title or signal_name
+
+ widget = self.getWidget()
+ widget.setStackData(
+ nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis,
+ signal_name=signal_name,
+ xlabel=x_label, ylabel=y_label, zlabel=z_label,
+ title=title)
+ # Override the colormap, while setStack overwrite it
+ widget.getStackView().setColormap(self.defaultColormap())
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+ if info.isComplex:
+ return DataView.UNSUPPORTED
+ if info.hasNXdata and not info.isInvalidNXdata:
+ if nxdata.get_default(data, validate=False).is_volume:
+ return 200
+
+ return DataView.UNSUPPORTED
+
+class _NXdataComplexVolumeAsStackView(DataView):
+ def __init__(self, parent):
+ DataView.__init__(self, parent,
+ label="NXdata (2D)",
+ icon=icons.getQIcon("view-nexus"),
+ modeId=NXDATA_VOLUME_AS_STACK_MODE)
+ self._is_complex_data = False
+
+ def createWidget(self, parent):
+ from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot
+ widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap())
+ widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog())
+ return widget
+
+ def axesNames(self, data, info):
+ # disabled (used by default axis selector widget in Hdf5Viewer)
+ return None
+
+ def clear(self):
+ self.getWidget().clear()
+
+ def setData(self, data):
+ data = self.normalizeData(data)
+ nxd = nxdata.get_default(data, validate=False)
+ signal_name = nxd.signal_name
+ z_axis, y_axis, x_axis = nxd.axes[-3:]
+ z_label, y_label, x_label = nxd.axes_names[-3:]
+ title = nxd.title or signal_name
+
+ self.getWidget().setImageData(
+ [nxd.signal] + nxd.auxiliary_signals,
+ x_axis=x_axis, y_axis=y_axis,
+ signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names,
+ xlabel=x_label, ylabel=y_label, title=nxd.title)
+
+ def getDataPriority(self, data, info):
+ data = self.normalizeData(data)
+ if not info.isComplex:
+ return DataView.UNSUPPORTED
+ if info.hasNXdata and not info.isInvalidNXdata:
+ if nxdata.get_default(data, validate=False).is_volume:
+ return 200
+
+ return DataView.UNSUPPORTED
+
+
class _NXdataView(CompositeDataView):
"""Composite view displaying NXdata groups using the most adequate
widget depending on the dimensionality."""
@@ -1382,5 +1790,17 @@ class _NXdataView(CompositeDataView):
self.addView(_NXdataScalarView(parent))
self.addView(_NXdataCurveView(parent))
self.addView(_NXdataXYVScatterView(parent))
+ self.addView(_NXdataComplexImageView(parent))
self.addView(_NXdataImageView(parent))
self.addView(_NXdataStackView(parent))
+
+ # The 3D view can be displayed using 2 ways
+ nx3dViews = SelectManyDataView(parent)
+ nx3dViews.addView(_NXdataVolumeAsStackView(parent))
+ nx3dViews.addView(_NXdataComplexVolumeAsStackView(parent))
+ try:
+ nx3dViews.addView(_NXdataVolumeView(parent))
+ except Exception:
+ _logger.warning("NXdataVolumeView is not available")
+ _logger.debug("Backtrace", exc_info=True)
+ self.addView(nx3dViews)
diff --git a/silx/gui/data/Hdf5TableView.py b/silx/gui/data/Hdf5TableView.py
index 9e28fbf..d7c33f3 100644
--- a/silx/gui/data/Hdf5TableView.py
+++ b/silx/gui/data/Hdf5TableView.py
@@ -30,12 +30,14 @@ from __future__ import division
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "05/07/2018"
+__date__ = "12/02/2019"
import collections
import functools
import os.path
import logging
+import h5py
+
from silx.gui import qt
import silx.io
from .TextFormatter import TextFormatter
@@ -44,11 +46,6 @@ from silx.gui.widgets import HierarchicalTableView
from ..hdf5.Hdf5Formatter import Hdf5Formatter
from ..hdf5._utils import htmlFromDict
-try:
- import h5py
-except ImportError:
- h5py = None
-
_logger = logging.getLogger(__name__)
@@ -198,11 +195,9 @@ class _CellFilterAvailableData(_CellData):
}
def __init__(self, filterId):
- import h5py.version
if h5py.version.hdf5_version_tuple >= (1, 10, 2):
# Previous versions only returns True if the filter was first used
# to decode a dataset
- import h5py.h5z
self.__availability = h5py.h5z.filter_avail(filterId)
else:
self.__availability = "na"
@@ -416,7 +411,7 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
self.__data.addHeaderRow(headerLabel="Data info")
- if h5py is not None and hasattr(obj, "id") and hasattr(obj.id, "get_type"):
+ if hasattr(obj, "id") and hasattr(obj.id, "get_type"):
# display the HDF5 type
self.__data.addHeaderValueRow("HDF5 type", self.__formatHdf5Type)
self.__data.addHeaderValueRow("dtype", self.__formatDType)
diff --git a/silx/gui/data/HexaTableView.py b/silx/gui/data/HexaTableView.py
index c86c0af..1617f0a 100644
--- a/silx/gui/data/HexaTableView.py
+++ b/silx/gui/data/HexaTableView.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,11 +28,13 @@ hexadecimal viewer.
"""
from __future__ import division
-import numpy
import collections
+
+import numpy
+import six
+
from silx.gui import qt
import silx.io.utils
-from silx.third_party import six
from silx.gui.widgets.TableWidget import CopySelectedCellsAction
__authors__ = ["V. Valls"]
diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py
index f7c479d..c3aefd3 100644
--- a/silx/gui/data/NXdataWidgets.py
+++ b/silx/gui/data/NXdataWidgets.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,19 +26,24 @@
"""
__authors__ = ["P. Knobel"]
__license__ = "MIT"
-__date__ = "10/10/2018"
+__date__ = "12/11/2018"
+import logging
import numpy
from silx.gui import qt
from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
from silx.gui.plot import Plot1D, Plot2D, StackView, ScatterView
+from silx.gui.plot.ComplexImageView import ComplexImageView
from silx.gui.colors import Colormap
from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
from silx.math.calibration import ArrayCalibration, NoCalibration, LinearCalibration
+_logger = logging.getLogger(__name__)
+
+
class ArrayCurvePlot(qt.QWidget):
"""
Widget for plotting a curve from a multi-dimensional signal array
@@ -72,21 +77,16 @@ class ArrayCurvePlot(qt.QWidget):
self._plot = Plot1D(self)
- self.selectorDock = qt.QDockWidget("Data selector", self._plot)
- # not closable
- self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable |
- qt.QDockWidget.DockWidgetFloatable)
- self._selector = NumpyAxesSelector(self.selectorDock)
+ self._selector = NumpyAxesSelector(self)
self._selector.setNamedAxesSelectorVisibility(False)
self.__selector_is_connected = False
- self.selectorDock.setWidget(self._selector)
- self._plot.addTabbedDockWidget(self.selectorDock)
self._plot.sigActiveCurveChanged.connect(self._setYLabelFromActiveLegend)
- layout = qt.QGridLayout()
+ layout = qt.QVBoxLayout()
layout.setContentsMargins(0, 0, 0, 0)
- layout.addWidget(self._plot, 0, 0)
+ layout.addWidget(self._plot)
+ layout.addWidget(self._selector)
self.setLayout(layout)
@@ -130,9 +130,9 @@ class ArrayCurvePlot(qt.QWidget):
self._selector.setAxisNames(["Y"])
if len(ys[0].shape) < 2:
- self.selectorDock.hide()
+ self._selector.hide()
else:
- self.selectorDock.show()
+ self._selector.show()
self._plot.setGraphTitle(title or "")
self._updateCurve()
@@ -182,6 +182,9 @@ class ArrayCurvePlot(qt.QWidget):
break
def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
self._plot.clear()
@@ -339,11 +342,8 @@ class ArrayImagePlot(qt.QWidget):
normalization=Colormap.LINEAR))
self._plot.getIntensityHistogramAction().setVisible(True)
- self.selectorDock = qt.QDockWidget("Data selector", self._plot)
# not closable
- self.selectorDock.setFeatures(qt.QDockWidget.DockWidgetMovable |
- qt.QDockWidget.DockWidgetFloatable)
- self._selector = NumpyAxesSelector(self.selectorDock)
+ self._selector = NumpyAxesSelector(self)
self._selector.setNamedAxesSelectorVisibility(False)
self._selector.selectionChanged.connect(self._updateImage)
@@ -355,9 +355,8 @@ class ArrayImagePlot(qt.QWidget):
layout = qt.QVBoxLayout()
layout.addWidget(self._plot)
+ layout.addWidget(self._selector)
layout.addWidget(self._auxSigSlider)
- self.selectorDock.setWidget(self._selector)
- self._plot.addTabbedDockWidget(self.selectorDock)
self.setLayout(layout)
@@ -413,9 +412,9 @@ class ArrayImagePlot(qt.QWidget):
self._selector.setData(signals[0])
if len(signals[0].shape) <= img_ndim:
- self.selectorDock.hide()
+ self._selector.hide()
else:
- self.selectorDock.show()
+ self._selector.show()
self._auxSigSlider.setMaximum(len(signals) - 1)
if len(signals) > 1:
@@ -425,6 +424,7 @@ class ArrayImagePlot(qt.QWidget):
self._auxSigSlider.setValue(0)
self._updateImage()
+ self._plot.resetZoom()
self._selector.selectionChanged.connect(self._updateImage)
self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged)
@@ -492,12 +492,202 @@ class ArrayImagePlot(qt.QWidget):
self._plot.setGraphTitle(title)
self._plot.getXAxis().setLabel(self.__x_axis_name)
self._plot.getYAxis().setLabel(self.__y_axis_name)
- self._plot.resetZoom()
def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
self._plot.clear()
+class ArrayComplexImagePlot(qt.QWidget):
+ """
+ Widget for plotting an image of complex from a multi-dimensional signal array
+ and two 1D axes array.
+
+ The signal array can have an arbitrary number of dimensions, the only
+ limitation being that the last two dimensions must have the same length as
+ the axes arrays.
+
+ Sliders are provided to select indices on the first (n - 2) dimensions of
+ the signal array, and the plot is updated to show the image corresponding
+ to the selection.
+
+ If one or both of the axes does not have regularly spaced values, the
+ the image is plotted as a coloured scatter plot.
+ """
+ def __init__(self, parent=None, colormap=None):
+ """
+
+ :param parent: Parent QWidget
+ """
+ super(ArrayComplexImagePlot, self).__init__(parent)
+
+ self.__signals = None
+ self.__signals_names = None
+ self.__x_axis = None
+ self.__x_axis_name = None
+ self.__y_axis = None
+ self.__y_axis_name = None
+
+ self._plot = ComplexImageView(self)
+ if colormap is not None:
+ for mode in (ComplexImageView.ComplexMode.ABSOLUTE,
+ ComplexImageView.ComplexMode.SQUARE_AMPLITUDE,
+ ComplexImageView.ComplexMode.REAL,
+ ComplexImageView.ComplexMode.IMAGINARY):
+ self._plot.setColormap(colormap, mode)
+
+ self._plot.getPlot().getIntensityHistogramAction().setVisible(True)
+ self._plot.setKeepDataAspectRatio(True)
+
+ # not closable
+ self._selector = NumpyAxesSelector(self)
+ self._selector.setNamedAxesSelectorVisibility(False)
+ self._selector.selectionChanged.connect(self._updateImage)
+
+ self._auxSigSlider = HorizontalSliderWithBrowser(parent=self)
+ self._auxSigSlider.setMinimum(0)
+ self._auxSigSlider.setValue(0)
+ self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged)
+ self._auxSigSlider.setToolTip("Select auxiliary signals")
+
+ layout = qt.QVBoxLayout()
+ layout.addWidget(self._plot)
+ layout.addWidget(self._selector)
+ layout.addWidget(self._auxSigSlider)
+
+ self.setLayout(layout)
+
+ def _sliderIdxChanged(self, value):
+ self._updateImage()
+
+ def getPlot(self):
+ """Returns the plot used for the display
+
+ :rtype: PlotWidget
+ """
+ return self._plot.getPlot()
+
+ def setImageData(self, signals,
+ x_axis=None, y_axis=None,
+ signals_names=None,
+ xlabel=None, ylabel=None,
+ title=None):
+ """
+
+ :param signals: list of n-D datasets, whose last 2 dimensions are used as the
+ image's values, or list of 3D datasets interpreted as RGBA image.
+ :param x_axis: 1-D dataset used as the image's x coordinates. If
+ provided, its lengths must be equal to the length of the last
+ dimension of ``signal``.
+ :param y_axis: 1-D dataset used as the image's y. If provided,
+ its lengths must be equal to the length of the 2nd to last
+ dimension of ``signal``.
+ :param signals_names: Names for each image, used as subtitle and legend.
+ :param xlabel: Label for X axis
+ :param ylabel: Label for Y axis
+ :param title: Graph title
+ """
+ self._selector.selectionChanged.disconnect(self._updateImage)
+ self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged)
+
+ self.__signals = signals
+ self.__signals_names = signals_names
+ self.__x_axis = x_axis
+ self.__x_axis_name = xlabel
+ self.__y_axis = y_axis
+ self.__y_axis_name = ylabel
+ self.__title = title
+
+ self._selector.clear()
+ self._selector.setAxisNames(["Y", "X"])
+ self._selector.setData(signals[0])
+
+ if len(signals[0].shape) <= 2:
+ self._selector.hide()
+ else:
+ self._selector.show()
+
+ self._auxSigSlider.setMaximum(len(signals) - 1)
+ if len(signals) > 1:
+ self._auxSigSlider.show()
+ else:
+ self._auxSigSlider.hide()
+ self._auxSigSlider.setValue(0)
+
+ self._updateImage()
+ self._plot.getPlot().resetZoom()
+
+ self._selector.selectionChanged.connect(self._updateImage)
+ self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged)
+
+ def _updateImage(self):
+ selection = self._selector.selection()
+ auxSigIdx = self._auxSigSlider.value()
+
+ images = [img[selection] for img in self.__signals]
+ image = images[auxSigIdx]
+
+ x_axis = self.__x_axis
+ y_axis = self.__y_axis
+
+ if x_axis is None and y_axis is None:
+ xcalib = NoCalibration()
+ ycalib = NoCalibration()
+ else:
+ if x_axis is None:
+ # no calibration
+ x_axis = numpy.arange(image.shape[1])
+ elif numpy.isscalar(x_axis) or len(x_axis) == 1:
+ # constant axis
+ x_axis = x_axis * numpy.ones((image.shape[1], ))
+ elif len(x_axis) == 2:
+ # linear calibration
+ x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1]
+
+ if y_axis is None:
+ y_axis = numpy.arange(image.shape[0])
+ elif numpy.isscalar(y_axis) or len(y_axis) == 1:
+ y_axis = y_axis * numpy.ones((image.shape[0], ))
+ elif len(y_axis) == 2:
+ y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1]
+
+ xcalib = ArrayCalibration(x_axis)
+ ycalib = ArrayCalibration(y_axis)
+
+ self._plot.setData(image)
+ if xcalib.is_affine():
+ xorigin, xscale = xcalib(0), xcalib.get_slope()
+ else:
+ _logger.warning("Unsupported complex image X axis calibration")
+ xorigin, xscale = 0., 1.
+
+ if ycalib.is_affine():
+ yorigin, yscale = ycalib(0), ycalib.get_slope()
+ else:
+ _logger.warning("Unsupported complex image Y axis calibration")
+ yorigin, yscale = 0., 1.
+
+ self._plot.setOrigin((xorigin, yorigin))
+ self._plot.setScale((xscale, yscale))
+
+ title = ""
+ if self.__title:
+ title += self.__title
+ if not title.strip().endswith(self.__signals_names[auxSigIdx]):
+ title += "\n" + self.__signals_names[auxSigIdx]
+ self._plot.setGraphTitle(title)
+ self._plot.getXAxis().setLabel(self.__x_axis_name)
+ self._plot.getYAxis().setLabel(self.__y_axis_name)
+
+ def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
+ self._plot.setData(None)
+
+
class ArrayStackPlot(qt.QWidget):
"""
Widget for plotting a n-D array (n >= 3) as a stack of images.
@@ -665,4 +855,171 @@ class ArrayStackPlot(qt.QWidget):
self.__x_axis_name])
def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
self._stack_view.clear()
+
+
+class ArrayVolumePlot(qt.QWidget):
+ """
+ Widget for plotting a n-D array (n >= 3) as a 3D scalar field.
+ Three axis arrays can be provided to calibrate the axes.
+
+ The signal array can have an arbitrary number of dimensions, the only
+ limitation being that the last 3 dimensions must have the same length as
+ the axes arrays.
+
+ Sliders are provided to select indices on the first (n - 3) dimensions of
+ the signal array, and the plot is updated to load the stack corresponding
+ to the selection.
+ """
+ def __init__(self, parent=None):
+ """
+
+ :param parent: Parent QWidget
+ """
+ super(ArrayVolumePlot, self).__init__(parent)
+
+ self.__signal = None
+ self.__signal_name = None
+ # the Z, Y, X axes apply to the last three dimensions of the signal
+ # (in that order)
+ self.__z_axis = None
+ self.__z_axis_name = None
+ self.__y_axis = None
+ self.__y_axis_name = None
+ self.__x_axis = None
+ self.__x_axis_name = None
+
+ from ._VolumeWindow import VolumeWindow
+
+ self._view = VolumeWindow(self)
+
+ self._hline = qt.QFrame(self)
+ self._hline.setFrameStyle(qt.QFrame.HLine)
+ self._hline.setFrameShadow(qt.QFrame.Sunken)
+ self._legend = qt.QLabel(self)
+ self._selector = NumpyAxesSelector(self)
+ self._selector.setNamedAxesSelectorVisibility(False)
+ self.__selector_is_connected = False
+
+ layout = qt.QVBoxLayout()
+ layout.addWidget(self._view)
+ layout.addWidget(self._hline)
+ layout.addWidget(self._legend)
+ layout.addWidget(self._selector)
+
+ self.setLayout(layout)
+
+ def getVolumeView(self):
+ """Returns the plot used for the display
+
+ :rtype: SceneWindow
+ """
+ return self._view
+
+ def setData(self, signal,
+ x_axis=None, y_axis=None, z_axis=None,
+ signal_name=None,
+ xlabel=None, ylabel=None, zlabel=None,
+ title=None):
+ """
+
+ :param signal: n-D dataset, whose last 3 dimensions are used as the
+ 3D stack values.
+ :param x_axis: 1-D dataset used as the image's x coordinates. If
+ provided, its lengths must be equal to the length of the last
+ dimension of ``signal``.
+ :param y_axis: 1-D dataset used as the image's y. If provided,
+ its lengths must be equal to the length of the 2nd to last
+ dimension of ``signal``.
+ :param z_axis: 1-D dataset used as the image's z. If provided,
+ its lengths must be equal to the length of the 3rd to last
+ dimension of ``signal``.
+ :param signal_name: Label used in the legend
+ :param xlabel: Label for X axis
+ :param ylabel: Label for Y axis
+ :param zlabel: Label for Z axis
+ :param title: Graph title
+ """
+ if self.__selector_is_connected:
+ self._selector.selectionChanged.disconnect(self._updateVolume)
+ self.__selector_is_connected = False
+
+ self.__signal = signal
+ self.__signal_name = signal_name or ""
+ self.__x_axis = x_axis
+ self.__x_axis_name = xlabel
+ self.__y_axis = y_axis
+ self.__y_axis_name = ylabel
+ self.__z_axis = z_axis
+ self.__z_axis_name = zlabel
+
+ self._selector.setData(signal)
+ self._selector.setAxisNames(["Y", "X", "Z"])
+
+ self._updateVolume()
+
+ # the legend label shows the selection slice producing the volume
+ # (only interesting for ndim > 3)
+ if signal.ndim > 3:
+ self._selector.setVisible(True)
+ self._legend.setVisible(True)
+ self._hline.setVisible(True)
+ else:
+ self._selector.setVisible(False)
+ self._legend.setVisible(False)
+ self._hline.setVisible(False)
+
+ if not self.__selector_is_connected:
+ self._selector.selectionChanged.connect(self._updateVolume)
+ self.__selector_is_connected = True
+
+ def _updateVolume(self):
+ """Update displayed stack according to the current axes selector
+ data."""
+ x_axis = self.__x_axis
+ y_axis = self.__y_axis
+ z_axis = self.__z_axis
+
+ offset = []
+ scale = []
+ for axis in [x_axis, y_axis, z_axis]:
+ if axis is None:
+ calibration = NoCalibration()
+ elif len(axis) == 2:
+ calibration = LinearCalibration(
+ y_intercept=axis[0], slope=axis[1])
+ else:
+ calibration = ArrayCalibration(axis)
+ if not calibration.is_affine():
+ _logger.warning("Axis has not linear values, ignored")
+ offset.append(0.)
+ scale.append(1.)
+ else:
+ offset.append(calibration(0))
+ scale.append(calibration.get_slope())
+
+ legend = self.__signal_name + "["
+ for sl in self._selector.selection():
+ if sl == slice(None):
+ legend += ":, "
+ else:
+ legend += str(sl) + ", "
+ legend = legend[:-2] + "]"
+ self._legend.setText("Displayed data: " + legend)
+
+ # Update SceneWidget
+ data = self._selector.selectedData()
+
+ volumeView = self.getVolumeView()
+ volumeView.setData(data, offset=offset, scale=scale)
+ volumeView.setAxesLabels(
+ self.__x_axis_name, self.__y_axis_name, self.__z_axis_name)
+
+ def clear(self):
+ old = self._selector.blockSignals(True)
+ self._selector.clear()
+ self._selector.blockSignals(old)
+ self.getVolumeView().clear()
diff --git a/silx/gui/data/NumpyAxesSelector.py b/silx/gui/data/NumpyAxesSelector.py
index 4530aa9..e6da0d4 100644
--- a/silx/gui/data/NumpyAxesSelector.py
+++ b/silx/gui/data/NumpyAxesSelector.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -31,13 +31,18 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "29/01/2018"
+import logging
import numpy
import functools
from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
from silx.gui import qt
+from silx.gui.utils import blockSignals
import silx.utils.weakref
+_logger = logging.getLogger(__name__)
+
+
class _Axis(qt.QWidget):
"""Widget displaying an axis.
@@ -110,6 +115,8 @@ class _Axis(qt.QWidget):
if axisName == "" and self.__axes.count() == 0:
self.__axes.setCurrentIndex(-1)
self.__updateSliderVisibility()
+ return
+
for index in range(self.__axes.count()):
name = self.__axes.itemData(index)
if name == axisName:
@@ -121,7 +128,7 @@ class _Axis(qt.QWidget):
def axisName(self):
"""Returns the selected axis name.
- If no names are selected, an empty string is retruned.
+ If no name is selected, an empty string is returned.
:rtype: str
"""
@@ -136,11 +143,11 @@ class _Axis(qt.QWidget):
:param List[str] axesNames: List of available names
"""
self.__axes.clear()
- previous = self.__axes.blockSignals(True)
- self.__axes.addItem(" ", "")
- for axis in axesNames:
- self.__axes.addItem(axis, axis)
- self.__axes.blockSignals(previous)
+ with blockSignals(self.__axes):
+ self.__axes.addItem(" ", "")
+ for axis in axesNames:
+ self.__axes.addItem(axis, axis)
+
self.__updateSliderVisibility()
def setCustomAxis(self, axesNames):
@@ -168,12 +175,19 @@ class _Axis(qt.QWidget):
self.__slider.setVisible(isVisible)
def value(self):
- """Returns the current selected position in the axis.
+ """Returns the currently selected position in the axis.
:rtype: int
"""
return self.__slider.value()
+ def setValue(self, value):
+ """Set the currently selected position in the axis.
+
+ :param int value:
+ """
+ self.__slider.setValue(value)
+
def __sliderValueChanged(self, value):
"""Called when the selected position in the axis change.
@@ -183,18 +197,14 @@ class _Axis(qt.QWidget):
def setNamedAxisSelectorVisibility(self, visible):
"""Hide or show the named axis combobox.
- If both the selector and the slider are hidden,
- hide the entire widget.
+
+ If both the selector and the slider are hidden, hide the entire widget.
:param visible: boolean
"""
self.__axes.setVisible(visible)
name = self.axisName()
-
- if not visible and name != "":
- self.setVisible(False)
- else:
- self.setVisible(True)
+ self.setVisible(visible or name == "")
class NumpyAxesSelector(qt.QWidget):
@@ -236,7 +246,6 @@ class NumpyAxesSelector(qt.QWidget):
self.__data = None
self.__selectedData = None
- self.__selection = tuple()
self.__axis = []
self.__axisNames = []
self.__customAxisNames = set([])
@@ -268,13 +277,12 @@ class NumpyAxesSelector(qt.QWidget):
if delta < 0:
delta = 0
for index, axis in enumerate(self.__axis):
- previous = axis.blockSignals(True)
- axis.setAxisNames(self.__axisNames)
- if index >= delta and index - delta < len(self.__axisNames):
- axis.setAxisName(self.__axisNames[index - delta])
- else:
- axis.setAxisName("")
- axis.blockSignals(previous)
+ with blockSignals(axis):
+ axis.setAxisNames(self.__axisNames)
+ if index >= delta and index - delta < len(self.__axisNames):
+ axis.setAxisName(self.__axisNames[index - delta])
+ else:
+ axis.setAxisName("")
self.__updateSelectedData()
def setCustomAxis(self, axesNames):
@@ -372,9 +380,8 @@ class NumpyAxesSelector(qt.QWidget):
# If there is no other solution we set the name at the same place
axisChanged = False
availableWidget = axis
- previous = availableWidget.blockSignals(True)
- availableWidget.setAxisName(missingName)
- availableWidget.blockSignals(previous)
+ with blockSignals(availableWidget):
+ availableWidget.setAxisName(missingName)
else:
# there is a duplicated name somewhere
# we swap it with the missing name or with nothing
@@ -387,9 +394,8 @@ class NumpyAxesSelector(qt.QWidget):
break
if missingName is None:
missingName = ""
- previous = dupWidget.blockSignals(True)
- dupWidget.setAxisName(missingName)
- dupWidget.blockSignals(previous)
+ with blockSignals(dupWidget):
+ dupWidget.setAxisName(missingName)
if self.__data is None:
return
@@ -402,70 +408,164 @@ class NumpyAxesSelector(qt.QWidget):
It fires a `selectionChanged` event.
"""
- if self.__data is None:
+ permutation = self.permutation()
+
+ if self.__data is None or permutation is None:
+ # No data or not all the expected axes are there
if self.__selectedData is not None:
self.__selectedData = None
- self.__selection = tuple()
self.selectionChanged.emit()
return
- selection = []
- axisNames = []
- for slider in self.__axis:
- name = slider.axisName()
- if name == "":
- selection.append(slider.value())
- else:
- selection.append(slice(None))
- axisNames.append(name)
- self.__selection = tuple(selection)
# get a view with few fixed dimensions
# with a h5py dataset, it create a copy
# TODO we can reuse the same memory in case of a copy
- view = self.__data[self.__selection]
-
- if set(self.__axisNames) - set(axisNames) != set([]):
- # Not all the expected axis are there
- if self.__selectedData is not None:
- self.__selectedData = None
- self.__selection = tuple()
- self.selectionChanged.emit()
- return
-
- # order axis as expected
- source = []
- destination = []
- order = []
- for index, name in enumerate(self.__axisNames):
- destination.append(index)
- source.append(axisNames.index(name))
- for _, s in sorted(zip(destination, source)):
- order.append(s)
- view = numpy.transpose(view, order)
-
- self.__selectedData = view
+ self.__selectedData = numpy.transpose(self.__data[self.selection()], permutation)
self.selectionChanged.emit()
def data(self):
"""Returns the input data.
- :rtype: numpy.ndarray
+ :rtype: Union[numpy.ndarray,None]
"""
- return self.__data
+ if self.__data is None:
+ return None
+ else:
+ return numpy.array(self.__data, copy=False)
def selectedData(self):
"""Returns the output data.
- :rtype: numpy.ndarray
+ This is equivalent to::
+
+ numpy.transpose(self.data()[self.selection()], self.permutation())
+
+ :rtype: Union[numpy.ndarray,None]
"""
- return self.__selectedData
+ if self.__selectedData is None:
+ return None
+ else:
+ return numpy.array(self.__selectedData, copy=False)
+
+ def permutation(self):
+ """Returns the axes permutation to convert data subset to selected data.
+
+ If permutation cannot be computer, it returns None.
+
+ :rtype: Union[List[int],None]
+ """
+ if self.__data is None:
+ return None
+ else:
+ indices = []
+ for name in self.__axisNames:
+ index = 0
+ for axis in self.__axis:
+ if axis.axisName() == name:
+ indices.append(index)
+ break
+ if axis.axisName() != "":
+ index += 1
+ else:
+ _logger.warning("No axis corresponding to: %s", name)
+ return None
+ return tuple(indices)
def selection(self):
"""Returns the selection tuple used to slice the data.
:rtype: tuple
"""
- return self.__selection
+ if self.__data is None:
+ return tuple()
+ else:
+ return tuple([axis.value() if axis.axisName() == "" else slice(None)
+ for axis in self.__axis])
+
+ def setSelection(self, selection, permutation=None):
+ """Set the selection along each dimension.
+
+ tuple returned by :meth:`selection` can be provided as input,
+ provided that it is for the same the number of axes and
+ the same number of dimensions of the data.
+
+ :param List[Union[int,slice,None]] selection:
+ The selection tuple with as one element for each dimension of the data.
+ If an element is None, then the whole dimension is selected.
+ :param Union[List[int],None] permutation:
+ The data axes indices to transpose.
+ If not given, no permutation is applied
+ :raise ValueError:
+ When the selection does not match current data shape and number of axes.
+ """
+ data_shape = self.__data.shape if self.__data is not None else ()
+
+ # Check selection
+ if len(selection) != len(data_shape):
+ raise ValueError(
+ "Selection length (%d) and data ndim (%d) mismatch" %
+ (len(selection), len(data_shape)))
+
+ # Check selection type
+ selectedDataNDim = 0
+ for element, size in zip(selection, data_shape):
+ if isinstance(element, int):
+ if not 0 <= element < size:
+ raise ValueError(
+ "Selected index (%d) outside data dimension range [0-%d]" %
+ (element, size))
+ elif element is None or element == slice(None):
+ selectedDataNDim += 1
+ else:
+ raise ValueError("Unsupported element in selection: %s" % element)
+
+ ndim = len(self.__axisNames)
+ if selectedDataNDim != ndim:
+ raise ValueError(
+ "Selection dimensions (%d) and number of axes (%d) mismatch" %
+ (selectedDataNDim, ndim))
+
+ # check permutation
+ if permutation is None:
+ permutation = tuple(range(ndim))
+
+ if set(permutation) != set(range(ndim)):
+ raise ValueError(
+ "Error in provided permutation: "
+ "Wrong size, elements out of range or duplicates")
+
+ inversePermutation = numpy.argsort(permutation)
+
+ axisNameChanged = False
+ customValueChanged = []
+ with blockSignals(*self.__axis):
+ index = 0
+ for element, axis in zip(selection, self.__axis):
+ if isinstance(element, int):
+ name = ""
+ else:
+ name = self.__axisNames[inversePermutation[index]]
+ index += 1
+
+ if axis.axisName() != name:
+ axis.setAxisName(name)
+ axisNameChanged = True
+
+ for element, axis in zip(selection, self.__axis):
+ value = element if isinstance(element, int) else 0
+ if axis.value() != value:
+ axis.setValue(value)
+
+ name = axis.axisName()
+ if name in self.__customAxisNames:
+ customValueChanged.append((name, value))
+
+ # Send signals that where disabled
+ if axisNameChanged:
+ self.selectedAxisChanged.emit()
+ for name, value in customValueChanged:
+ self.customAxisChanged.emit(name, value)
+ self.__updateSelectedData()
def setNamedAxesSelectorVisibility(self, visible):
"""Show or hide the combo-boxes allowing to map the plot axes
diff --git a/silx/gui/data/TextFormatter.py b/silx/gui/data/TextFormatter.py
index 1401634..98c37d7 100644
--- a/silx/gui/data/TextFormatter.py
+++ b/silx/gui/data/TextFormatter.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,16 +29,15 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "24/07/2018"
-import numpy
+import logging
import numbers
-from silx.third_party import six
+
+import numpy
+import six
+
from silx.gui import qt
-import logging
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
_logger = logging.getLogger(__name__)
@@ -322,10 +321,9 @@ class TextFormatter(qt.QObject):
if dtype.kind == 'S':
return self.__formatCharString(data)
elif dtype.kind == 'O':
- if h5py is not None:
- text = self.__formatH5pyObject(data, dtype)
- if text is not None:
- return text
+ text = self.__formatH5pyObject(data, dtype)
+ if text is not None:
+ return text
try:
# Try ascii/utf-8
text = "%s" % data.decode("utf-8")
@@ -339,15 +337,14 @@ class TextFormatter(qt.QObject):
elif isinstance(data, (numpy.integer)):
if dtype is None:
dtype = data.dtype
- if h5py is not None:
- enumType = h5py.check_dtype(enum=dtype)
- if enumType is not None:
- for key, value in enumType.items():
- if value == data:
- result = {}
- result["name"] = key
- result["value"] = data
- return self.__enumFormat % result
+ enumType = h5py.check_dtype(enum=dtype)
+ if enumType is not None:
+ for key, value in enumType.items():
+ if value == data:
+ result = {}
+ result["name"] = key
+ result["value"] = data
+ return self.__enumFormat % result
return self.__integerFormat % data
elif isinstance(data, (numbers.Integral)):
return self.__integerFormat % data
@@ -373,21 +370,20 @@ class TextFormatter(qt.QObject):
template = self.__floatFormat
params = (data.real)
return template % params
- elif h5py is not None and isinstance(data, h5py.h5r.Reference):
+ elif isinstance(data, h5py.h5r.Reference):
dtype = h5py.special_dtype(ref=h5py.Reference)
text = self.__formatH5pyObject(data, dtype)
return text
- elif h5py is not None and isinstance(data, h5py.h5r.RegionReference):
+ elif isinstance(data, h5py.h5r.RegionReference):
dtype = h5py.special_dtype(ref=h5py.RegionReference)
text = self.__formatH5pyObject(data, dtype)
return text
elif isinstance(data, numpy.object_) or dtype is not None:
if dtype is None:
dtype = data.dtype
- if h5py is not None:
- text = self.__formatH5pyObject(data, dtype)
- if text is not None:
- return text
+ text = self.__formatH5pyObject(data, dtype)
+ if text is not None:
+ return text
# That's a numpy object
return str(data)
return str(data)
diff --git a/silx/gui/data/_VolumeWindow.py b/silx/gui/data/_VolumeWindow.py
new file mode 100644
index 0000000..03b6876
--- /dev/null
+++ b/silx/gui/data/_VolumeWindow.py
@@ -0,0 +1,148 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2019 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides a widget to visualize 3D arrays"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "22/03/2019"
+
+
+import numpy
+
+from .. import qt
+from ..plot3d.SceneWindow import SceneWindow
+from ..plot3d.items import ScalarField3D, ComplexField3D, ItemChangedType
+
+
+class VolumeWindow(SceneWindow):
+ """Extends SceneWindow with a convenient API for 3D array
+
+ :param QWidget: parent
+ """
+
+ def __init__(self, parent):
+ super(VolumeWindow, self).__init__(parent)
+ self.__firstData = True
+ # Hide global parameter dock
+ self.getGroupResetWidget().parent().setVisible(False)
+
+ def setAxesLabels(self, xlabel=None, ylabel=None, zlabel=None):
+ """Set the text labels of the axes.
+
+ :param Union[str,None] xlabel: Label of the X axis
+ :param Union[str,None] ylabel: Label of the Y axis
+ :param Union[str,None] zlabel: Label of the Z axis
+ """
+ sceneWidget = self.getSceneWidget()
+ sceneWidget.getSceneGroup().setAxesLabels(
+ 'X' if xlabel is None else xlabel,
+ 'Y' if ylabel is None else ylabel,
+ 'Z' if zlabel is None else zlabel)
+
+ def clear(self):
+ """Clear any currently displayed data"""
+ sceneWidget = self.getSceneWidget()
+ items = sceneWidget.getItems()
+ if (len(items) == 1 and
+ isinstance(items[0], (ScalarField3D, ComplexField3D))):
+ items[0].setData(None)
+ else: # Safety net
+ sceneWidget.clearItems()
+
+ @staticmethod
+ def __computeIsolevel(data):
+ """Returns a suitable isolevel value for data
+
+ :param numpy.ndarray data:
+ :rtype: float
+ """
+ data = data[numpy.isfinite(data)]
+ if len(data) == 0:
+ return 0
+ else:
+ return numpy.mean(data) + numpy.std(data)
+
+ def setData(self, data, offset=(0., 0., 0.), scale=(1., 1., 1.)):
+ """Set the 3D array data to display.
+
+ :param numpy.ndarray data: 3D array of float or complex
+ :param List[float] offset: (tx, ty, tz) coordinates of the origin
+ :param List[float] scale: (sx, sy, sz) scale for each dimension
+ """
+ sceneWidget = self.getSceneWidget()
+ dataMaxCoords = numpy.array(list(reversed(data.shape))) - 1
+
+ previousItems = sceneWidget.getItems()
+ if (len(previousItems) == 1 and
+ isinstance(previousItems[0], (ScalarField3D, ComplexField3D)) and
+ numpy.iscomplexobj(data) == isinstance(previousItems[0], ComplexField3D)):
+ # Reuse existing volume item
+ volume = sceneWidget.getItems()[0]
+ volume.setData(data, copy=False)
+ # Make sure the plane goes through the dataset
+ for plane in volume.getCutPlanes():
+ point = numpy.array(plane.getPoint())
+ if numpy.any(point < (0, 0, 0)) or numpy.any(point > dataMaxCoords):
+ plane.setPoint(dataMaxCoords // 2)
+ else:
+ # Add a new volume
+ sceneWidget.clearItems()
+ volume = sceneWidget.addVolume(data, copy=False)
+ volume.setLabel('Volume')
+ for plane in volume.getCutPlanes():
+ # Make plane going through the center of the data
+ plane.setPoint(dataMaxCoords // 2)
+ plane.setVisible(False)
+ plane.sigItemChanged.connect(self.__cutPlaneUpdated)
+ volume.addIsosurface(self.__computeIsolevel, '#FF0000FF')
+
+ # Expand the parameter tree
+ model = self.getParamTreeView().model()
+ index = qt.QModelIndex() # Invalid index for top level
+ while 1:
+ rowCount = model.rowCount(parent=index)
+ if rowCount == 0:
+ break
+ index = model.index(rowCount - 1, 0, parent=index)
+ self.getParamTreeView().setExpanded(index, True)
+ if not index.isValid():
+ break
+
+ volume.setTranslation(*offset)
+ volume.setScale(*scale)
+
+ if self.__firstData: # Only center for first dataset
+ self.__firstData = False
+ sceneWidget.centerScene()
+
+ def __cutPlaneUpdated(self, event):
+ """Handle the change of visibility of the cut plane
+
+ :param event: Kind of update
+ """
+ if event == ItemChangedType.VISIBLE:
+ plane = self.sender()
+ if plane.isVisible():
+ self.getSceneWidget().selection().setCurrentItem(plane)
diff --git a/silx/gui/data/test/test_arraywidget.py b/silx/gui/data/test/test_arraywidget.py
index 50ffc84..6bcbbd3 100644
--- a/silx/gui/data/test/test_arraywidget.py
+++ b/silx/gui/data/test/test_arraywidget.py
@@ -36,10 +36,7 @@ from silx.gui import qt
from silx.gui.data import ArrayTableWidget
from silx.gui.utils.testutils import TestCaseQt
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class TestArrayWidget(TestCaseQt):
@@ -190,7 +187,6 @@ class TestArrayWidget(TestCaseQt):
self.assertIs(b0, b1)
-@unittest.skipIf(h5py is None, "Could not import h5py")
class TestH5pyArrayWidget(TestCaseQt):
"""Basic test for ArrayTableWidget with a dataset.
diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py
index a681f33..12a640e 100644
--- a/silx/gui/data/test/test_dataviewer.py
+++ b/silx/gui/data/test/test_dataviewer.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -24,7 +24,7 @@
# ###########################################################################*/
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "23/04/2018"
+__date__ = "19/02/2019"
import os
import tempfile
@@ -42,10 +42,7 @@ from silx.gui.data.DataViewerFrame import DataViewerFrame
from silx.gui.utils.testutils import SignalListener
from silx.gui.utils.testutils import TestCaseQt
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class _DataViewMock(DataView):
@@ -170,8 +167,6 @@ class AbstractDataViewerTests(TestCaseQt):
self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId())
def test_3d_h5_dataset(self):
- if h5py is None:
- self.skipTest("h5py library is not available")
with self.h5_temporary_file() as h5file:
dataset = h5file["data"]
widget = self.create_widget()
@@ -242,12 +237,13 @@ class AbstractDataViewerTests(TestCaseQt):
# replace a view that is a child of a composite view
widget = self.create_widget()
view = _DataViewMock(widget)
- widget.replaceView(DataViews.NXDATA_INVALID_MODE,
- view)
+ replaced = widget.replaceView(DataViews.NXDATA_INVALID_MODE,
+ view)
+ self.assertTrue(replaced)
nxdata_view = widget.getViewFromModeId(DataViews.NXDATA_MODE)
self.assertNotIn(DataViews.NXDATA_INVALID_MODE,
- [v.modeId() for v in nxdata_view.availableViews()])
- self.assertTrue(view in nxdata_view.availableViews())
+ [v.modeId() for v in nxdata_view.getViews()])
+ self.assertTrue(view in nxdata_view.getViews())
class TestDataViewer(AbstractDataViewerTests):
diff --git a/silx/gui/data/test/test_numpyaxesselector.py b/silx/gui/data/test/test_numpyaxesselector.py
index 6b7b58c..d37cff7 100644
--- a/silx/gui/data/test/test_numpyaxesselector.py
+++ b/silx/gui/data/test/test_numpyaxesselector.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -37,10 +37,7 @@ from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
from silx.gui.utils.testutils import SignalListener
from silx.gui.utils.testutils import TestCaseQt
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class TestNumpyAxesSelector(TestCaseQt):
@@ -79,7 +76,7 @@ class TestNumpyAxesSelector(TestCaseQt):
widget.setAxisNames(["x", "y", "z", "boum"])
widget.setData(data[0])
result = widget.selectedData()
- self.assertEqual(result, None)
+ self.assertIsNone(result)
widget.setData(data)
result = widget.selectedData()
self.assertTrue(numpy.array_equal(result, expectedResult))
@@ -121,8 +118,6 @@ class TestNumpyAxesSelector(TestCaseQt):
os.unlink(tmp_name)
def test_h5py_dataset(self):
- if h5py is None:
- self.skipTest("h5py library is not available")
with self.h5_temporary_file() as h5file:
dataset = h5file["data"]
expectedResult = dataset[0]
diff --git a/silx/gui/data/test/test_textformatter.py b/silx/gui/data/test/test_textformatter.py
index 850aa00..1a63074 100644
--- a/silx/gui/data/test/test_textformatter.py
+++ b/silx/gui/data/test/test_textformatter.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -29,17 +29,15 @@ __date__ = "12/12/2017"
import unittest
import shutil
import tempfile
+
import numpy
+import six
from silx.gui.utils.testutils import TestCaseQt
from silx.gui.utils.testutils import SignalListener
from ..TextFormatter import TextFormatter
-from silx.third_party import six
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class TestTextFormatter(TestCaseQt):
@@ -50,7 +48,7 @@ class TestTextFormatter(TestCaseQt):
self.assertIsNot(formatter, copy)
copy.setFloatFormat("%.3f")
self.assertEqual(formatter.integerFormat(), copy.integerFormat())
- self.assertNotEquals(formatter.floatFormat(), copy.floatFormat())
+ self.assertNotEqual(formatter.floatFormat(), copy.floatFormat())
self.assertEqual(formatter.useQuoteForText(), copy.useQuoteForText())
self.assertEqual(formatter.imaginaryUnit(), copy.imaginaryUnit())
@@ -108,8 +106,6 @@ class TestTextFormatterWithH5py(TestCaseQt):
@classmethod
def setUpClass(cls):
super(TestTextFormatterWithH5py, cls).setUpClass()
- if h5py is None:
- raise unittest.SkipTest("h5py is not available")
cls.tmpDirectory = tempfile.mkdtemp()
cls.h5File = h5py.File("%s/formatter.h5" % cls.tmpDirectory, mode="w")
diff --git a/silx/gui/dialog/AbstractDataFileDialog.py b/silx/gui/dialog/AbstractDataFileDialog.py
index 40045fe..29e7bb5 100644
--- a/silx/gui/dialog/AbstractDataFileDialog.py
+++ b/silx/gui/dialog/AbstractDataFileDialog.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -28,29 +28,36 @@ This module contains an :class:`AbstractDataFileDialog`.
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "05/03/2018"
+__date__ = "05/03/2019"
import sys
import os
import logging
-import numpy
import functools
+from distutils.version import LooseVersion
+
+import numpy
+import six
+
import silx.io.url
from silx.gui import qt
from silx.gui.hdf5.Hdf5TreeModel import Hdf5TreeModel
from . import utils
-from silx.third_party import six
from .FileTypeComboBox import FileTypeComboBox
-try:
- import fabio
-except ImportError:
- fabio = None
+
+import fabio
_logger = logging.getLogger(__name__)
+DEFAULT_SIDEBAR_URL = True
+"""Set it to false to disable initilializing of the sidebar urls with the
+default Qt list. This could allow to disable a behaviour known to segfault on
+some version of PyQt."""
+
+
class _IconProvider(object):
FileDialogToParentDir = qt.QStyle.SP_CustomBase + 1
@@ -143,14 +150,22 @@ class _SideBar(qt.QListView):
:rtype: List[str]
"""
urls = []
- if qt.qVersion().startswith("5.") and sys.platform in ["linux", "linux2"]:
+ version = LooseVersion(qt.qVersion())
+ feed_sidebar = True
+
+ if not DEFAULT_SIDEBAR_URL:
+ _logger.debug("Skip default sidebar URLs (from setted variable)")
+ feed_sidebar = False
+ elif version.version[0] == 4 and sys.platform in ["win32"]:
+ # Avoid locking the GUI 5min in case of use of network driver
+ _logger.debug("Skip default sidebar URLs (avoid lock when using network drivers)")
+ feed_sidebar = False
+ elif version < LooseVersion("5.11.2") and qt.BINDING == "PyQt5" and sys.platform in ["linux", "linux2"]:
# Avoid segfault on PyQt5 + gtk
_logger.debug("Skip default sidebar URLs (avoid PyQt5 segfault)")
- pass
- elif qt.qVersion().startswith("4.") and sys.platform in ["win32"]:
- # Avoid 5min of locked GUI relative to network driver
- _logger.debug("Skip default sidebar URLs (avoid lock when using network drivers)")
- else:
+ feed_sidebar = False
+
+ if feed_sidebar:
# Get default shortcut
# There is no other way
d = qt.QFileDialog(self)
@@ -453,9 +468,13 @@ class _FabioData(object):
def shape(self):
if self.__fabioFile.nframes == 0:
return None
+ if self.__fabioFile.nframes == 1:
+ return [slice(None), slice(None)]
return [self.__fabioFile.nframes, slice(None), slice(None)]
def __getitem__(self, selector):
+ if self.__fabioFile.nframes == 1 and selector == tuple():
+ return self.__fabioFile.data
if isinstance(selector, tuple) and len(selector) == 1:
selector = selector[0]
@@ -527,6 +546,10 @@ class AbstractDataFileDialog(qt.QDialog):
def _init(self):
self.setWindowTitle("Open")
+ self.__openedFiles = []
+ """Store the list of files opened by the model itself."""
+ # FIXME: It should be managed one by one by Hdf5Item itself
+
self.__directory = None
self.__directoryLoadedFilter = None
self.__errorWhileLoadingFile = None
@@ -576,10 +599,6 @@ class AbstractDataFileDialog(qt.QDialog):
self.__fileTypeCombo.setCurrentIndex(0)
self.__filterSelected(0)
- self.__openedFiles = []
- """Store the list of files opened by the model itself."""
- # FIXME: It should be managed one by one by Hdf5Item itself
-
# It is not possible to override the QObject destructor nor
# to access to the content of the Python object with the `destroyed`
# signal cause the Python method was already removed with the QWidget,
@@ -1023,15 +1042,16 @@ class AbstractDataFileDialog(qt.QDialog):
return
self.__directoryLoadedFilter = path
self.__processing += 1
+ if self.__fileModel is None:
+ return
index = self.__fileModel.setRootPath(path)
if not index.isValid():
+ # There is a problem with this path
+ # No asynchronous process will be waked up
self.__processing -= 1
self.__browser.setRootIndex(index, model=self.__fileModel)
self.__clearData()
self.__updatePath()
- else:
- # asynchronous process
- pass
def __directoryLoaded(self, path):
if self.__directoryLoadedFilter is not None:
@@ -1040,6 +1060,8 @@ class AbstractDataFileDialog(qt.QDialog):
# The first click on the sidebar sent 2 events
self.__processing -= 1
return
+ if self.__fileModel is None:
+ return
index = self.__fileModel.index(path)
self.__browser.setRootIndex(index, model=self.__fileModel)
self.__updatePath()
@@ -1061,8 +1083,6 @@ class AbstractDataFileDialog(qt.QDialog):
def __openFabioFile(self, filename):
self.__closeFile()
try:
- if fabio is None:
- raise ImportError("Fabio module is not available")
self.__fabio = fabio.open(filename)
self.__openedFiles.append(self.__fabio)
self.__selectedFile = filename
@@ -1108,10 +1128,10 @@ class AbstractDataFileDialog(qt.QDialog):
if codec.is_autodetect():
if self.__isSilxHavePriority(filename):
openners.append(self.__openSilxFile)
- if fabio is not None and self._isFabioFilesSupported():
+ if self._isFabioFilesSupported():
openners.append(self.__openFabioFile)
else:
- if fabio is not None and self._isFabioFilesSupported():
+ if self._isFabioFilesSupported():
openners.append(self.__openFabioFile)
openners.append(self.__openSilxFile)
elif codec.is_silx_codec():
@@ -1159,10 +1179,9 @@ class AbstractDataFileDialog(qt.QDialog):
is_fabio_have_priority = not codec.is_silx_codec() and not self.__isSilxHavePriority(path)
if is_fabio_decoder or is_fabio_have_priority:
# Then it's flat frame container
- if fabio is not None:
- self.__openFabioFile(path)
- if self.__fabio is not None:
- selectedData = _FabioData(self.__fabio)
+ self.__openFabioFile(path)
+ if self.__fabio is not None:
+ selectedData = _FabioData(self.__fabio)
else:
assert(False)
@@ -1221,6 +1240,7 @@ class AbstractDataFileDialog(qt.QDialog):
if self.__previewWidget is not None:
self.__previewWidget.setData(None)
if self.__selectorWidget is not None:
+ self.__selectorWidget.setData(None)
self.__selectorWidget.hide()
self.__selectedData = None
self.__data = None
@@ -1238,6 +1258,8 @@ class AbstractDataFileDialog(qt.QDialog):
If :meth:`_isDataSupported` returns false, this function will be
inhibited and no data will be selected.
"""
+ if isinstance(data, _FabioData):
+ data = data[()]
if self.__previewWidget is not None:
fromDataSelector = self.__selectedData is not None
self.__previewWidget.setData(data, fromDataSelector=fromDataSelector)
@@ -1305,8 +1327,10 @@ class AbstractDataFileDialog(qt.QDialog):
filename = ""
dataPath = None
- if useSelectorWidget and self.__selectorWidget is not None and self.__selectorWidget.isVisible():
+ if useSelectorWidget and self.__selectorWidget is not None and self.__selectorWidget.isUsed():
slicing = self.__selectorWidget.slicing()
+ if slicing == tuple():
+ slicing = None
else:
slicing = None
@@ -1471,9 +1495,7 @@ class AbstractDataFileDialog(qt.QDialog):
self.__clearData()
if self.__selectorWidget is not None:
- self.__selectorWidget.setVisible(url.data_slice() is not None)
- if url.data_slice() is not None:
- self.__selectorWidget.setSlicing(url.data_slice())
+ self.__selectorWidget.selectSlicing(url.data_slice())
else:
self.__errorWhileLoadingFile = (url.file_path(), "File not found")
self.__clearData()
diff --git a/silx/gui/dialog/ColormapDialog.py b/silx/gui/dialog/ColormapDialog.py
index cbbfa5a..dddec4c 100644
--- a/silx/gui/dialog/ColormapDialog.py
+++ b/silx/gui/dialog/ColormapDialog.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -63,9 +63,10 @@ from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
__license__ = "MIT"
-__date__ = "23/05/2018"
+__date__ = "27/11/2018"
+import enum
import logging
import numpy
@@ -73,11 +74,12 @@ import numpy
from .. import qt
from ..colors import Colormap, preferredColormaps
from ..plot import PlotWidget
+from ..plot.items.axis import Axis
from silx.gui.widgets.FloatEdit import FloatEdit
import weakref
from silx.math.combo import min_max
-from silx.third_party import enum
from silx.gui import icons
+from silx.gui.widgets.ColormapNameComboBox import ColormapNameComboBox
from silx.math.histogram import Histogramnd
_logger = logging.getLogger(__name__)
@@ -149,76 +151,6 @@ class _BoundaryWidget(qt.QWidget):
self._updateDisplayedText()
-class _ColormapNameCombox(qt.QComboBox):
- def __init__(self, parent=None):
- qt.QComboBox.__init__(self, parent)
- self.__initItems()
-
- ORIGINAL_NAME = qt.Qt.UserRole + 1
-
- def __initItems(self):
- for colormapName in preferredColormaps():
- index = self.count()
- self.addItem(str.title(colormapName))
- self.setItemIcon(index, self.getIconPreview(colormapName))
- self.setItemData(index, colormapName, role=self.ORIGINAL_NAME)
-
- def getIconPreview(self, colormapName):
- """Return an icon preview from a LUT name.
-
- This icons are cached into a global structure.
-
- :param str colormapName: str
- :rtype: qt.QIcon
- """
- if colormapName not in _colormapIconPreview:
- icon = self.createIconPreview(colormapName)
- _colormapIconPreview[colormapName] = icon
- return _colormapIconPreview[colormapName]
-
- def createIconPreview(self, colormapName):
- """Create and return an icon preview from a LUT name.
-
- This icons are cached into a global structure.
-
- :param str colormapName: Name of the LUT
- :rtype: qt.QIcon
- """
- colormap = Colormap(colormapName)
- size = 32
- lut = colormap.getNColors(size)
- if lut is None or len(lut) == 0:
- return qt.QIcon()
-
- pixmap = qt.QPixmap(size, size)
- painter = qt.QPainter(pixmap)
- for i in range(size):
- rgb = lut[i]
- r, g, b = rgb[0], rgb[1], rgb[2]
- painter.setPen(qt.QColor(r, g, b))
- painter.drawPoint(qt.QPoint(i, 0))
-
- painter.drawPixmap(0, 1, size, size - 1, pixmap, 0, 0, size, 1)
- painter.end()
-
- return qt.QIcon(pixmap)
-
- def getCurrentName(self):
- return self.itemData(self.currentIndex(), self.ORIGINAL_NAME)
-
- def findColormap(self, name):
- return self.findData(name, role=self.ORIGINAL_NAME)
-
- def setCurrentName(self, name):
- index = self.findColormap(name)
- if index < 0:
- index = self.count()
- self.addItem(str.title(name))
- self.setItemIcon(index, self.getIconPreview(name))
- self.setItemData(index, name, role=self.ORIGINAL_NAME)
- self.setCurrentIndex(index)
-
-
@enum.unique
class _DataInPlotMode(enum.Enum):
"""Enum for each mode of display of the data in the plot."""
@@ -255,6 +187,7 @@ class ColormapDialog(qt.QDialog):
the self.setcolormap is a callback)
"""
+ self.__displayInvalidated = False
self._histogramData = None
self._minMaxWasEdited = False
self._initialRange = None
@@ -275,21 +208,20 @@ class ColormapDialog(qt.QDialog):
formLayout.setSpacing(0)
# Colormap row
- self._comboBoxColormap = _ColormapNameCombox(parent=formWidget)
- self._comboBoxColormap.currentIndexChanged[int].connect(self._updateName)
+ self._comboBoxColormap = ColormapNameComboBox(parent=formWidget)
+ self._comboBoxColormap.currentIndexChanged[int].connect(self._updateLut)
formLayout.addRow('Colormap:', self._comboBoxColormap)
# Normalization row
self._normButtonLinear = qt.QRadioButton('Linear')
self._normButtonLinear.setChecked(True)
self._normButtonLog = qt.QRadioButton('Log')
- self._normButtonLog.toggled.connect(self._activeLogNorm)
normButtonGroup = qt.QButtonGroup(self)
normButtonGroup.setExclusive(True)
normButtonGroup.addButton(self._normButtonLinear)
normButtonGroup.addButton(self._normButtonLog)
- self._normButtonLinear.toggled[bool].connect(self._updateLinearNorm)
+ normButtonGroup.buttonClicked[qt.QAbstractButton].connect(self._updateNormalization)
normLayout = qt.QHBoxLayout()
normLayout.setContentsMargins(0, 0, 0, 0)
@@ -388,9 +320,17 @@ class ColormapDialog(qt.QDialog):
self.setFixedSize(self.sizeHint())
self._applyColormap()
+ def _displayLater(self):
+ self.__displayInvalidated = True
+
def showEvent(self, event):
self.visibleChanged.emit(True)
super(ColormapDialog, self).showEvent(event)
+ if self.isVisible():
+ if self.__displayInvalidated:
+ self._applyColormap()
+ self._updateDataInPlot()
+ self.__displayInvalidated = False
def closeEvent(self, event):
if not self.isModal():
@@ -434,6 +374,54 @@ class ColormapDialog(qt.QDialog):
def sizeHint(self):
return self.layout().minimumSize()
+ def _computeView(self, dataMin, dataMax):
+ """Compute the location of the view according to the bound of the data
+
+ :rtype: Tuple(float, float)
+ """
+ marginRatio = 1.0 / 6.0
+ scale = self._plot.getXAxis().getScale()
+
+ if self._dataRange is not None:
+ if scale == Axis.LOGARITHMIC:
+ minRange = self._dataRange[1]
+ else:
+ minRange = self._dataRange[0]
+ maxRange = self._dataRange[2]
+ if minRange is not None:
+ dataMin = min(dataMin, minRange)
+ dataMax = max(dataMax, maxRange)
+
+ if self._histogramData is not None:
+ info = min_max(self._histogramData[1])
+ if scale == Axis.LOGARITHMIC:
+ minHisto = info.min_positive
+ else:
+ minHisto = info.minimum
+ maxHisto = info.maximum
+ if minHisto is not None:
+ dataMin = min(dataMin, minHisto)
+ dataMax = max(dataMax, maxHisto)
+
+ if scale == Axis.LOGARITHMIC:
+ epsilon = numpy.finfo(numpy.float32).eps
+ if dataMin == 0:
+ dataMin = epsilon
+ if dataMax < dataMin:
+ dataMax = dataMin + epsilon
+ marge = marginRatio * abs(numpy.log10(dataMax) - numpy.log10(dataMin))
+ viewMin = 10**(numpy.log10(dataMin) - marge)
+ viewMax = 10**(numpy.log10(dataMax) + marge)
+ else: # scale == Axis.LINEAR:
+ marge = marginRatio * abs(dataMax - dataMin)
+ if marge < 0.0001:
+ # Smaller that the QLineEdit precision
+ marge = 0.0001
+ viewMin = dataMin - marge
+ viewMax = dataMax + marge
+
+ return viewMin, viewMax
+
def _plotUpdate(self, updateMarkers=True):
"""Update the plot content
@@ -454,27 +442,8 @@ class ColormapDialog(qt.QDialog):
if minData > maxData:
# avoid a full collapse
minData, maxData = maxData, minData
- minimum = minData
- maximum = maxData
- if self._dataRange is not None:
- minRange = self._dataRange[0]
- maxRange = self._dataRange[2]
- minimum = min(minimum, minRange)
- maximum = max(maximum, maxRange)
-
- if self._histogramData is not None:
- minHisto = self._histogramData[1][0]
- maxHisto = self._histogramData[1][-1]
- minimum = min(minimum, minHisto)
- maximum = max(maximum, maxHisto)
-
- marge = abs(maximum - minimum) / 6.0
- if marge < 0.0001:
- # Smaller that the QLineEdit precision
- marge = 0.0001
-
- minView, maxView = minimum - marge, maximum + marge
+ minView, maxView = self._computeView(minData, maxData)
if updateMarkers:
# Save the state in we are not moving the markers
@@ -483,6 +452,9 @@ class ColormapDialog(qt.QDialog):
minView = min(minView, self._initialRange[0])
maxView = max(maxView, self._initialRange[1])
+ if minView > minData:
+ # Hide the min range
+ minData = minView
x = [minView, minData, maxData, maxView]
y = [0, 0, 1, 1]
@@ -491,28 +463,40 @@ class ColormapDialog(qt.QDialog):
color='black',
symbol='o',
linestyle='-',
+ z=2,
resetzoom=False)
+ scale = self._plot.getXAxis().getScale()
+
if updateMarkers:
- minDraggable = (self._colormap().isEditable() and
- not self._minValue.isAutoChecked())
- self._plot.addXMarker(
- self._minValue.getFiniteValue(),
- legend='Min',
- text='Min',
- draggable=minDraggable,
- color='blue',
- constraint=self._plotMinMarkerConstraint)
-
- maxDraggable = (self._colormap().isEditable() and
- not self._maxValue.isAutoChecked())
- self._plot.addXMarker(
- self._maxValue.getFiniteValue(),
- legend='Max',
- text='Max',
- draggable=maxDraggable,
- color='blue',
- constraint=self._plotMaxMarkerConstraint)
+ posMin = self._minValue.getFiniteValue()
+ posMax = self._maxValue.getFiniteValue()
+
+ def isDisplayable(pos):
+ if scale == Axis.LOGARITHMIC:
+ return pos > 0.0
+ return True
+
+ if isDisplayable(posMin):
+ minDraggable = (self._colormap().isEditable() and
+ not self._minValue.isAutoChecked())
+ self._plot.addXMarker(
+ posMin,
+ legend='Min',
+ text='Min',
+ draggable=minDraggable,
+ color='blue',
+ constraint=self._plotMinMarkerConstraint)
+ if isDisplayable(posMax):
+ maxDraggable = (self._colormap().isEditable() and
+ not self._maxValue.isAutoChecked())
+ self._plot.addXMarker(
+ posMax,
+ legend='Max',
+ text='Max',
+ draggable=maxDraggable,
+ color='blue',
+ constraint=self._plotMaxMarkerConstraint)
self._plot.resetZoom()
@@ -546,7 +530,7 @@ class ColormapDialog(qt.QDialog):
"""Compute the data range as used by :meth:`setDataRange`.
:param data: The data to process
- :rtype: Tuple(float, float, float)
+ :rtype: List[Union[None,float]]
"""
if data is None or len(data) == 0:
return None, None, None
@@ -557,10 +541,7 @@ class ColormapDialog(qt.QDialog):
dataRange = None
if dataRange is not None:
- min_positive = dataRange.min_positive
- if min_positive is None:
- min_positive = float('nan')
- dataRange = dataRange.minimum, min_positive, dataRange.maximum
+ dataRange = dataRange.minimum, dataRange.min_positive, dataRange.maximum
if dataRange is None or len(dataRange) != 3:
qt.QMessageBox.warning(
@@ -571,7 +552,7 @@ class ColormapDialog(qt.QDialog):
return dataRange
@staticmethod
- def computeHistogram(data):
+ def computeHistogram(data, scale=Axis.LINEAR):
"""Compute the data histogram as used by :meth:`setHistogram`.
:param data: The data to process
@@ -588,7 +569,12 @@ class ColormapDialog(qt.QDialog):
if len(_data) == 0:
return None, None
+ if scale == Axis.LOGARITHMIC:
+ _data = numpy.log10(_data)
xmin, xmax = min_max(_data, min_positive=False, finite=True)
+ if xmin is None:
+ return None, None
+
nbins = min(256, int(numpy.sqrt(_data.size)))
data_range = xmin, xmax
@@ -601,7 +587,10 @@ class ColormapDialog(qt.QDialog):
_data = _data.ravel().astype(numpy.float32)
histogram = Histogramnd(_data, n_bins=nbins, histo_range=data_range)
- return histogram.histo, histogram.edges[0]
+ bins = histogram.edges[0]
+ if scale == Axis.LOGARITHMIC:
+ bins = 10**bins
+ return histogram.histo, bins
def _getData(self):
if self._data is None:
@@ -624,7 +613,10 @@ class ColormapDialog(qt.QDialog):
else:
self._data = weakref.ref(data, self._dataAboutToFinalize)
- self._updateDataInPlot()
+ if self.isVisible():
+ self._updateDataInPlot()
+ else:
+ self._displayLater()
def _setDataInPlotMode(self, mode):
if self._dataInPlotMode == mode:
@@ -660,10 +652,15 @@ class ColormapDialog(qt.QDialog):
self.setDataRange(*result)
elif mode == _DataInPlotMode.HISTOGRAM:
# The histogram should be done in a worker thread
- result = self.computeHistogram(data)
+ result = self.computeHistogram(data, scale=self._plot.getXAxis().getScale())
self.setHistogram(*result)
self.setDataRange()
+ def _invalidateHistogram(self):
+ """Recompute the histogram if it is displayed"""
+ if self._dataInPlotMode == _DataInPlotMode.HISTOGRAM:
+ self._updateDataInPlot()
+
def _colormapAboutToFinalize(self, weakrefColormap):
"""Callback when the data weakref is about to be finalized."""
if self._colormap is weakrefColormap:
@@ -706,7 +703,8 @@ class ColormapDialog(qt.QDialog):
legend="Histogram",
color='gray',
align='center',
- fill=True)
+ fill=True,
+ z=1)
self._updateMinMaxData()
def getColormap(self):
@@ -727,9 +725,9 @@ class ColormapDialog(qt.QDialog):
"""
colormap = self.getColormap()
if colormap is not None and self._colormapStoredState is not None:
- if self._colormap()._toDict() != self._colormapStoredState:
+ if colormap != self._colormapStoredState:
self._ignoreColormapChange = True
- colormap._setFromDict(self._colormapStoredState)
+ colormap.setFromColormap(self._colormapStoredState)
self._ignoreColormapChange = False
self._applyColormap()
@@ -740,18 +738,25 @@ class ColormapDialog(qt.QDialog):
:param float positiveMin: The positive minimum of the data
:param float maximum: The maximum of the data
"""
- if minimum is None or positiveMin is None or maximum is None:
+ scale = self._plot.getXAxis().getScale()
+ if scale == Axis.LOGARITHMIC:
+ dataMin, dataMax = positiveMin, maximum
+ else:
+ dataMin, dataMax = minimum, maximum
+
+ if dataMin is None or dataMax is None:
self._dataRange = None
self._plot.remove(legend='Range', kind='histogram')
else:
hist = numpy.array([1])
- bin_edges = numpy.array([minimum, maximum])
+ bin_edges = numpy.array([dataMin, dataMax])
self._plot.addHistogram(hist,
bin_edges,
legend="Range",
color='gray',
align='center',
- fill=True)
+ fill=True,
+ z=1)
self._dataRange = minimum, positiveMin, maximum
self._updateMinMaxData()
@@ -801,7 +806,7 @@ class ColormapDialog(qt.QDialog):
"""
colormap = self.getColormap()
if colormap is not None:
- self._colormapStoredState = colormap._toDict()
+ self._colormapStoredState = colormap.copy()
else:
self._colormapStoredState = None
@@ -830,8 +835,11 @@ class ColormapDialog(qt.QDialog):
self._colormap = colormap
self.storeCurrentState()
- self._updateResetButton()
- self._applyColormap()
+ if self.isVisible():
+ self._applyColormap()
+ else:
+ self._updateResetButton()
+ self._displayLater()
def _updateResetButton(self):
resetButton = self._buttonsNonModal.button(qt.QDialogButtonBox.Reset)
@@ -839,7 +847,7 @@ class ColormapDialog(qt.QDialog):
colormap = self.getColormap()
if colormap is not None and colormap.isEditable():
# can reset only in the case the colormap changed
- rStateEnabled = colormap._toDict() != self._colormapStoredState
+ rStateEnabled = colormap != self._colormapStoredState
resetButton.setEnabled(rStateEnabled)
def _applyColormap(self):
@@ -856,12 +864,8 @@ class ColormapDialog(qt.QDialog):
self._maxValue.setEnabled(False)
else:
self._ignoreColormapChange = True
-
- if colormap.getName() is not None:
- name = colormap.getName()
- self._comboBoxColormap.setCurrentName(name)
- self._comboBoxColormap.setEnabled(self._colormap().isEditable())
-
+ self._comboBoxColormap.setCurrentLut(colormap)
+ self._comboBoxColormap.setEnabled(colormap.isEditable())
assert colormap.getNormalization() in Colormap.NORMALIZATIONS
self._normButtonLinear.setChecked(
colormap.getNormalization() == Colormap.LINEAR)
@@ -870,12 +874,17 @@ class ColormapDialog(qt.QDialog):
vmin = colormap.getVMin()
vmax = colormap.getVMax()
dataRange = colormap.getColormapRange()
- self._normButtonLinear.setEnabled(self._colormap().isEditable())
- self._normButtonLog.setEnabled(self._colormap().isEditable())
+ self._normButtonLinear.setEnabled(colormap.isEditable())
+ self._normButtonLog.setEnabled(colormap.isEditable())
self._minValue.setValue(vmin or dataRange[0], isAuto=vmin is None)
self._maxValue.setValue(vmax or dataRange[1], isAuto=vmax is None)
- self._minValue.setEnabled(self._colormap().isEditable())
- self._maxValue.setEnabled(self._colormap().isEditable())
+ self._minValue.setEnabled(colormap.isEditable())
+ self._maxValue.setEnabled(colormap.isEditable())
+
+ axis = self._plot.getXAxis()
+ scale = axis.LINEAR if colormap.getNormalization() == Colormap.LINEAR else axis.LOGARITHMIC
+ axis.setScale(scale)
+
self._ignoreColormapChange = False
self._plotUpdate()
@@ -908,26 +917,47 @@ class ColormapDialog(qt.QDialog):
self._plotUpdate()
self._updateResetButton()
- def _updateName(self):
+ def _updateLut(self):
if self._ignoreColormapChange is True:
return
- if self._colormap():
+ colormap = self._colormap()
+ if colormap is not None:
self._ignoreColormapChange = True
- self._colormap().setName(
- self._comboBoxColormap.getCurrentName())
+ name = self._comboBoxColormap.getCurrentName()
+ if name is not None:
+ colormap.setName(name)
+ else:
+ lut = self._comboBoxColormap.getCurrentColors()
+ colormap.setColormapLUT(lut)
self._ignoreColormapChange = False
- def _updateLinearNorm(self, isNormLinear):
+ def _updateNormalization(self, button):
if self._ignoreColormapChange is True:
return
+ if not button.isChecked():
+ return
- if self._colormap():
+ if button is self._normButtonLinear:
+ norm = Colormap.LINEAR
+ scale = Axis.LINEAR
+ elif button is self._normButtonLog:
+ norm = Colormap.LOGARITHM
+ scale = Axis.LOGARITHMIC
+ else:
+ assert(False)
+
+ colormap = self.getColormap()
+ if colormap is not None:
self._ignoreColormapChange = True
- norm = Colormap.LINEAR if isNormLinear else Colormap.LOGARITHM
- self._colormap().setNormalization(norm)
+ colormap.setNormalization(norm)
+ axis = self._plot.getXAxis()
+ axis.setScale(scale)
self._ignoreColormapChange = False
+ self._invalidateHistogram()
+ self._updateMinMaxData()
+
def _minMaxTextEdited(self, text):
"""Handle _minValue and _maxValue textEdited signal"""
self._minMaxWasEdited = True
@@ -975,13 +1005,3 @@ class ColormapDialog(qt.QDialog):
else:
# Use QDialog keyPressEvent
super(ColormapDialog, self).keyPressEvent(event)
-
- def _activeLogNorm(self, isLog):
- if self._ignoreColormapChange is True:
- return
- if self._colormap():
- self._ignoreColormapChange = True
- norm = Colormap.LOGARITHM if isLog is True else Colormap.LINEAR
- self._colormap().setNormalization(norm)
- self._ignoreColormapChange = False
- self._updateMinMaxData()
diff --git a/silx/gui/dialog/DataFileDialog.py b/silx/gui/dialog/DataFileDialog.py
index 7ff1258..d2d76a3 100644
--- a/silx/gui/dialog/DataFileDialog.py
+++ b/silx/gui/dialog/DataFileDialog.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,16 +30,14 @@ __authors__ = ["V. Valls"]
__license__ = "MIT"
__date__ = "14/02/2018"
+import enum
import logging
from silx.gui import qt
from silx.gui.hdf5.Hdf5Formatter import Hdf5Formatter
import silx.io
from .AbstractDataFileDialog import AbstractDataFileDialog
-from silx.third_party import enum
-try:
- import fabio
-except ImportError:
- fabio = None
+
+import fabio
_logger = logging.getLogger(__name__)
diff --git a/silx/gui/dialog/FileTypeComboBox.py b/silx/gui/dialog/FileTypeComboBox.py
index 07b11cf..92529bc 100644
--- a/silx/gui/dialog/FileTypeComboBox.py
+++ b/silx/gui/dialog/FileTypeComboBox.py
@@ -28,12 +28,9 @@ This module contains utilitaries used by other dialog modules.
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "06/02/2018"
+__date__ = "17/01/2019"
-try:
- import fabio
-except ImportError:
- fabio = None
+import fabio
import silx.io
from silx.gui import qt
@@ -82,7 +79,7 @@ class FileTypeComboBox(qt.QComboBox):
def __initItems(self):
self.clear()
- if fabio is not None and self.__fabioUrlSupported:
+ if self.__fabioUrlSupported:
self.__insertFabioFormats()
self.__insertSilxFormats()
self.__insertAllSupported()
@@ -138,21 +135,36 @@ class FileTypeComboBox(qt.QComboBox):
def __insertFabioFormats(self):
formats = fabio.fabioformats.get_classes(reader=True)
+ from fabio import fabioutils
+ if hasattr(fabioutils, "COMPRESSED_EXTENSIONS"):
+ compressedExtensions = fabioutils.COMPRESSED_EXTENSIONS
+ else:
+ # Support for fabio < 0.9
+ compressedExtensions = set(["gz", "bz2"])
+
extensions = []
allExtensions = set([])
+ def extensionsIterator(reader):
+ for extension in reader.DEFAULT_EXTENSIONS:
+ yield "*.%s" % extension
+ for compressedExtension in compressedExtensions:
+ for extension in reader.DEFAULT_EXTENSIONS:
+ yield "*.%s.%s" % (extension, compressedExtension)
+
for reader in formats:
if not hasattr(reader, "DESCRIPTION"):
continue
if not hasattr(reader, "DEFAULT_EXTENSIONS"):
continue
- ext = reader.DEFAULT_EXTENSIONS
- ext = ["*.%s" % e for e in ext]
+ displayext = reader.DEFAULT_EXTENSIONS
+ displayext = ["*.%s" % e for e in displayext]
+ ext = list(extensionsIterator(reader))
allExtensions.update(ext)
if ext == []:
ext = ["*"]
- extensions.append((reader.DESCRIPTION, ext, reader.codec_name()))
+ extensions.append((reader.DESCRIPTION, displayext, ext, reader.codec_name()))
extensions = list(sorted(extensions))
allExtensions = list(sorted(list(allExtensions)))
@@ -162,13 +174,14 @@ class FileTypeComboBox(qt.QComboBox):
self.setItemData(index, Codec(any_fabio=True), role=self.CODEC_ROLE)
for e in extensions:
+ description, displayExt, allExt, _codecName = e
index = self.count()
if len(e[1]) < 10:
- self.addItem("%s%s (%s)" % (self.INDENTATION, e[0], " ".join(e[1])))
+ self.addItem("%s%s (%s)" % (self.INDENTATION, description, " ".join(displayExt)))
else:
- self.addItem(e[0])
- codec = Codec(fabio_codec=e[2])
- self.setItemData(index, e[1], role=self.EXTENSIONS_ROLE)
+ self.addItem("%s%s" % (self.INDENTATION, description))
+ codec = Codec(fabio_codec=_codecName)
+ self.setItemData(index, allExt, role=self.EXTENSIONS_ROLE)
self.setItemData(index, codec, role=self.CODEC_ROLE)
def itemExtensions(self, index):
diff --git a/silx/gui/dialog/ImageFileDialog.py b/silx/gui/dialog/ImageFileDialog.py
index c324071..d015bd2 100644
--- a/silx/gui/dialog/ImageFileDialog.py
+++ b/silx/gui/dialog/ImageFileDialog.py
@@ -28,7 +28,7 @@ This module contains an :class:`ImageFileDialog`.
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "12/02/2018"
+__date__ = "05/03/2019"
import logging
from silx.gui.plot import actions
@@ -36,10 +36,6 @@ from silx.gui import qt
from silx.gui.plot.PlotWidget import PlotWidget
from .AbstractDataFileDialog import AbstractDataFileDialog
import silx.io
-try:
- import fabio
-except ImportError:
- fabio = None
_logger = logging.getLogger(__name__)
@@ -64,7 +60,7 @@ class _ImageSelection(qt.QWidget):
def isUsed(self):
if self.__shape is None:
- return None
+ return False
return len(self.__shape) > 2
def getSelectedData(self, data):
@@ -73,6 +69,10 @@ class _ImageSelection(qt.QWidget):
return image
def setData(self, data):
+ if data is None:
+ self.__visibleSliders = 0
+ return
+
shape = data.shape
if self.__shape is not None:
# clean up
@@ -117,6 +117,22 @@ class _ImageSelection(qt.QWidget):
break
self.__axis[i].setValue(value)
+ def selectSlicing(self, slicing):
+ """Select a slicing.
+
+ The provided value could be unconsistent and therefore is not supposed
+ to be retrivable with a getter.
+
+ :param Union[None,Tuple[int]] slicing:
+ """
+ if slicing is None:
+ # Create a default slicing
+ needed = self.__visibleSliders
+ slicing = (0,) * needed
+ if len(slicing) < self.__visibleSliders:
+ slicing = slicing + (0,) * (self.__visibleSliders - len(slicing))
+ self.setSlicing(slicing)
+
class _ImagePreview(qt.QWidget):
"""Provide a preview of the selected image"""
diff --git a/silx/gui/dialog/SafeFileSystemModel.py b/silx/gui/dialog/SafeFileSystemModel.py
index 198e089..26954e3 100644
--- a/silx/gui/dialog/SafeFileSystemModel.py
+++ b/silx/gui/dialog/SafeFileSystemModel.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -34,8 +34,10 @@ import sys
import os.path
import logging
import weakref
+
+import six
+
from silx.gui import qt
-from silx.third_party import six
from .SafeFileIconProvider import SafeFileIconProvider
_logger = logging.getLogger(__name__)
diff --git a/silx/gui/dialog/test/test_colormapdialog.py b/silx/gui/dialog/test/test_colormapdialog.py
index 6e50193..8dad196 100644
--- a/silx/gui/dialog/test/test_colormapdialog.py
+++ b/silx/gui/dialog/test/test_colormapdialog.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,13 +26,11 @@
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "23/05/2018"
+__date__ = "09/11/2018"
-import doctest
import unittest
-from silx.gui.utils.testutils import qWaitForWindowExposedAndActivate
from silx.gui import qt
from silx.gui.dialog import ColormapDialog
from silx.gui.utils.testutils import TestCaseQt
@@ -43,27 +41,6 @@ from silx.gui.plot.PlotWindow import PlotWindow
import numpy.random
-# Makes sure a QApplication exists
-_qapp = qt.QApplication.instance() or qt.QApplication([])
-
-
-def _tearDownQt(docTest):
- """Tear down to use for test from docstring.
-
- Checks that dialog widget is displayed
- """
- dialogWidget = docTest.globs['dialog']
- qWaitForWindowExposedAndActivate(dialogWidget)
- dialogWidget.setAttribute(qt.Qt.WA_DeleteOnClose)
- dialogWidget.close()
- del dialogWidget
- _qapp.processEvents()
-
-
-cmapDocTestSuite = doctest.DocTestSuite(ColormapDialog, tearDown=_tearDownQt)
-"""Test suite of tests from the module's docstrings."""
-
-
class TestColormapDialog(TestCaseQt, ParametricTestCase):
"""Test the ColormapDialog."""
def setUp(self):
@@ -86,10 +63,12 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
editing the same colormap"""
colormapDiag2 = ColormapDialog.ColormapDialog()
colormapDiag2.setColormap(self.colormap)
+ colormapDiag2.show()
self.colormapDiag.setColormap(self.colormap)
+ self.colormapDiag.show()
- self.colormapDiag._comboBoxColormap.setCurrentName('red')
- self.colormapDiag._normButtonLog.setChecked(True)
+ self.colormapDiag._comboBoxColormap._setCurrentName('red')
+ self.colormapDiag._normButtonLog.click()
self.assertTrue(self.colormap.getName() == 'red')
self.assertTrue(self.colormapDiag.getColormap().getName() == 'red')
self.assertTrue(self.colormap.getNormalization() == 'log')
@@ -178,6 +157,7 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
def testSetColormapIsCorrect(self):
"""Make sure the interface fir the colormap when set a new colormap"""
self.colormap.setName('red')
+ self.colormapDiag.show()
for norm in (Colormap.NORMALIZATIONS):
for autoscale in (True, False):
if autoscale is True:
@@ -211,7 +191,7 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
self.colormapDiag.show()
del self.colormap
self.assertTrue(self.colormapDiag.getColormap() is None)
- self.colormapDiag._comboBoxColormap.setCurrentName('blue')
+ self.colormapDiag._comboBoxColormap._setCurrentName('blue')
def testColormapEditedOutside(self):
"""Make sure the GUI is still up to date if the colormap is modified
@@ -274,7 +254,7 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
cb = self.colormapDiag._comboBoxColormap
self.assertTrue(cb.getCurrentName() == colormapName)
cb.setCurrentIndex(0)
- index = cb.findColormap(colormapName)
+ index = cb.findLutName(colormapName)
assert index is not 0 # if 0 then the rest of the test has no sense
cb.setCurrentIndex(index)
self.assertTrue(cb.getCurrentName() == colormapName)
@@ -283,6 +263,7 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
"""Test that the colormapDialog is correctly updated when changing the
colormap editable status"""
colormap = Colormap(normalization='linear', vmin=1.0, vmax=10.0)
+ self.colormapDiag.show()
self.colormapDiag.setColormap(colormap)
for editable in (True, False):
with self.subTest(editable=editable):
@@ -302,7 +283,7 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase):
# False
self.colormapDiag.setModal(False)
colormap.setEditable(True)
- self.colormapDiag._normButtonLog.setChecked(True)
+ self.colormapDiag._normButtonLog.click()
resetButton = self.colormapDiag._buttonsNonModal.button(qt.QDialogButtonBox.Reset)
self.assertTrue(resetButton.isEnabled())
colormap.setEditable(False)
@@ -387,7 +368,6 @@ class TestColormapAction(TestCaseQt):
def suite():
test_suite = unittest.TestSuite()
- test_suite.addTest(cmapDocTestSuite)
for testClass in (TestColormapDialog, TestColormapAction):
test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(
testClass))
diff --git a/silx/gui/dialog/test/test_datafiledialog.py b/silx/gui/dialog/test/test_datafiledialog.py
index aff6bc4..b60ea12 100644
--- a/silx/gui/dialog/test/test_datafiledialog.py
+++ b/silx/gui/dialog/test/test_datafiledialog.py
@@ -26,7 +26,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "05/10/2018"
+__date__ = "08/03/2019"
import unittest
@@ -36,16 +36,8 @@ import shutil
import os
import io
import weakref
-
-try:
- import fabio
-except ImportError:
- fabio = None
-try:
- import h5py
-except ImportError:
- h5py = None
-
+import fabio
+import h5py
import silx.io.url
from silx.gui import qt
from silx.gui.utils import testutils
@@ -62,36 +54,33 @@ def setUpModule():
data = numpy.arange(100 * 100)
data.shape = 100, 100
- if fabio is not None:
- filename = _tmpDirectory + "/singleimage.edf"
- image = fabio.edfimage.EdfImage(data=data)
- image.write(filename)
-
- if h5py is not None:
- filename = _tmpDirectory + "/data.h5"
- f = h5py.File(filename, "w")
- f["scalar"] = 10
- f["image"] = data
- f["cube"] = [data, data + 1, data + 2]
- f["complex_image"] = data * 1j
- f["group/image"] = data
- f["nxdata/foo"] = 10
- f["nxdata"].attrs["NX_class"] = u"NXdata"
- f.close()
-
- if h5py is not None:
- directory = os.path.join(_tmpDirectory, "data")
- os.mkdir(directory)
- filename = os.path.join(directory, "data.h5")
- f = h5py.File(filename, "w")
- f["scalar"] = 10
- f["image"] = data
- f["cube"] = [data, data + 1, data + 2]
- f["complex_image"] = data * 1j
- f["group/image"] = data
- f["nxdata/foo"] = 10
- f["nxdata"].attrs["NX_class"] = u"NXdata"
- f.close()
+ filename = _tmpDirectory + "/singleimage.edf"
+ image = fabio.edfimage.EdfImage(data=data)
+ image.write(filename)
+
+ filename = _tmpDirectory + "/data.h5"
+ f = h5py.File(filename, "w")
+ f["scalar"] = 10
+ f["image"] = data
+ f["cube"] = [data, data + 1, data + 2]
+ f["complex_image"] = data * 1j
+ f["group/image"] = data
+ f["nxdata/foo"] = 10
+ f["nxdata"].attrs["NX_class"] = u"NXdata"
+ f.close()
+
+ directory = os.path.join(_tmpDirectory, "data")
+ os.mkdir(directory)
+ filename = os.path.join(directory, "data.h5")
+ f = h5py.File(filename, "w")
+ f["scalar"] = 10
+ f["image"] = data
+ f["cube"] = [data, data + 1, data + 2]
+ f["complex_image"] = data * 1j
+ f["group/image"] = data
+ f["nxdata/foo"] = 10
+ f["nxdata"].attrs["NX_class"] = u"NXdata"
+ f.close()
filename = _tmpDirectory + "/badformat.h5"
with io.open(filename, "wb") as f:
@@ -141,7 +130,7 @@ class _UtilsMixin(object):
path2_ = os.path.normcase(path2)
if path1_ == path2_:
# Use the unittest API to log and display error
- self.assertNotEquals(path1, path2)
+ self.assertNotEqual(path1, path2)
class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
@@ -185,8 +174,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.result(), qt.QDialog.Rejected)
def testSelectRoot_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -211,8 +198,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.result(), qt.QDialog.Accepted)
def testSelectGroup_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -243,8 +228,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.result(), qt.QDialog.Accepted)
def testSelectDataset_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -275,8 +258,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.result(), qt.QDialog.Accepted)
def testClickOnBackToParentTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -307,8 +288,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(url.text(), _tmpDirectory)
def testClickOnBackToRootTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -332,8 +311,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# self.assertFalse(button.isEnabled())
def testClickOnBackToDirectoryTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -361,8 +338,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.allowedLeakingWidgets = 1
def testClickOnHistoryTools(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -402,8 +377,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(url.text(), path3)
def testSelectImageFromEdf(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -412,13 +385,11 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
filename = _tmpDirectory + "/singleimage.edf"
url = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/scan_0/instrument/detector_0/data")
dialog.selectUrl(url.path())
- self.assertTrue(dialog._selectedData().shape, (100, 100))
+ self.assertEqual(dialog._selectedData().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), url.path())
def testSelectImage(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -428,13 +399,11 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/image").path()
dialog.selectUrl(path)
# test
- self.assertTrue(dialog._selectedData().shape, (100, 100))
+ self.assertEqual(dialog._selectedData().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectScalar(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -449,8 +418,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectGroup(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -467,8 +434,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(uri.data_path(), "/group")
def testSelectRoot(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -485,8 +450,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(uri.data_path(), "/")
def testSelectH5_Activate(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -516,11 +479,12 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
filename = _tmpDirectory + "/badformat.h5"
- index = browser.rootIndex().model().index(filename)
+ index = browser.model().index(filename)
+ browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
# test
- self.assertTrue(dialog.selectedUrl(), filename)
+ self.assertSamePath(dialog.selectedUrl(), filename)
def _countSelectableItems(self, model, rootIndex):
selectable = 0
@@ -533,10 +497,6 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
return selectable
def testFilterExtensions(self):
- if h5py is None:
- self.skipTest("h5py is missing")
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -558,8 +518,6 @@ class TestDataFileDialog_FilterDataset(testutils.TestCaseQt, _UtilsMixin):
return dialog
def testSelectGroup_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -585,8 +543,6 @@ class TestDataFileDialog_FilterDataset(testutils.TestCaseQt, _UtilsMixin):
self.assertFalse(button.isEnabled())
def testSelectDataset_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -632,8 +588,6 @@ class TestDataFileDialog_FilterGroup(testutils.TestCaseQt, _UtilsMixin):
return dialog
def testSelectGroup_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -666,8 +620,6 @@ class TestDataFileDialog_FilterGroup(testutils.TestCaseQt, _UtilsMixin):
self.assertRaises(Exception, dialog.selectedData)
def testSelectDataset_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -711,8 +663,6 @@ class TestDataFileDialog_FilterNXdata(testutils.TestCaseQt, _UtilsMixin):
return dialog
def testSelectGroupRefused_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -740,8 +690,6 @@ class TestDataFileDialog_FilterNXdata(testutils.TestCaseQt, _UtilsMixin):
self.assertRaises(Exception, dialog.selectedData)
def testSelectNXdataAccepted_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
dialog.show()
@@ -906,7 +854,7 @@ class TestDataFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
dialog2 = self.createDialog()
result = dialog2.restoreState(state)
self.assertTrue(result)
- self.assertNotEquals(dialog2.directory(), directory)
+ self.assertNotEqual(dialog2.directory(), directory)
def testHistory(self):
dialog = self.createDialog()
@@ -944,8 +892,6 @@ class TestDataFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
self.assertIsNone(dialog._selectedData())
def testBadSubpath(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
self.qWaitForPendingActions(dialog)
@@ -965,8 +911,6 @@ class TestDataFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(url.data_path(), "/group")
def testUnsupportedSlicingPath(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
self.qWaitForPendingActions(dialog)
dialog.selectUrl(_tmpDirectory + "/data.h5?path=/cube&slice=0")
diff --git a/silx/gui/dialog/test/test_imagefiledialog.py b/silx/gui/dialog/test/test_imagefiledialog.py
index 66469f3..c019afb 100644
--- a/silx/gui/dialog/test/test_imagefiledialog.py
+++ b/silx/gui/dialog/test/test_imagefiledialog.py
@@ -26,7 +26,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "05/10/2018"
+__date__ = "08/03/2019"
import unittest
@@ -36,16 +36,8 @@ import shutil
import os
import io
import weakref
-
-try:
- import fabio
-except ImportError:
- fabio = None
-try:
- import h5py
-except ImportError:
- h5py = None
-
+import fabio
+import h5py
import silx.io.url
from silx.gui import qt
from silx.gui.utils import testutils
@@ -63,42 +55,39 @@ def setUpModule():
data = numpy.arange(100 * 100)
data.shape = 100, 100
- if fabio is not None:
- filename = _tmpDirectory + "/singleimage.edf"
- image = fabio.edfimage.EdfImage(data=data)
- image.write(filename)
+ filename = _tmpDirectory + "/singleimage.edf"
+ image = fabio.edfimage.EdfImage(data=data)
+ image.write(filename)
- filename = _tmpDirectory + "/multiframe.edf"
- image = fabio.edfimage.EdfImage(data=data)
- image.appendFrame(data=data + 1)
- image.appendFrame(data=data + 2)
- image.write(filename)
+ filename = _tmpDirectory + "/multiframe.edf"
+ image = fabio.edfimage.EdfImage(data=data)
+ image.appendFrame(data=data + 1)
+ image.appendFrame(data=data + 2)
+ image.write(filename)
- filename = _tmpDirectory + "/singleimage.msk"
- image = fabio.fit2dmaskimage.Fit2dMaskImage(data=data % 2 == 1)
- image.write(filename)
+ filename = _tmpDirectory + "/singleimage.msk"
+ image = fabio.fit2dmaskimage.Fit2dMaskImage(data=data % 2 == 1)
+ image.write(filename)
- if h5py is not None:
- filename = _tmpDirectory + "/data.h5"
- f = h5py.File(filename, "w")
+ filename = _tmpDirectory + "/data.h5"
+ with h5py.File(filename, "w") as f:
f["scalar"] = 10
f["image"] = data
f["cube"] = [data, data + 1, data + 2]
+ f["single_frame"] = [data + 5]
f["complex_image"] = data * 1j
f["group/image"] = data
- f.close()
- if h5py is not None:
- directory = os.path.join(_tmpDirectory, "data")
- os.mkdir(directory)
- filename = os.path.join(directory, "data.h5")
- f = h5py.File(filename, "w")
+ directory = os.path.join(_tmpDirectory, "data")
+ os.mkdir(directory)
+ filename = os.path.join(directory, "data.h5")
+ with h5py.File(filename, "w") as f:
f["scalar"] = 10
f["image"] = data
f["cube"] = [data, data + 1, data + 2]
+ f["single_frame"] = [data + 5]
f["complex_image"] = data * 1j
f["group/image"] = data
- f.close()
filename = _tmpDirectory + "/badformat.edf"
with io.open(filename, "wb") as f:
@@ -148,7 +137,7 @@ class _UtilsMixin(object):
path2_ = os.path.normcase(path2)
if path1_ == path2_:
# Use the unittest API to log and display error
- self.assertNotEquals(path1, path2)
+ self.assertNotEqual(path1, path2)
class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
@@ -192,8 +181,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.result(), qt.QDialog.Rejected)
def testDisplayAndClickOpen(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -259,8 +246,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(dialog.viewMode(), qt.QFileDialog.List)
def testClickOnBackToParentTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -291,8 +276,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(url.text(), _tmpDirectory)
def testClickOnBackToRootTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -316,8 +299,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
# self.assertFalse(button.isEnabled())
def testClickOnBackToDirectoryTool(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -345,8 +326,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.allowedLeakingWidgets = 1
def testClickOnHistoryTools(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -386,8 +365,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(url.text(), path3)
def testSelectImageFromEdf(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -396,14 +373,12 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
filename = _tmpDirectory + "/singleimage.edf"
path = filename
dialog.selectUrl(path)
- self.assertTrue(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
path = silx.io.url.DataUrl(scheme="fabio", file_path=filename).path()
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectImageFromEdf_Activate(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -421,13 +396,11 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
# test
- self.assertTrue(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectFrameFromEdf(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -438,14 +411,12 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
dialog.selectUrl(path)
# test
image = dialog.selectedImage()
- self.assertTrue(image.shape, (100, 100))
- self.assertTrue(image[0, 0], 1)
+ self.assertEqual(image.shape, (100, 100))
+ self.assertEqual(image[0, 0], 1)
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectImageFromMsk(self):
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -455,13 +426,11 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(scheme="fabio", file_path=filename).path()
dialog.selectUrl(path)
# test
- self.assertTrue(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectImageFromH5(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -471,13 +440,11 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/image").path()
dialog.selectUrl(path)
# test
- self.assertTrue(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectH5_Activate(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -498,8 +465,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(dialog.selectedUrl(), path)
def testSelectFrameFromH5(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.show()
self.qWaitForWindowExposed(dialog)
@@ -509,8 +474,23 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/cube", data_slice=(1, )).path()
dialog.selectUrl(path)
# test
- self.assertTrue(dialog.selectedImage().shape, (100, 100))
- self.assertTrue(dialog.selectedImage()[0, 0], 1)
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage()[0, 0], 1)
+ self.assertSamePath(dialog.selectedFile(), filename)
+ self.assertSamePath(dialog.selectedUrl(), path)
+
+ def testSelectSingleFrameFromH5(self):
+ dialog = self.createDialog()
+ dialog.show()
+ self.qWaitForWindowExposed(dialog)
+
+ # init state
+ filename = _tmpDirectory + "/data.h5"
+ path = silx.io.url.DataUrl(scheme="silx", file_path=filename, data_path="/single_frame", data_slice=(0, )).path()
+ dialog.selectUrl(path)
+ # test
+ self.assertEqual(dialog.selectedImage().shape, (100, 100))
+ self.assertEqual(dialog.selectedImage()[0, 0], 5)
self.assertSamePath(dialog.selectedFile(), filename)
self.assertSamePath(dialog.selectedUrl(), path)
@@ -524,11 +504,12 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
self.qWaitForPendingActions(dialog)
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
filename = _tmpDirectory + "/badformat.edf"
- index = browser.rootIndex().model().index(filename)
+ index = browser.model().index(filename)
+ browser.selectIndex(index)
browser.activated.emit(index)
self.qWaitForPendingActions(dialog)
# test
- self.assertTrue(dialog.selectedUrl(), filename)
+ self.assertSamePath(dialog.selectedUrl(), filename)
def _countSelectableItems(self, model, rootIndex):
selectable = 0
@@ -541,10 +522,6 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin):
return selectable
def testFilterExtensions(self):
- if h5py is None:
- self.skipTest("h5py is missing")
- if fabio is None:
- self.skipTest("fabio is missing")
dialog = self.createDialog()
browser = testutils.findChildren(dialog, qt.QWidget, name="browser")[0]
filters = testutils.findChildren(dialog, qt.QWidget, name="fileTypeCombo")[0]
@@ -588,7 +565,7 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
result = dialog2.restoreState(state)
self.qWaitForPendingActions(dialog2)
self.assertTrue(result)
- self.assertTrue(dialog2.colormap().getNormalization(), "log")
+ self.assertEqual(dialog2.colormap().getNormalization(), "log")
def printState(self):
"""
@@ -685,7 +662,7 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
result = dialog.restoreState(state)
self.assertTrue(result)
colormap = dialog.colormap()
- self.assertTrue(colormap.getNormalization(), "log")
+ self.assertEqual(colormap.getNormalization(), "log")
def testRestoreRobusness(self):
"""What's happen if you try to open a config file with a different
@@ -711,7 +688,7 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
dialog2 = self.createDialog()
result = dialog2.restoreState(state)
self.assertTrue(result)
- self.assertNotEquals(dialog2.directory(), directory)
+ self.assertNotEqual(dialog2.directory(), directory)
def testHistory(self):
dialog = self.createDialog()
@@ -745,16 +722,12 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
self.assertSamePath(dialog.directory(), _tmpDirectory)
def testBadDataType(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.selectUrl(_tmpDirectory + "/data.h5::/complex_image")
self.qWaitForPendingActions(dialog)
self.assertIsNone(dialog._selectedData())
def testBadDataShape(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
dialog.selectUrl(_tmpDirectory + "/data.h5::/unknown")
self.qWaitForPendingActions(dialog)
@@ -773,8 +746,6 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
self.assertIsNone(dialog._selectedData())
def testBadSubpath(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
self.qWaitForPendingActions(dialog)
@@ -794,8 +765,6 @@ class TestImageFileDialogApi(testutils.TestCaseQt, _UtilsMixin):
self.assertEqual(url.data_path(), "/group")
def testBadSlicingPath(self):
- if h5py is None:
- self.skipTest("h5py is missing")
dialog = self.createDialog()
self.qWaitForPendingActions(dialog)
dialog.selectUrl(_tmpDirectory + "/data.h5::/cube[a;45,-90]")
diff --git a/silx/gui/dialog/utils.py b/silx/gui/dialog/utils.py
index 1c16b44..e2334f9 100644
--- a/silx/gui/dialog/utils.py
+++ b/silx/gui/dialog/utils.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -33,8 +33,10 @@ __date__ = "25/10/2017"
import os
import sys
import types
+
+import six
+
from silx.gui import qt
-from silx.third_party import six
def samefile(path1, path2):
diff --git a/silx/gui/fit/FitWidget.py b/silx/gui/fit/FitWidget.py
index 78230b1..c3804e1 100644
--- a/silx/gui/fit/FitWidget.py
+++ b/silx/gui/fit/FitWidget.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2017 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
#
# This file is part of the PyMca X-ray Fluorescence Toolkit developed at
# the ESRF by the Software group.
@@ -315,8 +315,8 @@ class FitWidget(qt.QWidget):
configuration.update(self.configure())
def setdata(self, x, y, sigmay=None, xmin=None, xmax=None):
- warnings.warning("Method renamed to setData",
- DeprecationWarning)
+ warnings.warn("Method renamed to setData",
+ DeprecationWarning)
self.setData(x, y, sigmay, xmin, xmax)
def setData(self, x, y, sigmay=None, xmin=None, xmax=None):
@@ -525,8 +525,8 @@ class FitWidget(qt.QWidget):
self._emitSignal(ddict)
def startfit(self):
- warnings.warning("Method renamed to startFit",
- DeprecationWarning)
+ warnings.warn("Method renamed to startFit",
+ DeprecationWarning)
self.startFit()
def startFit(self):
diff --git a/silx/gui/hdf5/Hdf5Formatter.py b/silx/gui/hdf5/Hdf5Formatter.py
index 6802142..5754fe8 100644
--- a/silx/gui/hdf5/Hdf5Formatter.py
+++ b/silx/gui/hdf5/Hdf5Formatter.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,14 +30,12 @@ __license__ = "MIT"
__date__ = "06/06/2018"
import numpy
-from silx.third_party import six
+import six
+
from silx.gui import qt
from silx.gui.data.TextFormatter import TextFormatter
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
class Hdf5Formatter(qt.QObject):
@@ -162,10 +160,9 @@ class Hdf5Formatter(qt.QObject):
compound = [self.humanReadableDType(d) for d in compound]
return "compound(%s)" % ", ".join(compound)
elif numpy.issubdtype(dtype, numpy.integer):
- if h5py is not None:
- enumType = h5py.check_dtype(enum=dtype)
- if enumType is not None:
- return "enum"
+ enumType = h5py.check_dtype(enum=dtype)
+ if enumType is not None:
+ return "enum"
text = str(dtype.newbyteorder('N'))
if numpy.issubdtype(dtype, numpy.floating):
diff --git a/silx/gui/hdf5/Hdf5Item.py b/silx/gui/hdf5/Hdf5Item.py
index b3c313e..11a08b6 100644..100755
--- a/silx/gui/hdf5/Hdf5Item.py
+++ b/silx/gui/hdf5/Hdf5Item.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -25,11 +25,13 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "03/09/2018"
+__date__ = "17/01/2019"
import logging
import collections
+import enum
+
from .. import qt
from .. import icons
from . import _utils
@@ -37,13 +39,23 @@ from .Hdf5Node import Hdf5Node
import silx.io.utils
from silx.gui.data.TextFormatter import TextFormatter
from ..hdf5.Hdf5Formatter import Hdf5Formatter
-from ...third_party import six
_logger = logging.getLogger(__name__)
_formatter = TextFormatter()
_hdf5Formatter = Hdf5Formatter(textFormatter=_formatter)
# FIXME: The formatter should be an attribute of the Hdf5Model
+class DescriptionType(enum.Enum):
+ """List of available kind of description.
+ """
+ ERROR = "error"
+ DESCRIPTION = "description"
+ TITLE = "title"
+ PROGRAM = "program"
+ NAME = "name"
+ VALUE = "value"
+
+
class Hdf5Item(Hdf5Node):
"""Subclass of :class:`qt.QStandardItem` to represent an HDF5-like
item (dataset, file, group or link) as an element of a HDF5-like
@@ -62,6 +74,7 @@ class Hdf5Item(Hdf5Node):
self.__error = None
self.__text = text
self.__linkClass = linkClass
+ self.__description = None
self.__nx_class = None
Hdf5Node.__init__(self, parent, populateAll=populateAll)
@@ -217,14 +230,32 @@ class Hdf5Item(Hdf5Node):
def _populateChild(self, populateAll=False):
if self.isGroupObj():
- for name in self.obj:
+ keys = []
+ try:
+ for name in self.obj:
+ keys.append(name)
+ except Exception:
+ lib_name = self.obj.__class__.__module__.split(".")[0]
+ _logger.error("Internal %s error. The file is corrupted.", lib_name)
+ _logger.debug("Backtrace", exc_info=True)
+ if keys == []:
+ # If the file was open in READ_ONLY we still can reach something
+ # https://github.com/silx-kit/silx/issues/2262
+ try:
+ for name in self.obj:
+ keys.append(name)
+ except Exception:
+ lib_name = self.obj.__class__.__module__.split(".")[0]
+ _logger.error("Internal %s error (second time). The file is corrupted.", lib_name)
+ _logger.debug("Backtrace", exc_info=True)
+ for name in keys:
try:
class_ = self.obj.get(name, getclass=True)
link = self.obj.get(name, getclass=True, getlink=True)
link = silx.io.utils.get_h5_class(class_=link)
except Exception:
lib_name = self.obj.__class__.__module__.split(".")[0]
- _logger.warning("Internal %s error", lib_name, exc_info=True)
+ _logger.error("Internal %s error", lib_name)
_logger.debug("Backtrace", exc_info=True)
class_ = None
try:
@@ -344,14 +375,25 @@ class Hdf5Item(Hdf5Node):
def nexusClassName(self):
"""Returns the Nexus class name"""
if self.__nx_class is None:
- self.__nx_class = self.obj.attrs.get("NX_class", None)
- if self.__nx_class is None:
- self.__nx_class = ""
+ obj = self.obj.attrs.get("NX_class", None)
+ if obj is None:
+ text = ""
else:
- if six.PY2:
- self.__nx_class = self.__nx_class.decode()
- elif not isinstance(self.__nx_class, str):
- self.__nx_class = str(self.__nx_class, "UTF-8")
+ text = self._getFormatter().textFormatter().toString(obj)
+ text = text.strip('"')
+ # Check NX_class formatting
+ lower = text.lower()
+ if lower.startswith('nx'):
+ formatedNX_class = 'NX' + lower[2:]
+ if lower == 'nxcansas':
+ formatedNX_class = 'NXcanSAS' # That's the only class with capital letters...
+ if text != formatedNX_class:
+ _logger.error("NX_class: %s is malformed (should be %s)",
+ text,
+ formatedNX_class)
+ text = formatedNX_class
+
+ self.__nx_class = text
return self.__nx_class
def dataName(self, role):
@@ -414,31 +456,131 @@ class Hdf5Item(Hdf5Node):
return self._getFormatter().humanReadableValue(self.obj)
return None
+ _NEXUS_CLASS_TO_VALUE_CHILDREN = {
+ 'NXaperture': (
+ (DescriptionType.DESCRIPTION, 'description'),
+ ),
+ 'NXbeam_stop': (
+ (DescriptionType.DESCRIPTION, 'description'),
+ ),
+ 'NXdetector': (
+ (DescriptionType.NAME, 'local_name'),
+ (DescriptionType.DESCRIPTION, 'description')
+ ),
+ 'NXentry': (
+ (DescriptionType.TITLE, 'title'),
+ ),
+ 'NXenvironment': (
+ (DescriptionType.NAME, 'short_name'),
+ (DescriptionType.NAME, 'name'),
+ (DescriptionType.DESCRIPTION, 'description')
+ ),
+ 'NXinstrument': (
+ (DescriptionType.NAME, 'name'),
+ ),
+ 'NXlog': (
+ (DescriptionType.DESCRIPTION, 'description'),
+ ),
+ 'NXmirror': (
+ (DescriptionType.DESCRIPTION, 'description'),
+ ),
+ 'NXpositioner': (
+ (DescriptionType.NAME, 'name'),
+ ),
+ 'NXprocess': (
+ (DescriptionType.PROGRAM, 'program'),
+ ),
+ 'NXsample': (
+ (DescriptionType.TITLE, 'short_title'),
+ (DescriptionType.NAME, 'name'),
+ (DescriptionType.DESCRIPTION, 'description')
+ ),
+ 'NXsample_component': (
+ (DescriptionType.NAME, 'name'),
+ (DescriptionType.DESCRIPTION, 'description')
+ ),
+ 'NXsensor': (
+ (DescriptionType.NAME, 'short_name'),
+ (DescriptionType.NAME, 'name')
+ ),
+ 'NXsource': (
+ (DescriptionType.NAME, 'name'),
+ ), # or its 'short_name' attribute... This is not supported
+ 'NXsubentry': (
+ (DescriptionType.DESCRIPTION, 'definition'),
+ (DescriptionType.PROGRAM, 'program_name'),
+ (DescriptionType.TITLE, 'title'),
+ ),
+ }
+ """Mapping from NeXus class to child names containing data to use as value"""
+
+ def __computeDataDescription(self):
+ """Compute the data description of this item
+
+ :rtype: Tuple[kind, str]
+ """
+ if self.__isBroken or self.__error is not None:
+ self.obj # lazy loading of the object
+ return DescriptionType.ERROR, self.__error
+
+ if self.h5Class == silx.io.utils.H5Type.DATASET:
+ return DescriptionType.VALUE, self._getFormatter().humanReadableValue(self.obj)
+
+ elif self.isGroupObj() and self.nexusClassName:
+ # For NeXus groups, try to find a title or name
+ # By default, look for a title (most application definitions should have one)
+ defaultSequence = ((DescriptionType.TITLE, 'title'),)
+ sequence = self._NEXUS_CLASS_TO_VALUE_CHILDREN.get(self.nexusClassName, defaultSequence)
+ for kind, child_name in sequence:
+ for index in range(self.childCount()):
+ child = self.child(index)
+ if (isinstance(child, Hdf5Item) and
+ child.h5Class == silx.io.utils.H5Type.DATASET and
+ child.basename == child_name):
+ return kind, self._getFormatter().humanReadableValue(child.obj)
+
+ description = self.obj.attrs.get("desc", None)
+ if description is not None:
+ return DescriptionType.DESCRIPTION, description
+ else:
+ return None, None
+
+ def __getDataDescription(self):
+ """Returns a cached version of the data description
+
+ As the data description have to reach inside the HDF5 tree, the result
+ is cached. A better implementation could be to use a MRU cache, to avoid
+ to allocate too much data.
+
+ :rtype: Tuple[kind, str]
+ """
+ if self.__description is None:
+ self.__description = self.__computeDataDescription()
+ return self.__description
+
def dataDescription(self, role):
"""Data for the description column"""
if role == qt.Qt.DecorationRole:
+ kind, _label = self.__getDataDescription()
+ if kind is not None:
+ icon = icons.getQIcon("description-%s" % kind.value)
+ return icon
return None
if role == qt.Qt.TextAlignmentRole:
return qt.Qt.AlignTop | qt.Qt.AlignLeft
if role == qt.Qt.DisplayRole:
- if self.__isBroken:
- self.obj # lazy loading of the object
- return self.__error
- if "desc" in self.obj.attrs:
- text = self.obj.attrs["desc"]
- else:
- return ""
- return text
+ _kind, label = self.__getDataDescription()
+ return label
if role == qt.Qt.ToolTipRole:
if self.__error is not None:
self.obj # lazy loading of the object
self.__initH5Object()
return self.__error
- if "desc" in self.obj.attrs:
- text = self.obj.attrs["desc"]
+ kind, label = self.__getDataDescription()
+ if label is not None:
+ return "<b>%s</b><br/>%s" % (kind.value.capitalize(), label)
else:
return ""
- return "Description: %s" % text
return None
def dataNode(self, role):
diff --git a/silx/gui/hdf5/Hdf5TreeModel.py b/silx/gui/hdf5/Hdf5TreeModel.py
index 438200b..152f3e5 100644
--- a/silx/gui/hdf5/Hdf5TreeModel.py
+++ b/silx/gui/hdf5/Hdf5TreeModel.py
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "08/10/2018"
+__date__ = "12/03/2019"
import os
@@ -360,9 +360,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
def mimeTypes(self):
types = []
- if self.__fileMoveEnabled:
- types.append(_utils.Hdf5NodeMimeData.MIME_TYPE)
- if self.__datasetDragEnabled:
+ if self.__fileMoveEnabled or self.__datasetDragEnabled:
types.append(_utils.Hdf5DatasetMimeData.MIME_TYPE)
return types
@@ -386,7 +384,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
node = self.nodeFromIndex(indexes[0])
if self.__fileMoveEnabled and node.parent is self.__root:
- mimeData = _utils.Hdf5NodeMimeData(node=node)
+ mimeData = _utils.Hdf5DatasetMimeData(node=node, isRoot=True)
elif self.__datasetDragEnabled:
mimeData = _utils.Hdf5DatasetMimeData(node=node)
else:
@@ -413,23 +411,24 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
if action == qt.Qt.IgnoreAction:
return True
- if self.__fileMoveEnabled and mimedata.hasFormat(_utils.Hdf5NodeMimeData.MIME_TYPE):
- dragNode = mimedata.node()
- parentNode = self.nodeFromIndex(parentIndex)
- if parentNode is not dragNode.parent:
- return False
+ if self.__fileMoveEnabled and mimedata.hasFormat(_utils.Hdf5DatasetMimeData.MIME_TYPE):
+ if mimedata.isRoot():
+ dragNode = mimedata.node()
+ parentNode = self.nodeFromIndex(parentIndex)
+ if parentNode is not dragNode.parent:
+ return False
- if row == -1:
- # append to the parent
- row = parentNode.childCount()
- else:
- # insert at row
- pass
+ if row == -1:
+ # append to the parent
+ row = parentNode.childCount()
+ else:
+ # insert at row
+ pass
- dragNodeParent = dragNode.parent
- sourceRow = dragNodeParent.indexOfChild(dragNode)
- self.moveRow(parentIndex, sourceRow, parentIndex, row)
- return True
+ dragNodeParent = dragNode.parent
+ sourceRow = dragNodeParent.indexOfChild(dragNode)
+ self.moveRow(parentIndex, sourceRow, parentIndex, row)
+ return True
if self.__fileDropEnabled and mimedata.hasFormat("text/uri-list"):
@@ -571,7 +570,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel):
drag-and-drop"""
obj = node.obj
for f in self.__openedFiles:
- if f in obj:
+ if f is obj:
_logger.debug("Close file %s", obj.filename)
obj.close()
self.__openedFiles.remove(obj)
diff --git a/silx/gui/hdf5/NexusSortFilterProxyModel.py b/silx/gui/hdf5/NexusSortFilterProxyModel.py
index 216e992..9c3533f 100644
--- a/silx/gui/hdf5/NexusSortFilterProxyModel.py
+++ b/silx/gui/hdf5/NexusSortFilterProxyModel.py
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "24/07/2018"
+__date__ = "29/11/2018"
import logging
@@ -108,6 +108,8 @@ class NexusSortFilterProxyModel(qt.QSortFilterProxyModel):
def __isNXnode(self, node):
"""Returns true if the node is an NX concept"""
+ if not hasattr(node, "h5Class"):
+ return False
class_ = node.h5Class
if class_ is None or class_ != silx.io.utils.H5Type.GROUP:
return False
diff --git a/silx/gui/hdf5/_utils.py b/silx/gui/hdf5/_utils.py
index 6a34933..aaab228 100644
--- a/silx/gui/hdf5/_utils.py
+++ b/silx/gui/hdf5/_utils.py
@@ -28,12 +28,15 @@ package `silx.gui.hdf5` package.
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "04/05/2018"
+__date__ = "17/01/2019"
import logging
-from .. import qt
+import os.path
+
import silx.io.utils
+import silx.io.url
+from .. import qt
from silx.utils.html import escape
_logger = logging.getLogger(__name__)
@@ -107,11 +110,22 @@ class Hdf5DatasetMimeData(qt.QMimeData):
MIME_TYPE = "application/x-internal-h5py-dataset"
- def __init__(self, node=None, dataset=None):
+ SILX_URI_TYPE = "application/x-silx-uri"
+
+ def __init__(self, node=None, dataset=None, isRoot=False):
qt.QMimeData.__init__(self)
self.__dataset = dataset
self.__node = node
+ self.__isRoot = isRoot
self.setData(self.MIME_TYPE, "".encode(encoding='utf-8'))
+ if node is not None:
+ h5Node = H5Node(node)
+ silxUrl = h5Node.url
+ self.setText(silxUrl)
+ self.setData(self.SILX_URI_TYPE, silxUrl.encode(encoding='utf-8'))
+
+ def isRoot(self):
+ return self.__isRoot
def node(self):
return self.__node
@@ -122,20 +136,6 @@ class Hdf5DatasetMimeData(qt.QMimeData):
return self.__dataset
-class Hdf5NodeMimeData(qt.QMimeData):
- """Mimedata class to identify an internal drag and drop of a Hdf5Node."""
-
- MIME_TYPE = "application/x-internal-h5py-node"
-
- def __init__(self, node=None):
- qt.QMimeData.__init__(self)
- self.__node = node
- self.setData(self.MIME_TYPE, "".encode(encoding='utf-8'))
-
- def node(self):
- return self.__node
-
-
class H5Node(object):
"""Adapter over an h5py object to provide missing informations from h5py
nodes, like internal node path and filename (which are not provided by
@@ -419,3 +419,43 @@ class H5Node(object):
:rtype: str
"""
return self.physical_name.split("/")[-1]
+
+ @property
+ def data_url(self):
+ """Returns a :class:`silx.io.url.DataUrl` object identify this node in the file
+ system.
+
+ :rtype: ~silx.io.url.DataUrl
+ """
+ absolute_filename = os.path.abspath(self.local_filename)
+ return silx.io.url.DataUrl(scheme="silx",
+ file_path=absolute_filename,
+ data_path=self.local_name)
+
+ @property
+ def url(self):
+ """Returns an URL object identifying this node in the file
+ system.
+
+ This URL can be used in different ways.
+
+ .. code-block:: python
+
+ # Parsing the URL
+ import silx.io.url
+ dataurl = silx.io.url.DataUrl(item.url)
+ # dataurl provides access to URL fields
+
+ # Open a numpy array
+ import silx.io
+ dataset = silx.io.get_data(item.url)
+
+ # Open an hdf5 object (URL targetting a file or a group)
+ import silx.io
+ with silx.io.open(item.url) as h5:
+ ...your stuff...
+
+ :rtype: str
+ """
+ data_url = self.data_url
+ return data_url.path()
diff --git a/silx/gui/hdf5/test/test_hdf5.py b/silx/gui/hdf5/test/test_hdf5.py
index 1751a21..4bb43ff 100644..100755
--- a/silx/gui/hdf5/test/test_hdf5.py
+++ b/silx/gui/hdf5/test/test_hdf5.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -26,7 +26,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "03/05/2018"
+__date__ = "12/03/2019"
import time
@@ -43,10 +43,7 @@ from silx.gui.utils.testutils import SignalListener
from silx.io import commonh5
import weakref
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
_tmpDirectory = None
@@ -56,14 +53,13 @@ def setUpModule():
global _tmpDirectory
_tmpDirectory = tempfile.mkdtemp(prefix=__name__)
- if h5py is not None:
- filename = _tmpDirectory + "/data.h5"
+ filename = _tmpDirectory + "/data.h5"
- # create h5 data
- f = h5py.File(filename, "w")
- g = f.create_group("arrays")
- g.create_dataset("scalar", data=10)
- f.close()
+ # create h5 data
+ f = h5py.File(filename, "w")
+ g = f.create_group("arrays")
+ g.create_dataset("scalar", data=10)
+ f.close()
def tearDownModule():
@@ -91,8 +87,6 @@ class TestHdf5TreeModel(TestCaseQt):
def setUp(self):
super(TestHdf5TreeModel, self).setUp()
- if h5py is None:
- self.skipTest("h5py is not available")
def waitForPendingOperations(self, model):
for _ in range(10):
@@ -127,8 +121,6 @@ class TestHdf5TreeModel(TestCaseQt):
model.appendFile(filename)
self.assertEqual(model.rowCount(qt.QModelIndex()), 1)
# clean up
- index = model.index(0, 0, qt.QModelIndex())
- h5File = model.data(index, hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
ref = weakref.ref(model)
model = None
self.qWaitForDestroy(ref)
@@ -232,7 +224,7 @@ class TestHdf5TreeModel(TestCaseQt):
def testSupportedDrop(self):
model = hdf5.Hdf5TreeModel()
- self.assertNotEquals(model.supportedDropActions(), 0)
+ self.assertNotEqual(model.supportedDropActions(), 0)
model.setFileMoveEnabled(False)
model.setFileDropEnabled(False)
@@ -240,11 +232,42 @@ class TestHdf5TreeModel(TestCaseQt):
model.setFileMoveEnabled(False)
model.setFileDropEnabled(True)
- self.assertNotEquals(model.supportedDropActions(), 0)
+ self.assertNotEqual(model.supportedDropActions(), 0)
model.setFileMoveEnabled(True)
model.setFileDropEnabled(False)
- self.assertNotEquals(model.supportedDropActions(), 0)
+ self.assertNotEqual(model.supportedDropActions(), 0)
+
+ def testCloseFile(self):
+ """A file inserted as a filename is open and closed internally."""
+ filename = _tmpDirectory + "/data.h5"
+ model = hdf5.Hdf5TreeModel()
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 0)
+ model.insertFile(filename)
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 1)
+ index = model.index(0, 0)
+ h5File = model.data(index, role=hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ model.removeIndex(index)
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 0)
+ self.assertFalse(bool(h5File.id.valid), "The HDF5 file was not closed")
+
+ def testNotCloseFile(self):
+ """A file inserted as an h5py object is not open (then not closed)
+ internally."""
+ filename = _tmpDirectory + "/data.h5"
+ try:
+ h5File = h5py.File(filename)
+ model = hdf5.Hdf5TreeModel()
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 0)
+ model.insertH5pyObject(h5File)
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 1)
+ index = model.index(0, 0)
+ h5File = model.data(index, role=hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ model.removeIndex(index)
+ self.assertEqual(model.rowCount(qt.QModelIndex()), 0)
+ self.assertTrue(bool(h5File.id.valid), "The HDF5 file was unexpetedly closed")
+ finally:
+ h5File.close()
def testDropExternalFile(self):
filename = _tmpDirectory + "/data.h5"
@@ -290,7 +313,7 @@ class TestHdf5TreeModel(TestCaseQt):
self.assertEqual(displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], "")
self.assertEqual(displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "")
self.assertEqual(displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "")
+ self.assertEqual(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], None)
self.assertEqual(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "File")
def testGroupData(self):
@@ -322,7 +345,7 @@ class TestHdf5TreeModel(TestCaseQt):
self.assertEqual(displayed[hdf5.Hdf5TreeModel.TYPE_COLUMN, qt.Qt.DisplayRole], value.dtype.name)
self.assertEqual(displayed[hdf5.Hdf5TreeModel.SHAPE_COLUMN, qt.Qt.DisplayRole], "3")
self.assertEqual(displayed[hdf5.Hdf5TreeModel.VALUE_COLUMN, qt.Qt.DisplayRole], "[1 2 3]")
- self.assertEqual(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "")
+ self.assertEqual(displayed[hdf5.Hdf5TreeModel.DESCRIPTION_COLUMN, qt.Qt.DisplayRole], "[1 2 3]")
self.assertEqual(displayed[hdf5.Hdf5TreeModel.NODE_COLUMN, qt.Qt.DisplayRole], "Dataset")
def testDropLastAsFirst(self):
@@ -571,8 +594,6 @@ class TestH5Node(TestCaseQt):
@classmethod
def setUpClass(cls):
super(TestH5Node, cls).setUpClass()
- if h5py is None:
- raise unittest.SkipTest("h5py is not available")
cls.tmpDirectory = tempfile.mkdtemp()
cls.h5Filename = cls.createResource(cls.tmpDirectory)
@@ -809,8 +830,6 @@ class TestHdf5TreeView(TestCaseQt):
def setUp(self):
super(TestHdf5TreeView, self).setUp()
- if h5py is None:
- self.skipTest("h5py is not available")
def testCreate(self):
view = hdf5.Hdf5TreeView()
diff --git a/silx/gui/icons.py b/silx/gui/icons.py
index ef99591..1493b92 100644
--- a/silx/gui/icons.py
+++ b/silx/gui/icons.py
@@ -29,7 +29,7 @@ Use :func:`getQIcon` to create Qt QIcon from the name identifying an icon.
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "05/10/2018"
+__date__ = "07/01/2019"
import os
@@ -213,11 +213,12 @@ class MultiImageAnimatedIcon(AbstractAnimatedIcon):
self.__frames = []
for i in range(100):
try:
- filename = getQFile("%s/%02d" % (filename, i))
+ frame_filename = os.sep.join((filename, ("%02d" %i)))
+ frame_file = getQFile(frame_filename)
except ValueError:
break
try:
- icon = qt.QIcon(filename.fileName())
+ icon = qt.QIcon(frame_file.fileName())
except ValueError:
break
self.__frames.append(icon)
@@ -420,4 +421,5 @@ def getQFile(name):
qfile = qt.QFile(filename)
if qfile.exists():
return qfile
+ _logger.debug("File '%s' not found.", filename)
raise ValueError('Not an icon name: %s' % name)
diff --git a/silx/gui/plot/ColorBar.py b/silx/gui/plot/ColorBar.py
index fd4d34e..fd4fdf8 100644
--- a/silx/gui/plot/ColorBar.py
+++ b/silx/gui/plot/ColorBar.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -251,10 +251,13 @@ class ColorBarWidget(qt.QWidget):
def _defaultColormapChanged(self, event):
"""Handle plot default colormap changed"""
- if (event['event'] == 'defaultColormapChanged' and
- self.getPlot().getActiveImage() is None):
- # No active image, take default colormap update into account
- self._syncWithDefaultColormap()
+ if event['event'] == 'defaultColormapChanged':
+ plot = self.getPlot()
+ if (plot is not None and
+ plot.getActiveImage() is None and
+ plot._getActiveItem(kind='scatter') is None):
+ # No active item, take default colormap update into account
+ self._syncWithDefaultColormap()
def _syncWithDefaultColormap(self, data=None):
"""Update colorbar according to plot default colormap"""
@@ -801,7 +804,7 @@ class _TickBar(qt.QWidget):
if self._norm == colors.Colormap.LINEAR:
return 1 - (val - self._vmin) / (self._vmax - self._vmin)
elif self._norm == colors.Colormap.LOGARITHM:
- return 1 - (numpy.log10(val) - numpy.log10(self._vmin)) / (numpy.log10(self._vmax) - numpy.log(self._vmin))
+ return 1 - (numpy.log10(val) - numpy.log10(self._vmin)) / (numpy.log10(self._vmax) - numpy.log10(self._vmin))
else:
raise ValueError('Norm is not recognized')
@@ -864,16 +867,16 @@ class _TickBar(qt.QWidget):
def _guessType(self, font):
"""Try fo find the better format to display the tick's labels
- :param QFont font: the font we want want to use durint the painting
+ :param QFont font: the font we want to use during the painting
"""
form = self._getStandardFormat()
fm = qt.QFontMetrics(font)
width = 0
for tick in self.ticks:
- width = max(fm.width(form.format(tick)), width)
+ width = max(fm.boundingRect(form.format(tick)).width(), width)
- # if the length of the string are too long we are mooving to scientific
+ # if the length of the string are too long we are moving to scientific
# display
if width > _TickBar._WIDTH_DISP_VAL - _TickBar._LINE_WIDTH:
return self._getScientificForm()
diff --git a/silx/gui/plot/CompareImages.py b/silx/gui/plot/CompareImages.py
index 88b257d..3875be4 100644
--- a/silx/gui/plot/CompareImages.py
+++ b/silx/gui/plot/CompareImages.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2018 European Synchrotron Radiation Facility
+# Copyright (c) 2018-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -30,6 +30,7 @@ __license__ = "MIT"
__date__ = "23/07/2018"
+import enum
import logging
import numpy
import weakref
@@ -42,13 +43,16 @@ from silx.gui import plot
from silx.gui import icons
from silx.gui.colors import Colormap
from silx.gui.plot import tools
-from silx.third_party import enum
_logger = logging.getLogger(__name__)
from silx.opencl import ocl
if ocl is not None:
- from silx.opencl import sift
+ try:
+ from silx.opencl import sift
+ except ImportError:
+ # sift module is not available (e.g., in official Debian packages)
+ sift = None
else: # No OpenCL device or no pyopencl
sift = None
@@ -62,6 +66,7 @@ class VisualizationMode(enum.Enum):
HORIZONTAL_LINE = 'hline'
COMPOSITE_RED_BLUE_GRAY = "rbgchannel"
COMPOSITE_RED_BLUE_GRAY_NEG = "rbgnegchannel"
+ COMPOSITE_A_MINUS_B = "aminusb"
@enum.unique
@@ -161,6 +166,16 @@ class CompareImagesToolBar(qt.QToolBar):
self.__ycChannelModeAction = action
self.__visualizationGroup.addAction(action)
+ icon = icons.getQIcon("compare-mode-a-minus-b")
+ action = qt.QAction(icon, "Raw A minus B compare mode", self)
+ action.setIconVisibleInMenu(True)
+ action.setCheckable(True)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_W))
+ action.setProperty("mode", VisualizationMode.COMPOSITE_A_MINUS_B)
+ menu.addAction(action)
+ self.__ycChannelModeAction = action
+ self.__visualizationGroup.addAction(action)
+
menu = qt.QMenu(self)
self.__alignmentAction = qt.QAction(self)
self.__alignmentAction.setMenu(menu)
@@ -539,6 +554,11 @@ class CompareImages(qt.QMainWindow):
def __init__(self, parent=None, backend=None):
qt.QMainWindow.__init__(self, parent)
+ self._resetZoomActive = True
+ self._colormap = Colormap()
+ """Colormap shared by all modes, except the compose images (rgb image)"""
+ self._colormapKeyPoints = Colormap('spring')
+ """Colormap used for sift keypoints"""
if parent is None:
self.setWindowTitle('Compare images')
@@ -553,6 +573,7 @@ class CompareImages(qt.QMainWindow):
self.__previousSeparatorPosition = None
self.__plot = plot.PlotWidget(parent=self, backend=backend)
+ self.__plot.setDefaultColormap(self._colormap)
self.__plot.getXAxis().setLabel('Columns')
self.__plot.getYAxis().setLabel('Rows')
if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
@@ -630,6 +651,14 @@ class CompareImages(qt.QMainWindow):
"""
return self.__plot
+ def getColormap(self):
+ """
+
+ :return: colormap used for compare image
+ :rtype: silx.gui.colors.Colormap
+ """
+ return self._colormap
+
def getRawPixelData(self, x, y):
"""Return the raw pixel of each image data from axes positions.
@@ -835,7 +864,8 @@ class CompareImages(qt.QMainWindow):
self.__raw1 = image1
self.__raw2 = image2
self.__updateData()
- self.__plot.resetZoom()
+ if self.isAutoResetZoom():
+ self.__plot.resetZoom()
def setImage1(self, image1):
"""Set image1 to be compared.
@@ -850,7 +880,8 @@ class CompareImages(qt.QMainWindow):
"""
self.__raw1 = image1
self.__updateData()
- self.__plot.resetZoom()
+ if self.isAutoResetZoom():
+ self.__plot.resetZoom()
def setImage2(self, image2):
"""Set image2 to be compared.
@@ -865,7 +896,8 @@ class CompareImages(qt.QMainWindow):
"""
self.__raw2 = image2
self.__updateData()
- self.__plot.resetZoom()
+ if self.isAutoResetZoom():
+ self.__plot.resetZoom()
def __updateKeyPoints(self):
"""Update the displayed keypoints using cached keypoints.
@@ -878,11 +910,11 @@ class CompareImages(qt.QMainWindow):
y=data[1],
z=1,
value=data[2],
- legend="keypoints",
- colormap=Colormap("spring"))
+ colormap=self._colormapKeyPoints,
+ legend="keypoints")
def __updateData(self):
- """Compute aligned image when the alignement mode changes.
+ """Compute aligned image when the alignment mode changes.
This function cache input images which are used when
vertical/horizontal separators moves.
@@ -943,6 +975,9 @@ class CompareImages(qt.QMainWindow):
elif mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY:
data1 = self.__composeImage(data1, data2, mode)
data2 = numpy.empty((0, 0))
+ elif mode == VisualizationMode.COMPOSITE_A_MINUS_B:
+ data1 = self.__composeImage(data1, data2, mode)
+ data2 = numpy.empty((0, 0))
elif mode == VisualizationMode.ONLY_A:
data2 = numpy.empty((0, 0))
elif mode == VisualizationMode.ONLY_B:
@@ -977,7 +1012,8 @@ class CompareImages(qt.QMainWindow):
else:
vmin = min(self.__data1.min(), self.__data2.min())
vmax = max(self.__data1.max(), self.__data2.max())
- colormap = Colormap(vmin=vmin, vmax=vmax)
+ colormap = self.getColormap()
+ colormap.setVRange(vmin=vmin, vmax=vmax)
self.__image1.setColormap(colormap)
self.__image2.setColormap(colormap)
@@ -1025,6 +1061,13 @@ class CompareImages(qt.QMainWindow):
:rtype: numpy.ndarray
"""
assert(data1.shape[0:2] == data2.shape[0:2])
+ if mode == VisualizationMode.COMPOSITE_A_MINUS_B:
+ # TODO: this calculation has no interest of generating a 'composed'
+ # rgb image, this could be moved in an other function or doc
+ # should be modified
+ _type = data1.dtype
+ result = data1.astype(numpy.float64) - data2.astype(numpy.float64)
+ return result
mode1 = self.__getImageMode(data1)
if mode1 in ["rgb", "rgba"]:
intensity1 = self.__luminosityImage(data1)
@@ -1188,3 +1231,19 @@ class CompareImages(qt.QMainWindow):
data2 = result["result"]
self.__transformation = self.__toAffineTransformation(result)
return data1, data2
+
+ def setAutoResetZoom(self, activate=True):
+ """
+
+ :param bool activate: True if we want to activate the automatic
+ plot reset zoom when setting images.
+ """
+ self._resetZoomActive = activate
+
+ def isAutoResetZoom(self):
+ """
+
+ :return: True if the automatic call to resetzoom is activated
+ :rtype: bool
+ """
+ return self._resetZoomActive
diff --git a/silx/gui/plot/ComplexImageView.py b/silx/gui/plot/ComplexImageView.py
index bbcb0a5..c8470ab 100644
--- a/silx/gui/plot/ComplexImageView.py
+++ b/silx/gui/plot/ComplexImageView.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2017-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2017-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -39,6 +39,7 @@ import logging
import collections
import numpy
+from ...utils.deprecation import deprecated
from .. import qt, icons
from .PlotWindow import Plot2D
from . import items
@@ -170,16 +171,16 @@ class _ComplexDataToolButton(qt.QToolButton):
"""
_MODES = collections.OrderedDict([
- (ImageComplexData.Mode.ABSOLUTE, ('math-amplitude', 'Amplitude')),
- (ImageComplexData.Mode.SQUARE_AMPLITUDE,
+ (ImageComplexData.ComplexMode.ABSOLUTE, ('math-amplitude', 'Amplitude')),
+ (ImageComplexData.ComplexMode.SQUARE_AMPLITUDE,
('math-square-amplitude', 'Square amplitude')),
- (ImageComplexData.Mode.PHASE, ('math-phase', 'Phase')),
- (ImageComplexData.Mode.REAL, ('math-real', 'Real part')),
- (ImageComplexData.Mode.IMAGINARY,
+ (ImageComplexData.ComplexMode.PHASE, ('math-phase', 'Phase')),
+ (ImageComplexData.ComplexMode.REAL, ('math-real', 'Real part')),
+ (ImageComplexData.ComplexMode.IMAGINARY,
('math-imaginary', 'Imaginary part')),
- (ImageComplexData.Mode.AMPLITUDE_PHASE,
+ (ImageComplexData.ComplexMode.AMPLITUDE_PHASE,
('math-phase-color', 'Amplitude and Phase')),
- (ImageComplexData.Mode.LOG10_AMPLITUDE_PHASE,
+ (ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE,
('math-phase-color-log', 'Log10(Amp.) and Phase'))
])
@@ -208,7 +209,7 @@ class _ComplexDataToolButton(qt.QToolButton):
self.setPopupMode(qt.QToolButton.InstantPopup)
- self._modeChanged(self._plot2DComplex.getVisualizationMode())
+ self._modeChanged(self._plot2DComplex.getComplexMode())
self._plot2DComplex.sigVisualizationModeChanged.connect(
self._modeChanged)
@@ -217,7 +218,8 @@ class _ComplexDataToolButton(qt.QToolButton):
icon, text = self._MODES[mode]
self.setIcon(icons.getQIcon(icon))
self.setToolTip('Display the ' + text.lower())
- self._rangeDialogAction.setEnabled(mode == ImageComplexData.Mode.LOG10_AMPLITUDE_PHASE)
+ self._rangeDialogAction.setEnabled(
+ mode == ImageComplexData.ComplexMode.LOG10_AMPLITUDE_PHASE)
def _triggered(self, action):
"""Handle triggering of menu actions"""
@@ -244,8 +246,8 @@ class _ComplexDataToolButton(qt.QToolButton):
else: # update mode
mode = action.data()
- if isinstance(mode, ImageComplexData.Mode):
- self._plot2DComplex.setVisualizationMode(mode)
+ if isinstance(mode, ImageComplexData.ComplexMode):
+ self._plot2DComplex.setComplexMode(mode)
def _rangeChanged(self, range_):
"""Handle updates of range in the dialog"""
@@ -258,8 +260,8 @@ class ComplexImageView(qt.QWidget):
:param parent: See :class:`QMainWindow`
"""
- Mode = ImageComplexData.Mode
- """Also expose the modes inside the class"""
+ ComplexMode = ImageComplexData.ComplexMode
+ """Complex Modes enumeration"""
sigDataChanged = qt.Signal()
"""Signal emitted when data has changed."""
@@ -301,7 +303,7 @@ class ComplexImageView(qt.QWidget):
if event is items.ItemChangedType.DATA:
self.sigDataChanged.emit()
elif event is items.ItemChangedType.VISUALIZATION_MODE:
- mode = self.getVisualizationMode()
+ mode = self.getComplexMode()
self.sigVisualizationModeChanged.emit(mode)
def getPlot(self):
@@ -344,15 +346,34 @@ class ComplexImageView(qt.QWidget):
False to return internal data (do not modify!)
:rtype: numpy.ndarray of float with 2 dims or RGBA image (uint8).
"""
- mode = self.getVisualizationMode()
- if mode in (self.Mode.AMPLITUDE_PHASE,
- self.Mode.LOG10_AMPLITUDE_PHASE):
+ mode = self.getComplexMode()
+ if mode in (self.ComplexMode.AMPLITUDE_PHASE,
+ self.ComplexMode.LOG10_AMPLITUDE_PHASE):
return self._plotImage.getRgbaImageData(copy=copy)
else:
return self._plotImage.getData(copy=copy)
+ # Backward compatibility
+
+ Mode = ComplexMode
+
+ @classmethod
+ @deprecated(replacement='supportedComplexModes', since_version='0.11.0')
+ def getSupportedVisualizationModes(cls):
+ return cls.supportedComplexModes()
+
+ @deprecated(replacement='setComplexMode', since_version='0.11.0')
+ def setVisualizationMode(self, mode):
+ return self.setComplexMode(mode)
+
+ @deprecated(replacement='getComplexMode', since_version='0.11.0')
+ def getVisualizationMode(self):
+ return self.getComplexMode()
+
+ # Image item proxy
+
@staticmethod
- def getSupportedVisualizationModes():
+ def supportedComplexModes():
"""Returns the supported visualization modes.
Supported visualization modes are:
@@ -365,26 +386,33 @@ class ComplexImageView(qt.QWidget):
- log10_amplitude_phase:
Color-coded phase with log10(amplitude) as alpha.
- :rtype: tuple of str
+ :rtype: List[ComplexMode]
"""
- return tuple(ImageComplexData.Mode)
+ return ImageComplexData.supportedComplexModes()
- def setVisualizationMode(self, mode):
+ def setComplexMode(self, mode):
"""Set the mode of visualization of the complex data.
- See :meth:`getSupportedVisualizationModes` for the list of
+ See :meth:`supportedComplexModes` for the list of
supported modes.
- :param str mode: The mode to use.
+ How-to change visualization mode::
+
+ widget = ComplexImageView()
+ widget.setComplexMode(ComplexImageView.ComplexMode.PHASE)
+ # or
+ widget.setComplexMode('phase')
+
+ :param Unions[ComplexMode,str] mode: The mode to use.
"""
- self._plotImage.setVisualizationMode(mode)
+ self._plotImage.setComplexMode(mode)
- def getVisualizationMode(self):
+ def getComplexMode(self):
"""Get the current visualization mode of the complex data.
- :rtype: Mode
+ :rtype: ComplexMode
"""
- return self._plotImage.getVisualizationMode()
+ return self._plotImage.getComplexMode()
def _setAmplitudeRangeInfo(self, max_=None, delta=2):
"""Set the amplitude range to display for 'log10_amplitude_phase' mode.
@@ -402,8 +430,6 @@ class ComplexImageView(qt.QWidget):
:rtype: 2-tuple"""
return self._plotImage._getAmplitudeRangeInfo()
- # Image item proxy
-
def setColormap(self, colormap, mode=None):
"""Set the colormap to use for amplitude, phase, real or imaginary.
@@ -411,14 +437,14 @@ class ComplexImageView(qt.QWidget):
amplitude and phase.
:param ~silx.gui.colors.Colormap colormap: The colormap
- :param Mode mode: If specified, set the colormap of this specific mode
+ :param ComplexMode mode: If specified, set the colormap of this specific mode
"""
self._plotImage.setColormap(colormap, mode)
def getColormap(self, mode=None):
"""Returns the colormap used to display the data.
- :param Mode mode: If specified, set the colormap of this specific mode
+ :param ComplexMode mode: If specified, set the colormap of this specific mode
:rtype: ~silx.gui.colors.Colormap
"""
return self._plotImage.getColormap(mode=mode)
diff --git a/silx/gui/plot/CurvesROIWidget.py b/silx/gui/plot/CurvesROIWidget.py
index 81e684e..4508c60 100644
--- a/silx/gui/plot/CurvesROIWidget.py
+++ b/silx/gui/plot/CurvesROIWidget.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2018 European Synchrotron Radiation Facility
+# Copyright (c) 2004-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -22,50 +22,45 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""Widget to handle regions of interest (ROI) on curves displayed in a PlotWindow.
+"""
+Widget to handle regions of interest (:class:`ROI`) on curves displayed in a
+:class:`PlotWindow`.
This widget is meant to work with :class:`PlotWindow`.
-
-ROI are defined by :
-
-- A name (`ROI` column)
-- A type. The type is the label of the x axis.
- This can be used to apply or not some ROI to a curve and do some post processing.
-- The x coordinate of the left limit (`from` column)
-- The x coordinate of the right limit (`to` column)
-- Raw counts: Sum of the curve's values in the defined Region Of Intereset.
-
- .. image:: img/rawCounts.png
-
-- Net counts: Raw counts minus background
-
- .. image:: img/netCounts.png
"""
-__authors__ = ["V.A. Sole", "T. Vincent"]
+__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
__license__ = "MIT"
-__date__ = "13/11/2017"
+__date__ = "13/03/2018"
from collections import OrderedDict
-
import logging
import os
import sys
-import weakref
-
+import functools
import numpy
-
from silx.io import dictdump
from silx.utils import deprecation
-
+from silx.utils.weakref import WeakMethodProxy
+from silx.utils.proxy import docstring
from .. import icons, qt
+from silx.math.combo import min_max
+import weakref
+from silx.gui.widgets.TableWidget import TableWidget
+from . import items
+from .items.roi import _RegionOfInterestBase
_logger = logging.getLogger(__name__)
class CurvesROIWidget(qt.QWidget):
- """Widget displaying a table of ROI information.
+ """
+ Widget displaying a table of ROI information.
+
+ Implements also the following behavior:
+
+ * if the roiTable has no ROI when showing create the default ICR one
:param parent: See :class:`QWidget`
:param str name: The title of this widget
@@ -80,7 +75,6 @@ class CurvesROIWidget(qt.QWidget):
Type of events:
- AddROI, DelROI, LoadROI and ResetROI with keys: 'roilist', 'roidict'
-
- selectionChanged with keys: 'row', 'col' 'roi', 'key', 'colheader',
'rowheader'
"""
@@ -91,26 +85,44 @@ class CurvesROIWidget(qt.QWidget):
super(CurvesROIWidget, self).__init__(parent)
if name is not None:
self.setWindowTitle(name)
+ self.__lastSigROISignal = None
+ """Store the last value emitted for the sigRoiSignal. In the case the
+ active curve change we need to add this extra step in order to make
+ sure we won't send twice the sigROISignal.
+ This come from the fact sigROISignal is connected to the
+ activeROIChanged signal which is emitted when raw and net counts
+ values are changing but are not embed in the sigROISignal.
+ """
assert plot is not None
self._plotRef = weakref.ref(plot)
+ self._showAllMarkers = False
+ self.currentROI = None
layout = qt.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(0)
- ##############
+
self.headerLabel = qt.QLabel(self)
self.headerLabel.setAlignment(qt.Qt.AlignHCenter)
self.setHeader()
layout.addWidget(self.headerLabel)
- ##############
- self.roiTable = ROITable(self)
+
+ widgetAllCheckbox = qt.QWidget(parent=self)
+ self._showAllCheckBox = qt.QCheckBox("show all ROI",
+ parent=widgetAllCheckbox)
+ widgetAllCheckbox.setLayout(qt.QHBoxLayout())
+ spacer = qt.QWidget(parent=widgetAllCheckbox)
+ spacer.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
+ widgetAllCheckbox.layout().addWidget(spacer)
+ widgetAllCheckbox.layout().addWidget(self._showAllCheckBox)
+ layout.addWidget(widgetAllCheckbox)
+
+ self.roiTable = ROITable(self, plot=plot)
rheight = self.roiTable.horizontalHeader().sizeHint().height()
self.roiTable.setMinimumHeight(4 * rheight)
- self.fillFromROIDict = self.roiTable.fillFromROIDict
- self.getROIListAndDict = self.roiTable.getROIListAndDict
layout.addWidget(self.roiTable)
self._roiFileDir = qt.QDir.home().absolutePath()
- #################
+ self._showAllCheckBox.toggled.connect(self.roiTable.showAllMarkers)
hbox = qt.QWidget(self)
hboxlayout = qt.QHBoxLayout(hbox)
@@ -127,7 +139,8 @@ class CurvesROIWidget(qt.QWidget):
self.addButton.setToolTip('Remove the selected ROI')
self.resetButton = qt.QPushButton(hbox)
self.resetButton.setText("Reset")
- self.addButton.setToolTip('Clear all created ROIs. We only let the default ROI')
+ self.addButton.setToolTip('Clear all created ROIs. We only let the '
+ 'default ROI')
hboxlayout.addWidget(self.addButton)
hboxlayout.addWidget(self.delButton)
@@ -149,19 +162,22 @@ class CurvesROIWidget(qt.QWidget):
layout.addWidget(hbox)
+ # Signal / Slot connections
self.addButton.clicked.connect(self._add)
self.delButton.clicked.connect(self._del)
self.resetButton.clicked.connect(self._reset)
self.loadButton.clicked.connect(self._load)
self.saveButton.clicked.connect(self._save)
- self.roiTable.sigROITableSignal.connect(self._forward)
- self.currentROI = None
- self._middleROIMarkerFlag = False
+ self.roiTable.activeROIChanged.connect(self._emitCurrentROISignal)
+
self._isConnected = False # True if connected to plot signals
self._isInit = False
+ # expose API
+ self.getROIListAndDict = self.roiTable.getROIListAndDict
+
def getPlotWidget(self):
"""Returns the associated PlotWidget or None
@@ -173,10 +189,6 @@ class CurvesROIWidget(qt.QWidget):
self._visibilityChangedHandler(visible=True)
qt.QWidget.showEvent(self, event)
- def hideEvent(self, event):
- self._visibilityChangedHandler(visible=False)
- qt.QWidget.hideEvent(self, event)
-
@property
def roiFileDir(self):
"""The directory from which to load/save ROI from/to files."""
@@ -188,135 +200,81 @@ class CurvesROIWidget(qt.QWidget):
def roiFileDir(self, roiFileDir):
self._roiFileDir = str(roiFileDir)
- def setRois(self, roidict, order=None):
- """Set the ROIs by providing a dictionary of ROI information.
-
- The dictionary keys are the ROI names.
- Each value is a sub-dictionary of ROI info with the following fields:
-
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
-
-
- :param roidict: Dictionary of ROIs
- :param str order: Field used for ordering the ROIs.
- One of "from", "to", "type".
- None (default) for no ordering, or same order as specified
- in parameter ``roidict`` if provided as an OrderedDict.
- """
- if order is None or order.lower() == "none":
- roilist = list(roidict.keys())
- else:
- assert order in ["from", "to", "type"]
- roilist = sorted(roidict.keys(),
- key=lambda roi_name: roidict[roi_name].get(order))
-
- return self.roiTable.fillFromROIDict(roilist, roidict)
+ def setRois(self, rois, order=None):
+ return self.roiTable.setRois(rois, order)
def getRois(self, order=None):
- """Return the currently defined ROIs, as an ordered dict.
-
- The dictionary keys are the ROI names.
- Each value is a sub-dictionary of ROI info with the following fields:
-
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
-
-
- :param order: Field used for ordering the ROIs.
- One of "from", "to", "type", "netcounts", "rawcounts".
- None (default) to get the same order as displayed in the widget.
- :return: Ordered dictionary of ROI information
- """
- roilist, roidict = self.roiTable.getROIListAndDict()
- if order is None or order.lower() == "none":
- ordered_roilist = roilist
- else:
- assert order in ["from", "to", "type", "netcounts", "rawcounts"]
- ordered_roilist = sorted(roidict.keys(),
- key=lambda roi_name: roidict[roi_name].get(order))
-
- return OrderedDict([(name, roidict[name]) for name in ordered_roilist])
+ return self.roiTable.getRois(order)
def setMiddleROIMarkerFlag(self, flag=True):
- """Activate or deactivate middle marker.
-
- This allows shifting both min and max limits at once, by dragging
- a marker located in the middle.
-
- :param bool flag: True to activate middle ROI marker
- """
- if flag:
- self._middleROIMarkerFlag = True
- else:
- self._middleROIMarkerFlag = False
+ return self.roiTable.setMiddleROIMarkerFlag(flag)
def _add(self):
"""Add button clicked handler"""
+ def getNextRoiName():
+ rois = self.roiTable.getRois(order=None)
+ roisNames = []
+ [roisNames.append(roiName) for roiName in rois]
+ nrois = len(rois)
+ if nrois == 0:
+ return "ICR"
+ else:
+ i = 1
+ newroi = "newroi %d" % i
+ while newroi in roisNames:
+ i += 1
+ newroi = "newroi %d" % i
+ return newroi
+ roi = ROI(name=getNextRoiName())
+
+ if roi.getName() == "ICR":
+ roi.setType("Default")
+ else:
+ roi.setType(self.getPlotWidget().getXAxis().getLabel())
+
+ xmin, xmax = self.getPlotWidget().getXAxis().getLimits()
+ fromdata = xmin + 0.25 * (xmax - xmin)
+ todata = xmin + 0.75 * (xmax - xmin)
+ if roi.isICR():
+ fromdata, dummy0, todata, dummy1 = self._getAllLimits()
+ roi.setFrom(fromdata)
+ roi.setTo(todata)
+ self.roiTable.addRoi(roi)
+
+ # back compatibility pymca roi signals
ddict = {}
ddict['event'] = "AddROI"
- roilist, roidict = self.roiTable.getROIListAndDict()
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def _del(self):
"""Delete button clicked handler"""
- row = self.roiTable.currentRow()
- if row >= 0:
- index = self.roiTable.labels.index('Type')
- text = str(self.roiTable.item(row, index).text())
- if text.upper() != 'DEFAULT':
- index = self.roiTable.labels.index('ROI')
- key = str(self.roiTable.item(row, index).text())
- else:
- # This is to prevent deleting ICR ROI, that is
- # usually initialized as "Default" type.
- return
- roilist, roidict = self.roiTable.getROIListAndDict()
- row = roilist.index(key)
- del roilist[row]
- del roidict[key]
- if len(roilist) > 0:
- currentroi = roilist[0]
- else:
- currentroi = None
-
- self.roiTable.fillFromROIDict(roilist=roilist,
- roidict=roidict,
- currentroi=currentroi)
- ddict = {}
- ddict['event'] = "DelROI"
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
- self.sigROIWidgetSignal.emit(ddict)
-
- def _forward(self, ddict):
- """Broadcast events from ROITable signal"""
+ self.roiTable.deleteActiveRoi()
+
+ # back compatibility pymca roi signals
+ ddict = {}
+ ddict['event'] = "DelROI"
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def _reset(self):
"""Reset button clicked handler"""
+ self.roiTable.clear()
+ old = self.blockSignals(True) # avoid several sigROISignal emission
+ self._add()
+ self.blockSignals(old)
+
+ # back compatibility pymca roi signals
ddict = {}
ddict['event'] = "ResetROI"
- roilist0, roidict0 = self.roiTable.getROIListAndDict()
- index = 0
- for key in roilist0:
- if roidict0[key]['type'].upper() == 'DEFAULT':
- index = roilist0.index(key)
- break
- roilist = []
- roidict = {}
- if len(roilist0):
- roilist.append(roilist0[index])
- roidict[roilist[0]] = {}
- roidict[roilist[0]].update(roidict0[roilist[0]])
- self.roiTable.fillFromROIDict(roilist=roilist, roidict=roidict)
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def _load(self):
"""Load button clicked handler"""
@@ -334,32 +292,22 @@ class CurvesROIWidget(qt.QWidget):
dialog.close()
self.roiFileDir = os.path.dirname(outputFile)
- self.load(outputFile)
+ self.roiTable.load(outputFile)
+
+ # back compatibility pymca roi signals
+ ddict = {}
+ ddict['event'] = "LoadROI"
+ ddict['roilist'] = self.roiTable.roidict.values()
+ ddict['roidict'] = self.roiTable.roidict
+ self.sigROIWidgetSignal.emit(ddict)
+ # end back compatibility pymca roi signals
def load(self, filename):
"""Load ROI widget information from a file storing a dict of ROI.
:param str filename: The file from which to load ROI
"""
- rois = dictdump.load(filename)
- currentROI = None
- if self.roiTable.rowCount():
- item = self.roiTable.item(self.roiTable.currentRow(), 0)
- if item is not None:
- currentROI = str(item.text())
-
- # Remove rawcounts and netcounts from ROIs
- for roi in rois['ROI']['roidict'].values():
- roi.pop('rawcounts', None)
- roi.pop('netcounts', None)
-
- self.roiTable.fillFromROIDict(roilist=rois['ROI']['roilist'],
- roidict=rois['ROI']['roidict'],
- currentroi=currentROI)
-
- roilist, roidict = self.roiTable.getROIListAndDict()
- event = {'event': 'LoadROI', 'roilist': roilist, 'roidict': roidict}
- self.sigROIWidgetSignal.emit(event)
+ self.roiTable.load(filename)
def _save(self):
"""Save button clicked handler"""
@@ -396,142 +344,24 @@ class CurvesROIWidget(qt.QWidget):
:param str filename: The file to which to save the ROIs
"""
- roilist, roidict = self.roiTable.getROIListAndDict()
- datadict = {'ROI': {'roilist': roilist, 'roidict': roidict}}
- dictdump.dump(datadict, filename)
+ self.roiTable.save(filename)
def setHeader(self, text='ROIs'):
"""Set the header text of this widget"""
self.headerLabel.setText("<b>%s<\b>" % text)
- def _roiSignal(self, ddict):
- """Handle ROI widget signal"""
- _logger.debug("CurvesROIWidget._roiSignal %s", str(ddict))
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- if ddict['event'] == "AddROI":
- xmin, xmax = plot.getXAxis().getLimits()
- fromdata = xmin + 0.25 * (xmax - xmin)
- todata = xmin + 0.75 * (xmax - xmin)
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if self._middleROIMarkerFlag:
- plot.remove('ROI middle', kind='marker')
- roiList, roiDict = self.roiTable.getROIListAndDict()
- nrois = len(roiList)
- if nrois == 0:
- newroi = "ICR"
- fromdata, dummy0, todata, dummy1 = self._getAllLimits()
- draggable = False
- color = 'black'
- else:
- # find the next index free for newroi.
- for i in range(nrois):
- i += 1
- newroi = "newroi %d" % i
- if newroi not in roiList:
- break
- color = 'blue'
- draggable = True
- plot.addXMarker(fromdata,
- legend='ROI min',
- text='ROI min',
- color=color,
- draggable=draggable)
- plot.addXMarker(todata,
- legend='ROI max',
- text='ROI max',
- color=color,
- draggable=draggable)
- if draggable and self._middleROIMarkerFlag:
- pos = 0.5 * (fromdata + todata)
- plot.addXMarker(pos,
- legend='ROI middle',
- text="",
- color='yellow',
- draggable=draggable)
- roiList.append(newroi)
- roiDict[newroi] = {}
- if newroi == "ICR":
- roiDict[newroi]['type'] = "Default"
- else:
- roiDict[newroi]['type'] = plot.getXAxis().getLabel()
- roiDict[newroi]['from'] = fromdata
- roiDict[newroi]['to'] = todata
- self.roiTable.fillFromROIDict(roilist=roiList,
- roidict=roiDict,
- currentroi=newroi)
- self.currentROI = newroi
- self.calculateRois()
- elif ddict['event'] in ['DelROI', "ResetROI"]:
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if self._middleROIMarkerFlag:
- plot.remove('ROI middle', kind='marker')
- roiList, roiDict = self.roiTable.getROIListAndDict()
- roiDictKeys = list(roiDict.keys())
- if len(roiDictKeys):
- currentroi = roiDictKeys[0]
- else:
- # create again the ICR
- ddict = {"event": "AddROI"}
- return self._roiSignal(ddict)
-
- self.roiTable.fillFromROIDict(roilist=roiList,
- roidict=roiDict,
- currentroi=currentroi)
- self.currentROI = currentroi
-
- elif ddict['event'] == 'LoadROI':
- self.calculateRois()
+ @deprecation.deprecated(replacement="calculateRois",
+ reason="CamelCase convention",
+ since_version="0.7")
+ def calculateROIs(self, *args, **kw):
+ self.calculateRois(*args, **kw)
- elif ddict['event'] == 'selectionChanged':
- _logger.debug("Selection changed")
- self.roilist, self.roidict = self.roiTable.getROIListAndDict()
- fromdata = ddict['roi']['from']
- todata = ddict['roi']['to']
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if ddict['key'] == 'ICR':
- draggable = False
- color = 'black'
- else:
- draggable = True
- color = 'blue'
- plot.addXMarker(fromdata,
- legend='ROI min',
- text='ROI min',
- color=color,
- draggable=draggable)
- plot.addXMarker(todata,
- legend='ROI max',
- text='ROI max',
- color=color,
- draggable=draggable)
- if draggable and self._middleROIMarkerFlag:
- pos = 0.5 * (fromdata + todata)
- plot.addXMarker(pos,
- legend='ROI middle',
- text="",
- color='yellow',
- draggable=True)
- self.currentROI = ddict['key']
- if ddict['colheader'] in ['From', 'To']:
- dict0 = {}
- dict0['event'] = "SetActiveCurveEvent"
- dict0['legend'] = plot.getActiveCurve(just_legend=1)
- plot.setActiveCurve(dict0['legend'])
- elif ddict['colheader'] == 'Raw Counts':
- pass
- elif ddict['colheader'] == 'Net Counts':
- pass
- else:
- self._emitCurrentROISignal()
+ def calculateRois(self, roiList=None, roiDict=None):
+ """Compute ROI information"""
+ return self.roiTable.calculateRois()
- else:
- _logger.debug("Unknown or ignored event %s", ddict['event'])
+ def showAllMarkers(self, _show=True):
+ self.roiTable.showAllMarkers(_show)
def _getAllLimits(self):
"""Retrieve the limits based on the curves."""
@@ -565,429 +395,1125 @@ class CurvesROIWidget(qt.QWidget):
return xmin, ymin, xmax, ymax
- @deprecation.deprecated(replacement="calculateRois",
- reason="CamelCase convention")
- def calculateROIs(self, *args, **kw):
- self.calculateRois(*args, **kw)
+ def showEvent(self, event):
+ self._visibilityChangedHandler(visible=True)
+ qt.QWidget.showEvent(self, event)
- def calculateRois(self, roiList=None, roiDict=None):
- """Compute ROI information"""
- if roiList is None or roiDict is None:
- roiList, roiDict = self.roiTable.getROIListAndDict()
+ def hideEvent(self, event):
+ self._visibilityChangedHandler(visible=False)
+ qt.QWidget.hideEvent(self, event)
- plot = self.getPlotWidget()
- if plot is None:
- activeCurve = None
+ def _visibilityChangedHandler(self, visible):
+ """Handle widget's visibility updates.
+
+ It is connected to plot signals only when visible.
+ """
+ if visible:
+ # if no ROI existing yet, add the default one
+ if self.roiTable.rowCount() is 0:
+ old = self.blockSignals(True) # avoid several sigROISignal emission
+ self._add()
+ self.blockSignals(old)
+ self.calculateRois()
+
+ def fillFromROIDict(self, *args, **kwargs):
+ self.roiTable.fillFromROIDict(*args, **kwargs)
+
+ def _emitCurrentROISignal(self):
+ ddict = {}
+ ddict['event'] = "currentROISignal"
+ if self.roiTable.activeRoi is not None:
+ ddict['ROI'] = self.roiTable.activeRoi.toDict()
+ ddict['current'] = self.roiTable.activeRoi.getName()
else:
- activeCurve = plot.getActiveCurve(just_legend=False)
+ ddict['current'] = None
+
+ if self.__lastSigROISignal != ddict:
+ self.__lastSigROISignal = ddict
+ self.sigROISignal.emit(ddict)
+
+ @property
+ def currentRoi(self):
+ return self.roiTable.activeRoi
+
+
+class _FloatItem(qt.QTableWidgetItem):
+ """
+ Simple QTableWidgetItem overloading the < operator to deal with ordering
+ """
+ def __init__(self):
+ qt.QTableWidgetItem.__init__(self, type=qt.QTableWidgetItem.Type)
+
+ def __lt__(self, other):
+ if self.text() in ('', ROITable.INFO_NOT_FOUND):
+ return False
+ if other.text() in ('', ROITable.INFO_NOT_FOUND):
+ return True
+ return float(self.text()) < float(other.text())
+
+
+class ROITable(TableWidget):
+ """Table widget displaying ROI information.
+
+ See :class:`QTableWidget` for constructor arguments.
+
+ Behavior: listen at the active curve changed only when the widget is
+ visible. Otherwise won't compute the row and net counts...
+ """
+
+ activeROIChanged = qt.Signal()
+ """Signal emitted when the active roi changed or when the value of the
+ active roi are changing"""
+
+ COLUMNS_INDEX = OrderedDict([
+ ('ID', 0),
+ ('ROI', 1),
+ ('Type', 2),
+ ('From', 3),
+ ('To', 4),
+ ('Raw Counts', 5),
+ ('Net Counts', 6),
+ ('Raw Area', 7),
+ ('Net Area', 8),
+ ])
+
+ COLUMNS = list(COLUMNS_INDEX.keys())
+
+ INFO_NOT_FOUND = '????????'
+
+ def __init__(self, parent=None, plot=None, rois=None):
+ super(ROITable, self).__init__(parent)
+ self._showAllMarkers = False
+ self._userIsEditingRoi = False
+ """bool used to avoid conflict when editing the ROI object"""
+ self._isConnected = False
+ self._roiToItems = {}
+ self._roiDict = {}
+ """dict of ROI object. Key is ROi id, value is the ROI object"""
+ self._markersHandler = _RoiMarkerManager()
- if activeCurve is None:
- xproc = None
- yproc = None
- self.setHeader()
+ """
+ Associate for each marker legend used when the `_showAllMarkers` option
+ is active a roi.
+ """
+ self.setColumnCount(len(self.COLUMNS))
+ self.setPlot(plot)
+ self.__setTooltip()
+ self.setSortingEnabled(True)
+ self.itemChanged.connect(self._itemChanged)
+
+ @property
+ def roidict(self):
+ return self._getRoiDict()
+
+ @property
+ def activeRoi(self):
+ return self._markersHandler._activeRoi
+
+ def _getRoiDict(self):
+ ddict = {}
+ for id in self._roiDict:
+ ddict[self._roiDict[id].getName()] = self._roiDict[id]
+ return ddict
+
+ def clear(self):
+ """
+ .. note:: clear the interface only. keep the roidict...
+ """
+ self._markersHandler.clear()
+ self._roiToItems = {}
+ self._roiDict = {}
+
+ qt.QTableWidget.clear(self)
+ self.setRowCount(0)
+ self.setHorizontalHeaderLabels(self.COLUMNS)
+ header = self.horizontalHeader()
+ if hasattr(header, 'setSectionResizeMode'): # Qt5
+ header.setSectionResizeMode(qt.QHeaderView.ResizeToContents)
+ else: # Qt4
+ header.setResizeMode(qt.QHeaderView.ResizeToContents)
+ self.sortByColumn(0, qt.Qt.AscendingOrder)
+ self.hideColumn(self.COLUMNS_INDEX['ID'])
+
+ def setPlot(self, plot):
+ self.clear()
+ self.plot = plot
+
+ def __setTooltip(self):
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['ROI']).setToolTip(
+ 'Region of interest identifier')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['Type']).setToolTip(
+ 'Type of the ROI')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['From']).setToolTip(
+ 'X-value of the min point')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['To']).setToolTip(
+ 'X-value of the max point')
+ self.horizontalHeaderItem(self.COLUMNS_INDEX['Raw Counts']).setToolTip(
+ 'Estimation of the inte