summaryrefslogtreecommitdiff
path: root/silx/gui
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui')
-rw-r--r--silx/gui/__init__.py49
-rw-r--r--silx/gui/_glutils/Context.py75
-rw-r--r--silx/gui/_glutils/FramebufferTexture.py165
-rw-r--r--silx/gui/_glutils/OpenGLWidget.py423
-rw-r--r--silx/gui/_glutils/Program.py202
-rw-r--r--silx/gui/_glutils/Texture.py352
-rw-r--r--silx/gui/_glutils/VertexBuffer.py266
-rw-r--r--silx/gui/_glutils/__init__.py43
-rw-r--r--silx/gui/_glutils/font.py163
-rw-r--r--silx/gui/_glutils/gl.py168
-rw-r--r--silx/gui/_glutils/utils.py121
-rwxr-xr-xsilx/gui/colors.py1326
-rw-r--r--silx/gui/console.py202
-rw-r--r--silx/gui/data/ArrayTableModel.py670
-rw-r--r--silx/gui/data/ArrayTableWidget.py492
-rw-r--r--silx/gui/data/DataViewer.py593
-rw-r--r--silx/gui/data/DataViewerFrame.py217
-rw-r--r--silx/gui/data/DataViewerSelector.py175
-rw-r--r--silx/gui/data/DataViews.py2059
-rw-r--r--silx/gui/data/Hdf5TableView.py646
-rw-r--r--silx/gui/data/HexaTableView.py286
-rw-r--r--silx/gui/data/NXdataWidgets.py1081
-rw-r--r--silx/gui/data/NumpyAxesSelector.py578
-rw-r--r--silx/gui/data/RecordTableView.py447
-rw-r--r--silx/gui/data/TextFormatter.py395
-rw-r--r--silx/gui/data/_RecordPlot.py92
-rw-r--r--silx/gui/data/_VolumeWindow.py148
-rw-r--r--silx/gui/data/__init__.py35
-rw-r--r--silx/gui/data/setup.py41
-rw-r--r--silx/gui/data/test/__init__.py45
-rw-r--r--silx/gui/data/test/test_arraywidget.py329
-rw-r--r--silx/gui/data/test/test_dataviewer.py314
-rw-r--r--silx/gui/data/test/test_numpyaxesselector.py161
-rw-r--r--silx/gui/data/test/test_textformatter.py212
-rw-r--r--silx/gui/dialog/AbstractDataFileDialog.py1742
-rw-r--r--silx/gui/dialog/ColormapDialog.py1771
-rw-r--r--silx/gui/dialog/DataFileDialog.py340
-rw-r--r--silx/gui/dialog/DatasetDialog.py122
-rw-r--r--silx/gui/dialog/FileTypeComboBox.py226
-rw-r--r--silx/gui/dialog/GroupDialog.py230
-rw-r--r--silx/gui/dialog/ImageFileDialog.py354
-rw-r--r--silx/gui/dialog/SafeFileIconProvider.py154
-rw-r--r--silx/gui/dialog/SafeFileSystemModel.py804
-rw-r--r--silx/gui/dialog/__init__.py29
-rw-r--r--silx/gui/dialog/setup.py40
-rw-r--r--silx/gui/dialog/test/__init__.py49
-rw-r--r--silx/gui/dialog/test/test_colormapdialog.py453
-rw-r--r--silx/gui/dialog/test/test_datafiledialog.py939
-rw-r--r--silx/gui/dialog/test/test_imagefiledialog.py784
-rw-r--r--silx/gui/dialog/utils.py106
-rw-r--r--silx/gui/fit/BackgroundWidget.py534
-rw-r--r--silx/gui/fit/FitConfig.py543
-rw-r--r--silx/gui/fit/FitWidget.py739
-rw-r--r--silx/gui/fit/FitWidgets.py559
-rw-r--r--silx/gui/fit/Parameters.py882
-rw-r--r--silx/gui/fit/__init__.py28
-rw-r--r--silx/gui/fit/setup.py43
-rw-r--r--silx/gui/fit/test/__init__.py43
-rw-r--r--silx/gui/fit/test/testBackgroundWidget.py83
-rw-r--r--silx/gui/fit/test/testFitConfig.py95
-rw-r--r--silx/gui/fit/test/testFitWidget.py135
-rw-r--r--silx/gui/hdf5/Hdf5Formatter.py241
-rw-r--r--silx/gui/hdf5/Hdf5HeaderView.py195
-rwxr-xr-xsilx/gui/hdf5/Hdf5Item.py642
-rw-r--r--silx/gui/hdf5/Hdf5LoadingItem.py77
-rw-r--r--silx/gui/hdf5/Hdf5Node.py238
-rw-r--r--silx/gui/hdf5/Hdf5TreeModel.py778
-rw-r--r--silx/gui/hdf5/Hdf5TreeView.py271
-rw-r--r--silx/gui/hdf5/NexusSortFilterProxyModel.py224
-rw-r--r--silx/gui/hdf5/__init__.py44
-rw-r--r--silx/gui/hdf5/_utils.py461
-rw-r--r--silx/gui/hdf5/setup.py41
-rw-r--r--silx/gui/hdf5/test/__init__.py39
-rwxr-xr-xsilx/gui/hdf5/test/test_hdf5.py1140
-rw-r--r--silx/gui/icons.py425
-rw-r--r--silx/gui/plot/AlphaSlider.py300
-rw-r--r--silx/gui/plot/ColorBar.py881
-rw-r--r--silx/gui/plot/Colormap.py42
-rw-r--r--silx/gui/plot/ColormapDialog.py43
-rw-r--r--silx/gui/plot/Colors.py90
-rw-r--r--silx/gui/plot/CompareImages.py1249
-rw-r--r--silx/gui/plot/ComplexImageView.py518
-rw-r--r--silx/gui/plot/CurvesROIWidget.py1584
-rw-r--r--silx/gui/plot/ImageStack.py636
-rw-r--r--silx/gui/plot/ImageView.py854
-rw-r--r--silx/gui/plot/Interaction.py350
-rw-r--r--silx/gui/plot/ItemsSelectionDialog.py286
-rwxr-xr-xsilx/gui/plot/LegendSelector.py1036
-rw-r--r--silx/gui/plot/LimitsHistory.py83
-rw-r--r--silx/gui/plot/MaskToolsWidget.py919
-rw-r--r--silx/gui/plot/PlotActions.py67
-rw-r--r--silx/gui/plot/PlotEvents.py166
-rw-r--r--silx/gui/plot/PlotInteraction.py1748
-rw-r--r--silx/gui/plot/PlotToolButtons.py592
-rw-r--r--silx/gui/plot/PlotTools.py43
-rwxr-xr-xsilx/gui/plot/PlotWidget.py3621
-rw-r--r--silx/gui/plot/PlotWindow.py994
-rw-r--r--silx/gui/plot/PrintPreviewToolButton.py392
-rw-r--r--silx/gui/plot/Profile.py352
-rw-r--r--silx/gui/plot/ProfileMainWindow.py110
-rw-r--r--silx/gui/plot/ROIStatsWidget.py780
-rw-r--r--silx/gui/plot/ScatterMaskToolsWidget.py621
-rw-r--r--silx/gui/plot/ScatterView.py405
-rw-r--r--silx/gui/plot/StackView.py1254
-rw-r--r--silx/gui/plot/StatsWidget.py1661
-rw-r--r--silx/gui/plot/_BaseMaskToolsWidget.py1282
-rw-r--r--silx/gui/plot/__init__.py71
-rw-r--r--silx/gui/plot/_utils/__init__.py93
-rw-r--r--silx/gui/plot/_utils/delaunay.py62
-rw-r--r--silx/gui/plot/_utils/dtime_ticklayout.py442
-rw-r--r--silx/gui/plot/_utils/panzoom.py292
-rw-r--r--silx/gui/plot/_utils/setup.py42
-rw-r--r--silx/gui/plot/_utils/test/__init__.py43
-rw-r--r--silx/gui/plot/_utils/test/test_dtime_ticklayout.py93
-rw-r--r--silx/gui/plot/_utils/test/test_ticklayout.py92
-rw-r--r--silx/gui/plot/_utils/ticklayout.py267
-rw-r--r--silx/gui/plot/actions/PlotAction.py78
-rw-r--r--silx/gui/plot/actions/PlotToolAction.py150
-rw-r--r--silx/gui/plot/actions/__init__.py42
-rwxr-xr-xsilx/gui/plot/actions/control.py694
-rw-r--r--silx/gui/plot/actions/fit.py403
-rw-r--r--silx/gui/plot/actions/histogram.py392
-rw-r--r--silx/gui/plot/actions/io.py818
-rw-r--r--silx/gui/plot/actions/medfilt.py147
-rw-r--r--silx/gui/plot/actions/mode.py104
-rwxr-xr-xsilx/gui/plot/backends/BackendBase.py578
-rwxr-xr-xsilx/gui/plot/backends/BackendMatplotlib.py1544
-rwxr-xr-xsilx/gui/plot/backends/BackendOpenGL.py1420
-rw-r--r--silx/gui/plot/backends/__init__.py29
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py1375
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py1219
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotImage.py756
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotItem.py99
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotTriangles.py197
-rw-r--r--silx/gui/plot/backends/glutils/GLSupport.py158
-rw-r--r--silx/gui/plot/backends/glutils/GLText.py287
-rw-r--r--silx/gui/plot/backends/glutils/GLTexture.py241
-rw-r--r--silx/gui/plot/backends/glutils/PlotImageFile.py153
-rw-r--r--silx/gui/plot/backends/glutils/__init__.py46
-rw-r--r--silx/gui/plot/items/__init__.py52
-rw-r--r--silx/gui/plot/items/_arc_roi.py878
-rw-r--r--silx/gui/plot/items/_pick.py72
-rw-r--r--silx/gui/plot/items/_roi_base.py835
-rw-r--r--silx/gui/plot/items/axis.py569
-rw-r--r--silx/gui/plot/items/complex.py386
-rw-r--r--silx/gui/plot/items/core.py1734
-rw-r--r--silx/gui/plot/items/curve.py326
-rw-r--r--silx/gui/plot/items/histogram.py389
-rw-r--r--silx/gui/plot/items/image.py617
-rwxr-xr-xsilx/gui/plot/items/marker.py281
-rw-r--r--silx/gui/plot/items/roi.py1519
-rw-r--r--silx/gui/plot/items/scatter.py973
-rw-r--r--silx/gui/plot/items/shape.py288
-rw-r--r--silx/gui/plot/matplotlib/Colormap.py249
-rw-r--r--silx/gui/plot/matplotlib/__init__.py37
-rw-r--r--silx/gui/plot/setup.py54
-rw-r--r--silx/gui/plot/stats/__init__.py33
-rw-r--r--silx/gui/plot/stats/stats.py890
-rw-r--r--silx/gui/plot/stats/statshandler.py202
-rw-r--r--silx/gui/plot/test/__init__.py92
-rw-r--r--silx/gui/plot/test/testAlphaSlider.py218
-rw-r--r--silx/gui/plot/test/testColorBar.py354
-rw-r--r--silx/gui/plot/test/testCompareImages.py117
-rw-r--r--silx/gui/plot/test/testComplexImageView.py95
-rw-r--r--silx/gui/plot/test/testCurvesROIWidget.py469
-rw-r--r--silx/gui/plot/test/testImageStack.py197
-rw-r--r--silx/gui/plot/test/testImageView.py136
-rw-r--r--silx/gui/plot/test/testInteraction.py89
-rw-r--r--silx/gui/plot/test/testItem.py340
-rw-r--r--silx/gui/plot/test/testLegendSelector.py142
-rw-r--r--silx/gui/plot/test/testLimitConstraints.py125
-rw-r--r--silx/gui/plot/test/testMaskToolsWidget.py316
-rw-r--r--silx/gui/plot/test/testPixelIntensityHistoAction.py157
-rw-r--r--silx/gui/plot/test/testPlotInteraction.py172
-rwxr-xr-xsilx/gui/plot/test/testPlotWidget.py2072
-rw-r--r--silx/gui/plot/test/testPlotWidgetNoBackend.py631
-rw-r--r--silx/gui/plot/test/testPlotWindow.py185
-rw-r--r--silx/gui/plot/test/testRoiStatsWidget.py290
-rw-r--r--silx/gui/plot/test/testSaveAction.py143
-rw-r--r--silx/gui/plot/test/testScatterMaskToolsWidget.py318
-rw-r--r--silx/gui/plot/test/testScatterView.py134
-rw-r--r--silx/gui/plot/test/testStackView.py261
-rw-r--r--silx/gui/plot/test/testStats.py1058
-rw-r--r--silx/gui/plot/test/testUtilsAxis.py214
-rw-r--r--silx/gui/plot/test/utils.py94
-rw-r--r--silx/gui/plot/tools/CurveLegendsWidget.py247
-rw-r--r--silx/gui/plot/tools/LimitsToolBar.py131
-rw-r--r--silx/gui/plot/tools/PositionInfo.py376
-rw-r--r--silx/gui/plot/tools/RadarView.py361
-rw-r--r--silx/gui/plot/tools/__init__.py50
-rw-r--r--silx/gui/plot/tools/profile/ScatterProfileToolBar.py54
-rw-r--r--silx/gui/plot/tools/profile/__init__.py38
-rw-r--r--silx/gui/plot/tools/profile/core.py525
-rw-r--r--silx/gui/plot/tools/profile/editors.py307
-rw-r--r--silx/gui/plot/tools/profile/manager.py1076
-rw-r--r--silx/gui/plot/tools/profile/rois.py1156
-rw-r--r--silx/gui/plot/tools/profile/toolbar.py172
-rw-r--r--silx/gui/plot/tools/roi.py1417
-rw-r--r--silx/gui/plot/tools/test/__init__.py52
-rw-r--r--silx/gui/plot/tools/test/testCurveLegendsWidget.py125
-rw-r--r--silx/gui/plot/tools/test/testProfile.py673
-rw-r--r--silx/gui/plot/tools/test/testROI.py694
-rw-r--r--silx/gui/plot/tools/test/testScatterProfileToolBar.py196
-rw-r--r--silx/gui/plot/tools/test/testTools.py147
-rw-r--r--silx/gui/plot/tools/toolbars.py362
-rw-r--r--silx/gui/plot/utils/__init__.py30
-rw-r--r--silx/gui/plot/utils/axis.py403
-rw-r--r--silx/gui/plot/utils/intersections.py101
-rw-r--r--silx/gui/plot3d/ParamTreeView.py546
-rw-r--r--silx/gui/plot3d/Plot3DWidget.py460
-rw-r--r--silx/gui/plot3d/Plot3DWindow.py88
-rw-r--r--silx/gui/plot3d/SFViewParamTree.py1817
-rw-r--r--silx/gui/plot3d/ScalarFieldView.py1552
-rw-r--r--silx/gui/plot3d/SceneWidget.py687
-rw-r--r--silx/gui/plot3d/SceneWindow.py219
-rw-r--r--silx/gui/plot3d/__init__.py40
-rw-r--r--silx/gui/plot3d/_model/__init__.py35
-rw-r--r--silx/gui/plot3d/_model/core.py372
-rw-r--r--silx/gui/plot3d/_model/items.py1760
-rw-r--r--silx/gui/plot3d/_model/model.py184
-rw-r--r--silx/gui/plot3d/actions/Plot3DAction.py71
-rw-r--r--silx/gui/plot3d/actions/__init__.py34
-rw-r--r--silx/gui/plot3d/actions/io.py336
-rw-r--r--silx/gui/plot3d/actions/mode.py178
-rw-r--r--silx/gui/plot3d/actions/viewpoint.py231
-rw-r--r--silx/gui/plot3d/items/__init__.py43
-rw-r--r--silx/gui/plot3d/items/_pick.py265
-rw-r--r--silx/gui/plot3d/items/clipplane.py136
-rw-r--r--silx/gui/plot3d/items/core.py779
-rw-r--r--silx/gui/plot3d/items/image.py425
-rw-r--r--silx/gui/plot3d/items/mesh.py792
-rw-r--r--silx/gui/plot3d/items/mixins.py288
-rw-r--r--silx/gui/plot3d/items/scatter.py617
-rw-r--r--silx/gui/plot3d/items/volume.py886
-rw-r--r--silx/gui/plot3d/scene/__init__.py34
-rw-r--r--silx/gui/plot3d/scene/axes.py258
-rw-r--r--silx/gui/plot3d/scene/camera.py353
-rw-r--r--silx/gui/plot3d/scene/core.py343
-rw-r--r--silx/gui/plot3d/scene/cutplane.py390
-rw-r--r--silx/gui/plot3d/scene/event.py225
-rw-r--r--silx/gui/plot3d/scene/function.py654
-rw-r--r--silx/gui/plot3d/scene/interaction.py701
-rw-r--r--silx/gui/plot3d/scene/primitives.py2524
-rw-r--r--silx/gui/plot3d/scene/test/__init__.py43
-rw-r--r--silx/gui/plot3d/scene/test/test_transform.py91
-rw-r--r--silx/gui/plot3d/scene/test/test_utils.py275
-rw-r--r--silx/gui/plot3d/scene/text.py535
-rw-r--r--silx/gui/plot3d/scene/transform.py1027
-rw-r--r--silx/gui/plot3d/scene/utils.py662
-rw-r--r--silx/gui/plot3d/scene/viewport.py603
-rw-r--r--silx/gui/plot3d/scene/window.py430
-rw-r--r--silx/gui/plot3d/setup.py50
-rw-r--r--silx/gui/plot3d/test/__init__.py75
-rw-r--r--silx/gui/plot3d/test/testGL.py84
-rw-r--r--silx/gui/plot3d/test/testScalarFieldView.py139
-rw-r--r--silx/gui/plot3d/test/testSceneWidget.py84
-rw-r--r--silx/gui/plot3d/test/testSceneWidgetPicking.py326
-rw-r--r--silx/gui/plot3d/test/testSceneWindow.py245
-rw-r--r--silx/gui/plot3d/test/testStatsWidget.py216
-rw-r--r--silx/gui/plot3d/tools/GroupPropertiesWidget.py202
-rw-r--r--silx/gui/plot3d/tools/PositionInfoWidget.py219
-rw-r--r--silx/gui/plot3d/tools/ViewpointTools.py84
-rw-r--r--silx/gui/plot3d/tools/__init__.py34
-rw-r--r--silx/gui/plot3d/tools/test/__init__.py41
-rw-r--r--silx/gui/plot3d/tools/test/testPositionInfoWidget.py101
-rw-r--r--silx/gui/plot3d/tools/toolbars.py209
-rw-r--r--silx/gui/plot3d/utils/__init__.py28
-rw-r--r--silx/gui/plot3d/utils/mng.py121
-rw-r--r--silx/gui/printer.py62
-rw-r--r--silx/gui/qt/__init__.py60
-rw-r--r--silx/gui/qt/_macosx.py68
-rw-r--r--silx/gui/qt/_pyside_dynamic.py239
-rw-r--r--silx/gui/qt/_pyside_missing.py274
-rw-r--r--silx/gui/qt/_qt.py289
-rw-r--r--silx/gui/qt/_utils.py71
-rw-r--r--silx/gui/qt/inspect.py87
-rw-r--r--silx/gui/setup.py55
-rw-r--r--silx/gui/test/__init__.py113
-rwxr-xr-xsilx/gui/test/test_colors.py619
-rw-r--r--silx/gui/test/test_console.py91
-rw-r--r--silx/gui/test/test_icons.py158
-rw-r--r--silx/gui/test/test_qt.py201
-rw-r--r--silx/gui/test/utils.py43
-rwxr-xr-xsilx/gui/utils/__init__.py76
-rw-r--r--silx/gui/utils/concurrent.py105
-rw-r--r--silx/gui/utils/glutils/__init__.py199
-rw-r--r--silx/gui/utils/image.py143
-rw-r--r--silx/gui/utils/matplotlib.py71
-rw-r--r--silx/gui/utils/projecturl.py77
-rwxr-xr-xsilx/gui/utils/qtutils.py196
-rw-r--r--silx/gui/utils/signal.py141
-rwxr-xr-xsilx/gui/utils/test/__init__.py56
-rw-r--r--silx/gui/utils/test/test.py76
-rw-r--r--silx/gui/utils/test/test_async.py138
-rw-r--r--silx/gui/utils/test/test_glutils.py66
-rw-r--r--silx/gui/utils/test/test_image.py90
-rwxr-xr-xsilx/gui/utils/test/test_qtutils.py75
-rw-r--r--silx/gui/utils/test/test_testutils.py55
-rw-r--r--silx/gui/utils/testutils.py518
-rw-r--r--silx/gui/widgets/BoxLayoutDockWidget.py90
-rw-r--r--silx/gui/widgets/ColormapNameComboBox.py166
-rw-r--r--silx/gui/widgets/ElidedLabel.py137
-rw-r--r--silx/gui/widgets/FloatEdit.py65
-rw-r--r--silx/gui/widgets/FlowLayout.py177
-rw-r--r--silx/gui/widgets/FrameBrowser.py324
-rw-r--r--silx/gui/widgets/HierarchicalTableView.py172
-rwxr-xr-xsilx/gui/widgets/LegendIconWidget.py514
-rw-r--r--silx/gui/widgets/MedianFilterDialog.py80
-rw-r--r--silx/gui/widgets/MultiModeAction.py83
-rw-r--r--silx/gui/widgets/PeriodicTable.py831
-rw-r--r--silx/gui/widgets/PrintGeometryDialog.py222
-rw-r--r--silx/gui/widgets/PrintPreview.py728
-rw-r--r--silx/gui/widgets/RangeSlider.py765
-rw-r--r--silx/gui/widgets/TableWidget.py626
-rw-r--r--silx/gui/widgets/ThreadPoolPushButton.py238
-rw-r--r--silx/gui/widgets/UrlSelectionTable.py172
-rw-r--r--silx/gui/widgets/WaitingPushButton.py245
-rw-r--r--silx/gui/widgets/__init__.py27
-rw-r--r--silx/gui/widgets/setup.py41
-rw-r--r--silx/gui/widgets/test/__init__.py59
-rw-r--r--silx/gui/widgets/test/test_boxlayoutdockwidget.py83
-rw-r--r--silx/gui/widgets/test/test_elidedlabel.py111
-rw-r--r--silx/gui/widgets/test/test_flowlayout.py77
-rw-r--r--silx/gui/widgets/test/test_framebrowser.py73
-rw-r--r--silx/gui/widgets/test/test_hierarchicaltableview.py117
-rw-r--r--silx/gui/widgets/test/test_legendiconwidget.py74
-rw-r--r--silx/gui/widgets/test/test_periodictable.py163
-rw-r--r--silx/gui/widgets/test/test_printpreview.py74
-rw-r--r--silx/gui/widgets/test/test_rangeslider.py114
-rw-r--r--silx/gui/widgets/test/test_tablewidget.py61
-rw-r--r--silx/gui/widgets/test/test_threadpoolpushbutton.py135
331 files changed, 0 insertions, 127699 deletions
diff --git a/silx/gui/__init__.py b/silx/gui/__init__.py
deleted file mode 100644
index b796e20..0000000
--- a/silx/gui/__init__.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# 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
-# 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 package provides a set of Qt widgets.
-
-It contains the following sub-packages and modules:
-
-- silx.gui.colors: Functions to handle colors and colormap
-- silx.gui.console: IPython console widget
-- silx.gui.data:
- Widgets for displaying data arrays using table views and plot widgets
-- silx.gui.dialog: Specific dialog widgets
-- silx.gui.fit: Widgets for controlling curve fitting
-- silx.gui.hdf5: Widgets for displaying content relative to HDF5 format
-- silx.gui.icons: Functions to access embedded icons
-- silx.gui.plot: Widgets for 1D and 2D plotting and related tools
-- silx.gui.plot3d: Widgets for visualizing data in 3D based on OpenGL
-- silx.gui.printer: Shared printer used by the library
-- silx.gui.qt: Common wrapper over different Python Qt binding
-- silx.gui.utils: Miscellaneous helpers for Qt
-- silx.gui.widgets: Miscellaneous standalone widgets
-
-See silx documentation: http://www.silx.org/doc/silx/latest/
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "23/05/2016"
diff --git a/silx/gui/_glutils/Context.py b/silx/gui/_glutils/Context.py
deleted file mode 100644
index c62dbb9..0000000
--- a/silx/gui/_glutils/Context.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# 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
-# 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.
-#
-# ###########################################################################*/
-"""Abstraction of OpenGL context.
-
-It defines a way to get current OpenGL context to support multiple
-OpenGL contexts.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-import contextlib
-
-
-class _DEFAULT_CONTEXT(object):
- """The default value for OpenGL context"""
- pass
-
-_context = _DEFAULT_CONTEXT
-"""The current OpenGL context"""
-
-
-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
- """
- return _context
-
-
-def setCurrent(context=_DEFAULT_CONTEXT):
- """Set a platform dependent OpenGL context
-
- :param context: Platform dependent GL context
- """
- global _context
- _context = context
-
-
-@contextlib.contextmanager
-def current(context):
- """Context manager setting the platform-dependent GL context
-
- :param context: Platform dependent GL context
- """
- previous_context = getCurrent()
- setCurrent(context)
- yield
- setCurrent(previous_context)
diff --git a/silx/gui/_glutils/FramebufferTexture.py b/silx/gui/_glutils/FramebufferTexture.py
deleted file mode 100644
index e065030..0000000
--- a/silx/gui/_glutils/FramebufferTexture.py
+++ /dev/null
@@ -1,165 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2020 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.
-#
-# ###########################################################################*/
-"""Association of a texture and a framebuffer object for off-screen rendering.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-
-import logging
-
-from . import gl
-from .Texture import Texture
-
-
-_logger = logging.getLogger(__name__)
-
-
-class FramebufferTexture(object):
- """Framebuffer with a texture.
-
- Aimed at off-screen rendering to texture.
-
- :param internalFormat: OpenGL texture internal format
- :param shape: Shape (height, width) of the framebuffer and texture
- :type shape: 2-tuple of int
- :param stencilFormat: Stencil renderbuffer format
- :param depthFormat: Depth renderbuffer format
- :param kwargs: Extra arguments for :class:`Texture` constructor
- """
-
- _PACKED_FORMAT = gl.GL_DEPTH24_STENCIL8, gl.GL_DEPTH_STENCIL
-
- def __init__(self,
- internalFormat,
- shape,
- stencilFormat=gl.GL_DEPTH24_STENCIL8,
- depthFormat=gl.GL_DEPTH24_STENCIL8,
- **kwargs):
-
- self._texture = Texture(internalFormat, shape=shape, **kwargs)
- self._texture.prepare()
-
- self._previousFramebuffer = 0 # Used by with statement
-
- self._name = gl.glGenFramebuffers(1)
-
- with self: # Bind FBO
- # Attachments
- gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER,
- gl.GL_COLOR_ATTACHMENT0,
- gl.GL_TEXTURE_2D,
- self._texture.name,
- 0)
-
- height, width = self._texture.shape
-
- if stencilFormat is not None:
- self._stencilId = gl.glGenRenderbuffers(1)
- gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._stencilId)
- gl.glRenderbufferStorage(gl.GL_RENDERBUFFER,
- stencilFormat,
- width, height)
- gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER,
- gl.GL_STENCIL_ATTACHMENT,
- gl.GL_RENDERBUFFER,
- self._stencilId)
- else:
- self._stencilId = None
-
- if depthFormat is not None:
- if self._stencilId and depthFormat in self._PACKED_FORMAT:
- self._depthId = self._stencilId
- else:
- self._depthId = gl.glGenRenderbuffers(1)
- gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._depthId)
- gl.glRenderbufferStorage(gl.GL_RENDERBUFFER,
- depthFormat,
- width, height)
- gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER,
- gl.GL_DEPTH_ATTACHMENT,
- gl.GL_RENDERBUFFER,
- self._depthId)
- else:
- self._depthId = None
-
- assert (gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) ==
- gl.GL_FRAMEBUFFER_COMPLETE)
-
- @property
- def shape(self):
- """Shape of the framebuffer (height, width)"""
- return self._texture.shape
-
- @property
- def texture(self):
- """The texture this framebuffer is rendering to.
-
- The life-cycle of the texture is managed by this object"""
- return self._texture
-
- @property
- def name(self):
- """OpenGL name of the framebuffer"""
- if self._name is not None:
- return self._name
- else:
- raise RuntimeError("No OpenGL framebuffer resource, \
- discard has already been called")
-
- def bind(self):
- """Bind this framebuffer for rendering"""
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.name)
-
- # with statement
-
- def __enter__(self):
- self._previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING)
- self.bind()
-
- def __exit__(self, exctype, excvalue, traceback):
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._previousFramebuffer)
- self._previousFramebuffer = None
-
- def discard(self):
- """Delete associated OpenGL resources including texture"""
- if self._name is not None:
- gl.glDeleteFramebuffers(self._name)
- self._name = None
-
- if self._stencilId is not None:
- gl.glDeleteRenderbuffers(self._stencilId)
- if self._stencilId == self._depthId:
- self._depthId = None
- self._stencilId = None
- if self._depthId is not None:
- gl.glDeleteRenderbuffers(self._depthId)
- self._depthId = None
-
- self._texture.discard() # Also discard the texture
- else:
- _logger.warning("Discard has already been called")
diff --git a/silx/gui/_glutils/OpenGLWidget.py b/silx/gui/_glutils/OpenGLWidget.py
deleted file mode 100644
index 5e3fcb8..0000000
--- a/silx/gui/_glutils/OpenGLWidget.py
+++ /dev/null
@@ -1,423 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 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 package provides a compatibility layer for OpenGL widget.
-
-It provides a compatibility layer for Qt OpenGL widget used in silx
-across Qt<=5.3 QtOpenGL.QGLWidget and QOpenGLWidget.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "22/11/2019"
-
-
-import logging
-import sys
-
-from .. import qt
-from ..utils.glutils import isOpenGLAvailable
-from .._glutils import gl
-
-
-_logger = logging.getLogger(__name__)
-
-
-if not hasattr(qt, 'QOpenGLWidget') and not hasattr(qt, 'QGLWidget'):
- OpenGLWidget = None
-
-else:
- if hasattr(qt, 'QOpenGLWidget'): # PyQt>=5.4
- _logger.info('Using QOpenGLWidget')
- _BaseOpenGLWidget = qt.QOpenGLWidget
-
- else:
- _logger.info('Using QGLWidget')
- _BaseOpenGLWidget = qt.QGLWidget
-
- class _OpenGLWidget(_BaseOpenGLWidget):
- """Wrapper over QOpenGLWidget and QGLWidget"""
-
- sigOpenGLContextError = qt.Signal(str)
- """Signal emitted when an OpenGL context error is detected at runtime.
-
- It provides the error reason as a str.
- """
-
- def __init__(self, parent,
- alphaBufferSize=0,
- depthBufferSize=24,
- stencilBufferSize=8,
- version=(2, 0),
- f=qt.Qt.WindowFlags()):
- # True if using QGLWidget, False if using QOpenGLWidget
- self.__legacy = not hasattr(qt, 'QOpenGLWidget')
-
- self.__devicePixelRatio = 1.0
- self.__requestedOpenGLVersion = int(version[0]), int(version[1])
- self.__isValid = False
-
- if self.__legacy: # QGLWidget
- format_ = qt.QGLFormat()
- format_.setAlphaBufferSize(alphaBufferSize)
- format_.setAlpha(alphaBufferSize != 0)
- format_.setDepthBufferSize(depthBufferSize)
- format_.setDepth(depthBufferSize != 0)
- format_.setStencilBufferSize(stencilBufferSize)
- format_.setStencil(stencilBufferSize != 0)
- format_.setVersion(*self.__requestedOpenGLVersion)
- format_.setDoubleBuffer(True)
-
- super(_OpenGLWidget, self).__init__(format_, parent, None, f)
-
- else: # QOpenGLWidget
- super(_OpenGLWidget, self).__init__(parent, f)
-
- format_ = qt.QSurfaceFormat()
- format_.setAlphaBufferSize(alphaBufferSize)
- format_.setDepthBufferSize(depthBufferSize)
- format_.setStencilBufferSize(stencilBufferSize)
- format_.setVersion(*self.__requestedOpenGLVersion)
- format_.setSwapBehavior(qt.QSurfaceFormat.DoubleBuffer)
- self.setFormat(format_)
-
- # Enable receiving mouse move events when no buttons are pressed
- self.setMouseTracking(True)
-
- def getDevicePixelRatio(self):
- """Returns the ratio device-independent / device pixel size
-
- It should be either 1.0 or 2.0.
-
- :return: Scale factor between screen and Qt units
- :rtype: float
- """
- return self.__devicePixelRatio
-
- def getRequestedOpenGLVersion(self):
- """Returns the requested OpenGL version.
-
- :return: (major, minor)
- :rtype: 2-tuple of int"""
- return self.__requestedOpenGLVersion
-
- def getOpenGLVersion(self):
- """Returns the available OpenGL version.
-
- :return: (major, minor)
- :rtype: 2-tuple of int"""
- if self.__legacy: # QGLWidget
- supportedVersion = 0, 0
-
- # Go through all OpenGL version flags checking support
- flags = self.format().openGLVersionFlags()
- for version in ((1, 1), (1, 2), (1, 3), (1, 4), (1, 5),
- (2, 0), (2, 1),
- (3, 0), (3, 1), (3, 2), (3, 3),
- (4, 0)):
- versionFlag = getattr(qt.QGLFormat,
- 'OpenGL_Version_%d_%d' % version)
- if not versionFlag & flags:
- break
- supportedVersion = version
- return supportedVersion
-
- else: # QOpenGLWidget
- return self.format().version()
-
- # QOpenGLWidget methods
-
- def isValid(self):
- """Returns True if OpenGL is available.
-
- This adds extra checks to Qt isValid method.
-
- :rtype: bool
- """
- return self.__isValid and super(_OpenGLWidget, self).isValid()
-
- def defaultFramebufferObject(self):
- """Returns the framebuffer object handle.
-
- See :meth:`QOpenGLWidget.defaultFramebufferObject`
- """
- if self.__legacy: # QGLWidget
- return 0
- else: # QOpenGLWidget
- return super(_OpenGLWidget, self).defaultFramebufferObject()
-
- # *GL overridden methods
-
- def initializeGL(self):
- parent = self.parent()
- if parent is None:
- _logger.error('_OpenGLWidget has no parent')
- return
-
- # Check OpenGL version
- if self.getOpenGLVersion() >= self.getRequestedOpenGLVersion():
- try:
- gl.glGetError() # clear any previous error (if any)
- version = gl.glGetString(gl.GL_VERSION)
- except:
- version = None
-
- if version:
- self.__isValid = True
- else:
- errMsg = 'OpenGL not available'
- if sys.platform.startswith('linux'):
- errMsg += ': If connected remotely, ' \
- 'GLX forwarding might be disabled.'
- _logger.error(errMsg)
- self.sigOpenGLContextError.emit(errMsg)
- self.__isValid = False
-
- else:
- errMsg = 'OpenGL %d.%d not available' % \
- self.getRequestedOpenGLVersion()
- _logger.error('OpenGL widget disabled: %s', errMsg)
- self.sigOpenGLContextError.emit(errMsg)
- self.__isValid = False
-
- if self.isValid():
- parent.initializeGL()
-
- def paintGL(self):
- parent = self.parent()
- if parent is None:
- _logger.error('_OpenGLWidget has no parent')
- return
-
- if qt.BINDING in ('PyQt5', 'PySide2'):
- devicePixelRatio = self.window().windowHandle().devicePixelRatio()
-
- if devicePixelRatio != self.getDevicePixelRatio():
- # Update devicePixelRatio and call resizeOpenGL
- # as resizeGL is not always called.
- self.__devicePixelRatio = devicePixelRatio
- self.makeCurrent()
- parent.resizeGL(self.width(), self.height())
-
- if self.isValid():
- parent.paintGL()
-
- def resizeGL(self, width, height):
- parent = self.parent()
- if parent is None:
- _logger.error('_OpenGLWidget has no parent')
- return
-
- if self.isValid():
- # Call parent resizeGL with device-independent pixel unit
- # This works over both QGLWidget and QOpenGLWidget
- parent.resizeGL(self.width(), self.height())
-
-
-class OpenGLWidget(qt.QWidget):
- """OpenGL widget wrapper over QGLWidget and QOpenGLWidget
-
- This wrapper API implements a subset of QOpenGLWidget API.
- The constructor takes a different set of arguments.
- Methods returning object like :meth:`context` returns either
- QGL* or QOpenGL* objects.
-
- :param parent: Parent widget see :class:`QWidget`
- :param int alphaBufferSize:
- Size in bits of the alpha channel (default: 0).
- Set to 0 to disable alpha channel.
- :param int depthBufferSize:
- Size in bits of the depth buffer (default: 24).
- Set to 0 to disable depth buffer.
- :param int stencilBufferSize:
- Size in bits of the stencil buffer (default: 8).
- Set to 0 to disable stencil buffer
- :param version: Requested OpenGL version (default: (2, 0)).
- :type version: 2-tuple of int
- :param f: see :class:`QWidget`
- """
-
- def __init__(self, parent=None,
- alphaBufferSize=0,
- depthBufferSize=24,
- stencilBufferSize=8,
- version=(2, 0),
- f=qt.Qt.WindowFlags()):
- super(OpenGLWidget, self).__init__(parent, f)
-
- layout = qt.QHBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- self.setLayout(layout)
-
- self.__context = None
-
- _check = isOpenGLAvailable(version=version, runtimeCheck=False)
- if _OpenGLWidget is None or not _check:
- _logger.error('OpenGL-based widget disabled: %s', _check.error)
- self.__openGLWidget = None
- label = self._createErrorQLabel(_check.error)
- self.layout().addWidget(label)
-
- else:
- self.__openGLWidget = _OpenGLWidget(
- parent=self,
- alphaBufferSize=alphaBufferSize,
- depthBufferSize=depthBufferSize,
- stencilBufferSize=stencilBufferSize,
- version=version,
- f=f)
- # Async connection need, otherwise issue when hiding OpenGL
- # widget while doing the rendering..
- self.__openGLWidget.sigOpenGLContextError.connect(
- self._handleOpenGLInitError, qt.Qt.QueuedConnection)
- self.layout().addWidget(self.__openGLWidget)
-
- @staticmethod
- def _createErrorQLabel(error):
- """Create QLabel displaying error message in place of OpenGL widget
-
- :param str error: The error message to display"""
- label = qt.QLabel()
- label.setText('OpenGL-based widget disabled:\n%s' % error)
- label.setAlignment(qt.Qt.AlignCenter)
- label.setWordWrap(True)
- return label
-
- def _handleOpenGLInitError(self, error):
- """Handle runtime errors in OpenGL widget"""
- if self.__openGLWidget is not None:
- self.__openGLWidget.setVisible(False)
- self.__openGLWidget.setParent(None)
- self.__openGLWidget = None
-
- label = self._createErrorQLabel(error)
- self.layout().addWidget(label)
-
- # Additional API
-
- def getDevicePixelRatio(self):
- """Returns the ratio device-independent / device pixel size
-
- It should be either 1.0 or 2.0.
-
- :return: Scale factor between screen and Qt units
- :rtype: float
- """
- if self.__openGLWidget is None:
- return 1.
- else:
- return self.__openGLWidget.getDevicePixelRatio()
-
- def getDotsPerInch(self):
- """Returns current screen resolution as device pixels per inch.
-
- :rtype: float
- """
- screen = self.window().windowHandle().screen()
- if screen is not None:
- # TODO check if this is correct on different OS/screen
- # OK on macOS10.12/qt5.13.2
- dpi = screen.physicalDotsPerInch() * self.getDevicePixelRatio()
- else: # Fallback
- dpi = 96. * self.getDevicePixelRatio()
- return dpi
-
- def getOpenGLVersion(self):
- """Returns the available OpenGL version.
-
- :return: (major, minor)
- :rtype: 2-tuple of int"""
- if self.__openGLWidget is None:
- return 0, 0
- else:
- return self.__openGLWidget.getOpenGLVersion()
-
- # QOpenGLWidget API
-
- def isValid(self):
- """Returns True if OpenGL with the requested version is available.
-
- :rtype: bool
- """
- if self.__openGLWidget is None:
- return False
- else:
- return self.__openGLWidget.isValid()
-
- def context(self):
- """Return Qt OpenGL context object or None.
-
- See :meth:`QOpenGLWidget.context` and :meth:`QGLWidget.context`
- """
- if self.__openGLWidget is None:
- return None
- else:
- # Keep a reference on QOpenGLContext to make
- # else PyQt5 keeps creating a new one.
- self.__context = self.__openGLWidget.context()
- return self.__context
-
- def defaultFramebufferObject(self):
- """Returns the framebuffer object handle.
-
- See :meth:`QOpenGLWidget.defaultFramebufferObject`
- """
- if self.__openGLWidget is None:
- return 0
- else:
- return self.__openGLWidget.defaultFramebufferObject()
-
- def makeCurrent(self):
- """Make the underlying OpenGL widget's context current.
-
- See :meth:`QOpenGLWidget.makeCurrent`
- """
- if self.__openGLWidget is not None:
- self.__openGLWidget.makeCurrent()
-
- def update(self):
- """Async update of the OpenGL widget.
-
- See :meth:`QOpenGLWidget.update`
- """
- if self.__openGLWidget is not None:
- self.__openGLWidget.update()
-
- # QOpenGLWidget API to override
-
- def initializeGL(self):
- """Override to implement OpenGL initialization."""
- pass
-
- def paintGL(self):
- """Override to implement OpenGL rendering."""
- pass
-
- def resizeGL(self, width, height):
- """Override to implement resize of OpenGL framebuffer.
-
- :param int width: Width in device-independent pixels
- :param int height: Height in device-independent pixels
- """
- pass
diff --git a/silx/gui/_glutils/Program.py b/silx/gui/_glutils/Program.py
deleted file mode 100644
index 87eec5f..0000000
--- a/silx/gui/_glutils/Program.py
+++ /dev/null
@@ -1,202 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# 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
-# 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 class to handle shader program compilation."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-
-import logging
-import weakref
-
-import numpy
-
-from . import Context, gl
-
-_logger = logging.getLogger(__name__)
-
-
-class Program(object):
- """Wrap OpenGL shader program.
-
- The program is compiled lazily (i.e., at first program :meth:`use`).
- When the program is compiled, it stores attributes and uniforms locations.
- So, attributes and uniforms must be used after :meth:`use`.
-
- This object supports multiple OpenGL contexts.
-
- :param str vertexShader: The source of the vertex shader.
- :param str fragmentShader: The source of the fragment shader.
- :param str attrib0:
- Attribute's name to bind to position 0 (default: 'position').
- On certain platform, this attribute MUST be active and with an
- array attached to it in order for the rendering to occur....
- """
-
- def __init__(self, vertexShader, fragmentShader,
- attrib0='position'):
- self._vertexShader = vertexShader
- self._fragmentShader = fragmentShader
- self._attrib0 = attrib0
- self._programs = weakref.WeakKeyDictionary()
-
- @staticmethod
- def _compileGL(vertexShader, fragmentShader, attrib0):
- program = gl.glCreateProgram()
-
- gl.glBindAttribLocation(program, 0, attrib0.encode('ascii'))
-
- vertex = gl.glCreateShader(gl.GL_VERTEX_SHADER)
- gl.glShaderSource(vertex, vertexShader)
- gl.glCompileShader(vertex)
- if gl.glGetShaderiv(vertex, gl.GL_COMPILE_STATUS) != gl.GL_TRUE:
- raise RuntimeError(gl.glGetShaderInfoLog(vertex))
- gl.glAttachShader(program, vertex)
- gl.glDeleteShader(vertex)
-
- fragment = gl.glCreateShader(gl.GL_FRAGMENT_SHADER)
- gl.glShaderSource(fragment, fragmentShader)
- gl.glCompileShader(fragment)
- if gl.glGetShaderiv(fragment,
- gl.GL_COMPILE_STATUS) != gl.GL_TRUE:
- raise RuntimeError(gl.glGetShaderInfoLog(fragment))
- gl.glAttachShader(program, fragment)
- gl.glDeleteShader(fragment)
-
- gl.glLinkProgram(program)
- if gl.glGetProgramiv(program, gl.GL_LINK_STATUS) != gl.GL_TRUE:
- raise RuntimeError(gl.glGetProgramInfoLog(program))
-
- attributes = {}
- for index in range(gl.glGetProgramiv(program,
- gl.GL_ACTIVE_ATTRIBUTES)):
- name = gl.glGetActiveAttrib(program, index)[0]
- namestr = name.decode('ascii')
- attributes[namestr] = gl.glGetAttribLocation(program, name)
-
- uniforms = {}
- for index in range(gl.glGetProgramiv(program, gl.GL_ACTIVE_UNIFORMS)):
- name = gl.glGetActiveUniform(program, index)[0]
- namestr = name.decode('ascii')
- uniforms[namestr] = gl.glGetUniformLocation(program, name)
-
- return program, attributes, uniforms
-
- def _getProgramInfo(self):
- glcontext = Context.getCurrent()
- if glcontext not in self._programs:
- raise RuntimeError(
- "Program was not compiled for current OpenGL context.")
- return self._programs[glcontext]
-
- @property
- def attributes(self):
- """Vertex attributes names and locations as a dict of {str: int}.
-
- WARNING:
- Read-only usage.
- To use only with a valid OpenGL context and after :meth:`use`
- has been called for this context.
- """
- return self._getProgramInfo()[1]
-
- @property
- def uniforms(self):
- """Program uniforms names and locations as a dict of {str: int}.
-
- WARNING:
- Read-only usage.
- To use only with a valid OpenGL context and after :meth:`use`
- has been called for this context.
- """
- return self._getProgramInfo()[2]
-
- @property
- def program(self):
- """OpenGL id of the program.
-
- WARNING:
- To use only with a valid OpenGL context and after :meth:`use`
- has been called for this context.
- """
- return self._getProgramInfo()[0]
-
- # def discard(self):
- # pass # Not implemented yet
-
- def use(self):
- """Make use of the program, compiling it if necessary"""
- glcontext = Context.getCurrent()
-
- if glcontext not in self._programs:
- self._programs[glcontext] = self._compileGL(
- self._vertexShader,
- self._fragmentShader,
- self._attrib0)
-
- if _logger.getEffectiveLevel() <= logging.DEBUG:
- gl.glValidateProgram(self.program)
- if gl.glGetProgramiv(
- self.program, gl.GL_VALIDATE_STATUS) != gl.GL_TRUE:
- _logger.debug('Cannot validate program: %s',
- gl.glGetProgramInfoLog(self.program))
-
- gl.glUseProgram(self.program)
-
- def setUniformMatrix(self, name, value, transpose=True, safe=False):
- """Wrap glUniformMatrix[2|3|4]fv
-
- :param str name: The name of the uniform.
- :param value: The 2D matrix (or the array of matrices, 3D).
- Matrices are 2x2, 3x3 or 4x4.
- :type value: numpy.ndarray with 2 or 3 dimensions of float32
- :param bool transpose: Whether to transpose (True, default) or not.
- :param bool safe: False: raise an error if no uniform with this name;
- True: silently ignores it.
-
- :raises KeyError: if no uniform corresponds to name.
- """
- assert value.dtype == numpy.float32
-
- shape = value.shape
- assert len(shape) in (2, 3)
- assert shape[-1] in (2, 3, 4)
- assert shape[-1] == shape[-2] # As in OpenGL|ES 2.0
-
- location = self.uniforms.get(name)
- if location is not None:
- count = 1 if len(shape) == 2 else shape[0]
- transpose = gl.GL_TRUE if transpose else gl.GL_FALSE
-
- if shape[-1] == 2:
- gl.glUniformMatrix2fv(location, count, transpose, value)
- elif shape[-1] == 3:
- gl.glUniformMatrix3fv(location, count, transpose, value)
- elif shape[-1] == 4:
- gl.glUniformMatrix4fv(location, count, transpose, value)
-
- elif not safe:
- raise KeyError('No uniform: %s' % name)
diff --git a/silx/gui/_glutils/Texture.py b/silx/gui/_glutils/Texture.py
deleted file mode 100644
index c72135a..0000000
--- a/silx/gui/_glutils/Texture.py
+++ /dev/null
@@ -1,352 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2020 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 class wrapping OpenGL 2D and 3D texture."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "04/10/2016"
-
-
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-
-from ctypes import c_void_p
-import logging
-
-import numpy
-
-from . import gl, utils
-
-
-_logger = logging.getLogger(__name__)
-
-
-class Texture(object):
- """Base class to wrap OpenGL 2D and 3D texture
-
- :param internalFormat: OpenGL texture internal format
- :param data: The data to copy to the texture or None for an empty texture
- :type data: numpy.ndarray or None
- :param format_: Input data format if different from internalFormat
- :param shape: If data is None, shape of the texture
- (height, width) or (depth, height, width)
- :type shape: List[int]
- :param int texUnit: The texture unit to use
- :param minFilter: OpenGL texture minimization filter (default: GL_NEAREST)
- :param magFilter: OpenGL texture magnification filter (default: GL_LINEAR)
- :param wrap: Texture wrap mode for dimensions: (t, s) or (r, t, s)
- If a single value is provided, it used for all dimensions.
- :type wrap: OpenGL wrap mode or 2 or 3-tuple of wrap mode
- """
-
- def __init__(self, internalFormat, data=None, format_=None,
- shape=None, texUnit=0,
- minFilter=None, magFilter=None, wrap=None):
-
- self._internalFormat = internalFormat
- if format_ is None:
- format_ = self.internalFormat
-
- if data is None:
- assert shape is not None
- else:
- assert shape is None
- data = numpy.array(data, copy=False, order='C')
- if format_ != gl.GL_RED:
- shape = data.shape[:-1] # Last dimension is channels
- else:
- shape = data.shape
-
- self._deferredUpdates = [(format_, data, None)]
-
- assert len(shape) in (2, 3)
- self._shape = tuple(shape)
- self._ndim = len(shape)
-
- self.texUnit = texUnit
-
- self._texParameterUpdates = {} # Store texture params to update
-
- self._minFilter = minFilter if minFilter is not None else gl.GL_NEAREST
- self._texParameterUpdates[gl.GL_TEXTURE_MIN_FILTER] = self._minFilter
-
- self._magFilter = magFilter if magFilter is not None else gl.GL_LINEAR
- self._texParameterUpdates[gl.GL_TEXTURE_MAG_FILTER] = self._magFilter
-
- self._name = None # Store texture ID
-
- if wrap is not None:
- if not isinstance(wrap, abc.Iterable):
- wrap = [wrap] * self.ndim
-
- assert len(wrap) == self.ndim
-
- self._texParameterUpdates[gl.GL_TEXTURE_WRAP_S] = wrap[-1]
- self._texParameterUpdates[gl.GL_TEXTURE_WRAP_T] = wrap[-2]
- if self.ndim == 3:
- self._texParameterUpdates[gl.GL_TEXTURE_WRAP_R] = wrap[0]
-
- @property
- def target(self):
- """OpenGL target type of this texture"""
- return gl.GL_TEXTURE_2D if self.ndim == 2 else gl.GL_TEXTURE_3D
-
- @property
- def ndim(self):
- """The number of dimensions: 2 or 3"""
- return self._ndim
-
- @property
- def internalFormat(self):
- """Texture internal format"""
- return self._internalFormat
-
- @property
- def shape(self):
- """Shape of the texture: (height, width) or (depth, height, width)"""
- return self._shape
-
- @property
- def name(self):
- """OpenGL texture name.
-
- It is None if not initialized or already discarded.
- """
- return self._name
-
- @property
- def minFilter(self):
- """Minifying function parameter (GL_TEXTURE_MIN_FILTER)"""
- return self._minFilter
-
- @minFilter.setter
- def minFilter(self, minFilter):
- if minFilter != self.minFilter:
- self._minFilter = minFilter
- self._texParameterUpdates[gl.GL_TEXTURE_MIN_FILTER] = minFilter
-
- @property
- def magFilter(self):
- """Magnification function parameter (GL_TEXTURE_MAG_FILTER)"""
- return self._magFilter
-
- @magFilter.setter
- def magFilter(self, magFilter):
- if magFilter != self.magFilter:
- self._magFilter = magFilter
- self._texParameterUpdates[gl.GL_TEXTURE_MAG_FILTER] = magFilter
-
- def _isPrepareRequired(self) -> bool:
- """Returns True if OpenGL texture needs to be updated.
-
- :rtype: bool
- """
- return (self._name is None or
- self._texParameterUpdates or
- self._deferredUpdates)
-
- def _prepareAndBind(self, texUnit=None):
- """Synchronizes the OpenGL texture"""
- if self._name is None:
- self._name = gl.glGenTextures(1)
-
- self._bind(texUnit)
-
- # Synchronizes texture parameters
- for pname, param in self._texParameterUpdates.items():
- gl.glTexParameter(self.target, pname, param)
- self._texParameterUpdates = {}
-
- # Copy data to texture
- for format_, data, offset in self._deferredUpdates:
- gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
-
- # This are the defaults, useless to set if not modified
- # gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, 0)
- # gl.glPixelStorei(gl.GL_UNPACK_SKIP_PIXELS, 0)
- # gl.glPixelStorei(gl.GL_UNPACK_SKIP_ROWS, 0)
- # gl.glPixelStorei(gl.GL_UNPACK_IMAGE_HEIGHT, 0)
- # gl.glPixelStorei(gl.GL_UNPACK_SKIP_IMAGES, 0)
-
- if data is None:
- data = c_void_p(0)
- type_ = gl.GL_UNSIGNED_BYTE
- else:
- type_ = utils.numpyToGLType(data.dtype)
-
- if offset is None: # Initialize texture
- if self.ndim == 2:
- _logger.debug(
- 'Creating 2D texture shape: (%d, %d),'
- ' internal format: %s, format: %s, type: %s',
- self.shape[0], self.shape[1],
- str(self.internalFormat), str(format_), str(type_))
-
- gl.glTexImage2D(
- gl.GL_TEXTURE_2D,
- 0,
- self.internalFormat,
- self.shape[1],
- self.shape[0],
- 0,
- format_,
- type_,
- data)
-
- else:
- _logger.debug(
- 'Creating 3D texture shape: (%d, %d, %d),'
- ' internal format: %s, format: %s, type: %s',
- self.shape[0], self.shape[1], self.shape[2],
- str(self.internalFormat), str(format_), str(type_))
-
- gl.glTexImage3D(
- gl.GL_TEXTURE_3D,
- 0,
- self.internalFormat,
- self.shape[2],
- self.shape[1],
- self.shape[0],
- 0,
- format_,
- type_,
- data)
-
- else: # Update already existing texture
- if self.ndim == 2:
- gl.glTexSubImage2D(gl.GL_TEXTURE_2D,
- 0,
- offset[1],
- offset[0],
- data.shape[1],
- data.shape[0],
- format_,
- type_,
- data)
-
- else:
- gl.glTexSubImage3D(gl.GL_TEXTURE_3D,
- 0,
- offset[2],
- offset[1],
- offset[0],
- data.shape[2],
- data.shape[1],
- data.shape[0],
- format_,
- type_,
- data)
-
- self._deferredUpdates = []
-
- def _bind(self, texUnit=None):
- """Bind the texture to a texture unit.
-
- :param int texUnit: The texture unit to use
- """
- if texUnit is None:
- texUnit = self.texUnit
- gl.glActiveTexture(gl.GL_TEXTURE0 + texUnit)
- gl.glBindTexture(self.target, self.name)
-
- def _unbind(self, texUnit=None):
- """Reset texture binding to a texture unit.
-
- :param int texUnit: The texture unit to use
- """
- if texUnit is None:
- texUnit = self.texUnit
- gl.glActiveTexture(gl.GL_TEXTURE0 + texUnit)
- gl.glBindTexture(self.target, 0)
-
- def prepare(self):
- """Synchronizes the OpenGL texture.
-
- This method must be called with a current OpenGL context.
- """
- if self._isPrepareRequired():
- self._prepareAndBind()
- self._unbind()
-
- def bind(self, texUnit=None):
- """Bind the texture to a texture unit.
-
- The OpenGL texture is updated if needed.
-
- This method must be called with a current OpenGL context.
-
- :param int texUnit: The texture unit to use
- """
- if self._isPrepareRequired():
- self._prepareAndBind(texUnit)
- else:
- self._bind(texUnit)
-
- def discard(self):
- """Delete associated OpenGL texture.
-
- This method must be called with a current OpenGL context.
- """
- if self._name is not None:
- gl.glDeleteTextures(self._name)
- self._name = None
- else:
- _logger.warning("Texture not initialized or already discarded")
-
- # with statement
-
- def __enter__(self):
- self.bind()
-
- def __exit__(self, exc_type, exc_val, exc_tb):
- self._unbind()
-
- def update(self, format_, data, offset=(0, 0, 0), copy=True):
- """Update the content of the texture.
-
- Texture is not resized, so data must fit into texture with the
- given offset.
-
- This update is performed lazily during next call to
- :meth:`prepare` or :meth:`bind`.
- Data MUST not be changed until then.
-
- :param format_: The OpenGL format of the data
- :param data: The data to use to update the texture
- :param List[int] offset: Offset in the texture where to copy the data
- :param bool copy:
- True (default) to copy data, False to use as is (do not modify)
- """
- data = numpy.array(data, copy=copy, order='C')
- offset = tuple(offset)
-
- assert data.ndim == self.ndim
- assert len(offset) >= self.ndim
- for i in range(self.ndim):
- assert offset[i] + data.shape[i] <= self.shape[i]
-
- self._deferredUpdates.append((format_, data, offset))
diff --git a/silx/gui/_glutils/VertexBuffer.py b/silx/gui/_glutils/VertexBuffer.py
deleted file mode 100644
index b74b748..0000000
--- a/silx/gui/_glutils/VertexBuffer.py
+++ /dev/null
@@ -1,266 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-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 module provides a class managing an OpenGL vertex buffer."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "10/01/2017"
-
-
-import logging
-from ctypes import c_void_p
-import numpy
-
-from . import gl
-from .utils import numpyToGLType, sizeofGLType
-
-
-_logger = logging.getLogger(__name__)
-
-
-class VertexBuffer(object):
- """Object handling an OpenGL vertex buffer object
-
- :param data: Data used to fill the vertex buffer
- :type data: numpy.ndarray or None
- :param int size: Size in bytes of the buffer or None for data size
- :param usage: OpenGL vertex buffer expected usage pattern:
- GL_STREAM_DRAW, GL_STATIC_DRAW (default) or GL_DYNAMIC_DRAW
- :param target: Target buffer:
- GL_ARRAY_BUFFER (default) or GL_ELEMENT_ARRAY_BUFFER
- """
- # OpenGL|ES 2.0 subset:
- _USAGES = gl.GL_STREAM_DRAW, gl.GL_STATIC_DRAW, gl.GL_DYNAMIC_DRAW
- _TARGETS = gl.GL_ARRAY_BUFFER, gl.GL_ELEMENT_ARRAY_BUFFER
-
- def __init__(self,
- data=None,
- size=None,
- usage=None,
- target=None):
- if usage is None:
- usage = gl.GL_STATIC_DRAW
- assert usage in self._USAGES
-
- if target is None:
- target = gl.GL_ARRAY_BUFFER
- assert target in self._TARGETS
-
- self._target = target
- self._usage = usage
-
- self._name = gl.glGenBuffers(1)
- self.bind()
-
- if data is None:
- assert size is not None
- self._size = size
- gl.glBufferData(self._target,
- self._size,
- c_void_p(0),
- self._usage)
- else:
- data = numpy.array(data, copy=False, order='C')
- if size is not None:
- assert size <= data.nbytes
-
- self._size = size or data.nbytes
- gl.glBufferData(self._target,
- self._size,
- data,
- self._usage)
-
- gl.glBindBuffer(self._target, 0)
-
- @property
- def target(self):
- """The target buffer of the vertex buffer"""
- return self._target
-
- @property
- def usage(self):
- """The expected usage of the vertex buffer"""
- return self._usage
-
- @property
- def name(self):
- """OpenGL Vertex Buffer object name (int)"""
- if self._name is not None:
- return self._name
- else:
- raise RuntimeError("No OpenGL buffer resource, \
- discard has already been called")
-
- @property
- def size(self):
- """Size in bytes of the Vertex Buffer Object (int)"""
- if self._size is not None:
- return self._size
- else:
- raise RuntimeError("No OpenGL buffer resource, \
- discard has already been called")
-
- def bind(self):
- """Bind the vertex buffer"""
- gl.glBindBuffer(self._target, self.name)
-
- def update(self, data, offset=0, size=None):
- """Update vertex buffer content.
-
- :param numpy.ndarray data: The data to put in the vertex buffer
- :param int offset: Offset in bytes in the buffer where to put the data
- :param int size: If provided, size of data to copy
- """
- data = numpy.array(data, copy=False, order='C')
- if size is None:
- size = data.nbytes
- assert offset + size <= self.size
- with self:
- gl.glBufferSubData(self._target, offset, size, data)
-
- def discard(self):
- """Delete the vertex buffer"""
- if self._name is not None:
- gl.glDeleteBuffers(self._name)
- self._name = None
- self._size = None
- else:
- _logger.warning("Discard has already been called")
-
- # with statement
-
- def __enter__(self):
- self.bind()
-
- def __exit__(self, exctype, excvalue, traceback):
- gl.glBindBuffer(self._target, 0)
-
-
-class VertexBufferAttrib(object):
- """Describes data stored in a vertex buffer
-
- Convenient class to store info for glVertexAttribPointer calls
-
- :param VertexBuffer vbo: The vertex buffer storing the data
- :param int type_: The OpenGL type of the data
- :param int size: The number of data elements stored in the VBO
- :param int dimension: The number of `type_` element(s) in [1, 4]
- :param int offset: Start offset of data in the vertex buffer
- :param int stride: Data stride in the vertex buffer
- """
-
- _GL_TYPES = gl.GL_UNSIGNED_BYTE, gl.GL_FLOAT, gl.GL_INT
-
- def __init__(self,
- vbo,
- type_,
- size,
- dimension=1,
- offset=0,
- stride=0,
- normalization=False):
- self.vbo = vbo
- assert type_ in self._GL_TYPES
- self.type_ = type_
- self.size = size
- assert 1 <= dimension <= 4
- self.dimension = dimension
- self.offset = offset
- self.stride = stride
- self.normalization = bool(normalization)
-
- @property
- def itemsize(self):
- """Size in bytes of a vertex buffer element (int)"""
- return self.dimension * sizeofGLType(self.type_)
-
- itemSize = itemsize # Backward compatibility
-
- def setVertexAttrib(self, attribute):
- """Call glVertexAttribPointer with objects information"""
- normalization = gl.GL_TRUE if self.normalization else gl.GL_FALSE
- with self.vbo:
- gl.glVertexAttribPointer(attribute,
- self.dimension,
- self.type_,
- normalization,
- self.stride,
- c_void_p(self.offset))
-
- def copy(self):
- return VertexBufferAttrib(self.vbo,
- self.type_,
- self.size,
- self.dimension,
- self.offset,
- self.stride,
- self.normalization)
-
-
-def vertexBuffer(arrays, prefix=None, suffix=None, usage=None):
- """Create a single vertex buffer from multiple 1D or 2D numpy arrays.
-
- It is possible to reserve memory before and after each array in the VBO
-
- :param arrays: Arrays of data to store
- :type arrays: Iterable of numpy.ndarray
- :param prefix: If given, number of elements to reserve before each array
- :type prefix: Iterable of int or None
- :param suffix: If given, number of elements to reserve after each array
- :type suffix: Iterable of int or None
- :param int usage: vertex buffer expected usage or None for default
- :returns: List of VertexBufferAttrib objects sharing the same vertex buffer
- """
- info = []
- vbosize = 0
-
- if prefix is None:
- prefix = (0,) * len(arrays)
- if suffix is None:
- suffix = (0,) * len(arrays)
-
- for data, pre, post in zip(arrays, prefix, suffix):
- data = numpy.array(data, copy=False, order='C')
- shape = data.shape
- assert len(shape) <= 2
- type_ = numpyToGLType(data.dtype)
- size = shape[0] + pre + post
- dimension = 1 if len(shape) == 1 else shape[1]
- sizeinbytes = size * dimension * sizeofGLType(type_)
- sizeinbytes = 4 * ((sizeinbytes + 3) >> 2) # 4 bytes alignment
- copyoffset = vbosize + pre * dimension * sizeofGLType(type_)
- info.append((data, type_, size, dimension,
- vbosize, sizeinbytes, copyoffset))
- vbosize += sizeinbytes
-
- vbo = VertexBuffer(size=vbosize, usage=usage)
-
- result = []
- for data, type_, size, dimension, offset, sizeinbytes, copyoffset in info:
- copysize = data.shape[0] * dimension * sizeofGLType(type_)
- vbo.update(data, offset=copyoffset, size=copysize)
- result.append(
- VertexBufferAttrib(vbo, type_, size, dimension, offset, 0))
- return result
diff --git a/silx/gui/_glutils/__init__.py b/silx/gui/_glutils/__init__.py
deleted file mode 100644
index e88affd..0000000
--- a/silx/gui/_glutils/__init__.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# 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
-# 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 package provides utility functions to handle OpenGL resources.
-
-The :mod:`gl` module provides a wrapper to OpenGL based on PyOpenGL.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-
-# OpenGL convenient functions
-from .OpenGLWidget import OpenGLWidget # 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/font.py b/silx/gui/_glutils/font.py
deleted file mode 100644
index 6a4c489..0000000
--- a/silx/gui/_glutils/font.py
+++ /dev/null
@@ -1,163 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 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.
-#
-# ###########################################################################*/
-"""Text rasterisation feature leveraging Qt font and text layout support."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "13/10/2016"
-
-
-import logging
-import numpy
-
-from ..utils.image import convertQImageToArray
-from .. import qt
-
-_logger = logging.getLogger(__name__)
-
-
-def getDefaultFontFamily():
- """Returns the default font family of the application"""
- return qt.QApplication.instance().font().family()
-
-
-# Font weights
-ULTRA_LIGHT = 0
-"""Lightest characters: Minimum font weight"""
-
-LIGHT = 25
-"""Light characters"""
-
-NORMAL = 50
-"""Normal characters"""
-
-SEMI_BOLD = 63
-"""Between normal and bold characters"""
-
-BOLD = 74
-"""Thicker characters"""
-
-BLACK = 87
-"""Really thick characters"""
-
-ULTRA_BLACK = 99
-"""Thickest characters: Maximum font weight"""
-
-
-def rasterText(text, font,
- size=-1,
- weight=-1,
- italic=False,
- devicePixelRatio=1.0):
- """Raster text using Qt.
-
- It supports multiple lines.
-
- :param str text: The text to raster
- :param font: Font name or QFont to use
- :type font: str or :class:`QFont`
- :param int size:
- Font size in points
- Used only if font is given as name.
- :param int weight:
- Font weight in [0, 99], see QFont.Weight.
- Used only if font is given as name.
- :param bool italic:
- True for italic font (default: False).
- Used only if font is given as name.
- :param float devicePixelRatio:
- The current ratio between device and device-independent pixel
- (default: 1.0)
- :return: Corresponding image in gray scale and baseline offset from top
- :rtype: (HxW numpy.ndarray of uint8, int)
- """
- if not text:
- _logger.info("Trying to raster empty text, replaced by white space")
- text = ' ' # Replace empty text by white space to produce an image
-
- if (devicePixelRatio != 1.0 and
- not hasattr(qt.QImage, 'setDevicePixelRatio')): # Qt 4
- _logger.error('devicePixelRatio not supported')
- devicePixelRatio = 1.0
-
- if not isinstance(font, qt.QFont):
- font = qt.QFont(font, size, weight, italic)
-
- # get text size
- image = qt.QImage(1, 1, qt.QImage.Format_RGB888)
- painter = qt.QPainter()
- painter.begin(image)
- painter.setPen(qt.Qt.white)
- painter.setFont(font)
- bounds = painter.boundingRect(
- qt.QRect(0, 0, 4096, 4096), qt.Qt.TextExpandTabs, text)
- painter.end()
-
- metrics = qt.QFontMetrics(font)
-
- # This does not provide the correct text bbox on macOS
- # size = metrics.size(qt.Qt.TextExpandTabs, text)
- # bounds = metrics.boundingRect(
- # qt.QRect(0, 0, size.width(), size.height()),
- # qt.Qt.TextExpandTabs,
- # text)
-
- # Add extra border and handle devicePixelRatio
- width = bounds.width() * devicePixelRatio + 2
- # align line size to 32 bits to ease conversion to numpy array
- width = 4 * ((width + 3) // 4)
- image = qt.QImage(int(width),
- int(bounds.height() * devicePixelRatio + 2),
- qt.QImage.Format_RGB888)
- if (devicePixelRatio != 1.0 and
- hasattr(image, 'setDevicePixelRatio')): # Qt 5
- image.setDevicePixelRatio(devicePixelRatio)
-
- # TODO if Qt5 use Format_Grayscale8 instead
- image.fill(0)
-
- # Raster text
- painter = qt.QPainter()
- painter.begin(image)
- painter.setPen(qt.Qt.white)
- painter.setFont(font)
- painter.drawText(bounds, qt.Qt.TextExpandTabs, text)
- painter.end()
-
- array = convertQImageToArray(image)
-
- # RGB to R
- array = numpy.ascontiguousarray(array[:, :, 0])
-
- # Remove leading and trailing empty columns but one on each side
- column_cumsum = numpy.cumsum(numpy.sum(array, axis=0))
- array = array[:, column_cumsum.argmin():column_cumsum.argmax() + 2]
-
- # Remove leading and trailing empty rows but one on each side
- row_cumsum = numpy.cumsum(numpy.sum(array, axis=1))
- min_row = row_cumsum.argmin()
- array = array[min_row:row_cumsum.argmax() + 2, :]
-
- return array, metrics.ascent() - min_row
diff --git a/silx/gui/_glutils/gl.py b/silx/gui/_glutils/gl.py
deleted file mode 100644
index 608d9ce..0000000
--- a/silx/gui/_glutils/gl.py
+++ /dev/null
@@ -1,168 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-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 module loads PyOpenGL and provides a namespace for OpenGL."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "25/07/2016"
-
-
-from contextlib import contextmanager as _contextmanager
-from ctypes import c_uint
-import logging
-
-_logger = logging.getLogger(__name__)
-
-import OpenGL
-# Set the following to true for debugging
-if _logger.getEffectiveLevel() <= logging.DEBUG:
- _logger.debug('Enabling PyOpenGL debug flags')
- OpenGL.ERROR_LOGGING = True
- OpenGL.ERROR_CHECKING = True
- OpenGL.ERROR_ON_COPY = True
-else:
- OpenGL.ERROR_LOGGING = False
- OpenGL.ERROR_CHECKING = False
- OpenGL.ERROR_ON_COPY = False
-
-import OpenGL.GL as _GL
-from OpenGL.GL import * # noqa
-
-# Extentions core in OpenGL 3
-from OpenGL.GL.ARB import framebuffer_object as _FBO
-from OpenGL.GL.ARB.framebuffer_object import * # noqa
-from OpenGL.GL.ARB.texture_rg import GL_R32F, GL_R16F # noqa
-from OpenGL.GL.ARB.texture_rg import GL_R16, GL_R8 # noqa
-
-# PyOpenGL 3.0.1 does not define it
-try:
- GLchar
-except NameError:
- from ctypes import c_char
- GLchar = c_char
-
-
-def testGL():
- """Test if required OpenGL version and extensions are available.
-
- This MUST be run with an active OpenGL context.
- """
- version = glGetString(GL_VERSION).split()[0] # get version number
- major, minor = int(version[0]), int(version[2])
- if major < 2 or (major == 2 and minor < 1):
- raise RuntimeError(
- "Requires at least OpenGL version 2.1, running with %s" % version)
-
- from OpenGL.GL.ARB.framebuffer_object import glInitFramebufferObjectARB
- from OpenGL.GL.ARB.texture_rg import glInitTextureRgARB
-
- if not glInitFramebufferObjectARB():
- raise RuntimeError(
- "OpenGL GL_ARB_framebuffer_object extension required !")
-
- if not glInitTextureRgARB():
- raise RuntimeError("OpenGL GL_ARB_texture_rg extension required !")
-
-
-# Additional setup
-if hasattr(glget, 'addGLGetConstant'):
- glget.addGLGetConstant(GL_FRAMEBUFFER_BINDING, (1,))
-
-
-@_contextmanager
-def enabled(capacity, enable=True):
- """Context manager enabling an OpenGL capacity.
-
- This is not checking the current state of the capacity.
-
- :param capacity: The OpenGL capacity enum to enable/disable
- :param bool enable:
- True (default) to enable during context, False to disable
- """
- if bool(enable) == glGetBoolean(capacity):
- # Already in the right state: noop
- yield
- elif enable:
- glEnable(capacity)
- yield
- glDisable(capacity)
- else:
- glDisable(capacity)
- yield
- glEnable(capacity)
-
-
-def disabled(capacity, disable=True):
- """Context manager disabling an OpenGL capacity.
-
- This is not checking the current state of the capacity.
-
- :param capacity: The OpenGL capacity enum to disable/enable
- :param bool disable:
- True (default) to disable during context, False to enable
- """
- return enabled(capacity, not disable)
-
-
-# Additional OpenGL wrapping
-
-def glGetActiveAttrib(program, index):
- """Wrap PyOpenGL glGetActiveAttrib"""
- bufsize = glGetProgramiv(program, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH)
- length = GLsizei()
- size = GLint()
- type_ = GLenum()
- name = (GLchar * bufsize)()
-
- _GL.glGetActiveAttrib(program, index, bufsize, length, size, type_, name)
- return name.value, size.value, type_.value
-
-
-def glDeleteRenderbuffers(buffers):
- if not hasattr(buffers, '__len__'): # Support single int argument
- buffers = [buffers]
- length = len(buffers)
- _FBO.glDeleteRenderbuffers(length, (c_uint * length)(*buffers))
-
-
-def glDeleteFramebuffers(buffers):
- if not hasattr(buffers, '__len__'): # Support single int argument
- buffers = [buffers]
- length = len(buffers)
- _FBO.glDeleteFramebuffers(length, (c_uint * length)(*buffers))
-
-
-def glDeleteBuffers(buffers):
- if not hasattr(buffers, '__len__'): # Support single int argument
- buffers = [buffers]
- length = len(buffers)
- _GL.glDeleteBuffers(length, (c_uint * length)(*buffers))
-
-
-def glDeleteTextures(textures):
- if not hasattr(textures, '__len__'): # Support single int argument
- textures = [textures]
- length = len(textures)
- _GL.glDeleteTextures((c_uint * length)(*textures))
diff --git a/silx/gui/_glutils/utils.py b/silx/gui/_glutils/utils.py
deleted file mode 100644
index d5627ef..0000000
--- a/silx/gui/_glutils/utils.py
+++ /dev/null
@@ -1,121 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# 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
-# 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 conversion functions between OpenGL and numpy types.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "10/01/2017"
-
-import numpy
-
-from OpenGL.constants import BYTE_SIZES as _BYTE_SIZES
-from OpenGL.constants import ARRAY_TO_GL_TYPE_MAPPING as _ARRAY_TO_GL_TYPE_MAPPING
-
-
-def sizeofGLType(type_):
- """Returns the size in bytes of an element of type `type_`"""
- return _BYTE_SIZES[type_]
-
-
-def isSupportedGLType(type_):
- """Test if a numpy type or dtype can be converted to a GL type."""
- return numpy.dtype(type_).char in _ARRAY_TO_GL_TYPE_MAPPING
-
-
-def numpyToGLType(type_):
- """Returns the GL type corresponding the provided numpy type or dtype."""
- return _ARRAY_TO_GL_TYPE_MAPPING[numpy.dtype(type_).char]
-
-
-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
deleted file mode 100755
index db837b5..0000000
--- a/silx/gui/colors.py
+++ /dev/null
@@ -1,1326 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2021 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 API to manage colors.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent", "H.Payno"]
-__license__ = "MIT"
-__date__ = "29/01/2019"
-
-import numpy
-import logging
-import collections
-import warnings
-
-from silx.gui import qt
-from silx.gui.utils import blockSignals
-from silx.math.combo import min_max
-from silx.math import colormap as _colormap
-from silx.utils.exceptions import NotEditableError
-from silx.utils import deprecation
-from silx.resources import resource_filename as _resource_filename
-
-
-_logger = logging.getLogger(__name__)
-
-try:
- import silx.gui.utils.matplotlib # noqa Initalize matplotlib
- from matplotlib import cm as _matplotlib_cm
- from matplotlib.pyplot import colormaps as _matplotlib_colormaps
-except ImportError:
- _logger.info("matplotlib not available, only embedded colormaps available")
- _matplotlib_cm = None
- _matplotlib_colormaps = None
-
-
-_COLORDICT = {}
-"""Dictionary of common colors."""
-
-_COLORDICT['b'] = _COLORDICT['blue'] = '#0000ff'
-_COLORDICT['r'] = _COLORDICT['red'] = '#ff0000'
-_COLORDICT['g'] = _COLORDICT['green'] = '#00ff00'
-_COLORDICT['k'] = _COLORDICT['black'] = '#000000'
-_COLORDICT['w'] = _COLORDICT['white'] = '#ffffff'
-_COLORDICT['pink'] = '#ff66ff'
-_COLORDICT['brown'] = '#a52a2a'
-_COLORDICT['orange'] = '#ff9900'
-_COLORDICT['violet'] = '#6600ff'
-_COLORDICT['gray'] = _COLORDICT['grey'] = '#a0a0a4'
-# _COLORDICT['darkGray'] = _COLORDICT['darkGrey'] = '#808080'
-# _COLORDICT['lightGray'] = _COLORDICT['lightGrey'] = '#c0c0c0'
-_COLORDICT['y'] = _COLORDICT['yellow'] = '#ffff00'
-_COLORDICT['m'] = _COLORDICT['magenta'] = '#ff00ff'
-_COLORDICT['c'] = _COLORDICT['cyan'] = '#00ffff'
-_COLORDICT['darkBlue'] = '#000080'
-_COLORDICT['darkRed'] = '#800000'
-_COLORDICT['darkGreen'] = '#008000'
-_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)),
- ('red', _LUT_DESCRIPTION('builtin', 'green', True)),
- ('green', _LUT_DESCRIPTION('builtin', 'pink', True)),
- ('blue', _LUT_DESCRIPTION('builtin', 'yellow', 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)),
- ('temperature', _LUT_DESCRIPTION('builtin', 'pink', True)),
- ('jet', _LUT_DESCRIPTION('matplotlib', 'pink', 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"""
-
-
-def rgba(color, colorDict=None):
- """Convert color code '#RRGGBB' and '#RRGGBBAA' to a tuple (R, G, B, A)
- of floats.
-
- 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
- """
- if colorDict is None:
- colorDict = _COLORDICT
-
- if hasattr(color, 'getRgbF'): # QColor support
- color = color.getRgbF()
-
- values = numpy.asarray(color).ravel()
-
- if values.dtype.kind in 'iuf': # integer or float
- # Color is an array
- assert len(values) in (3, 4)
-
- # Convert from integers in [0, 255] to float in [0, 1]
- if values.dtype.kind in 'iu':
- values = values / 255.
-
- # Clip to [0, 1]
- values[values < 0.] = 0.
- values[values > 1.] = 1.
-
- if len(values) == 3:
- return values[0], values[1], values[2], 1.
- else:
- return tuple(values)
-
- # We assume color is a string
- if not color.startswith('#'):
- color = colorDict[color]
-
- assert len(color) in (7, 9) and color[0] == '#'
- r = int(color[1:3], 16) / 255.
- g = int(color[3:5], 16) / 255.
- b = int(color[5:7], 16) / 255.
- a = int(color[7:9], 16) / 255. if len(color) == 9 else 1.
- return r, g, b, a
-
-
-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):
- """Get a color suitable for overlay over a colormap.
-
- :param str colormapName: The name of the colormap.
- :return: Name of the color.
- :rtype: str
- """
- description = _AVAILABLE_LUTS.get(colormapName, None)
- if description is not None:
- color = description.cursor_color
- if color is not None:
- return color
- return 'black'
-
-
-# Colormap loader
-
-_COLORMAP_CACHE = {}
-"""Cache already used colormaps as name: color LUT"""
-
-
-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]
-
-
-# Normalizations
-
-class _NormalizationMixIn:
- """Colormap normalization mix-in class"""
-
- DEFAULT_RANGE = 0, 1
- """Fallback for (vmin, vmax)"""
-
- def isValid(self, value):
- """Check if a value is in the valid range for this normalization.
-
- Override in subclass.
-
- :param Union[float,numpy.ndarray] value:
- :rtype: Union[bool,numpy.ndarray]
- """
- if isinstance(value, collections.abc.Iterable):
- return numpy.ones_like(value, dtype=numpy.bool_)
- else:
- return True
-
- def autoscale(self, data, mode):
- """Returns range for given data and autoscale mode.
-
- :param Union[None,numpy.ndarray] data:
- :param str mode: Autoscale mode, see :class:`Colormap`
- :returns: Range as (min, max)
- :rtype: Tuple[float,float]
- """
- data = None if data is None else numpy.array(data, copy=False)
- if data is None or data.size == 0:
- return self.DEFAULT_RANGE
-
- if mode == Colormap.MINMAX:
- vmin, vmax = self.autoscaleMinMax(data)
- elif mode == Colormap.STDDEV3:
- dmin, dmax = self.autoscaleMinMax(data)
- stdmin, stdmax = self.autoscaleMean3Std(data)
- if dmin is None:
- vmin = stdmin
- elif stdmin is None:
- vmin = dmin
- else:
- vmin = max(dmin, stdmin)
-
- if dmax is None:
- vmax = stdmax
- elif stdmax is None:
- vmax = dmax
- else:
- vmax = min(dmax, stdmax)
-
- else:
- raise ValueError('Unsupported mode: %s' % mode)
-
- # Check returned range and handle fallbacks
- if vmin is None or not numpy.isfinite(vmin):
- vmin = self.DEFAULT_RANGE[0]
- if vmax is None or not numpy.isfinite(vmax):
- vmax = self.DEFAULT_RANGE[1]
- if vmax < vmin:
- vmax = vmin
- return float(vmin), float(vmax)
-
- def autoscaleMinMax(self, data):
- """Autoscale using min/max
-
- :param numpy.ndarray data:
- :returns: (vmin, vmax)
- :rtype: Tuple[float,float]
- """
- data = data[self.isValid(data)]
- if data.size == 0:
- return None, None
- result = min_max(data, min_positive=False, finite=True)
- return result.minimum, result.maximum
-
- def autoscaleMean3Std(self, data):
- """Autoscale using mean+/-3std
-
- This implementation only works for normalization that do NOT
- use the data range.
- Override this method for normalization using the range.
-
- :param numpy.ndarray data:
- :returns: (vmin, vmax)
- :rtype: Tuple[float,float]
- """
- # Use [0, 1] as data range for normalization not using range
- normdata = self.apply(data, 0., 1.)
- if normdata.dtype.kind == 'f': # Replaces inf by NaN
- normdata[numpy.isfinite(normdata) == False] = numpy.nan
- if normdata.size == 0: # Fallback
- return None, None
-
- with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
- # Ignore nanmean "Mean of empty slice" warning and
- # nanstd "Degrees of freedom <= 0 for slice" warning
- mean, std = numpy.nanmean(normdata), numpy.nanstd(normdata)
-
- return self.revert(mean - 3 * std, 0., 1.), self.revert(mean + 3 * std, 0., 1.)
-
-
-class _LinearNormalizationMixIn(_NormalizationMixIn):
- """Colormap normalization mix-in class specific to autoscale taken from initial range"""
-
- def autoscaleMean3Std(self, data):
- """Autoscale using mean+/-3std
-
- Do the autoscale on the data itself, not the normalized data.
-
- :param numpy.ndarray data:
- :returns: (vmin, vmax)
- :rtype: Tuple[float,float]
- """
- if data.dtype.kind == 'f': # Replaces inf by NaN
- data = numpy.array(data, copy=True) # Work on a copy
- data[numpy.isfinite(data) == False] = numpy.nan
- if data.size == 0: # Fallback
- return None, None
- with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
- # Ignore nanmean "Mean of empty slice" warning and
- # nanstd "Degrees of freedom <= 0 for slice" warning
- mean, std = numpy.nanmean(data), numpy.nanstd(data)
- return mean - 3 * std, mean + 3 * std
-
-
-class _LinearNormalization(_colormap.LinearNormalization, _LinearNormalizationMixIn):
- """Linear normalization"""
- def __init__(self):
- _colormap.LinearNormalization.__init__(self)
- _LinearNormalizationMixIn.__init__(self)
-
-
-class _LogarithmicNormalization(_colormap.LogarithmicNormalization, _NormalizationMixIn):
- """Logarithm normalization"""
-
- DEFAULT_RANGE = 1, 10
-
- def __init__(self):
- _colormap.LogarithmicNormalization.__init__(self)
- _NormalizationMixIn.__init__(self)
-
- def isValid(self, value):
- return value > 0.
-
- def autoscaleMinMax(self, data):
- result = min_max(data, min_positive=True, finite=True)
- return result.min_positive, result.maximum
-
-
-class _SqrtNormalization(_colormap.SqrtNormalization, _NormalizationMixIn):
- """Square root normalization"""
-
- DEFAULT_RANGE = 0, 1
-
- def __init__(self):
- _colormap.SqrtNormalization.__init__(self)
- _NormalizationMixIn.__init__(self)
-
- def isValid(self, value):
- return value >= 0.
-
-
-class _GammaNormalization(_colormap.PowerNormalization, _LinearNormalizationMixIn):
- """Gamma correction normalization:
-
- Linear normalization to [0, 1] followed by power normalization.
-
- :param gamma: Gamma correction factor
- """
- def __init__(self, gamma):
- _colormap.PowerNormalization.__init__(self, gamma)
- _LinearNormalizationMixIn.__init__(self)
-
-
-class _ArcsinhNormalization(_colormap.ArcsinhNormalization, _NormalizationMixIn):
- """Inverse hyperbolic sine normalization"""
-
- def __init__(self):
- _colormap.ArcsinhNormalization.__init__(self)
- _NormalizationMixIn.__init__(self)
-
-
-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,
- either uint8 or float in [0, 1].
- If 'name' is None, then this array is used as the colormap.
- :param str normalization: Normalization: 'linear' (default) or 'log'
- :param vmin: Lower bound of the colormap or None for autoscale (default)
- :type vmin: Union[None, float]
- :param vmax: Upper bounds of the colormap or None for autoscale (default)
- :type vmax: Union[None, float]
- """
-
- LINEAR = 'linear'
- """constant for linear normalization"""
-
- LOGARITHM = 'log'
- """constant for logarithmic normalization"""
-
- SQRT = 'sqrt'
- """constant for square root normalization"""
-
- GAMMA = 'gamma'
- """Constant for gamma correction normalization"""
-
- ARCSINH = 'arcsinh'
- """constant for inverse hyperbolic sine normalization"""
-
- _BASIC_NORMALIZATIONS = {
- LINEAR: _LinearNormalization(),
- LOGARITHM: _LogarithmicNormalization(),
- SQRT: _SqrtNormalization(),
- ARCSINH: _ArcsinhNormalization(),
- }
- """Normalizations without parameters"""
-
- NORMALIZATIONS = LINEAR, LOGARITHM, SQRT, GAMMA, ARCSINH
- """Tuple of managed normalizations"""
-
- MINMAX = 'minmax'
- """constant for autoscale using min/max data range"""
-
- STDDEV3 = 'stddev3'
- """constant for autoscale using mean +/- 3*std(data)
- with a clamp on min/max of the data"""
-
- AUTOSCALE_MODES = (MINMAX, STDDEV3)
- """Tuple of managed auto scale algorithms"""
-
- sigChanged = qt.Signal()
- """Signal emitted when the colormap has changed."""
-
- _DEFAULT_NAN_COLOR = 255, 255, 255, 0
-
- def __init__(self, name=None, colors=None, normalization=LINEAR, vmin=None, vmax=None, autoscaleMode=MINMAX):
- qt.QObject.__init__(self)
- self._editable = True
- self.__gamma = 2.0
- # Default NaN color: fully transparent white
- self.__nanColor = numpy.array(self._DEFAULT_NAN_COLOR, dtype=numpy.uint8)
-
- assert normalization in Colormap.NORMALIZATIONS
- assert autoscaleMode in Colormap.AUTOSCALE_MODES
-
- 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."
- m += ' Autoscale will be performed.'
- m = m % (vmin, vmax)
- _logger.warning(m)
- vmin = None
- vmax = None
-
- 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._autoscaleMode = str(autoscaleMode)
- self._vmin = float(vmin) if vmin is not None else None
- self._vmax = float(vmax) if vmax is not None else None
-
- def setFromColormap(self, other):
- """Set this colormap using information from the `other` colormap.
-
- :param ~silx.gui.colors.Colormap other: Colormap to use as reference.
- """
- if not self.isEditable():
- raise NotEditableError('Colormap is not editable')
- if self == other:
- return
- with blockSignals(self):
- name = other.getName()
- if name is not None:
- self.setName(name)
- else:
- self.setColormapLUT(other.getColormapLUT())
- self.setNaNColor(other.getNaNColor())
- self.setNormalization(other.getNormalization())
- self.setGammaNormalizationParameter(
- other.getGammaNormalizationParameter())
- self.setAutoscaleMode(other.getAutoscaleMode())
- self.setVRange(*other.getVRange())
- self.setEditable(other.isEditable())
- 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 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:
- return numpy.array(self._colors, copy=True)
- else:
- nbColors = int(nbColors)
- colormap = self.copy()
- colormap.setNormalization(Colormap.LINEAR)
- colormap.setVRange(vmin=0, vmax=nbColors - 1)
- colors = colormap.applyToData(
- numpy.arange(nbColors, dtype=numpy.int32))
- 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.
-
- :param str name: The name of the colormap.
- At least the following names are supported: 'gray',
- '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')
- 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, 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._name is None:
- return numpy.array(self._colors, copy=copy)
- else:
- return None
-
- def setColormapLUT(self, colors):
- """Set the colors of the colormap.
-
- :param numpy.ndarray colors: the colors of the LUT.
- If float, it is converted from [0, 1] to uint8 range.
- Otherwise it is casted to uint8.
-
- .. warning: this will set the value of name to None
- """
- if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
- 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()
-
- def getNaNColor(self):
- """Returns the color to use for Not-A-Number floating point value.
-
- :rtype: QColor
- """
- return qt.QColor(*self.__nanColor)
-
- def setNaNColor(self, color):
- """Set the color to use for Not-A-Number floating point value.
-
- :param color: RGB(A) color to use for NaN values
- :type color: QColor, str, tuple of uint8 or float in [0., 1.]
- """
- color = (numpy.array(rgba(color)) * 255).astype(numpy.uint8)
- if not numpy.array_equal(self.__nanColor, color):
- self.__nanColor = color
- self.sigChanged.emit()
-
- def getNormalization(self):
- """Return the normalization of the colormap.
-
- See :meth:`setNormalization` for returned values.
-
- :return: the normalization of the colormap
- :rtype: str
- """
- return self._normalization
-
- def setNormalization(self, norm):
- """Set the colormap normalization.
-
- Accepted normalizations: 'log', 'linear', 'sqrt'
-
- :param str norm: the norm to set
- """
- assert norm in self.NORMALIZATIONS
- if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
- self._normalization = str(norm)
- self.sigChanged.emit()
-
- def setGammaNormalizationParameter(self, gamma: float) -> None:
- """Set the gamma correction parameter.
-
- Only used for gamma correction normalization.
-
- :param float gamma:
- :raise ValueError: If gamma is not valid
- """
- if gamma < 0. or not numpy.isfinite(gamma):
- raise ValueError("Gamma value not supported")
- if gamma != self.__gamma:
- self.__gamma = gamma
- self.sigChanged.emit()
-
- def getGammaNormalizationParameter(self) -> float:
- """Returns the gamma correction parameter value.
-
- :rtype: float
- """
- return self.__gamma
-
- def getAutoscaleMode(self):
- """Return the autoscale mode of the colormap ('minmax' or 'stddev3')
-
- :rtype: str
- """
- return self._autoscaleMode
-
- def setAutoscaleMode(self, mode):
- """Set the autoscale mode: either 'minmax' or 'stddev3'
-
- :param str mode: the mode to set
- """
- if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
- assert mode in self.AUTOSCALE_MODES
- if mode != self._autoscaleMode:
- self._autoscaleMode = mode
- 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
-
- :return: the lower bound of the colormap
- :rtype: float or None
- """
- return self._vmin
-
- def setVMin(self, vmin):
- """Set the minimal value of the colormap
-
- :param float vmin: Lower bound of the colormap or None for autoscale
- (default)
- value)
- """
- if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
- if vmin is not None:
- if self._vmax is not None and vmin > self._vmax:
- err = "Can't set vmin because vmin >= vmax. " \
- "vmin = %s, vmax = %s" % (vmin, self._vmax)
- raise ValueError(err)
-
- self._vmin = vmin
- self.sigChanged.emit()
-
- def getVMax(self):
- """Return the upper bounds of the colormap or None
-
- :return: the upper bounds of the colormap or None
- :rtype: float or None
- """
- return self._vmax
-
- def setVMax(self, vmax):
- """Set the maximal value of the colormap
-
- :param float vmax: Upper bounds of the colormap or None for autoscale
- (default)
- """
- if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
- if vmax is not None:
- if self._vmin is not None and vmax < self._vmin:
- err = "Can't set vmax because vmax <= vmin. " \
- "vmin = %s, vmax = %s" % (self._vmin, vmax)
- raise ValueError(err)
-
- self._vmax = vmax
- self.sigChanged.emit()
-
- def isEditable(self):
- """ Return if the colormap is editable or not
-
- :return: editable state of the colormap
- :rtype: bool
- """
- return self._editable
-
- def setEditable(self, editable):
- """
- Set the editable state of the colormap
-
- :param bool editable: is the colormap editable
- """
- assert type(editable) is bool
- self._editable = editable
- self.sigChanged.emit()
-
- def _getNormalizer(self):
- """Returns normalizer object"""
- normalization = self.getNormalization()
- if normalization == self.GAMMA:
- return _GammaNormalization(self.getGammaNormalizationParameter())
- else:
- return self._BASIC_NORMALIZATIONS[normalization]
-
- def _computeAutoscaleRange(self, data):
- """Compute the data range which will be used in autoscale mode.
-
- :param numpy.ndarray data: The data for which to compute the range
- :return: (vmin, vmax) range
- """
- return self._getNormalizer().autoscale(
- data, mode=self.getAutoscaleMode())
-
- def getColormapRange(self, data=None):
- """Return (vmin, vmax) the range of the colormap for the given data or item.
-
- :param Union[numpy.ndarray,~silx.gui.plot.items.ColormapMixIn] data:
- The data or item to use for autoscale bounds.
- :return: (vmin, vmax) corresponding to the colormap applied to data if provided.
- :rtype: tuple
- """
- vmin = self._vmin
- vmax = self._vmax
- assert vmin is None or vmax is None or vmin <= vmax # TODO handle this in setters
-
- normalizer = self._getNormalizer()
-
- # Handle invalid bounds as autoscale
- if vmin is not None and not normalizer.isValid(vmin):
- _logger.info(
- 'Invalid vmin, switching to autoscale for lower bound')
- vmin = None
- if vmax is not None and not normalizer.isValid(vmax):
- _logger.info(
- 'Invalid vmax, switching to autoscale for upper bound')
- vmax = None
-
- if vmin is None or vmax is None: # Handle autoscale
- from .plot.items.core import ColormapMixIn # avoid cyclic import
- if isinstance(data, ColormapMixIn):
- min_, max_ = data._getColormapAutoscaleRange(self)
- # Make sure min_, max_ are not None
- min_ = normalizer.DEFAULT_RANGE[0] if min_ is None else min_
- max_ = normalizer.DEFAULT_RANGE[1] if max_ is None else max_
- else:
- min_, max_ = normalizer.autoscale(
- data, mode=self.getAutoscaleMode())
-
- if vmin is None: # Set vmin respecting provided vmax
- vmin = min_ if vmax is None else min(min_, vmax)
-
- if vmax is None:
- vmax = max(max_, vmin) # Handle max_ <= 0 for log scale
-
- return vmin, vmax
-
- def getVRange(self):
- """Get the bounds of the colormap
-
- :rtype: Tuple(Union[float,None],Union[float,None])
- :returns: A tuple of 2 values for min and max. Or None instead of float
- for autoscale
- """
- return self.getVMin(), self.getVMax()
-
- def setVRange(self, vmin, vmax):
- """Set the bounds of the colormap
-
- :param vmin: Lower bound of the colormap or None for autoscale
- (default)
- :param vmax: Upper bounds of the colormap or None for autoscale
- (default)
- """
- if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
- if vmin is not None and vmax is not None:
- if vmin > vmax:
- err = "Can't set vmin and vmax because vmin >= vmax " \
- "vmin = %s, vmax = %s" % (vmin, vmax)
- raise ValueError(err)
-
- if self._vmin == vmin and self._vmax == vmax:
- return
-
- self._vmin = vmin
- self._vmax = vmax
- self.sigChanged.emit()
-
- def __getitem__(self, item):
- if item == 'autoscale':
- return self.isAutoscale()
- elif item == 'name':
- return self.getName()
- elif item == 'normalization':
- return self.getNormalization()
- elif item == 'vmin':
- return self.getVMin()
- elif item == 'vmax':
- return self.getVMax()
- elif item == 'colors':
- return self.getColormapLUT()
- elif item == 'autoscaleMode':
- return self.getAutoscaleMode()
- else:
- raise KeyError(item)
-
- def _toDict(self):
- """Return the equivalent colormap as a dictionary
- (old colormap representation)
-
- :return: the representation of the Colormap as a dictionary
- :rtype: dict
- """
- return {
- 'name': self._name,
- 'colors': self.getColormapLUT(),
- 'vmin': self._vmin,
- 'vmax': self._vmax,
- 'autoscale': self.isAutoscale(),
- 'normalization': self.getNormalization(),
- 'autoscaleMode': self.getAutoscaleMode(),
- }
-
- def _setFromDict(self, dic):
- """Set values to the colormap from a dictionary
-
- :param dict dic: the colormap as a dictionary
- """
- if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
- name = dic['name'] if 'name' in dic else None
- colors = dic['colors'] if 'colors' in dic else None
- if name is not None and colors is not None:
- if isinstance(colors, int):
- # Filter out argument which was supported but never used
- _logger.info("Unused 'colors' from colormap dictionary filterer.")
- colors = None
- vmin = dic['vmin'] if 'vmin' in dic else None
- vmax = dic['vmax'] if 'vmax' in dic else None
- if 'normalization' in dic:
- normalization = dic['normalization']
- else:
- warn = 'Normalization not given in the dictionary, '
- warn += 'set by default to ' + Colormap.LINEAR
- _logger.warning(warn)
- normalization = Colormap.LINEAR
-
- if name is None and colors is None:
- err = 'The colormap should have a name defined or a tuple of colors'
- raise ValueError(err)
- if normalization not in Colormap.NORMALIZATIONS:
- err = 'Given normalization is not recognized (%s)' % normalization
- raise ValueError(err)
-
- autoscaleMode = dic.get('autoscaleMode', Colormap.MINMAX)
- if autoscaleMode not in Colormap.AUTOSCALE_MODES:
- err = 'Given autoscale mode is not recognized (%s)' % autoscaleMode
- raise ValueError(err)
-
- # If autoscale, then set boundaries to None
- if dic.get('autoscale', False):
- vmin, vmax = None, None
-
- 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
- self._normalization = normalization
- self._autoscaleMode = autoscaleMode
-
- self.sigChanged.emit()
-
- @staticmethod
- def _fromDict(dic):
- colormap = Colormap()
- colormap._setFromDict(dic)
- return colormap
-
- def copy(self):
- """Return a copy of the Colormap.
-
- :rtype: silx.gui.colors.Colormap
- """
- colormap = Colormap(name=self._name,
- colors=self.getColormapLUT(),
- vmin=self._vmin,
- vmax=self._vmax,
- normalization=self.getNormalization(),
- autoscaleMode=self.getAutoscaleMode())
- colormap.setNaNColor(self.getNaNColor())
- colormap.setGammaNormalizationParameter(
- self.getGammaNormalizationParameter())
- colormap.setEditable(self.isEditable())
- return colormap
-
- def applyToData(self, data, reference=None):
- """Apply the colormap to the data
-
- :param Union[numpy.ndarray,~silx.gui.plot.item.ColormapMixIn] data:
- The data to convert or the item for which to apply the colormap.
- :param Union[numpy.ndarray,~silx.gui.plot.item.ColormapMixIn,None] reference:
- The data or item to use as reference to compute autoscale
- """
- if reference is None:
- reference = data
- vmin, vmax = self.getColormapRange(reference)
-
- if hasattr(data, "getColormappedData"): # Use item's data
- data = data.getColormappedData(copy=False)
-
- return _colormap.cmap(
- data,
- self._colors,
- vmin,
- vmax,
- self._getNormalizer(),
- self.__nanColor)
-
- @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',
- 'viridis', 'magma', 'inferno', 'plasma')
-
- :rtype: tuple
- """
- colormaps = set()
- if _matplotlib_colormaps is not None:
- colormaps.update(_matplotlib_colormaps())
- 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())
-
- def __eq__(self, other):
- """Compare colormap values and not pointers"""
- if other is None:
- return False
- if not isinstance(other, Colormap):
- return False
- if self.getNormalization() != other.getNormalization():
- return False
- if self.getNormalization() == self.GAMMA:
- delta = self.getGammaNormalizationParameter() - other.getGammaNormalizationParameter()
- if abs(delta) > 0.001:
- return False
- return (self.getName() == other.getName() and
- self.getAutoscaleMode() == other.getAutoscaleMode() and
- self.getVMin() == other.getVMin() and
- self.getVMax() == other.getVMax() and
- numpy.array_equal(self.getColormapLUT(), other.getColormapLUT())
- )
-
- _SERIAL_VERSION = 3
-
- def restoreState(self, byteArray):
- """
- Read the colormap state from a QByteArray.
-
- :param qt.QByteArray byteArray: Stream containing the state
- :return: True if the restoration sussseed
- :rtype: bool
- """
- if self.isEditable() is False:
- raise NotEditableError('Colormap is not editable')
- stream = qt.QDataStream(byteArray, qt.QIODevice.ReadOnly)
-
- className = stream.readQString()
- if className != self.__class__.__name__:
- _logger.warning("Classname mismatch. Found %s." % className)
- return False
-
- version = stream.readUInt32()
- if version not in numpy.arange(1, self._SERIAL_VERSION+1):
- _logger.warning("Serial version mismatch. Found %d." % version)
- return False
-
- name = stream.readQString()
- isNull = stream.readBool()
- if not isNull:
- vmin = stream.readQVariant()
- else:
- vmin = None
- isNull = stream.readBool()
- if not isNull:
- vmax = stream.readQVariant()
- else:
- vmax = None
-
- normalization = stream.readQString()
- if normalization == Colormap.GAMMA:
- gamma = stream.readFloat()
- else:
- gamma = None
-
- if version == 1:
- autoscaleMode = Colormap.MINMAX
- else:
- autoscaleMode = stream.readQString()
-
- if version <= 2:
- nanColor = self._DEFAULT_NAN_COLOR
- else:
- nanColor = stream.readInt32(), stream.readInt32(), stream.readInt32(), stream.readInt32()
-
- # emit change event only once
- old = self.blockSignals(True)
- try:
- self.setName(name)
- self.setNormalization(normalization)
- self.setAutoscaleMode(autoscaleMode)
- self.setVRange(vmin, vmax)
- if gamma is not None:
- self.setGammaNormalizationParameter(gamma)
- self.setNaNColor(nanColor)
- finally:
- self.blockSignals(old)
- self.sigChanged.emit()
- return True
-
- def saveState(self):
- """
- Save state of the colomap into a QDataStream.
-
- :rtype: qt.QByteArray
- """
- data = qt.QByteArray()
- stream = qt.QDataStream(data, qt.QIODevice.WriteOnly)
-
- stream.writeQString(self.__class__.__name__)
- stream.writeUInt32(self._SERIAL_VERSION)
- stream.writeQString(self.getName())
- stream.writeBool(self.getVMin() is None)
- if self.getVMin() is not None:
- stream.writeQVariant(self.getVMin())
- stream.writeBool(self.getVMax() is None)
- if self.getVMax() is not None:
- stream.writeQVariant(self.getVMax())
- stream.writeQString(self.getNormalization())
- if self.getNormalization() == Colormap.GAMMA:
- stream.writeFloat(self.getGammaNormalizationParameter())
- stream.writeQString(self.getAutoscaleMode())
- nanColor = self.getNaNColor()
- stream.writeInt32(nanColor.red())
- stream.writeInt32(nanColor.green())
- stream.writeInt32(nanColor.blue())
- stream.writeInt32(nanColor.alpha())
-
- return data
-
-
-_PREFERRED_COLORMAPS = None
-"""
-Tuple of preferred colormap names accessed with :meth:`preferredColormaps`.
-"""
-
-
-def preferredColormaps():
- """Returns the name of the preferred colormaps.
-
- This list is used by widgets allowing to change the colormap
- like the :class:`ColormapDialog` as a subset of colormap choices.
-
- :rtype: tuple of str
- """
- global _PREFERRED_COLORMAPS
- if _PREFERRED_COLORMAPS is None:
- # Initialize 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):
- """Set the list of preferred colormap names.
-
- Warning: If a colormap name is not available
- it will be removed from the list.
-
- :param colormaps: Not empty list of colormap names
- :type colormaps: iterable of str
- :raise ValueError: if the list of available preferred colormaps is empty.
- """
- supportedColormaps = Colormap.getSupportedColormaps()
- 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
deleted file mode 100644
index 5dc6336..0000000
--- a/silx/gui/console.py
+++ /dev/null
@@ -1,202 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module provides an IPython console widget.
-
-You can push variables - any python object - to the
-console's interactive namespace. This provides users with an advanced way
-of interacting with your program. For instance, if your program has a
-:class:`PlotWidget` or a :class:`PlotWindow`, you can push a reference to
-these widgets to allow your users to add curves, save data to files… by using
-the widgets' methods from the console.
-
-.. note::
-
- This module has a dependency on
- `qtconsole <https://pypi.org/project/qtconsole/>`_.
- An ``ImportError`` will be raised if it is
- imported while the dependencies are not satisfied.
-
-Basic usage example::
-
- from silx.gui import qt
- from silx.gui.console import IPythonWidget
-
- app = qt.QApplication([])
-
- hello_button = qt.QPushButton("Hello World!", None)
- hello_button.show()
-
- console = IPythonWidget()
- console.show()
- console.pushVariables({"the_button": hello_button})
-
- app.exec_()
-
-This program will display a console widget and a push button in two separate
-windows. You will be able to interact with the button from the console,
-for example change its text::
-
- >>> the_button.setText("Spam spam")
-
-An IPython interactive console is a powerful tool that enables you to work
-with data and plot it.
-See `this tutorial <https://plot.ly/python/ipython-notebook-tutorial/>`_
-for more information on some of the rich features of IPython.
-"""
-__authors__ = ["Tim Rae", "V.A. Sole", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "24/05/2016"
-
-import logging
-
-from . import qt
-
-_logger = logging.getLogger(__name__)
-
-
-# This widget cannot be used inside an interactive IPython shell.
-# It would raise MultipleInstanceError("Multiple incompatible subclass
-# instances of InProcessInteractiveShell are being created").
-try:
- __IPYTHON__
-except NameError:
- pass # Not in IPython
-else:
- msg = "Module " + __name__ + " cannot be used within an IPython shell"
- raise ImportError(msg)
-
-try:
- from qtconsole.rich_jupyter_widget import RichJupyterWidget as \
- _RichJupyterWidget
-except ImportError:
- 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(_RichJupyterWidget):
- """Live IPython console widget.
-
- .. image:: img/IPythonWidget.png
-
- :param custom_banner: Custom welcome message to be printed at the top of
- the console.
- """
-
- def __init__(self, parent=None, custom_banner=None, *args, **kwargs):
- if parent is not None:
- kwargs["parent"] = parent
- super(IPythonWidget, self).__init__(*args, **kwargs)
- if custom_banner is not None:
- self.banner = custom_banner
- 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()
-
- def stop():
- kernel_client.stop_channels()
- kernel_manager.shutdown_kernel()
- self.exit_requested.connect(stop)
-
- def sizeHint(self):
- """Return a reasonable default size for usage in :class:`PlotWindow`"""
- return qt.QSize(500, 300)
-
- def pushVariables(self, variable_dict):
- """ Given a dictionary containing name / value pairs, push those
- variables to the IPython console widget.
-
- :param variable_dict: Dictionary of variables to be pushed to the
- console's interactive namespace (```{variable_name: object, …}```)
- """
- self.kernel_manager.kernel.shell.push(variable_dict)
-
-
-class IPythonDockWidget(qt.QDockWidget):
- """Dock Widget including a :class:`IPythonWidget` inside
- a vertical layout.
-
- .. image:: img/IPythonDockWidget.png
-
- :param available_vars: Dictionary of variables to be pushed to the
- console's interactive namespace: ``{"variable_name": object, …}``
- :param custom_banner: Custom welcome message to be printed at the top of
- the console
- :param title: Dock widget title
- :param parent: Parent :class:`qt.QMainWindow` containing this
- :class:`qt.QDockWidget`
- """
- def __init__(self, parent=None, available_vars=None, custom_banner=None,
- title="Console"):
- super(IPythonDockWidget, self).__init__(title, parent)
-
- self.ipyconsole = IPythonWidget(custom_banner=custom_banner)
-
- self.layout().setContentsMargins(0, 0, 0, 0)
- self.setWidget(self.ipyconsole)
-
- if available_vars is not None:
- self.ipyconsole.pushVariables(available_vars)
-
- def showEvent(self, event):
- """Make sure this widget is raised when it is shown
- (when it is first created as a tab in PlotWindow or when it is shown
- again after hiding).
- """
- self.raise_()
-
-
-def main():
- """Run a Qt app with an IPython console"""
- app = qt.QApplication([])
- widget = IPythonDockWidget()
- widget.show()
- app.exec_()
-
-
-if __name__ == '__main__':
- main()
diff --git a/silx/gui/data/ArrayTableModel.py b/silx/gui/data/ArrayTableModel.py
deleted file mode 100644
index b7bd9c4..0000000
--- a/silx/gui/data/ArrayTableModel.py
+++ /dev/null
@@ -1,670 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2021 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 defines a data model for displaying and editing arrays of any
-number of dimensions in a table view.
-"""
-from __future__ import division
-import numpy
-import logging
-from silx.gui import qt
-from silx.gui.data.TextFormatter import TextFormatter
-
-__authors__ = ["V.A. Sole"]
-__license__ = "MIT"
-__date__ = "27/09/2017"
-
-
-_logger = logging.getLogger(__name__)
-
-
-def _is_array(data):
- """Return True if object implements all necessary attributes to be used
- as a numpy array.
-
- :param object data: Array-like object (numpy array, h5py dataset...)
- :return: boolean
- """
- # add more required attribute if necessary
- for attr in ("shape", "dtype"):
- if not hasattr(data, attr):
- return False
- return True
-
-
-class ArrayTableModel(qt.QAbstractTableModel):
- """This data model provides access to 2D slices in a N-dimensional
- array.
-
- A slice for a 3-D array is characterized by a perspective (the number of
- the axis orthogonal to the slice) and an index at which the slice
- intersects the orthogonal axis.
-
- In the n-D case, only slices parallel to the last two axes are handled. A
- slice is therefore characterized by a list of indices locating the
- slice on all the :math:`n - 2` orthogonal axes.
-
- :param parent: Parent QObject
- :param data: Numpy array, or object implementing a similar interface
- (e.g. h5py dataset)
- :param str fmt: Format string for representing numerical values.
- Default is ``"%g"``.
- :param sequence[int] perspective: See documentation
- of :meth:`setPerspective`.
- """
-
- MAX_NUMBER_OF_SECTIONS = 10e6
- """Maximum number of displayed rows and columns"""
-
- def __init__(self, parent=None, data=None, perspective=None):
- qt.QAbstractTableModel.__init__(self, parent)
-
- self._array = None
- """n-dimensional numpy array"""
-
- self._bgcolors = None
- """(n+1)-dimensional numpy array containing RGB(A) color data
- for the background color
- """
-
- self._fgcolors = None
- """(n+1)-dimensional numpy array containing RGB(A) color data
- for the foreground color
- """
-
- self._formatter = None
- """Formatter for text representation of data"""
-
- formatter = TextFormatter(self)
- formatter.setUseQuoteForText(False)
- self.setFormatter(formatter)
-
- self._index = None
- """This attribute stores the slice index, as a list of indices
- where the frame intersects orthogonal axis."""
-
- self._perspective = None
- """Sequence of dimensions orthogonal to the frame to be viewed.
- For an array with ``n`` dimensions, this is a sequence of ``n-2``
- integers. the first dimension is numbered ``0``.
- By default, the data frames use the last two dimensions as their axes
- and therefore the perspective is a sequence of the first ``n-2``
- dimensions.
- For example, for a 5-D array, the default perspective is ``(0, 1, 2)``
- and the default frames axes are ``(3, 4)``."""
-
- # set _data and _perspective
- self.setArrayData(data, perspective=perspective)
-
- def _getRowDim(self):
- """The row axis is the first axis parallel to the frames
- (lowest dimension number)
-
- Return None for 0-D (scalar) or 1-D arrays
- """
- n_dimensions = len(self._array.shape)
- if n_dimensions < 2:
- # scalar or 1D array: no row index
- return None
- # take all dimensions and remove the orthogonal ones
- frame_axes = set(range(0, n_dimensions)) - set(self._perspective)
- # sanity check
- assert len(frame_axes) == 2
- return min(frame_axes)
-
- def _getColumnDim(self):
- """The column axis is the second (highest dimension) axis parallel
- to the frames
-
- Return None for 0-D (scalar)
- """
- n_dimensions = len(self._array.shape)
- if n_dimensions < 1:
- # scalar: no column index
- return None
- frame_axes = set(range(0, n_dimensions)) - set(self._perspective)
- # sanity check
- assert (len(frame_axes) == 2) if n_dimensions > 1 else (len(frame_axes) == 1)
- return max(frame_axes)
-
- def _getIndexTuple(self, table_row, table_col):
- """Return the n-dimensional index of a value in the original array,
- based on its row and column indices in the table view
-
- :param table_row: Row index (0-based) of a table cell
- :param table_col: Column index (0-based) of a table cell
- :return: Tuple of indices of the element in the numpy array
- """
- row_dim = self._getRowDim()
- col_dim = self._getColumnDim()
-
- # get indices on all orthogonal axes
- selection = list(self._index)
- # insert indices on parallel axes
- if row_dim is not None:
- selection.insert(row_dim, table_row)
- if col_dim is not None:
- selection.insert(col_dim, table_col)
- return tuple(selection)
-
- # Methods to be implemented to subclass QAbstractTableModel
- def rowCount(self, parent_idx=None):
- """QAbstractTableModel method
- Return number of rows to be displayed in table"""
- row_dim = self._getRowDim()
- if row_dim is None:
- # 0-D and 1-D arrays
- return 1
- return min(self._array.shape[row_dim], self.MAX_NUMBER_OF_SECTIONS)
-
- def columnCount(self, parent_idx=None):
- """QAbstractTableModel method
- Return number of columns to be displayed in table"""
- col_dim = self._getColumnDim()
- if col_dim is None:
- # 0-D array
- return 1
- return min(self._array.shape[col_dim], self.MAX_NUMBER_OF_SECTIONS)
-
- def __isClipped(self, orientation=qt.Qt.Vertical) -> bool:
- """Returns whether or not array is clipped in a given orientation"""
- if orientation == qt.Qt.Vertical:
- dim = self._getRowDim()
- else:
- dim = self._getColumnDim()
- return (dim is not None and
- self._array.shape[dim] > self.MAX_NUMBER_OF_SECTIONS)
-
- def __isClippedIndex(self, index) -> bool:
- """Returns whether or not index's cell represents clipped data."""
- if not index.isValid():
- return False
- if index.row() == self.MAX_NUMBER_OF_SECTIONS - 2:
- return self.__isClipped(qt.Qt.Vertical)
- if index.column() == self.MAX_NUMBER_OF_SECTIONS - 2:
- return self.__isClipped(qt.Qt.Horizontal)
- return False
-
- def __clippedData(self, role=qt.Qt.DisplayRole):
- """Return data for cells representing clipped data"""
- if role == qt.Qt.DisplayRole:
- return "..."
- elif role == qt.Qt.ToolTipRole:
- return "Dataset is too large: display is clipped"
- else:
- return None
-
- def data(self, index, role=qt.Qt.DisplayRole):
- """QAbstractTableModel method to access data values
- in the format ready to be displayed"""
- if index.isValid():
- if self.__isClippedIndex(index): # Special displayed for clipped data
- return self.__clippedData(role)
-
- row, column = index.row(), index.column()
-
- # When clipped, display last data of the array in last column of the table
- if (self.__isClipped(qt.Qt.Vertical) and
- row == self.MAX_NUMBER_OF_SECTIONS - 1):
- row = self._array.shape[self._getRowDim()] - 1
- if (self.__isClipped(qt.Qt.Horizontal) and
- column == self.MAX_NUMBER_OF_SECTIONS - 1):
- column = self._array.shape[self._getColumnDim()] - 1
-
- selection = self._getIndexTuple(row, column)
-
- if role == qt.Qt.DisplayRole:
- return self._formatter.toString(self._array[selection], self._array.dtype)
-
- if role == qt.Qt.BackgroundRole and self._bgcolors is not None:
- r, g, b = self._bgcolors[selection][0:3]
- if self._bgcolors.shape[-1] == 3:
- return qt.QColor(r, g, b)
- if self._bgcolors.shape[-1] == 4:
- a = self._bgcolors[selection][3]
- return qt.QColor(r, g, b, a)
-
- if role == qt.Qt.ForegroundRole:
- if self._fgcolors is not None:
- r, g, b = self._fgcolors[selection][0:3]
- if self._fgcolors.shape[-1] == 3:
- return qt.QColor(r, g, b)
- if self._fgcolors.shape[-1] == 4:
- a = self._fgcolors[selection][3]
- return qt.QColor(r, g, b, a)
-
- # no fg color given, use black or white
- # based on luminosity threshold
- elif self._bgcolors is not None:
- r, g, b = self._bgcolors[selection][0:3]
- lum = 0.21 * r + 0.72 * g + 0.07 * b
- if lum < 128:
- return qt.QColor(qt.Qt.white)
- else:
- return qt.QColor(qt.Qt.black)
-
- def headerData(self, section, orientation, role=qt.Qt.DisplayRole):
- """QAbstractTableModel method
- Return the 0-based row or column index, for display in the
- horizontal and vertical headers"""
- if self.__isClipped(orientation): # Header is clipped
- if section == self.MAX_NUMBER_OF_SECTIONS - 2:
- # Represent clipped data
- return self.__clippedData(role)
-
- elif section == self.MAX_NUMBER_OF_SECTIONS - 1:
- # Display last index from data not table
- if role == qt.Qt.DisplayRole:
- if orientation == qt.Qt.Vertical:
- dim = self._getRowDim()
- else:
- dim = self._getColumnDim()
- return str(self._array.shape[dim] - 1)
- else:
- return None
-
- if role == qt.Qt.DisplayRole:
- return "%d" % section
- return None
-
- def flags(self, index):
- """QAbstractTableModel method to inform the view whether data
- is editable or not."""
- if not self._editable or self.__isClippedIndex(index):
- return qt.QAbstractTableModel.flags(self, index)
- return qt.QAbstractTableModel.flags(self, index) | qt.Qt.ItemIsEditable
-
- def setData(self, index, value, role=None):
- """QAbstractTableModel method to handle editing data.
- Cast the new value into the same format as the array before editing
- the array value."""
- if index.isValid() and role == qt.Qt.EditRole:
- try:
- # cast value to same type as array
- v = numpy.array(value, dtype=self._array.dtype).item()
- except ValueError:
- return False
-
- selection = self._getIndexTuple(index.row(),
- index.column())
- self._array[selection] = v
- self.dataChanged.emit(index, index)
- return True
- else:
- return False
-
- # Public methods
- def setArrayData(self, data, copy=True,
- perspective=None, editable=False):
- """Set the data array and the viewing perspective.
-
- You can set ``copy=False`` if you need more performances, when dealing
- with a large numpy array. In this case, a simple reference to the data
- is used to access the data, rather than a copy of the array.
-
- .. warning::
-
- Any change to the data model will affect your original data
- array, when using a reference rather than a copy..
-
- :param data: n-dimensional numpy array, or any object that can be
- converted to a numpy array using ``numpy.array(data)`` (e.g.
- a nested sequence).
- :param bool copy: If *True* (default), a copy of the array is stored
- and the original array is not modified if the table is edited.
- If *False*, then the behavior depends on the data type:
- if possible (if the original array is a proper numpy array)
- a reference to the original array is used.
- :param perspective: See documentation of :meth:`setPerspective`.
- If None, the default perspective is the list of the first ``n-2``
- dimensions, to view frames parallel to the last two axes.
- :param bool editable: Flag to enable editing data. Default *False*.
- """
- if qt.qVersion() > "4.6":
- self.beginResetModel()
- else:
- self.reset()
-
- if data is None:
- # empty array
- self._array = numpy.array([])
- elif copy:
- # copy requested (default)
- self._array = numpy.array(data, copy=True)
- if hasattr(data, "dtype"):
- # Avoid to lose the monkey-patched h5py dtype
- self._array.dtype = data.dtype
- elif not _is_array(data):
- raise TypeError("data is not a proper array. Try setting" +
- " copy=True to convert it into a numpy array" +
- " (this will cause the data to be copied!)")
- # # copy not requested, but necessary
- # _logger.warning(
- # "data is not an array-like object. " +
- # "Data must be copied.")
- # self._array = numpy.array(data, copy=True)
- else:
- # Copy explicitly disabled & data implements required attributes.
- # We can use a reference.
- self._array = data
-
- # reset colors to None if new data shape is inconsistent
- valid_color_shapes = (self._array.shape + (3,),
- self._array.shape + (4,))
- if self._bgcolors is not None:
- if self._bgcolors.shape not in valid_color_shapes:
- self._bgcolors = None
- if self._fgcolors is not None:
- if self._fgcolors.shape not in valid_color_shapes:
- self._fgcolors = None
-
- self.setEditable(editable)
-
- self._index = [0 for _i in range((len(self._array.shape) - 2))]
- self._perspective = tuple(perspective) if perspective is not None else\
- tuple(range(0, len(self._array.shape) - 2))
-
- if qt.qVersion() > "4.6":
- self.endResetModel()
-
- def setArrayColors(self, bgcolors=None, fgcolors=None):
- """Set the colors for all table cells by passing an array
- of RGB or RGBA values (integers between 0 and 255).
-
- The shape of the colors array must be consistent with the data shape.
-
- If the data array is n-dimensional, the colors array must be
- (n+1)-dimensional, with the first n-dimensions identical to the data
- array dimensions, and the last dimension length-3 (RGB) or
- length-4 (RGBA).
-
- :param bgcolors: RGB or RGBA colors array, defining the background color
- for each cell in the table.
- :param fgcolors: RGB or RGBA colors array, defining the foreground color
- (text color) for each cell in the table.
- """
- # array must be RGB or RGBA
- valid_shapes = (self._array.shape + (3,), self._array.shape + (4,))
- errmsg = "Inconsistent shape for color array, should be %s or %s" % valid_shapes
-
- if bgcolors is not None:
- if not _is_array(bgcolors):
- bgcolors = numpy.array(bgcolors)
- assert bgcolors.shape in valid_shapes, errmsg
-
- self._bgcolors = bgcolors
-
- if fgcolors is not None:
- if not _is_array(fgcolors):
- fgcolors = numpy.array(fgcolors)
- assert fgcolors.shape in valid_shapes, errmsg
-
- self._fgcolors = fgcolors
-
- def setEditable(self, editable):
- """Set flags to make the data editable.
-
- .. warning::
-
- If the data is a reference to a h5py dataset open in read-only
- mode, setting *editable=True* will fail and print a warning.
-
- .. warning::
-
- Making the data editable means that the underlying data structure
- in this data model will be modified.
- If the data is a reference to a public object (open with
- ``copy=False``), this could have side effects. If it is a
- reference to an HDF5 dataset, this means the file will be
- modified.
-
- :param bool editable: Flag to enable editing data.
- :return: True if setting desired flag succeeded, False if it failed.
- """
- self._editable = editable
- if hasattr(self._array, "file"):
- if hasattr(self._array.file, "mode"):
- if editable and self._array.file.mode == "r":
- _logger.warning(
- "Data is a HDF5 dataset open in read-only " +
- "mode. Editing must be disabled.")
- self._editable = False
- return False
- return True
-
- def getData(self, copy=True):
- """Return a copy of the data array, or a reference to it
- if *copy=False* is passed as parameter.
-
- In case the shape was modified, to convert 0-D or 1-D data
- into 2-D data, the original shape is restored in the returned data.
-
- :param bool copy: If *True* (default), return a copy of the data. If
- *False*, return a reference.
- :return: numpy array of data, or reference to original data object
- if *copy=False*
- """
- data = self._array if not copy else numpy.array(self._array, copy=True)
- return data
-
- def setFrameIndex(self, index):
- """Set the active slice index.
-
- This method is only relevant to arrays with at least 3 dimensions.
-
- :param index: Index of the active slice in the array.
- In the general n-D case, this is a sequence of :math:`n - 2`
- indices where the slice intersects the respective orthogonal axes.
- :raise IndexError: If any index in the index sequence is out of bound
- on its respective axis.
- """
- shape = self._array.shape
- if len(shape) < 3:
- # index is ignored
- return
-
- if qt.qVersion() > "4.6":
- self.beginResetModel()
- else:
- self.reset()
-
- if len(shape) == 3:
- len_ = shape[self._perspective[0]]
- # accept integers as index in the case of 3-D arrays
- if not hasattr(index, "__len__"):
- self._index = [index]
- else:
- self._index = index
- if not 0 <= self._index[0] < len_:
- raise ValueError("Index must be a positive integer " +
- "lower than %d" % len_)
- else:
- # general n-D case
- for i_, idx in enumerate(index):
- if not 0 <= idx < shape[self._perspective[i_]]:
- raise IndexError("Invalid index %d " % idx +
- "not in range 0-%d" % (shape[i_] - 1))
- self._index = index
-
- if qt.qVersion() > "4.6":
- self.endResetModel()
-
- def setFormatter(self, formatter):
- """Set the formatter object to be used to display data from the model
-
- :param TextFormatter formatter: Formatter to use
- """
- if formatter is self._formatter:
- return
-
- if qt.qVersion() > "4.6":
- self.beginResetModel()
-
- if self._formatter is not None:
- self._formatter.formatChanged.disconnect(self.__formatChanged)
-
- self._formatter = formatter
- if self._formatter is not None:
- self._formatter.formatChanged.connect(self.__formatChanged)
-
- if qt.qVersion() > "4.6":
- self.endResetModel()
- else:
- self.reset()
-
- def getFormatter(self):
- """Returns the text formatter used.
-
- :rtype: TextFormatter
- """
- return self._formatter
-
- def __formatChanged(self):
- """Called when the format changed.
- """
- self.reset()
-
- def setPerspective(self, perspective):
- """Set the perspective by defining a sequence listing all axes
- orthogonal to the frame or 2-D slice to be visualized.
-
- Alternatively, you can use :meth:`setFrameAxes` for the complementary
- approach of specifying the two axes parallel to the frame.
-
- In the 1-D or 2-D case, this parameter is irrelevant.
-
- In the 3-D case, if the unit vectors describing
- your axes are :math:`\vec{x}, \vec{y}, \vec{z}`, a perspective of 0
- means you slices are parallel to :math:`\vec{y}\vec{z}`, 1 means they
- are parallel to :math:`\vec{x}\vec{z}` and 2 means they
- are parallel to :math:`\vec{x}\vec{y}`.
-
- In the n-D case, this parameter is a sequence of :math:`n-2` axes
- numbers.
- For instance if you want to display 2-D frames whose axes are the
- second and third dimensions of a 5-D array, set the perspective to
- ``(0, 3, 4)``.
-
- :param perspective: Sequence of dimensions/axes orthogonal to the
- frames.
- :raise: IndexError if any value in perspective is higher than the
- number of dimensions minus one (first dimension is 0), or
- if the number of values is different from the number of dimensions
- minus two.
- """
- n_dimensions = len(self._array.shape)
- if n_dimensions < 3:
- _logger.warning(
- "perspective is not relevant for 1D and 2D arrays")
- return
-
- if not hasattr(perspective, "__len__"):
- # we can tolerate an integer for 3-D array
- if n_dimensions == 3:
- perspective = [perspective]
- else:
- raise ValueError("perspective must be a sequence of integers")
-
- # ensure unicity of dimensions in perspective
- perspective = tuple(set(perspective))
-
- if len(perspective) != n_dimensions - 2 or\
- min(perspective) < 0 or max(perspective) >= n_dimensions:
- raise IndexError(
- "Invalid perspective " + str(perspective) +
- " for %d-D array " % n_dimensions +
- "with shape " + str(self._array.shape))
-
- if qt.qVersion() > "4.6":
- self.beginResetModel()
- else:
- self.reset()
-
- self._perspective = perspective
-
- # reset index
- self._index = [0 for _i in range(n_dimensions - 2)]
-
- if qt.qVersion() > "4.6":
- self.endResetModel()
-
- def setFrameAxes(self, row_axis, col_axis):
- """Set the perspective by specifying the two axes parallel to the frame
- to be visualised.
-
- The complementary approach of defining the orthogonal axes can be used
- with :meth:`setPerspective`.
-
- :param int row_axis: Index (0-based) of the first dimension used as a frame
- axis
- :param int col_axis: Index (0-based) of the 2nd dimension used as a frame
- axis
- :raise: IndexError if axes are invalid
- """
- if row_axis > col_axis:
- _logger.warning("The dimension of the row axis must be lower " +
- "than the dimension of the column axis. Swapping.")
- row_axis, col_axis = min(row_axis, col_axis), max(row_axis, col_axis)
-
- n_dimensions = len(self._array.shape)
- if n_dimensions < 3:
- _logger.warning(
- "Frame axes cannot be changed for 1D and 2D arrays")
- return
-
- perspective = tuple(set(range(0, n_dimensions)) - {row_axis, col_axis})
-
- if len(perspective) != n_dimensions - 2 or\
- min(perspective) < 0 or max(perspective) >= n_dimensions:
- raise IndexError(
- "Invalid perspective " + str(perspective) +
- " for %d-D array " % n_dimensions +
- "with shape " + str(self._array.shape))
-
- if qt.qVersion() > "4.6":
- self.beginResetModel()
- else:
- self.reset()
-
- self._perspective = perspective
- # reset index
- self._index = [0 for _i in range(n_dimensions - 2)]
-
- if qt.qVersion() > "4.6":
- self.endResetModel()
-
-
-if __name__ == "__main__":
- app = qt.QApplication([])
- w = qt.QTableView()
- d = numpy.random.normal(0, 1, (5, 1000, 1000))
- for i in range(5):
- d[i, :, :] += i * 10
- m = ArrayTableModel(data=d)
- w.setModel(m)
- m.setFrameIndex(3)
- # m.setArrayData(numpy.ones((100,)))
- w.show()
- app.exec_()
diff --git a/silx/gui/data/ArrayTableWidget.py b/silx/gui/data/ArrayTableWidget.py
deleted file mode 100644
index cb8e915..0000000
--- a/silx/gui/data/ArrayTableWidget.py
+++ /dev/null
@@ -1,492 +0,0 @@
-# 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 module defines a widget designed to display data arrays with any
-number of dimensions as 2D frames (images, slices) in a table view.
-The dimensions not displayed in the table can be browsed using improved
-sliders.
-
-The widget uses a TableView that relies on a custom abstract item
-model: :class:`silx.gui.data.ArrayTableModel`.
-"""
-from __future__ import division
-import sys
-
-from silx.gui import qt
-from silx.gui.widgets.TableWidget import TableView
-from .ArrayTableModel import ArrayTableModel
-from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
-
-__authors__ = ["V.A. Sole", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "24/01/2017"
-
-
-class AxesSelector(qt.QWidget):
- """Widget with two combo-boxes to select two dimensions among
- all possible dimensions of an n-dimensional array.
-
- The first combobox contains values from :math:`0` to :math:`n-2`.
-
- The choices in the 2nd CB depend on the value selected in the first one.
- If the value selected in the first CB is :math:`m`, the second one lets you
- select values from :math:`m+1` to :math:`n-1`.
-
- The two axes can be used to select the row axis and the column axis t
- display a slice of the array data in a table view.
- """
- sigDimensionsChanged = qt.Signal(int, int)
- """Signal emitted whenever one of the comboboxes is changed.
- The signal carries the two selected dimensions."""
-
- def __init__(self, parent=None, n=None):
- qt.QWidget.__init__(self, parent)
- self.layout = qt.QHBoxLayout(self)
- self.layout.setContentsMargins(0, 2, 0, 2)
- self.layout.setSpacing(10)
-
- self.rowsCB = qt.QComboBox(self)
- self.columnsCB = qt.QComboBox(self)
-
- self.layout.addWidget(qt.QLabel("Rows dimension", self))
- self.layout.addWidget(self.rowsCB)
- self.layout.addWidget(qt.QLabel(" ", self))
- self.layout.addWidget(qt.QLabel("Columns dimension", self))
- self.layout.addWidget(self.columnsCB)
- self.layout.addStretch(1)
-
- self._slotsAreConnected = False
- if n is not None:
- self.setNDimensions(n)
-
- def setNDimensions(self, n):
- """Initialize combo-boxes depending on number of dimensions of array.
- Initially, the rows dimension is the second-to-last one, and the
- columns dimension is the last one.
-
- Link the CBs together. MAke them emit a signal when their value is
- changed.
-
- :param int n: Number of dimensions of array
- """
- # remember the number of dimensions and the rows dimension
- self.n = n
- self._rowsDim = n - 2
-
- # ensure slots are disconnected before (re)initializing widget
- if self._slotsAreConnected:
- self.rowsCB.currentIndexChanged.disconnect(self._rowDimChanged)
- self.columnsCB.currentIndexChanged.disconnect(self._colDimChanged)
-
- self._clear()
- self.rowsCB.addItems([str(i) for i in range(n - 1)])
- self.rowsCB.setCurrentIndex(n - 2)
- if n >= 1:
- self.columnsCB.addItem(str(n - 1))
- self.columnsCB.setCurrentIndex(0)
-
- # reconnect slots
- self.rowsCB.currentIndexChanged.connect(self._rowDimChanged)
- self.columnsCB.currentIndexChanged.connect(self._colDimChanged)
- self._slotsAreConnected = True
-
- # emit new dimensions
- if n > 2:
- self.sigDimensionsChanged.emit(n - 2, n - 1)
-
- def setDimensions(self, row_dim, col_dim):
- """Set the rows and columns dimensions.
-
- The rows dimension must be lower than the columns dimension.
-
- :param int row_dim: Rows dimension
- :param int col_dim: Columns dimension
- """
- if row_dim >= col_dim:
- raise IndexError("Row dimension must be lower than column dimension")
- if not (0 <= row_dim < self.n - 1):
- raise IndexError("Row dimension must be between 0 and %d" % (self.n - 2))
- if not (row_dim < col_dim <= self.n - 1):
- raise IndexError("Col dimension must be between %d and %d" % (row_dim + 1, self.n - 1))
-
- # set the rows dimension; this triggers an update of columnsCB
- self.rowsCB.setCurrentIndex(row_dim)
- # columnsCB first item is "row_dim + 1". So index of "col_dim" is
- # col_dim - (row_dim + 1)
- self.columnsCB.setCurrentIndex(col_dim - row_dim - 1)
-
- def getDimensions(self):
- """Return a 2-tuple of the rows dimension and the columns dimension.
-
- :return: 2-tuple of axes numbers (row_dimension, col_dimension)
- """
- return self._getRowDim(), self._getColDim()
-
- def _clear(self):
- """Empty the combo-boxes"""
- self.rowsCB.clear()
- self.columnsCB.clear()
-
- def _getRowDim(self):
- """Get rows dimension, selected in :attr:`rowsCB`
- """
- # rows combobox contains elements "0", ..."n-2",
- # so the selected dim is always equal to the index
- return self.rowsCB.currentIndex()
-
- def _getColDim(self):
- """Get columns dimension, selected in :attr:`columnsCB`"""
- # columns combobox contains elements "row_dim+1", "row_dim+2", ..., "n-1"
- # so the selected dim is equal to row_dim + 1 + index
- return self._rowsDim + 1 + self.columnsCB.currentIndex()
-
- def _rowDimChanged(self):
- """Update columns combobox when the rows dimension is changed.
-
- Emit :attr:`sigDimensionsChanged`"""
- old_col_dim = self._getColDim()
- new_row_dim = self._getRowDim()
-
- # clear cols CB
- self.columnsCB.currentIndexChanged.disconnect(self._colDimChanged)
- self.columnsCB.clear()
- # refill cols CB
- for i in range(new_row_dim + 1, self.n):
- self.columnsCB.addItem(str(i))
-
- # keep previous col dimension if possible
- new_col_cb_idx = old_col_dim - (new_row_dim + 1)
- if new_col_cb_idx < 0:
- # if row_dim is now greater than the previous col_dim,
- # we select a new col_dim = row_dim + 1 (first element in cols CB)
- new_col_cb_idx = 0
- self.columnsCB.setCurrentIndex(new_col_cb_idx)
-
- # reconnect slot
- self.columnsCB.currentIndexChanged.connect(self._colDimChanged)
-
- self._rowsDim = new_row_dim
-
- self.sigDimensionsChanged.emit(self._getRowDim(), self._getColDim())
-
- def _colDimChanged(self):
- """Emit :attr:`sigDimensionsChanged`"""
- self.sigDimensionsChanged.emit(self._getRowDim(), self._getColDim())
-
-
-def _get_shape(array_like):
- """Return shape of an array like object.
-
- In case the object is a nested sequence (list of lists, tuples...),
- the size of each dimension is assumed to be uniform, and is deduced from
- the length of the first sequence.
-
- :param array_like: Array like object: numpy array, hdf5 dataset,
- multi-dimensional sequence
- :return: Shape of array, as a tuple of integers
- """
- if hasattr(array_like, "shape"):
- return array_like.shape
-
- shape = []
- subsequence = array_like
- while hasattr(subsequence, "__len__"):
- shape.append(len(subsequence))
- subsequence = subsequence[0]
-
- return tuple(shape)
-
-
-class ArrayTableWidget(qt.QWidget):
- """This widget is designed to display data of 2D frames (images, slices)
- in a table view. The widget can load any n-dimensional array, and display
- any 2-D frame/slice in the array.
-
- The index of the dimensions orthogonal to the displayed frame can be set
- interactively using a browser widget (sliders, buttons and text entries).
-
- To set the data, use :meth:`setArrayData`.
- To select the perspective, use :meth:`setPerspective` or
- use :meth:`setFrameAxes`.
- To select the frame, use :meth:`setFrameIndex`.
-
- .. image:: img/ArrayTableWidget.png
- """
- def __init__(self, parent=None):
- """
-
- :param parent: parent QWidget
- :param labels: list of labels for each dimension of the array
- """
- qt.QWidget.__init__(self, parent)
- self.mainLayout = qt.QVBoxLayout(self)
- self.mainLayout.setContentsMargins(0, 0, 0, 0)
- self.mainLayout.setSpacing(0)
-
- self.browserContainer = qt.QWidget(self)
- self.browserLayout = qt.QGridLayout(self.browserContainer)
- self.browserLayout.setContentsMargins(0, 0, 0, 0)
- self.browserLayout.setSpacing(0)
-
- self._dimensionLabelsText = []
- """List of text labels sorted in the increasing order of the dimension
- they apply to."""
- self._browserLabels = []
- """List of QLabel widgets."""
- self._browserWidgets = []
- """List of HorizontalSliderWithBrowser widgets."""
-
- self.axesSelector = AxesSelector(self)
-
- self.view = TableView(self)
-
- self.mainLayout.addWidget(self.browserContainer)
- self.mainLayout.addWidget(self.axesSelector)
- self.mainLayout.addWidget(self.view)
-
- self.model = ArrayTableModel(self)
- self.view.setModel(self.model)
-
- def setArrayData(self, data, labels=None, copy=True, editable=False):
- """Set the data array. Update frame browsers and labels.
-
- :param data: Numpy array or similar object (e.g. nested sequence,
- h5py dataset...)
- :param labels: list of labels for each dimension of the array, or
- boolean ``True`` to use default labels ("dimension 0",
- "dimension 1", ...). `None` to disable labels (default).
- :param bool copy: If *True*, store a copy of *data* in the model. If
- *False*, store a reference to *data* if possible (only possible if
- *data* is a proper numpy array or an object that implements the
- same methods).
- :param bool editable: Flag to enable editing data. Default is *False*
- """
- self._data_shape = _get_shape(data)
-
- n_widgets = len(self._browserWidgets)
- n_dimensions = len(self._data_shape)
-
- # Reset text of labels
- self._dimensionLabelsText = []
- for i in range(n_dimensions):
- if labels in [True, 1]:
- label_text = "Dimension %d" % i
- elif labels is None or i >= len(labels):
- label_text = ""
- else:
- label_text = labels[i]
- self._dimensionLabelsText.append(label_text)
-
- # not enough widgets, create new ones (we need n_dim - 2)
- for i in range(n_widgets, n_dimensions - 2):
- browser = HorizontalSliderWithBrowser(self.browserContainer)
- self.browserLayout.addWidget(browser, i, 1)
- self._browserWidgets.append(browser)
- browser.valueChanged.connect(self._browserSlot)
- browser.setEnabled(False)
- browser.hide()
-
- label = qt.QLabel(self.browserContainer)
- self._browserLabels.append(label)
- self.browserLayout.addWidget(label, i, 0)
- label.hide()
-
- n_widgets = len(self._browserWidgets)
- for i in range(n_widgets):
- label = self._browserLabels[i]
- browser = self._browserWidgets[i]
-
- if (i + 2) < n_dimensions:
- label.setText(self._dimensionLabelsText[i])
- browser.setRange(0, self._data_shape[i] - 1)
- browser.setEnabled(True)
- browser.show()
- if labels is not None:
- label.show()
- else:
- label.hide()
- else:
- browser.setEnabled(False)
- browser.hide()
- label.hide()
-
- # set model
- self.model.setArrayData(data, copy=copy, editable=editable)
- # some linux distributions need this call
- self.view.setModel(self.model)
- if editable:
- self.view.enableCut()
- self.view.enablePaste()
-
- # initialize & connect axesSelector
- self.axesSelector.setNDimensions(n_dimensions)
- self.axesSelector.sigDimensionsChanged.connect(self.setFrameAxes)
-
- def setArrayColors(self, bgcolors=None, fgcolors=None):
- """Set the colors for all table cells by passing an array
- of RGB or RGBA values (integers between 0 and 255).
-
- The shape of the colors array must be consistent with the data shape.
-
- If the data array is n-dimensional, the colors array must be
- (n+1)-dimensional, with the first n-dimensions identical to the data
- array dimensions, and the last dimension length-3 (RGB) or
- length-4 (RGBA).
-
- :param bgcolors: RGB or RGBA colors array, defining the background color
- for each cell in the table.
- :param fgcolors: RGB or RGBA colors array, defining the foreground color
- (text color) for each cell in the table.
- """
- self.model.setArrayColors(bgcolors, fgcolors)
-
- def displayAxesSelector(self, isVisible):
- """Allow to display or hide the axes selector.
-
- :param bool isVisible: True to display the axes selector.
- """
- self.axesSelector.setVisible(isVisible)
-
- def setFrameIndex(self, index):
- """Set the active slice/image index in the n-dimensional array.
-
- A frame is a 2D array extracted from an array. This frame is
- necessarily parallel to 2 axes, and orthogonal to all other axes.
-
- The index of a frame is a sequence of indices along the orthogonal
- axes, where the frame intersects the respective axis. The indices
- are listed in the same order as the corresponding dimensions of the
- data array.
-
- For example, it the data array has 5 dimensions, and we are
- considering frames whose parallel axes are the 2nd and 4th dimensions
- of the array, the frame index will be a sequence of length 3
- corresponding to the indices where the frame intersects the 1st, 3rd
- and 5th axes.
-
- :param index: Sequence of indices defining the active data slice in
- a n-dimensional array. The sequence length is :math:`n-2`
- :raise: IndexError if any index in the index sequence is out of bound
- on its respective axis.
- """
- self.model.setFrameIndex(index)
-
- def _resetBrowsers(self, perspective):
- """Adjust limits for browsers based on the perspective and the
- size of the corresponding dimensions. Reset the index to 0.
- Update the dimension in the labels.
-
- :param perspective: Sequence of axes/dimensions numbers (0-based)
- defining the axes orthogonal to the frame.
- """
- # for 3D arrays we can accept an int rather than a 1-tuple
- if not hasattr(perspective, "__len__"):
- perspective = [perspective]
-
- # perspective must be sorted
- perspective = sorted(perspective)
-
- n_dimensions = len(self._data_shape)
- for i in range(n_dimensions - 2):
- browser = self._browserWidgets[i]
- label = self._browserLabels[i]
- browser.setRange(0, self._data_shape[perspective[i]] - 1)
- browser.setValue(0)
- label.setText(self._dimensionLabelsText[perspective[i]])
-
- def setPerspective(self, perspective):
- """Set the *perspective* by specifying which axes are orthogonal
- to the frame.
-
- For the opposite approach (defining parallel axes), use
- :meth:`setFrameAxes` instead.
-
- :param perspective: Sequence of unique axes numbers (0-based) defining
- the orthogonal axes. For a n-dimensional array, the sequence
- length is :math:`n-2`. The order is of the sequence is not taken
- into account (the dimensions are displayed in increasing order
- in the widget).
- """
- self.model.setPerspective(perspective)
- self._resetBrowsers(perspective)
-
- def setFrameAxes(self, row_axis, col_axis):
- """Set the *perspective* by specifying which axes are parallel
- to the frame.
-
- For the opposite approach (defining orthogonal axes), use
- :meth:`setPerspective` instead.
-
- :param int row_axis: Index (0-based) of the first dimension used as a frame
- axis
- :param int col_axis: Index (0-based) of the 2nd dimension used as a frame
- axis
- """
- self.model.setFrameAxes(row_axis, col_axis)
- n_dimensions = len(self._data_shape)
- perspective = tuple(set(range(0, n_dimensions)) - {row_axis, col_axis})
- self._resetBrowsers(perspective)
-
- def _browserSlot(self, value):
- index = []
- for browser in self._browserWidgets:
- if browser.isEnabled():
- index.append(browser.value())
- self.setFrameIndex(index)
- self.view.reset()
-
- def getData(self, copy=True):
- """Return a copy of the data array, or a reference to it if
- *copy=False* is passed as parameter.
-
- :param bool copy: If *True* (default), return a copy of the data. If
- *False*, return a reference.
- :return: Numpy array of data, or reference to original data object
- if *copy=False*
- """
- return self.model.getData(copy=copy)
-
-
-def main():
- import numpy
- a = qt.QApplication([])
- d = numpy.random.normal(0, 1, (4, 5, 1000, 1000))
- for j in range(4):
- for i in range(5):
- d[j, i, :, :] += i + 10 * j
- w = ArrayTableWidget()
- if "2" in sys.argv:
- print("sending a single image")
- w.setArrayData(d[0, 0])
- elif "3" in sys.argv:
- print("sending 5 images")
- w.setArrayData(d[0])
- else:
- print("sending 4 * 5 images ")
- w.setArrayData(d, labels=True)
- w.show()
- a.exec_()
-
-if __name__ == "__main__":
- main()
diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py
deleted file mode 100644
index 2e51439..0000000
--- a/silx/gui/data/DataViewer.py
+++ /dev/null
@@ -1,593 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module defines a widget designed to display data using the most adapted
-view from the ones provided by silx.
-"""
-from __future__ import division
-
-import logging
-import os.path
-import collections
-from silx.gui import qt
-from silx.gui.data import DataViews
-from silx.gui.data.DataViews import _normalizeData
-from silx.gui.utils import blockSignals
-from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
-
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "12/02/2019"
-
-
-_logger = logging.getLogger(__name__)
-
-
-DataSelection = collections.namedtuple("DataSelection",
- ["filename", "datapath",
- "slice", "permutation"])
-
-
-class DataViewer(qt.QFrame):
- """Widget to display any kind of data
-
- .. image:: img/DataViewer.png
-
- The method :meth:`setData` allows to set any data to the widget. Mostly
- `numpy.array` and `h5py.Dataset` are supported with adapted views. Other
- data types are displayed using a text viewer.
-
- A default view is automatically selected when a data is set. The method
- :meth:`setDisplayMode` allows to change the view. To have a graphical tool
- to select the view, prefer using the widget :class:`DataViewerFrame`.
-
- The dimension of the input data and the expected dimension of the selected
- view can differ. For example you can display an image (2D) from 4D
- data. In this case a :class:`NumpyAxesSelector` is displayed to allow the
- user to select the axis mapping and the slicing of other axes.
-
- .. code-block:: python
-
- import numpy
- data = numpy.random.rand(500,500)
- viewer = DataViewer()
- viewer.setData(data)
- viewer.setVisible(True)
- """
-
- displayedViewChanged = qt.Signal(object)
- """Emitted when the displayed view changes"""
-
- dataChanged = qt.Signal()
- """Emitted when the data changes"""
-
- currentAvailableViewsChanged = qt.Signal()
- """Emitted when the current available views (which support the current
- data) change"""
-
- def __init__(self, parent=None):
- """Constructor
-
- :param QWidget parent: The parent of the widget
- """
- super(DataViewer, self).__init__(parent)
-
- self.__stack = qt.QStackedWidget(self)
- self.__numpySelection = NumpyAxesSelector(self)
- self.__numpySelection.selectedAxisChanged.connect(self.__numpyAxisChanged)
- self.__numpySelection.selectionChanged.connect(self.__numpySelectionChanged)
- self.__numpySelection.customAxisChanged.connect(self.__numpyCustomAxisChanged)
-
- self.setLayout(qt.QVBoxLayout(self))
- self.layout().addWidget(self.__stack, 1)
-
- group = qt.QGroupBox(self)
- group.setLayout(qt.QVBoxLayout())
- group.layout().addWidget(self.__numpySelection)
- group.setTitle("Axis selection")
- self.__axisSelection = group
-
- self.layout().addWidget(self.__axisSelection)
-
- self.__currentAvailableViews = []
- self.__currentView = None
- self.__data = None
- self.__info = None
- self.__useAxisSelection = False
- self.__userSelectedView = None
- self.__hooks = None
-
- self.__views = []
- self.__index = {}
- """store stack index for each views"""
-
- self._initializeViews()
-
- def _initializeViews(self):
- """Inisialize the available views"""
- views = self.createDefaultViews(self.__stack)
- self.__views = list(views)
- self.setDisplayMode(DataViews.EMPTY_MODE)
-
- def setGlobalHooks(self, hooks):
- """Set a data view hooks for all the views
-
- :param DataViewHooks context: The hooks to use
- """
- self.__hooks = hooks
- for v in self.__views:
- v.setHooks(hooks)
-
- def createDefaultViews(self, parent=None):
- """Create and returns available views which can be displayed by default
- by the data viewer. It is called internally by the widget. It can be
- overwriten to provide a different set of viewers.
-
- :param QWidget parent: QWidget parent of the views
- :rtype: List[silx.gui.data.DataViews.DataView]
- """
- viewClasses = [
- DataViews._EmptyView,
- DataViews._Hdf5View,
- DataViews._NXdataView,
- DataViews._Plot1dView,
- DataViews._ImageView,
- DataViews._Plot3dView,
- DataViews._RawView,
- DataViews._StackView,
- DataViews._Plot2dRecordView,
- ]
- views = []
- for viewClass in viewClasses:
- try:
- view = viewClass(parent)
- views.append(view)
- except Exception:
- _logger.warning("%s instantiation failed. View is ignored" % viewClass.__name__)
- _logger.debug("Backtrace", exc_info=True)
-
- return views
-
- def clear(self):
- """Clear the widget"""
- self.setData(None)
-
- def normalizeData(self, data):
- """Returns a normalized data if the embed a numpy or a dataset.
- Else returns the data."""
- return _normalizeData(data)
-
- def __getStackIndex(self, view):
- """Get the stack index containing the view.
-
- :param silx.gui.data.DataViews.DataView view: The view
- """
- if view not in self.__index:
- widget = view.getWidget()
- index = self.__stack.addWidget(widget)
- self.__index[view] = index
- else:
- index = self.__index[view]
- return index
-
- def __clearCurrentView(self):
- """Clear the current selected view"""
- view = self.__currentView
- if view is not None:
- view.clear()
-
- def __numpyCustomAxisChanged(self, name, value):
- view = self.__currentView
- if view is not None:
- view.setCustomAxisValue(name, value)
-
- def __updateNumpySelectionAxis(self):
- """
- Update the numpy-selector according to the needed axis names
- """
- 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:
- self.__useAxisSelection = False
- self.__axisSelection.setVisible(False)
-
- def __updateDataInView(self):
- """
- Update the views using the current data
- """
- if self.__useAxisSelection:
- self.__displayedData = self.__numpySelection.selectedData()
-
- permutation = self.__numpySelection.permutation()
- normal = tuple(range(len(permutation)))
- if permutation == normal:
- permutation = None
- slicing = self.__numpySelection.selection()
- normal = tuple([slice(None)] * len(slicing))
- if slicing == normal:
- slicing = None
- else:
- self.__displayedData = self.__data
- permutation = None
- slicing = None
-
- try:
- filename = os.path.abspath(self.__data.file.filename)
- except:
- filename = None
-
- try:
- datapath = self.__data.name
- except:
- datapath = None
-
- # FIXME: maybe use DataUrl, with added support of permutation
- self.__displayedSelection = DataSelection(filename, datapath, slicing, permutation)
-
- # TODO: would be good to avoid that, it should be synchonous
- qt.QTimer.singleShot(10, self.__setDataInView)
-
- def __setDataInView(self):
- self.__currentView.setData(self.__displayedData)
- self.__currentView.setDataSelection(self.__displayedSelection)
-
- def setDisplayedView(self, view):
- """Set the displayed view.
-
- Change the displayed view according to the view itself.
-
- :param silx.gui.data.DataViews.DataView view: The DataView to use to display the data
- """
- self.__userSelectedView = view
- self._setDisplayedView(view)
-
- def _setDisplayedView(self, view):
- """Internal set of the displayed view.
-
- Change the displayed view according to the view itself.
-
- :param silx.gui.data.DataViews.DataView view: The DataView to use to display the data
- """
- if self.__currentView is view:
- return
- self.__clearCurrentView()
- self.__currentView = view
- self.__updateNumpySelectionAxis()
- self.__updateDataInView()
- stackIndex = self.__getStackIndex(self.__currentView)
- if self.__currentView is not None:
- self.__currentView.select()
- self.__stack.setCurrentIndex(stackIndex)
- self.displayedViewChanged.emit(view)
-
- def getViewFromModeId(self, modeId):
- """Returns the first available view which have the requested modeId.
- Return None if modeId does not correspond to an existing view.
-
- :param int modeId: Requested mode id
- :rtype: silx.gui.data.DataViews.DataView
- """
- for view in self.__views:
- if view.modeId() == modeId:
- return view
- return None
-
- def setDisplayMode(self, modeId):
- """Set the displayed view using display mode.
-
- Change the displayed view according to the requested mode.
-
- :param int modeId: Display mode, one of
-
- - `DataViews.EMPTY_MODE`: display nothing
- - `DataViews.PLOT1D_MODE`: display the data as a curve
- - `DataViews.IMAGE_MODE`: display the data as an image
- - `DataViews.PLOT3D_MODE`: display the data as an isosurface
- - `DataViews.RAW_MODE`: display the data as a table
- - `DataViews.STACK_MODE`: display the data as a stack of images
- - `DataViews.HDF5_MODE`: display the data as a table of HDF5 info
- - `DataViews.NXDATA_MODE`: display the data as NXdata
- """
- try:
- view = self.getViewFromModeId(modeId)
- except KeyError:
- raise ValueError("Display mode %s is unknown" % modeId)
- self._setDisplayedView(view)
-
- def displayedView(self):
- """Returns the current displayed view.
-
- :rtype: silx.gui.data.DataViews.DataView
- """
- return self.__currentView
-
- def addView(self, view):
- """Allow to add a view to the dataview.
-
- If the current data support this view, it will be displayed.
-
- :param DataView view: A dataview
- """
- if self.__hooks is not None:
- view.setHooks(self.__hooks)
- self.__views.append(view)
- # TODO It can be skipped if the view do not support the data
- self.__updateAvailableViews()
-
- def removeView(self, view):
- """Allow to remove a view which was available from the dataview.
-
- If the view was displayed, the widget will be updated.
-
- :param DataView view: A dataview
- """
- self.__views.remove(view)
- self.__stack.removeWidget(view.getWidget())
- # invalidate the full index. It will be updated as expected
- self.__index = {}
-
- if self.__userSelectedView is view:
- self.__userSelectedView = None
-
- if view is self.__currentView:
- self.__updateView()
- else:
- # TODO It can be skipped if the view is not part of the
- # available views
- self.__updateAvailableViews()
-
- def __updateAvailableViews(self):
- """
- Update available views from the current data.
- """
- data = self.__data
- info = self._getInfo()
- # sort available views according to priority
- 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
- self.__setCurrentAvailableViews(views)
-
- def __updateView(self):
- """Display the data using the widget which fit the best"""
- data = self.__data
-
- # update available views for this data
- self.__updateAvailableViews()
- available = self.__currentAvailableViews
-
- # display the view with the most priority (the default view)
- view = self.getDefaultViewFromAvailableViews(data, available)
- self.__clearCurrentView()
- try:
- self._setDisplayedView(view)
- except Exception as e:
- # in case there is a problem to read the data, try to use a safe
- # view
- view = self.getSafeViewFromAvailableViews(data, available)
- self._setDisplayedView(view)
- raise e
-
- def getSafeViewFromAvailableViews(self, data, available):
- """Returns a view which is sure to display something without failing
- on rendering.
-
- :param object data: data which will be displayed
- :param List[view] available: List of available views, from highest
- priority to lowest.
- :rtype: DataView
- """
- hdf5View = self.getViewFromModeId(DataViews.HDF5_MODE)
- if hdf5View in available:
- return hdf5View
- return self.getViewFromModeId(DataViews.EMPTY_MODE)
-
- def getDefaultViewFromAvailableViews(self, data, available):
- """Returns the default view which will be used according to available
- views.
-
- :param object data: data which will be displayed
- :param List[view] available: List of available views, from highest
- priority to lowest.
- :rtype: DataView
- """
- if len(available) > 0:
- # returns the view with the highest priority
- if self.__userSelectedView in available:
- return self.__userSelectedView
- self.__userSelectedView = None
- view = available[0]
- else:
- # else returns the empty view
- view = self.getViewFromModeId(DataViews.EMPTY_MODE)
- return view
-
- def __setCurrentAvailableViews(self, availableViews):
- """Set the current available viewa
-
- :param List[DataView] availableViews: Current available viewa
- """
- self.__currentAvailableViews = availableViews
- self.currentAvailableViewsChanged.emit()
-
- def currentAvailableViews(self):
- """Returns the list of available views for the current data
-
- :rtype: List[DataView]
- """
- 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
-
- :rtype: List[DataView]
- """
- return self.__views
-
- def setData(self, data):
- """Set the data to view.
-
- It mostly can be a h5py.Dataset or a numpy.ndarray. Other kind of
- objects will be displayed as text rendering.
-
- :param numpy.ndarray data: The data.
- """
- self.__data = data
- self._invalidateInfo()
- self.__displayedData = None
- self.__displayedSelection = None
- self.__updateView()
- self.__updateNumpySelectionAxis()
- self.__updateDataInView()
- self.dataChanged.emit()
-
- def __numpyAxisChanged(self):
- """
- Called when axis selection of the numpy-selector changed
- """
- self.__clearCurrentView()
-
- def __numpySelectionChanged(self):
- """
- Called when data selection of the numpy-selector changed
- """
- self.__updateDataInView()
-
- def data(self):
- """Returns the data"""
- return self.__data
-
- def _invalidateInfo(self):
- """Invalidate DataInfo cache."""
- self.__info = None
-
- def _getInfo(self):
- """Returns the DataInfo of the current selected data.
-
- This value is cached.
-
- :rtype: DataInfo
- """
- if self.__info is None:
- self.__info = DataViews.DataInfo(self.__data)
- return self.__info
-
- def displayMode(self):
- """Returns the current display mode"""
- return self.__currentView.modeId()
-
- def replaceView(self, modeId, newView):
- """Replace one of the builtin data views 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. One of:
-
- - `DataViews.EMPTY_MODE`
- - `DataViews.PLOT1D_MODE`
- - `DataViews.IMAGE_MODE`
- - `DataViews.PLOT2D_MODE`
- - `DataViews.COMPLEX_IMAGE_MODE`
- - `DataViews.PLOT3D_MODE`
- - `DataViews.RAW_MODE`
- - `DataViews.STACK_MODE`
- - `DataViews.HDF5_MODE`
- - `DataViews.NXDATA_MODE`
- - `DataViews.NXDATA_INVALID_MODE`
- - `DataViews.NXDATA_SCALAR_MODE`
- - `DataViews.NXDATA_CURVE_MODE`
- - `DataViews.NXDATA_XYVSCATTER_MODE`
- - `DataViews.NXDATA_IMAGE_MODE`
- - `DataViews.NXDATA_STACK_MODE`
-
- :param DataViews.DataView newView: New data view
- :return: True if replacement was successful, else False
- """
- assert isinstance(newView, DataViews.DataView)
- isReplaced = False
- for idx, view in enumerate(self.__views):
- if view.modeId() == modeId:
- if self.__hooks is not None:
- newView.setHooks(self.__hooks)
- self.__views[idx] = newView
- isReplaced = True
- break
- elif isinstance(view, DataViews.CompositeDataView):
- isReplaced = view.replaceView(modeId, newView)
- if isReplaced:
- break
-
- if isReplaced:
- self.__updateAvailableViews()
- return isReplaced
diff --git a/silx/gui/data/DataViewerFrame.py b/silx/gui/data/DataViewerFrame.py
deleted file mode 100644
index 9bfb95b..0000000
--- a/silx/gui/data/DataViewerFrame.py
+++ /dev/null
@@ -1,217 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# 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
-# 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 contains a DataViewer with a view selector.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "12/02/2019"
-
-from silx.gui import qt
-from .DataViewer import DataViewer
-from .DataViewerSelector import DataViewerSelector
-
-
-class DataViewerFrame(qt.QWidget):
- """
- A :class:`DataViewer` with a view selector.
-
- .. image:: img/DataViewerFrame.png
-
- This widget provides the same API as :class:`DataViewer`. Therefore, for more
- documentation, take a look at the documentation of the class
- :class:`DataViewer`.
-
- .. code-block:: python
-
- import numpy
- data = numpy.random.rand(500,500)
- viewer = DataViewerFrame()
- viewer.setData(data)
- viewer.setVisible(True)
-
- """
-
- displayedViewChanged = qt.Signal(object)
- """Emitted when the displayed view changes"""
-
- dataChanged = qt.Signal()
- """Emitted when the data changes"""
-
- def __init__(self, parent=None):
- """
- Constructor
-
- :param qt.QWidget parent:
- """
- super(DataViewerFrame, self).__init__(parent)
-
- class _DataViewer(DataViewer):
- """Overwrite methods to avoid to create views while the instance
- is not created. `initializeViews` have to be called manually."""
-
- def _initializeViews(self):
- pass
-
- def initializeViews(self):
- """Avoid to create views while the instance is not created."""
- super(_DataViewer, self)._initializeViews()
-
- def _createDefaultViews(self, parent):
- """Expose the original `createDefaultViews` function"""
- return super(_DataViewer, self).createDefaultViews()
-
- def createDefaultViews(self, parent=None):
- """Allow the DataViewerFrame to override this function"""
- return self.parent().createDefaultViews(parent)
-
- self.__dataViewer = _DataViewer(self)
- # initialize views when `self.__dataViewer` is set
- self.__dataViewer.initializeViews()
- self.__dataViewer.setFrameShape(qt.QFrame.StyledPanel)
- self.__dataViewer.setFrameShadow(qt.QFrame.Sunken)
- self.__dataViewerSelector = DataViewerSelector(self, self.__dataViewer)
- self.__dataViewerSelector.setFlat(True)
-
- layout = qt.QVBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
- layout.addWidget(self.__dataViewer, 1)
- layout.addWidget(self.__dataViewerSelector)
- self.setLayout(layout)
-
- self.__dataViewer.dataChanged.connect(self.__dataChanged)
- self.__dataViewer.displayedViewChanged.connect(self.__displayedViewChanged)
-
- def __dataChanged(self):
- """Called when the data is changed"""
- self.dataChanged.emit()
-
- def __displayedViewChanged(self, view):
- """Called when the displayed view changes"""
- self.displayedViewChanged.emit(view)
-
- def setGlobalHooks(self, hooks):
- """Set a data view hooks for all the views
-
- :param DataViewHooks context: The hooks to use
- """
- self.__dataViewer.setGlobalHooks(hooks)
-
- def getReachableViews(self):
- return self.__dataViewer.getReachableViews()
-
- def availableViews(self):
- """Returns the list of registered views
-
- :rtype: List[DataView]
- """
- return self.__dataViewer.availableViews()
-
- def currentAvailableViews(self):
- """Returns the list of available views for the current data
-
- :rtype: List[DataView]
- """
- return self.__dataViewer.currentAvailableViews()
-
- def createDefaultViews(self, parent=None):
- """Create and returns available views which can be displayed by default
- by the data viewer. It is called internally by the widget. It can be
- overwriten to provide a different set of viewers.
-
- :param QWidget parent: QWidget parent of the views
- :rtype: List[silx.gui.data.DataViews.DataView]
- """
- return self.__dataViewer._createDefaultViews(parent)
-
- def addView(self, view):
- """Allow to add a view to the dataview.
-
- If the current data support this view, it will be displayed.
-
- :param DataView view: A dataview
- """
- return self.__dataViewer.addView(view)
-
- def removeView(self, view):
- """Allow to remove a view which was available from the dataview.
-
- If the view was displayed, the widget will be updated.
-
- :param DataView view: A dataview
- """
- return self.__dataViewer.removeView(view)
-
- def setData(self, data):
- """Set the data to view.
-
- It mostly can be a h5py.Dataset or a numpy.ndarray. Other kind of
- objects will be displayed as text rendering.
-
- :param numpy.ndarray data: The data.
- """
- self.__dataViewer.setData(data)
-
- def data(self):
- """Returns the data"""
- return self.__dataViewer.data()
-
- def setDisplayedView(self, view):
- self.__dataViewer.setDisplayedView(view)
-
- def displayedView(self):
- return self.__dataViewer.displayedView()
-
- def displayMode(self):
- return self.__dataViewer.displayMode()
-
- def setDisplayMode(self, modeId):
- """Set the displayed view using display mode.
-
- Change the displayed view according to the requested mode.
-
- :param int modeId: Display mode, one of
-
- - `EMPTY_MODE`: display nothing
- - `PLOT1D_MODE`: display the data as a curve
- - `PLOT2D_MODE`: display the data as an image
- - `TEXT_MODE`: display the data as a text
- - `ARRAY_MODE`: display the data as a table
- """
- return self.__dataViewer.setDisplayMode(modeId)
-
- def getViewFromModeId(self, modeId):
- """See :meth:`DataViewer.getViewFromModeId`"""
- return self.__dataViewer.getViewFromModeId(modeId)
-
- def replaceView(self, modeId, newView):
- """Replace one of the builtin data views with a custom view.
- See :meth:`DataViewer.replaceView` for more documentation.
-
- :param DataViews.DataView newView: New data view
- :return: True if replacement was successful, else False
- """
- return self.__dataViewer.replaceView(modeId, newView)
diff --git a/silx/gui/data/DataViewerSelector.py b/silx/gui/data/DataViewerSelector.py
deleted file mode 100644
index a1e9947..0000000
--- a/silx/gui/data/DataViewerSelector.py
+++ /dev/null
@@ -1,175 +0,0 @@
-# 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 module defines a widget to be able to select the available view
-of the DataViewer.
-"""
-from __future__ import division
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "12/02/2019"
-
-import weakref
-import functools
-from silx.gui import qt
-import silx.utils.weakref
-
-
-class DataViewerSelector(qt.QWidget):
- """Widget to be able to select a custom view from the DataViewer"""
-
- def __init__(self, parent=None, dataViewer=None):
- """Constructor
-
- :param QWidget parent: The parent of the widget
- :param DataViewer dataViewer: The connected `DataViewer`
- """
- super(DataViewerSelector, self).__init__(parent)
-
- self.__group = None
- self.__buttons = {}
- self.__buttonLayout = None
- self.__buttonDummy = None
- self.__dataViewer = None
-
- # Create the fixed layout
- self.setLayout(qt.QHBoxLayout())
- layout = self.layout()
- layout.setContentsMargins(0, 0, 0, 0)
- self.__buttonLayout = qt.QHBoxLayout()
- self.__buttonLayout.setContentsMargins(0, 0, 0, 0)
- layout.addLayout(self.__buttonLayout)
- layout.addStretch(1)
-
- if dataViewer is not None:
- self.setDataViewer(dataViewer)
-
- def __updateButtons(self):
- if self.__group is not None:
- self.__group.deleteLater()
-
- # Clean up
- for _, b in self.__buttons.items():
- b.deleteLater()
- if self.__buttonDummy is not None:
- self.__buttonDummy.deleteLater()
- self.__buttonDummy = None
- self.__buttons = {}
- self.__buttonDummy = None
-
- self.__group = qt.QButtonGroup(self)
- if self.__dataViewer is None:
- return
-
- iconSize = qt.QSize(16, 16)
-
- for view in self.__dataViewer.getReachableViews():
- label = view.label()
- icon = view.icon()
- button = qt.QPushButton(label)
- button.setIcon(icon)
- button.setIconSize(iconSize)
- button.setCheckable(True)
- # the weak objects are needed to be able to destroy the widget safely
- weakView = weakref.ref(view)
- weakMethod = silx.utils.weakref.WeakMethodProxy(self.__setDisplayedView)
- callback = functools.partial(weakMethod, weakView)
- button.clicked.connect(callback)
- self.__buttonLayout.addWidget(button)
- self.__group.addButton(button)
- self.__buttons[view] = button
-
- button = qt.QPushButton("Dummy")
- button.setCheckable(True)
- button.setVisible(False)
- self.__buttonLayout.addWidget(button)
- self.__group.addButton(button)
- self.__buttonDummy = button
-
- self.__updateButtonsVisibility()
- self.__displayedViewChanged(self.__dataViewer.displayedView())
-
- def setDataViewer(self, dataViewer):
- """Define the dataviewer connected to this status bar
-
- :param DataViewer dataViewer: The connected `DataViewer`
- """
- if self.__dataViewer is dataViewer:
- return
- if self.__dataViewer is not None:
- self.__dataViewer.dataChanged.disconnect(self.__updateButtonsVisibility)
- self.__dataViewer.displayedViewChanged.disconnect(self.__displayedViewChanged)
- self.__dataViewer = dataViewer
- if self.__dataViewer is not None:
- self.__dataViewer.dataChanged.connect(self.__updateButtonsVisibility)
- self.__dataViewer.displayedViewChanged.connect(self.__displayedViewChanged)
- self.__updateButtons()
-
- def setFlat(self, isFlat):
- """Set the flat state of all the buttons.
-
- :param bool isFlat: True to display the buttons flatten.
- """
- for b in self.__buttons.values():
- b.setFlat(isFlat)
- self.__buttonDummy.setFlat(isFlat)
-
- def __displayedViewChanged(self, view):
- """Called on displayed view changes"""
- selectedButton = self.__buttons.get(view, self.__buttonDummy)
- selectedButton.setChecked(True)
-
- def __setDisplayedView(self, refView, clickEvent=None):
- """Display a data using the requested view
-
- :param DataView view: Requested view
- :param clickEvent: Event sent by the clicked event
- """
- if self.__dataViewer is None:
- return
- view = refView()
- if view is None:
- return
- self.__dataViewer.setDisplayedView(view)
-
- def __checkAvailableButtons(self):
- views = set(self.__dataViewer.getReachableViews())
- if views == set(self.__buttons.keys()):
- return
- # Recreate all the buttons
- # TODO: We dont have to create everything again
- # We expect the views stay quite stable
- self.__updateButtons()
-
- def __updateButtonsVisibility(self):
- """Called on data changed"""
- if self.__dataViewer is None:
- for b in self.__buttons.values():
- b.setVisible(False)
- else:
- self.__checkAvailableButtons()
- availableViews = set(self.__dataViewer.currentAvailableViews())
- for view, button in self.__buttons.items():
- button.setVisible(view in availableViews)
diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py
deleted file mode 100644
index b18a813..0000000
--- a/silx/gui/data/DataViews.py
+++ /dev/null
@@ -1,2059 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 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 defines a views used by :class:`silx.gui.data.DataViewer`.
-"""
-
-from collections import OrderedDict
-import logging
-import numbers
-import numpy
-import os
-
-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
-from silx.gui.hdf5 import H5Node
-from silx.io.nxdata import get_attr_as_unicode
-from silx.gui.colors import Colormap
-from silx.gui.dialog.ColormapDialog import ColormapDialog
-
-__authors__ = ["V. Valls", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "19/02/2019"
-
-_logger = logging.getLogger(__name__)
-
-
-# DataViewer modes
-EMPTY_MODE = 0
-PLOT1D_MODE = 10
-RECORD_PLOT_MODE = 15
-IMAGE_MODE = 20
-PLOT2D_MODE = 21
-COMPLEX_IMAGE_MODE = 22
-PLOT3D_MODE = 30
-RAW_MODE = 40
-RAW_ARRAY_MODE = 41
-RAW_RECORD_MODE = 42
-RAW_SCALAR_MODE = 43
-RAW_HEXA_MODE = 44
-STACK_MODE = 50
-HDF5_MODE = 60
-NXDATA_MODE = 70
-NXDATA_INVALID_MODE = 71
-NXDATA_SCALAR_MODE = 72
-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):
- """Returns a normalized data.
-
- If the data embed a numpy data or a dataset it is returned.
- Else returns the input data."""
- if isinstance(data, H5Node):
- if data.is_broken:
- return None
- return data.h5py_object
- return data
-
-
-def _normalizeComplex(data):
- """Returns a normalized complex data.
-
- If the data is a numpy data with complex, returns the
- absolute value.
- Else returns the input data."""
- if hasattr(data, "dtype"):
- isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating)
- else:
- isComplex = isinstance(data, numbers.Complex)
- if isComplex:
- data = numpy.absolute(data)
- return data
-
-
-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
- self.isNumeric = False
- self.isVoid = False
- self.isComplex = False
- self.isBoolean = False
- self.isRecord = False
- self.hasNXdata = False
- self.isInvalidNXdata = False
- self.countNumericColumns = 0
- self.shape = tuple()
- self.dim = 0
- self.size = 0
-
- if data is None:
- return
-
- if silx.io.is_group(data):
- nxd = nxdata.get_default(data)
- nx_class = get_attr_as_unicode(data, "NX_class")
- if nxd is not None:
- self.hasNXdata = True
- # can we plot it?
- is_scalar = nxd.signal_is_0d or nxd.interpretation in ["scalar", "scaler"]
- if not (is_scalar or nxd.is_curve or nxd.is_x_y_value_scatter or
- nxd.is_image or nxd.is_stack):
- # invalid: cannot be plotted by any widget
- self.isInvalidNXdata = True
- elif nx_class == "NXdata":
- # group claiming to be 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:
- def_entry = data.attrs["default"]
- 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
- elif silx.io.is_dataset(data) and data.shape != tuple():
- self.isArray = True
- else:
- self.isArray = False
-
- if silx.io.is_dataset(data):
- if "interpretation" in data.attrs:
- self.interpretation = get_attr_as_unicode(data, "interpretation")
- else:
- self.interpretation = None
- elif self.hasNXdata:
- self.interpretation = nxd.interpretation
- else:
- self.interpretation = None
-
- if hasattr(data, "dtype"):
- if numpy.issubdtype(data.dtype, numpy.void):
- # That's a real opaque type, else it is a structured type
- self.isVoid = data.dtype.fields is None
- self.isNumeric = numpy.issubdtype(data.dtype, numpy.number)
- self.isRecord = data.dtype.fields is not None
- self.isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating)
- self.isBoolean = numpy.issubdtype(data.dtype, numpy.bool_)
- elif self.hasNXdata:
- self.isNumeric = numpy.issubdtype(nxd.signal.dtype,
- numpy.number)
- self.isComplex = numpy.issubdtype(nxd.signal.dtype, numpy.complexfloating)
- self.isBoolean = numpy.issubdtype(nxd.signal.dtype, numpy.bool_)
- else:
- self.isNumeric = isinstance(data, numbers.Number)
- self.isComplex = isinstance(data, numbers.Complex)
- self.isBoolean = isinstance(data, bool)
- self.isRecord = False
-
- if hasattr(data, "shape"):
- self.shape = data.shape
- elif self.hasNXdata:
- self.shape = nxd.signal.shape
- else:
- self.shape = tuple()
- if self.shape is not None:
- self.dim = len(self.shape)
-
- if hasattr(data, "shape") and data.shape is None:
- # This test is expected to avoid to fall done on the h5py issue
- # https://github.com/h5py/h5py/issues/1044
- self.size = 0
- elif hasattr(data, "size"):
- self.size = int(data.size)
- else:
- self.size = 1
-
- if hasattr(data, "dtype"):
- if data.dtype.fields is not None:
- for field in data.dtype.fields:
- if numpy.issubdtype(data.dtype[field], numpy.number):
- self.countNumericColumns += 1
-
- def normalizeData(self, data):
- """Returns a normalized data if the embed a numpy or a dataset.
- 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."""
-
- def getColormap(self, view):
- """Returns a colormap for this view."""
- return None
-
- def getColormapDialog(self, view):
- """Returns a color dialog for this view."""
- return None
-
- def viewWidgetCreated(self, view, plot):
- """Called when the widget of the view was created"""
- return
-
-class DataView(object):
- """Holder for the data view."""
-
- UNSUPPORTED = -1
- """Priority returned when the requested data can't be displayed by the
- view."""
-
- TITLE_PATTERN = "{datapath}{slicing} {permuted}"
- """Pattern used to format the title of the plot.
-
- Supported fields: `{directory}`, `{filename}`, `{datapath}`, `{slicing}`, `{permuted}`.
- """
-
- def __init__(self, parent, modeId=None, icon=None, label=None):
- """Constructor
-
- :param qt.QWidget parent: Parent of the hold widget
- """
- self.__parent = parent
- self.__widget = None
- self.__modeId = modeId
- if label is None:
- label = self.__class__.__name__
- self.__label = label
- if icon is None:
- icon = qt.QIcon()
- self.__icon = icon
- self.__hooks = None
-
- def getHooks(self):
- """Returns the data viewer hooks used by this view.
-
- :rtype: DataViewHooks
- """
- return self.__hooks
-
- def setHooks(self, hooks):
- """Set the data view hooks to use with this view.
-
- :param DataViewHooks hooks: The data view hooks to use
- """
- self.__hooks = hooks
-
- def defaultColormap(self):
- """Returns a default colormap.
-
- :rtype: Colormap
- """
- colormap = None
- if self.__hooks is not None:
- colormap = self.__hooks.getColormap(self)
- if colormap is None:
- colormap = Colormap(name="viridis")
- return colormap
-
- def defaultColorDialog(self):
- """Returns a default color dialog.
-
- :rtype: ColormapDialog
- """
- dialog = None
- if self.__hooks is not None:
- dialog = self.__hooks.getColormapDialog(self)
- if dialog is None:
- dialog = ColormapDialog()
- dialog.setModal(False)
- return dialog
-
- def icon(self):
- """Returns the default icon"""
- return self.__icon
-
- def label(self):
- """Returns the default label"""
- return self.__label
-
- def modeId(self):
- """Returns the mode id"""
- return self.__modeId
-
- def normalizeData(self, data):
- """Returns a normalized data if the embed a numpy or a dataset.
- Else returns the data."""
- return _normalizeData(data)
-
- def customAxisNames(self):
- """Returns names of axes which can be custom by the user and provided
- to the view."""
- return []
-
- def setCustomAxisValue(self, name, value):
- """
- Set the value of a custom axis
-
- :param str name: Name of the custom axis
- :param int value: Value of the custom axis
- """
- pass
-
- def isWidgetInitialized(self):
- """Returns true if the widget is already initialized.
- """
- return self.__widget is not None
-
- def select(self):
- """Called when the view is selected to display the data.
- """
- return
-
- def getWidget(self):
- """Returns the widget hold in the view and displaying the data.
-
- :returns: qt.QWidget
- """
- if self.__widget is None:
- self.__widget = self.createWidget(self.__parent)
- hooks = self.getHooks()
- if hooks is not None:
- hooks.viewWidgetCreated(self, self.__widget)
- return self.__widget
-
- def createWidget(self, parent):
- """Create the the widget displaying the data
-
- :param qt.QWidget parent: Parent of the widget
- :returns: qt.QWidget
- """
- raise NotImplementedError()
-
- def clear(self):
- """Clear the data from the view"""
- return None
-
- def setData(self, data):
- """Set the data displayed by the view
-
- :param data: Data to display
- :type data: numpy.ndarray or h5py.Dataset
- """
- return None
-
- def __formatSlices(self, indices):
- """Format an iterable of slice objects
-
- :param indices: The slices to format
- :type indices: Union[None,List[Union[slice,int]]]
- :rtype: str
- """
- if indices is None:
- return ''
-
- def formatSlice(slice_):
- start, stop, step = slice_.start, slice_.stop, slice_.step
- string = ('' if start is None else str(start)) + ':'
- if stop is not None:
- string += str(stop)
- if step not in (None, 1):
- string += ':' + step
- return string
-
- return '[' + ', '.join(
- formatSlice(index) if isinstance(index, slice) else str(index)
- for index in indices) + ']'
-
- def titleForSelection(self, selection):
- """Build title from given selection information.
-
- :param NamedTuple selection: Data selected
- :rtype: str
- """
- if selection is None or selection.filename is None:
- return None
- else:
- directory, filename = os.path.split(selection.filename)
- try:
- slicing = self.__formatSlices(selection.slice)
- except Exception:
- _logger.debug("Error while formatting slices", exc_info=True)
- slicing = '[sliced]'
-
- permuted = '(permuted)' if selection.permutation is not None else ''
-
- try:
- title = self.TITLE_PATTERN.format(
- directory=directory,
- filename=filename,
- datapath=selection.datapath,
- slicing=slicing,
- permuted=permuted)
- except Exception:
- _logger.debug("Error while formatting title", exc_info=True)
- title = selection.datapath + slicing
-
- return title
-
- def setDataSelection(self, selection):
- """Set the data selection displayed by the view
-
- If called, it have to be called directly after `setData`.
-
- :param selection: Data selected
- :type selection: NamedTuple
- """
- pass
-
- def axesNames(self, data, info):
- """Returns names of the expected axes of the view, according to the
- input data. A none value will disable the default axes selectior.
-
- :param data: Data to display
- :type data: numpy.ndarray or h5py.Dataset
- :param DataInfo info: Pre-computed information on the data
- :rtype: list[str] or None
- """
- 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.
-
- - `UNSUPPORTED` means this view can't display this data
- - `1` means this view can display the data
- - `100` means this view should be used for this data
- - `1000` max value used by the views provided by silx
- - ...
-
- :param object data: The data to check
- :param DataInfo info: Pre-computed information on the data
- :rtype: int
- """
- return DataView.UNSUPPORTED
-
- def __lt__(self, other):
- return str(self) < str(other)
-
-
-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."""
-
- def __init__(self, parent, modeId=None, icon=None, label=None):
- """Constructor
-
- :param qt.QWidget parent: Parent of the hold widget
- """
- super(SelectOneDataView, self).__init__(parent, modeId, icon, label)
- self.__views = OrderedDict()
- self.__currentView = None
-
- def setHooks(self, hooks):
- """Set the data context to use with this view.
-
- :param DataViewHooks hooks: The data view hooks to use
- """
- super(SelectOneDataView, 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[dataView] = None
-
- 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):
- """Returns the best view according to priorities."""
- 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)
-
- if len(views) == 0:
- return None
- elif views[0][0] == DataView.UNSUPPORTED:
- return None
- else:
- return views[0][1]
-
- def customAxisNames(self):
- if self.__currentView is None:
- return
- return self.__currentView.customAxisNames()
-
- def setCustomAxisValue(self, name, value):
- if self.__currentView is None:
- return
- self.__currentView.setCustomAxisValue(name, value)
-
- def __updateDisplayedView(self):
- widget = self.getWidget()
- if self.__currentView is None:
- return
-
- # load the widget if it is not yet done
- index = self.__views[self.__currentView]
- if index is None:
- w = self.__currentView.getWidget()
- index = widget.addWidget(w)
- self.__views[self.__currentView] = index
- if widget.currentIndex() != index:
- widget.setCurrentIndex(index)
- self.__currentView.select()
-
- def select(self):
- self.__updateDisplayedView()
- if self.__currentView is not None:
- self.__currentView.select()
-
- def createWidget(self, parent):
- return qt.QStackedWidget()
-
- def clear(self):
- for v in self.__views.keys():
- v.clear()
-
- def setData(self, data):
- if self.__currentView is None:
- return
- self.__updateDisplayedView()
- self.__currentView.setData(data)
-
- def setDataSelection(self, selection):
- if self.__currentView is None:
- return
- self.__currentView.setDataSelection(selection)
-
- def axesNames(self, 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)
- self.__currentView = view
- if view is None:
- return DataView.UNSUPPORTED
- else:
- return view.getCachedDataPriority(data, info)
-
- 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 view in 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 = OrderedDict(
- (newView, None) if view is oldView else (view, idx) for
- view, idx in self.__views.items())
- 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"""
-
- def __init__(self, parent):
- DataView.__init__(self, parent, modeId=EMPTY_MODE)
-
- def axesNames(self, data, info):
- return None
-
- def createWidget(self, parent):
- return qt.QLabel(parent)
-
- def getDataPriority(self, data, info):
- return DataView.UNSUPPORTED
-
-
-class _Plot1dView(DataView):
- """View displaying data using a 1d plot"""
-
- def __init__(self, parent):
- super(_Plot1dView, self).__init__(
- parent=parent,
- modeId=PLOT1D_MODE,
- label="Curve",
- icon=icons.getQIcon("view-1d"))
- self.__resetZoomNextTime = True
-
- def createWidget(self, parent):
- from silx.gui import plot
- return plot.Plot1D(parent=parent)
-
- def clear(self):
- 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)
- plotWidget = self.getWidget()
- legend = "data"
- plotWidget.addCurve(legend=legend,
- x=range(len(data)),
- y=data,
- resetzoom=self.__resetZoomNextTime)
- plotWidget.setActiveCurve(legend)
- self.__resetZoomNextTime = True
-
- def setDataSelection(self, selection):
- self.getWidget().setGraphTitle(self.titleForSelection(selection))
-
- def axesNames(self, data, info):
- return ["y"]
-
- def getDataPriority(self, data, info):
- if info.size <= 0:
- return DataView.UNSUPPORTED
- if data is None or not info.isArray or not info.isNumeric:
- return DataView.UNSUPPORTED
- if info.dim < 1:
- return DataView.UNSUPPORTED
- if info.interpretation == "spectrum":
- return 1000
- if info.dim == 2 and info.shape[0] == 1:
- return 210
- if info.dim == 1:
- return 100
- else:
- return 10
-
-
-class _Plot2dRecordView(DataView):
- def __init__(self, parent):
- super(_Plot2dRecordView, self).__init__(
- parent=parent,
- modeId=RECORD_PLOT_MODE,
- label="Curve",
- icon=icons.getQIcon("view-1d"))
- self.__resetZoomNextTime = True
- self._data = None
- self._xAxisDropDown = None
- self._yAxisDropDown = None
- self.__fields = None
-
- def createWidget(self, parent):
- from ._RecordPlot import RecordPlot
- return RecordPlot(parent=parent)
-
- def clear(self):
- self.getWidget().clear()
- self.__resetZoomNextTime = True
-
- def normalizeData(self, data):
- data = DataView.normalizeData(self, data)
- data = _normalizeComplex(data)
- return data
-
- def setData(self, data):
- self._data = self.normalizeData(data)
-
- all_fields = sorted(self._data.dtype.fields.items(), key=lambda e: e[1][1])
- numeric_fields = [f[0] for f in all_fields if numpy.issubdtype(f[1][0], numpy.number)]
- if numeric_fields == self.__fields: # Reuse previously selected fields
- fieldNameX = self.getWidget().getXAxisFieldName()
- fieldNameY = self.getWidget().getYAxisFieldName()
- else:
- self.__fields = numeric_fields
-
- self.getWidget().setSelectableXAxisFieldNames(numeric_fields)
- self.getWidget().setSelectableYAxisFieldNames(numeric_fields)
- fieldNameX = None
- fieldNameY = numeric_fields[0]
-
- # If there is a field called time, use it for the x-axis by default
- if "time" in numeric_fields:
- fieldNameX = "time"
- # Use the first field that is not "time" for the y-axis
- if fieldNameY == "time" and len(numeric_fields) >= 2:
- fieldNameY = numeric_fields[1]
-
- self._plotData(fieldNameX, fieldNameY)
-
- if not self._xAxisDropDown:
- self._xAxisDropDown = self.getWidget().getAxesSelectionToolBar().getXAxisDropDown()
- self._yAxisDropDown = self.getWidget().getAxesSelectionToolBar().getYAxisDropDown()
- self._xAxisDropDown.activated.connect(self._onAxesSelectionChaned)
- self._yAxisDropDown.activated.connect(self._onAxesSelectionChaned)
-
- def setDataSelection(self, selection):
- self.getWidget().setGraphTitle(self.titleForSelection(selection))
-
- def _onAxesSelectionChaned(self):
- fieldNameX = self._xAxisDropDown.currentData()
- self._plotData(fieldNameX, self._yAxisDropDown.currentText())
-
- def _plotData(self, fieldNameX, fieldNameY):
- self.clear()
- ydata = self._data[fieldNameY]
- if fieldNameX is None:
- xdata = numpy.arange(len(ydata))
- else:
- xdata = self._data[fieldNameX]
- self.getWidget().addCurve(legend="data",
- x=xdata,
- y=ydata,
- resetzoom=self.__resetZoomNextTime)
- self.getWidget().setXAxisFieldName(fieldNameX)
- self.getWidget().setYAxisFieldName(fieldNameY)
- self.__resetZoomNextTime = True
-
- def axesNames(self, data, info):
- return ["data"]
-
- def getDataPriority(self, data, info):
- if info.size <= 0:
- return DataView.UNSUPPORTED
- if data is None or not info.isRecord:
- return DataView.UNSUPPORTED
- if info.dim < 1:
- return DataView.UNSUPPORTED
- if info.countNumericColumns < 2:
- return DataView.UNSUPPORTED
- if info.interpretation == "spectrum":
- return 1000
- if info.dim == 2 and info.shape[0] == 1:
- return 210
- if info.dim == 1:
- return 40
- else:
- return 10
-
-
-class _Plot2dView(DataView):
- """View displaying data using a 2d plot"""
-
- def __init__(self, parent):
- super(_Plot2dView, self).__init__(
- parent=parent,
- modeId=PLOT2D_MODE,
- label="Image",
- icon=icons.getQIcon("view-2d"))
- self.__resetZoomNextTime = True
-
- def createWidget(self, parent):
- from silx.gui import plot
- widget = plot.Plot2D(parent=parent)
- widget.setDefaultColormap(self.defaultColormap())
- widget.getColormapAction().setColorDialog(self.defaultColorDialog())
- widget.getIntensityHistogramAction().setVisible(True)
- widget.setKeepDataAspectRatio(True)
- widget.getXAxis().setLabel('X')
- widget.getYAxis().setLabel('Y')
- maskToolsWidget = widget.getMaskToolsDockWidget().widget()
- maskToolsWidget.setItemMaskUpdated(True)
- return widget
-
- def clear(self):
- 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)
- self.getWidget().addImage(legend="data",
- data=data,
- resetzoom=self.__resetZoomNextTime)
- self.__resetZoomNextTime = False
-
- def setDataSelection(self, selection):
- self.getWidget().setGraphTitle(self.titleForSelection(selection))
-
- def axesNames(self, data, info):
- return ["y", "x"]
-
- def getDataPriority(self, data, info):
- if info.size <= 0:
- return DataView.UNSUPPORTED
- if (data is None or
- not info.isArray or
- not (info.isNumeric or info.isBoolean)):
- return DataView.UNSUPPORTED
- if info.dim < 2:
- return DataView.UNSUPPORTED
- if info.interpretation == "image":
- return 1000
- if info.dim == 2:
- return 200
- else:
- return 190
-
-
-class _Plot3dView(DataView):
- """View displaying data using a 3d plot"""
-
- def __init__(self, parent):
- super(_Plot3dView, self).__init__(
- parent=parent,
- modeId=PLOT3D_MODE,
- label="Cube",
- icon=icons.getQIcon("view-3d"))
- try:
- from ._VolumeWindow import VolumeWindow # noqa
- except ImportError:
- _logger.warning("3D visualization is not available")
- _logger.debug("Backtrace", exc_info=True)
- raise
- self.__resetZoomNextTime = True
-
- def createWidget(self, parent):
- from ._VolumeWindow import VolumeWindow
-
- plot = VolumeWindow(parent)
- plot.setAxesLabels(*reversed(self.axesNames(None, None)))
- return plot
-
- def clear(self):
- self.getWidget().clear()
- self.__resetZoomNextTime = True
-
- def setData(self, data):
- data = self.normalizeData(data)
- self.getWidget().setData(data)
- self.__resetZoomNextTime = False
-
- def axesNames(self, data, info):
- return ["z", "y", "x"]
-
- def getDataPriority(self, data, info):
- if info.size <= 0:
- return DataView.UNSUPPORTED
- if data is None or not info.isArray or not info.isNumeric:
- return DataView.UNSUPPORTED
- if info.dim < 3:
- return DataView.UNSUPPORTED
- if min(data.shape) < 2:
- return DataView.UNSUPPORTED
- if info.dim == 3:
- return 100
- else:
- return 10
-
-
-class _ComplexImageView(DataView):
- """View displaying data using a ComplexImageView"""
-
- def __init__(self, parent):
- super(_ComplexImageView, self).__init__(
- parent=parent,
- modeId=COMPLEX_IMAGE_MODE,
- label="Complex Image",
- icon=icons.getQIcon("view-2d"))
-
- def createWidget(self, parent):
- from silx.gui.plot.ComplexImageView import ComplexImageView
- widget = ComplexImageView(parent=parent)
- 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)
- widget.getXAxis().setLabel('X')
- widget.getYAxis().setLabel('Y')
- maskToolsWidget = widget.getPlot().getMaskToolsDockWidget().widget()
- maskToolsWidget.setItemMaskUpdated(True)
- return widget
-
- def clear(self):
- self.getWidget().setData(None)
-
- def normalizeData(self, data):
- data = DataView.normalizeData(self, data)
- return data
-
- def setData(self, data):
- data = self.normalizeData(data)
- self.getWidget().setData(data)
-
- def setDataSelection(self, selection):
- self.getWidget().getPlot().setGraphTitle(
- self.titleForSelection(selection))
-
- def axesNames(self, data, info):
- return ["y", "x"]
-
- def getDataPriority(self, data, info):
- if info.size <= 0:
- return DataView.UNSUPPORTED
- if data is None or not info.isArray or not info.isComplex:
- return DataView.UNSUPPORTED
- if info.dim < 2:
- return DataView.UNSUPPORTED
- if info.interpretation == "image":
- return 1000
- if info.dim == 2:
- return 200
- else:
- return 190
-
-
-class _ArrayView(DataView):
- """View displaying data using a 2d table"""
-
- def __init__(self, parent):
- DataView.__init__(self, parent, modeId=RAW_ARRAY_MODE)
-
- def createWidget(self, parent):
- from silx.gui.data.ArrayTableWidget import ArrayTableWidget
- widget = ArrayTableWidget(parent)
- widget.displayAxesSelector(False)
- return widget
-
- def clear(self):
- self.getWidget().setArrayData(numpy.array([[]]))
-
- def setData(self, data):
- data = self.normalizeData(data)
- self.getWidget().setArrayData(data)
-
- def axesNames(self, data, info):
- return ["col", "row"]
-
- def getDataPriority(self, data, info):
- if info.size <= 0:
- return DataView.UNSUPPORTED
- if data is None or not info.isArray or info.isRecord:
- return DataView.UNSUPPORTED
- if info.dim < 2:
- return DataView.UNSUPPORTED
- if info.interpretation in ["scalar", "scaler"]:
- return 1000
- return 500
-
-
-class _StackView(DataView):
- """View displaying data using a stack of images"""
-
- def __init__(self, parent):
- super(_StackView, self).__init__(
- parent=parent,
- modeId=STACK_MODE,
- label="Image stack",
- icon=icons.getQIcon("view-2d-stack"))
- self.__resetZoomNextTime = True
-
- def customAxisNames(self):
- return ["depth"]
-
- def setCustomAxisValue(self, name, value):
- if name == "depth":
- self.getWidget().setFrameNumber(value)
- else:
- raise Exception("Unsupported axis")
-
- def createWidget(self, parent):
- from silx.gui import plot
- widget = plot.StackView(parent=parent)
- widget.setColormap(self.defaultColormap())
- widget.getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog())
- widget.setKeepDataAspectRatio(True)
- widget.setLabels(self.axesNames(None, None))
- # hide default option panel
- widget.setOptionVisible(False)
- maskToolWidget = widget.getPlotWidget().getMaskToolsDockWidget().widget()
- maskToolWidget.setItemMaskUpdated(True)
- return widget
-
- def clear(self):
- 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)
- self.getWidget().setStack(stack=data, reset=self.__resetZoomNextTime)
- # Override the colormap, while setStack overwrite it
- self.getWidget().setColormap(self.defaultColormap())
- self.__resetZoomNextTime = False
-
- def setDataSelection(self, selection):
- title = self.titleForSelection(selection)
- self.getWidget().setTitleCallback(
- lambda idx: "%s z=%d" % (title, idx))
-
- def axesNames(self, data, info):
- return ["depth", "y", "x"]
-
- def getDataPriority(self, data, info):
- if info.size <= 0:
- return DataView.UNSUPPORTED
- if data is None or not info.isArray or not info.isNumeric:
- return DataView.UNSUPPORTED
- if info.dim < 3:
- return DataView.UNSUPPORTED
- if info.interpretation == "image":
- return 500
- return 90
-
-
-class _ScalarView(DataView):
- """View displaying data using text"""
-
- def __init__(self, parent):
- DataView.__init__(self, parent, modeId=RAW_SCALAR_MODE)
-
- def createWidget(self, parent):
- widget = qt.QTextEdit(parent)
- widget.setTextInteractionFlags(qt.Qt.TextSelectableByMouse)
- widget.setAlignment(qt.Qt.AlignLeft | qt.Qt.AlignTop)
- self.__formatter = TextFormatter(parent)
- return widget
-
- def clear(self):
- self.getWidget().setText("")
-
- def setData(self, data):
- d = self.normalizeData(data)
- if silx.io.is_dataset(d):
- d = d[()]
- dtype = None
- if data is not None:
- if hasattr(data, "dtype"):
- dtype = data.dtype
- text = self.__formatter.toString(d, dtype)
- self.getWidget().setText(text)
-
- def axesNames(self, data, info):
- return []
-
- def getDataPriority(self, data, info):
- if info.size <= 0:
- return DataView.UNSUPPORTED
- data = self.normalizeData(data)
- if info.shape is None:
- return DataView.UNSUPPORTED
- if data is None:
- return DataView.UNSUPPORTED
- if silx.io.is_group(data):
- return DataView.UNSUPPORTED
- return 2
-
-
-class _RecordView(DataView):
- """View displaying data using text"""
-
- def __init__(self, parent):
- DataView.__init__(self, parent, modeId=RAW_RECORD_MODE)
-
- def createWidget(self, parent):
- from .RecordTableView import RecordTableView
- widget = RecordTableView(parent)
- widget.setWordWrap(False)
- return widget
-
- def clear(self):
- self.getWidget().setArrayData(None)
-
- def setData(self, data):
- data = self.normalizeData(data)
- widget = self.getWidget()
- widget.setArrayData(data)
- if len(data) < 100:
- widget.resizeRowsToContents()
- widget.resizeColumnsToContents()
-
- def axesNames(self, data, info):
- return ["data"]
-
- def getDataPriority(self, data, info):
- if info.size <= 0:
- return DataView.UNSUPPORTED
- if info.isRecord:
- return 40
- if data is None or not info.isArray:
- return DataView.UNSUPPORTED
- if info.dim == 1:
- if info.interpretation in ["scalar", "scaler"]:
- return 1000
- if info.shape[0] == 1:
- return 510
- return 500
- elif info.isRecord:
- return 40
- return DataView.UNSUPPORTED
-
-
-class _HexaView(DataView):
- """View displaying data using text"""
-
- def __init__(self, parent):
- DataView.__init__(self, parent, modeId=RAW_HEXA_MODE)
-
- def createWidget(self, parent):
- from .HexaTableView import HexaTableView
- widget = HexaTableView(parent)
- return widget
-
- def clear(self):
- self.getWidget().setArrayData(None)
-
- def setData(self, data):
- data = self.normalizeData(data)
- widget = self.getWidget()
- widget.setArrayData(data)
-
- def axesNames(self, data, info):
- return []
-
- def getDataPriority(self, data, info):
- if info.size <= 0:
- return DataView.UNSUPPORTED
- if info.isVoid:
- return 2000
- return DataView.UNSUPPORTED
-
-
-class _Hdf5View(DataView):
- """View displaying data using text"""
-
- def __init__(self, parent):
- super(_Hdf5View, self).__init__(
- parent=parent,
- modeId=HDF5_MODE,
- label="HDF5",
- icon=icons.getQIcon("view-hdf5"))
-
- def createWidget(self, parent):
- from .Hdf5TableView import Hdf5TableView
- widget = Hdf5TableView(parent)
- return widget
-
- def clear(self):
- widget = self.getWidget()
- widget.setData(None)
-
- def setData(self, data):
- widget = self.getWidget()
- widget.setData(data)
-
- def axesNames(self, data, info):
- return None
-
- def getDataPriority(self, data, info):
- widget = self.getWidget()
- if widget.isSupportedData(data):
- return 1
- else:
- return DataView.UNSUPPORTED
-
-
-class _RawView(CompositeDataView):
- """View displaying data as raw data.
-
- This implementation use a 2d-array view, or a record array view, or a
- raw text output.
- """
-
- def __init__(self, parent):
- super(_RawView, self).__init__(
- parent=parent,
- modeId=RAW_MODE,
- label="Raw",
- icon=icons.getQIcon("view-raw"))
- self.addView(_HexaView(parent))
- self.addView(_ScalarView(parent))
- self.addView(_ArrayView(parent))
- self.addView(_RecordView(parent))
-
-
-class _ImageView(CompositeDataView):
- """View displaying data as 2D image
-
- It choose between Plot2D and ComplexImageView widgets
- """
-
- def __init__(self, parent):
- super(_ImageView, self).__init__(
- parent=parent,
- modeId=IMAGE_MODE,
- label="Image",
- icon=icons.getQIcon("view-2d"))
- self.addView(_ComplexImageView(parent))
- self.addView(_Plot2dView(parent))
-
-
-class _InvalidNXdataView(DataView):
- """DataView showing a simple label with an error message
- to inform that a group with @NX_class=NXdata cannot be
- interpreted by any NXDataview."""
- def __init__(self, parent):
- DataView.__init__(self, parent,
- modeId=NXDATA_INVALID_MODE)
- self._msg = ""
-
- def createWidget(self, parent):
- widget = qt.QLabel(parent)
- widget.setWordWrap(True)
- widget.setStyleSheet("QLabel { color : red; }")
- return widget
-
- def axesNames(self, data, info):
- return []
-
- def clear(self):
- self.getWidget().setText("")
-
- def setData(self, data):
- self.getWidget().setText(self._msg)
-
- def getDataPriority(self, data, info):
- data = self.normalizeData(data)
-
- if not info.isInvalidNXdata:
- return DataView.UNSUPPORTED
-
- if info.hasNXdata:
- self._msg = "NXdata seems valid, but cannot be displayed "
- self._msg += "by any existing plot widget."
- else:
- nx_class = get_attr_as_unicode(data, "NX_class")
- if nx_class == "NXdata":
- # 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 == "NXroot" or silx.io.is_file(data):
- default_entry = data[data.attrs["default"]]
- default_nxdata_name = default_entry.attrs["default"]
- self._msg = "NXroot group provides a @default attribute "
- self._msg += "pointing to a NXentry which defines its own "
- self._msg += "@default attribute, "
- if default_nxdata_name not in default_entry:
- self._msg += " but no corresponding NXdata group exists."
- elif get_attr_as_unicode(default_entry[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."
- 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
-
-
-class _NXdataBaseDataView(DataView):
- """Base class for NXdata DataView"""
-
- def __init__(self, *args, **kwargs):
- DataView.__init__(self, *args, **kwargs)
-
- def _updateColormap(self, nxdata):
- """Update used colormap according to nxdata's SILX_style"""
- cmap_norm = nxdata.plot_style.signal_scale_type
- if cmap_norm is not None:
- self.defaultColormap().setNormalization(
- 'log' if cmap_norm == 'log' else 'linear')
-
-
-class _NXdataScalarView(_NXdataBaseDataView):
- """DataView using a table view for displaying NXdata scalars:
- 0-D signal or n-D signal with *@interpretation=scalar*"""
- def __init__(self, parent):
- _NXdataBaseDataView.__init__(
- self, parent, modeId=NXDATA_SCALAR_MODE)
-
- def createWidget(self, parent):
- from silx.gui.data.ArrayTableWidget import ArrayTableWidget
- widget = ArrayTableWidget(parent)
- # widget.displayAxesSelector(False)
- return widget
-
- def axesNames(self, data, info):
- return ["col", "row"]
-
- def clear(self):
- self.getWidget().setArrayData(numpy.array([[]]),
- labels=True)
-
- def setData(self, data):
- data = self.normalizeData(data)
- # data could be a NXdata or an NXentry
- nxd = nxdata.get_default(data, validate=False)
- signal = nxd.signal
- self.getWidget().setArrayData(signal,
- labels=True)
-
- 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.signal_is_0d or nxd.interpretation in ["scalar", "scaler"]:
- return 100
- return DataView.UNSUPPORTED
-
-
-class _NXdataCurveView(_NXdataBaseDataView):
- """DataView using a Plot1D for displaying NXdata curves:
- 1-D signal or n-D signal with *@interpretation=spectrum*.
-
- It also handles basic scatter plots:
- a 1-D signal with one axis whose values are not monotonically increasing.
- """
- def __init__(self, parent):
- _NXdataBaseDataView.__init__(
- self, parent, modeId=NXDATA_CURVE_MODE)
-
- def createWidget(self, parent):
- from silx.gui.data.NXdataWidgets import ArrayCurvePlot
- widget = ArrayCurvePlot(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)
- signals_names = [nxd.signal_name] + nxd.auxiliary_signals_names
- if nxd.axes_dataset_names[-1] is not None:
- x_errors = nxd.get_axis_errors(nxd.axes_dataset_names[-1])
- else:
- x_errors = None
-
- # this fix is necessary until the next release of PyMca (5.2.3 or 5.3.0)
- # see https://github.com/vasole/pymca/issues/144 and https://github.com/vasole/pymca/pull/145
- if not hasattr(self.getWidget(), "setCurvesData") and \
- hasattr(self.getWidget(), "setCurveData"):
- _logger.warning("Using deprecated ArrayCurvePlot API, "
- "without support of auxiliary signals")
- self.getWidget().setCurveData(nxd.signal, nxd.axes[-1],
- yerror=nxd.errors, xerror=x_errors,
- ylabel=nxd.signal_name, xlabel=nxd.axes_names[-1],
- title=nxd.title or nxd.signal_name)
- return
-
- self.getWidget().setCurvesData([nxd.signal] + nxd.auxiliary_signals, nxd.axes[-1],
- yerror=nxd.errors, xerror=x_errors,
- ylabels=signals_names, xlabel=nxd.axes_names[-1],
- title=nxd.title or signals_names[0],
- xscale=nxd.plot_style.axes_scale_types[-1],
- yscale=nxd.plot_style.signal_scale_type)
-
- def getDataPriority(self, data, info):
- data = self.normalizeData(data)
- if info.hasNXdata and not info.isInvalidNXdata:
- if nxdata.get_default(data, validate=False).is_curve:
- return 100
- return DataView.UNSUPPORTED
-
-
-class _NXdataXYVScatterView(_NXdataBaseDataView):
- """DataView using a Plot1D for displaying NXdata 3D scatters as
- a scatter of coloured points (1-D signal with 2 axes)"""
- def __init__(self, parent):
- _NXdataBaseDataView.__init__(
- self, parent, modeId=NXDATA_XYVSCATTER_MODE)
-
- def createWidget(self, parent):
- from silx.gui.data.NXdataWidgets import XYVScatterPlot
- widget = XYVScatterPlot(parent)
- widget.getScatterView().setColormap(self.defaultColormap())
- widget.getScatterView().getScatterToolBar().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)
-
- x_axis, y_axis = nxd.axes[-2:]
- if x_axis is None:
- x_axis = numpy.arange(nxd.signal.size)
- if y_axis is None:
- y_axis = numpy.arange(nxd.signal.size)
-
- x_label, y_label = nxd.axes_names[-2:]
- if x_label is not None:
- x_errors = nxd.get_axis_errors(x_label)
- else:
- x_errors = None
-
- if y_label is not None:
- y_errors = nxd.get_axis_errors(y_label)
- else:
- y_errors = None
-
- self._updateColormap(nxd)
-
- self.getWidget().setScattersData(y_axis, x_axis, values=[nxd.signal] + nxd.auxiliary_signals,
- yerror=y_errors, xerror=x_errors,
- ylabel=y_label, xlabel=x_label,
- title=nxd.title,
- scatter_titles=[nxd.signal_name] + nxd.auxiliary_signals_names,
- xscale=nxd.plot_style.axes_scale_types[-2],
- yscale=nxd.plot_style.axes_scale_types[-1])
-
- def getDataPriority(self, data, info):
- data = self.normalizeData(data)
- if info.hasNXdata and not info.isInvalidNXdata:
- if nxdata.get_default(data, validate=False).is_x_y_value_scatter:
- # It have to be a little more than a NX curve priority
- return 110
-
- return DataView.UNSUPPORTED
-
-
-class _NXdataImageView(_NXdataBaseDataView):
- """DataView using a Plot2D for displaying NXdata images:
- 2-D signal or n-D signals with *@interpretation=image*."""
- def __init__(self, parent):
- _NXdataBaseDataView.__init__(
- self, parent, modeId=NXDATA_IMAGE_MODE)
-
- def createWidget(self, parent):
- from silx.gui.data.NXdataWidgets import ArrayImagePlot
- widget = ArrayImagePlot(parent)
- widget.getPlot().setDefaultColormap(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)
- isRgba = nxd.interpretation == "rgba-image"
-
- self._updateColormap(nxd)
-
- # last two axes are Y & X
- img_slicing = slice(-2, None) if not isRgba else slice(-3, -1)
- y_axis, x_axis = nxd.axes[img_slicing]
- y_label, x_label = nxd.axes_names[img_slicing]
- y_scale, x_scale = nxd.plot_style.axes_scale_types[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, isRgba=isRgba,
- xscale=x_scale, yscale=y_scale)
-
- def getDataPriority(self, data, info):
- data = self.normalizeData(data)
-
- if info.hasNXdata and not info.isInvalidNXdata:
- if nxdata.get_default(data, validate=False).is_image:
- return 100
-
- return DataView.UNSUPPORTED
-
-
-class _NXdataComplexImageView(_NXdataBaseDataView):
- """DataView using a ComplexImageView for displaying NXdata complex images:
- 2-D signal or n-D signals with *@interpretation=image*."""
- def __init__(self, parent):
- _NXdataBaseDataView.__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)
-
- self._updateColormap(nxd)
-
- # 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(_NXdataBaseDataView):
- def __init__(self, parent):
- _NXdataBaseDataView.__init__(
- self, parent, modeId=NXDATA_STACK_MODE)
-
- def createWidget(self, parent):
- from silx.gui.data.NXdataWidgets import ArrayStackPlot
- widget = ArrayStackPlot(parent)
- widget.getStackView().setColormap(self.defaultColormap())
- widget.getStackView().getPlotWidget().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._updateColormap(nxd)
-
- 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.hasNXdata and not info.isInvalidNXdata:
- if nxdata.get_default(data, validate=False).is_stack:
- return 100
-
- return DataView.UNSUPPORTED
-
-
-class _NXdataVolumeView(_NXdataBaseDataView):
- def __init__(self, parent):
- _NXdataBaseDataView.__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 = super(_NXdataVolumeView, self).normalizeData(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(_NXdataBaseDataView):
- def __init__(self, parent):
- _NXdataBaseDataView.__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().getPlotWidget().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._updateColormap(nxd)
-
- 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(_NXdataBaseDataView):
- def __init__(self, parent):
- _NXdataBaseDataView.__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._updateColormap(nxd)
-
- 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."""
- def __init__(self, parent):
- super(_NXdataView, self).__init__(
- parent=parent,
- label="NXdata",
- modeId=NXDATA_MODE,
- icon=icons.getQIcon("view-nexus"))
-
- self.addView(_InvalidNXdataView(parent))
- 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
deleted file mode 100644
index 7749326..0000000
--- a/silx/gui/data/Hdf5TableView.py
+++ /dev/null
@@ -1,646 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 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 define model and widget to display 1D slices from numpy
-array using compound data types or hdf5 databases.
-"""
-from __future__ import division
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "12/02/2019"
-
-import collections
-import functools
-import os.path
-import logging
-import h5py
-import numpy
-
-from silx.gui import qt
-import silx.io
-from .TextFormatter import TextFormatter
-import silx.gui.hdf5
-from silx.gui.widgets import HierarchicalTableView
-from ..hdf5.Hdf5Formatter import Hdf5Formatter
-from ..hdf5._utils import htmlFromDict
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _CellData(object):
- """Store a table item
- """
- def __init__(self, value=None, isHeader=False, span=None, tooltip=None):
- """
- Constructor
-
- :param str value: Label of this property
- :param bool isHeader: True if the cell is an header
- :param tuple span: Tuple of row, column span
- """
- self.__value = value
- self.__isHeader = isHeader
- self.__span = span
- self.__tooltip = tooltip
-
- def isHeader(self):
- """Returns true if the property is a sub-header title.
-
- :rtype: bool
- """
- return self.__isHeader
-
- def value(self):
- """Returns the value of the item.
- """
- return self.__value
-
- def span(self):
- """Returns the span size of the cell.
-
- :rtype: tuple
- """
- return self.__span
-
- def tooltip(self):
- """Returns the tooltip of the item.
-
- :rtype: tuple
- """
- return self.__tooltip
-
- def invalidateValue(self):
- self.__value = None
-
- def invalidateToolTip(self):
- self.__tooltip = None
-
- def data(self, role):
- return None
-
-
-class _TableData(object):
- """Modelize a table with header, row and column span.
-
- It is mostly defined as a row based table.
- """
-
- def __init__(self, columnCount):
- """Constructor.
-
- :param int columnCount: Define the number of column of the table
- """
- self.__colCount = columnCount
- self.__data = []
-
- def rowCount(self):
- """Returns the number of rows.
-
- :rtype: int
- """
- return len(self.__data)
-
- def columnCount(self):
- """Returns the number of columns.
-
- :rtype: int
- """
- return self.__colCount
-
- def clear(self):
- """Remove all the cells of the table"""
- self.__data = []
-
- def cellAt(self, row, column):
- """Returns the cell at the row column location. Else None if there is
- nothing.
-
- :rtype: _CellData
- """
- if row < 0:
- return None
- if column < 0:
- return None
- if row >= len(self.__data):
- return None
- cells = self.__data[row]
- if column >= len(cells):
- return None
- return cells[column]
-
- def addHeaderRow(self, headerLabel):
- """Append the table with header on the full row.
-
- :param str headerLabel: label of the header.
- """
- item = _CellData(value=headerLabel, isHeader=True, span=(1, self.__colCount))
- self.__data.append([item])
-
- def addHeaderValueRow(self, headerLabel, value, tooltip=None):
- """Append the table with a row using the first column as an header and
- other cells as a single cell for the value.
-
- :param str headerLabel: label of the header.
- :param object value: value to store.
- """
- header = _CellData(value=headerLabel, isHeader=True)
- value = _CellData(value=value, span=(1, self.__colCount), tooltip=tooltip)
- self.__data.append([header, value])
-
- def addRow(self, *args):
- """Append the table with a row using arguments for each cells
-
- :param list[object] args: List of cell values for the row
- """
- row = []
- for value in args:
- if not isinstance(value, _CellData):
- value = _CellData(value=value)
- row.append(value)
- self.__data.append(row)
-
-
-class _CellFilterAvailableData(_CellData):
- """Cell rendering for availability of a filter"""
-
- _states = {
- True: ("Available", qt.QColor(0x000000), None, None),
- False: ("Not available", qt.QColor(0xFFFFFF), qt.QColor(0xFF0000),
- "You have to install this filter on your system to be able to read this dataset"),
- "na": ("n.a.", qt.QColor(0x000000), None,
- "This version of h5py/hdf5 is not able to display the information"),
- }
-
- def __init__(self, filterId):
- if h5py.version.hdf5_version_tuple >= (1, 10, 2):
- # Previous versions only returns True if the filter was first used
- # to decode a dataset
- self.__availability = h5py.h5z.filter_avail(filterId)
- else:
- self.__availability = "na"
- _CellData.__init__(self)
-
- def value(self):
- state = self._states[self.__availability]
- return state[0]
-
- def tooltip(self):
- state = self._states[self.__availability]
- return state[3]
-
- def data(self, role=qt.Qt.DisplayRole):
- state = self._states[self.__availability]
- if role == qt.Qt.TextColorRole:
- return state[1]
- elif role == qt.Qt.BackgroundColorRole:
- return state[2]
- else:
- return None
-
-
-class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel):
- """This data model provides access to HDF5 node content (File, Group,
- Dataset). Main info, like name, file, attributes... are displayed
- """
-
- def __init__(self, parent=None, data=None):
- """
- Constructor
-
- :param qt.QObject parent: Parent object
- :param object data: An h5py-like object (file, group or dataset)
- """
- super(Hdf5TableModel, self).__init__(parent)
-
- self.__obj = None
- self.__data = _TableData(columnCount=5)
- self.__formatter = None
- self.__hdf5Formatter = Hdf5Formatter(self)
- formatter = TextFormatter(self)
- self.setFormatter(formatter)
- self.setObject(data)
-
- def rowCount(self, parent_idx=None):
- """Returns number of rows to be displayed in table"""
- return self.__data.rowCount()
-
- def columnCount(self, parent_idx=None):
- """Returns number of columns to be displayed in table"""
- return self.__data.columnCount()
-
- def data(self, index, role=qt.Qt.DisplayRole):
- """QAbstractTableModel method to access data values
- in the format ready to be displayed"""
- if not index.isValid():
- return None
-
- cell = self.__data.cellAt(index.row(), index.column())
- if cell is None:
- return None
-
- if role == self.SpanRole:
- return cell.span()
- elif role == self.IsHeaderRole:
- return cell.isHeader()
- elif role in (qt.Qt.DisplayRole, qt.Qt.EditRole):
- value = cell.value()
- if callable(value):
- try:
- value = value(self.__obj)
- except Exception:
- cell.invalidateValue()
- raise
- return value
- elif role == qt.Qt.ToolTipRole:
- value = cell.tooltip()
- if callable(value):
- try:
- value = value(self.__obj)
- except Exception:
- cell.invalidateToolTip()
- raise
- return value
- else:
- return cell.data(role)
- return None
-
- def isSupportedObject(self, h5pyObject):
- """
- Returns true if the provided object can be modelized using this model.
- """
- isSupported = False
- isSupported = isSupported or silx.io.is_group(h5pyObject)
- isSupported = isSupported or silx.io.is_dataset(h5pyObject)
- isSupported = isSupported or isinstance(h5pyObject, silx.gui.hdf5.H5Node)
- return isSupported
-
- def setObject(self, h5pyObject):
- """Set the h5py-like object exposed by the model
-
- :param h5pyObject: A h5py-like object. It can be a `h5py.Dataset`,
- a `h5py.File`, a `h5py.Group`. It also can be a,
- `silx.gui.hdf5.H5Node` which is needed to display some local path
- information.
- """
- if qt.qVersion() > "4.6":
- self.beginResetModel()
-
- if h5pyObject is None or self.isSupportedObject(h5pyObject):
- self.__obj = h5pyObject
- else:
- _logger.warning("Object class %s unsupported. Object ignored.", type(h5pyObject))
- self.__initProperties()
-
- if qt.qVersion() > "4.6":
- self.endResetModel()
- else:
- self.reset()
-
- def __formatHdf5Type(self, dataset):
- """Format the HDF5 type"""
- return self.__hdf5Formatter.humanReadableHdf5Type(dataset)
-
- def __attributeTooltip(self, attribute):
- attributeDict = collections.OrderedDict()
- if hasattr(attribute, "shape"):
- attributeDict["Shape"] = self.__hdf5Formatter.humanReadableShape(attribute)
- attributeDict["Data type"] = self.__hdf5Formatter.humanReadableType(attribute, full=True)
- html = htmlFromDict(attributeDict, title="HDF5 Attribute")
- return html
-
- def __formatDType(self, dataset):
- """Format the numpy dtype"""
- return self.__hdf5Formatter.humanReadableType(dataset, full=True)
-
- def __formatShape(self, dataset):
- """Format the shape"""
- if dataset.shape is None or len(dataset.shape) <= 1:
- return self.__hdf5Formatter.humanReadableShape(dataset)
- size = dataset.size
- shape = self.__hdf5Formatter.humanReadableShape(dataset)
- return u"%s = %s" % (shape, size)
-
- def __formatChunks(self, dataset):
- """Format the shape"""
- chunks = dataset.chunks
- if chunks is None:
- return ""
- shape = " \u00D7 ".join([str(i) for i in chunks])
- sizes = numpy.product(chunks)
- text = "%s = %s" % (shape, sizes)
- return text
-
- def __initProperties(self):
- """Initialize the list of available properties according to the defined
- h5py-like object."""
- self.__data.clear()
- if self.__obj is None:
- return
-
- obj = self.__obj
-
- hdf5obj = obj
- if isinstance(obj, silx.gui.hdf5.H5Node):
- hdf5obj = obj.h5py_object
-
- if silx.io.is_file(hdf5obj):
- objectType = "File"
- elif silx.io.is_group(hdf5obj):
- objectType = "Group"
- elif silx.io.is_dataset(hdf5obj):
- objectType = "Dataset"
- else:
- objectType = obj.__class__.__name__
- self.__data.addHeaderRow(headerLabel="HDF5 %s" % objectType)
-
- SEPARATOR = "::"
-
- self.__data.addHeaderRow(headerLabel="Path info")
- showPhysicalLocation = True
- if isinstance(obj, silx.gui.hdf5.H5Node):
- # helpful informations if the object come from an HDF5 tree
- self.__data.addHeaderValueRow("Basename", lambda x: x.local_basename)
- self.__data.addHeaderValueRow("Name", lambda x: x.local_name)
- local = lambda x: x.local_filename + SEPARATOR + x.local_name
- self.__data.addHeaderValueRow("Local", local)
- else:
- # it's a real H5py object
- self.__data.addHeaderValueRow("Basename", lambda x: os.path.basename(x.name))
- self.__data.addHeaderValueRow("Name", lambda x: x.name)
- if obj.file is not None:
- self.__data.addHeaderValueRow("File", lambda x: x.file.filename)
- if hasattr(obj, "path"):
- # That's a link
- if hasattr(obj, "filename"):
- # External link
- link = lambda x: x.filename + SEPARATOR + x.path
- else:
- # Soft link
- link = lambda x: x.path
- self.__data.addHeaderValueRow("Link", link)
- showPhysicalLocation = False
-
- # External data (nothing to do with external links)
- nExtSources = 0
- firstExtSource = None
- extType = None
- if silx.io.is_dataset(hdf5obj):
- if hasattr(hdf5obj, "is_virtual"):
- if hdf5obj.is_virtual:
- extSources = hdf5obj.virtual_sources()
- if extSources:
- firstExtSource = extSources[0].file_name + SEPARATOR + extSources[0].dset_name
- extType = "Virtual"
- nExtSources = len(extSources)
- if hasattr(hdf5obj, "external"):
- extSources = hdf5obj.external
- if extSources:
- firstExtSource = extSources[0][0]
- extType = "Raw"
- nExtSources = len(extSources)
-
- if showPhysicalLocation:
- def _physical_location(x):
- if isinstance(obj, silx.gui.hdf5.H5Node):
- return x.physical_filename + SEPARATOR + x.physical_name
- elif silx.io.is_file(obj):
- return x.filename + SEPARATOR + x.name
- elif obj.file is not None:
- return x.file.filename + SEPARATOR + x.name
- else:
- # Guess it is a virtual node
- return "No physical location"
-
- self.__data.addHeaderValueRow("Physical", _physical_location)
-
- if extType:
- def _first_source(x):
- # Absolute path
- if os.path.isabs(firstExtSource):
- return firstExtSource
-
- # Relative path with respect to the file directory
- if isinstance(obj, silx.gui.hdf5.H5Node):
- filename = x.physical_filename
- elif silx.io.is_file(obj):
- filename = x.filename
- elif obj.file is not None:
- filename = x.file.filename
- else:
- return firstExtSource
-
- if firstExtSource[0] == ".":
- firstExtSource.pop(0)
- return os.path.join(os.path.dirname(filename), firstExtSource)
-
- self.__data.addHeaderRow(headerLabel="External sources")
- self.__data.addHeaderValueRow("Type", extType)
- self.__data.addHeaderValueRow("Count", str(nExtSources))
- self.__data.addHeaderValueRow("First", _first_source)
-
- if hasattr(obj, "dtype"):
-
- self.__data.addHeaderRow(headerLabel="Data info")
-
- 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)
- if hasattr(obj, "shape"):
- self.__data.addHeaderValueRow("shape", self.__formatShape)
- if hasattr(obj, "chunks") and obj.chunks is not None:
- self.__data.addHeaderValueRow("chunks", self.__formatChunks)
-
- # relative to compression
- # h5py expose compression, compression_opts but are not initialized
- # for external plugins, then we use id
- # h5py also expose fletcher32 and shuffle attributes, but it is also
- # part of the filters
- if hasattr(obj, "shape") and hasattr(obj, "id"):
- if hasattr(obj.id, "get_create_plist"):
- dcpl = obj.id.get_create_plist()
- if dcpl.get_nfilters() > 0:
- self.__data.addHeaderRow(headerLabel="Compression info")
- pos = _CellData(value="Position", isHeader=True)
- hdf5id = _CellData(value="HDF5 ID", isHeader=True)
- name = _CellData(value="Name", isHeader=True)
- options = _CellData(value="Options", isHeader=True)
- availability = _CellData(value="", isHeader=True)
- self.__data.addRow(pos, hdf5id, name, options, availability)
- for index in range(dcpl.get_nfilters()):
- filterId, name, options = self.__getFilterInfo(obj, index)
- pos = _CellData(value=str(index))
- hdf5id = _CellData(value=str(filterId))
- name = _CellData(value=name)
- options = _CellData(value=options)
- availability = _CellFilterAvailableData(filterId=filterId)
- self.__data.addRow(pos, hdf5id, name, options, availability)
-
- if hasattr(obj, "attrs"):
- if len(obj.attrs) > 0:
- self.__data.addHeaderRow(headerLabel="Attributes")
- for key in sorted(obj.attrs.keys()):
- callback = lambda key, x: self.__formatter.toString(x.attrs[key])
- callbackTooltip = lambda key, x: self.__attributeTooltip(x.attrs[key])
- self.__data.addHeaderValueRow(headerLabel=key,
- value=functools.partial(callback, key),
- tooltip=functools.partial(callbackTooltip, key))
-
- def __getFilterInfo(self, dataset, filterIndex):
- """Get a tuple of readable info from dataset filters
-
- :param h5py.Dataset dataset: A h5py dataset
- :param int filterId:
- """
- try:
- dcpl = dataset.id.get_create_plist()
- info = dcpl.get_filter(filterIndex)
- filterId, _flags, cdValues, name = info
- name = self.__formatter.toString(name)
- options = " ".join([self.__formatter.toString(i) for i in cdValues])
- return (filterId, name, options)
- except Exception:
- _logger.debug("Backtrace", exc_info=True)
- return (None, None, None)
-
- def object(self):
- """Returns the internal object modelized.
-
- :rtype: An h5py-like object
- """
- return self.__obj
-
- def setFormatter(self, formatter):
- """Set the formatter object to be used to display data from the model
-
- :param TextFormatter formatter: Formatter to use
- """
- if formatter is self.__formatter:
- return
-
- self.__hdf5Formatter.setTextFormatter(formatter)
-
- if qt.qVersion() > "4.6":
- self.beginResetModel()
-
- if self.__formatter is not None:
- self.__formatter.formatChanged.disconnect(self.__formatChanged)
-
- self.__formatter = formatter
- if self.__formatter is not None:
- self.__formatter.formatChanged.connect(self.__formatChanged)
-
- if qt.qVersion() > "4.6":
- self.endResetModel()
- else:
- self.reset()
-
- def getFormatter(self):
- """Returns the text formatter used.
-
- :rtype: TextFormatter
- """
- return self.__formatter
-
- def __formatChanged(self):
- """Called when the format changed.
- """
- self.reset()
-
-
-class Hdf5TableItemDelegate(HierarchicalTableView.HierarchicalItemDelegate):
- """Item delegate the :class:`Hdf5TableView` with read-only text editor"""
-
- def createEditor(self, parent, option, index):
- """See :meth:`QStyledItemDelegate.createEditor`"""
- editor = super().createEditor(parent, option, index)
- if isinstance(editor, qt.QLineEdit):
- editor.setReadOnly(True)
- editor.deselect()
- editor.textChanged.connect(self.__textChanged, qt.Qt.QueuedConnection)
- self.installEventFilter(editor)
- return editor
-
- def __textChanged(self, text):
- sender = self.sender()
- if sender is not None:
- sender.deselect()
-
- def eventFilter(self, watched, event):
- eventType = event.type()
- if eventType == qt.QEvent.FocusIn:
- watched.selectAll()
- qt.QTimer.singleShot(0, watched.selectAll)
- elif eventType == qt.QEvent.FocusOut:
- watched.deselect()
- return super().eventFilter(watched, event)
-
-
-class Hdf5TableView(HierarchicalTableView.HierarchicalTableView):
- """A widget to display metadata about a HDF5 node using a table."""
-
- def __init__(self, parent=None):
- super(Hdf5TableView, self).__init__(parent)
- self.setModel(Hdf5TableModel(self))
- self.setItemDelegate(Hdf5TableItemDelegate(self))
- self.setSelectionMode(qt.QAbstractItemView.NoSelection)
-
- def isSupportedData(self, data):
- """
- Returns true if the provided object can be modelized using this model.
- """
- return self.model().isSupportedObject(data)
-
- def setData(self, data):
- """Set the h5py-like object exposed by the model
-
- :param data: A h5py-like object. It can be a `h5py.Dataset`,
- a `h5py.File`, a `h5py.Group`. It also can be a,
- `silx.gui.hdf5.H5Node` which is needed to display some local path
- information.
- """
- model = self.model()
-
- model.setObject(data)
- header = self.horizontalHeader()
- if qt.qVersion() < "5.0":
- setResizeMode = header.setResizeMode
- else:
- setResizeMode = header.setSectionResizeMode
- setResizeMode(0, qt.QHeaderView.Fixed)
- setResizeMode(1, qt.QHeaderView.ResizeToContents)
- setResizeMode(2, qt.QHeaderView.Stretch)
- setResizeMode(3, qt.QHeaderView.ResizeToContents)
- setResizeMode(4, qt.QHeaderView.ResizeToContents)
- header.setStretchLastSection(False)
-
- for row in range(model.rowCount()):
- for column in range(model.columnCount()):
- index = model.index(row, column)
- if (index.isValid() and index.data(
- HierarchicalTableView.HierarchicalTableModel.IsHeaderRole) is False):
- self.openPersistentEditor(index)
diff --git a/silx/gui/data/HexaTableView.py b/silx/gui/data/HexaTableView.py
deleted file mode 100644
index 1617f0a..0000000
--- a/silx/gui/data/HexaTableView.py
+++ /dev/null
@@ -1,286 +0,0 @@
-# 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 module defines model and widget to display raw data using an
-hexadecimal viewer.
-"""
-from __future__ import division
-
-import collections
-
-import numpy
-import six
-
-from silx.gui import qt
-import silx.io.utils
-from silx.gui.widgets.TableWidget import CopySelectedCellsAction
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "23/05/2018"
-
-
-class _VoidConnector(object):
- """Byte connector to a numpy.void data.
-
- It uses a cache of 32 x 1KB and a direct read access API from HDF5.
- """
-
- def __init__(self, data):
- self.__cache = collections.OrderedDict()
- self.__len = data.itemsize
- self.__data = data
-
- def __getBuffer(self, bufferId):
- if bufferId not in self.__cache:
- pos = bufferId << 10
- data = self.__data
- if hasattr(data, "tobytes"):
- data = data.tobytes()[pos:pos + 1024]
- else:
- # Old fashion
- data = data.data[pos:pos + 1024]
-
- self.__cache[bufferId] = data
- if len(self.__cache) > 32:
- self.__cache.popitem()
- else:
- data = self.__cache[bufferId]
- return data
-
- def __getitem__(self, pos):
- """Returns the value of the byte at the given position.
-
- :param uint pos: Position of the byte
- :rtype: int
- """
- bufferId = pos >> 10
- bufferPos = pos & 0b1111111111
- data = self.__getBuffer(bufferId)
- value = data[bufferPos]
- if six.PY2:
- return ord(value)
- else:
- return value
-
- def __len__(self):
- """
- Returns the number of available bytes.
-
- :rtype: uint
- """
- return self.__len
-
-
-class HexaTableModel(qt.QAbstractTableModel):
- """This data model provides access to a numpy void data.
-
- Bytes are displayed one by one as a hexadecimal viewer.
-
- The 16th first columns display bytes as hexadecimal, the last column
- displays the same data as ASCII.
-
- :param qt.QObject parent: Parent object
- :param data: A numpy array or a h5py dataset
- """
- def __init__(self, parent=None, data=None):
- qt.QAbstractTableModel.__init__(self, parent)
-
- self.__data = None
- self.__connector = None
- self.setArrayData(data)
-
- if hasattr(qt.QFontDatabase, "systemFont"):
- self.__font = qt.QFontDatabase.systemFont(qt.QFontDatabase.FixedFont)
- else:
- self.__font = qt.QFont("Monospace")
- self.__font.setStyleHint(qt.QFont.TypeWriter)
- self.__palette = qt.QPalette()
-
- def rowCount(self, parent_idx=None):
- """Returns number of rows to be displayed in table"""
- if self.__connector is None:
- return 0
- return ((len(self.__connector) - 1) >> 4) + 1
-
- def columnCount(self, parent_idx=None):
- """Returns number of columns to be displayed in table"""
- return 0x10 + 1
-
- def data(self, index, role=qt.Qt.DisplayRole):
- """QAbstractTableModel method to access data values
- in the format ready to be displayed"""
- if not index.isValid():
- return None
-
- if self.__connector is None:
- return None
-
- row = index.row()
- column = index.column()
-
- if role == qt.Qt.DisplayRole:
- if column == 0x10:
- start = (row << 4)
- text = ""
- for i in range(0x10):
- pos = start + i
- if pos >= len(self.__connector):
- break
- value = self.__connector[pos]
- if value > 0x20 and value < 0x7F:
- text += chr(value)
- else:
- text += "."
- return text
- else:
- pos = (row << 4) + column
- if pos < len(self.__connector):
- value = self.__connector[pos]
- return "%02X" % value
- else:
- return ""
- elif role == qt.Qt.FontRole:
- return self.__font
-
- elif role == qt.Qt.BackgroundColorRole:
- pos = (row << 4) + column
- if column != 0x10 and pos >= len(self.__connector):
- return self.__palette.color(qt.QPalette.Disabled, qt.QPalette.Background)
- else:
- return None
-
- return None
-
- def headerData(self, section, orientation, role=qt.Qt.DisplayRole):
- """Returns the 0-based row or column index, for display in the
- horizontal and vertical headers"""
- if section == -1:
- # PyQt4 send -1 when there is columns but no rows
- return None
-
- if role == qt.Qt.DisplayRole:
- if orientation == qt.Qt.Vertical:
- return "%02X" % (section << 4)
- if orientation == qt.Qt.Horizontal:
- if section == 0x10:
- return "ASCII"
- else:
- return "%02X" % section
- elif role == qt.Qt.FontRole:
- return self.__font
- elif role == qt.Qt.TextAlignmentRole:
- if orientation == qt.Qt.Vertical:
- return qt.Qt.AlignRight
- if orientation == qt.Qt.Horizontal:
- if section == 0x10:
- return qt.Qt.AlignLeft
- else:
- return qt.Qt.AlignCenter
- return None
-
- def flags(self, index):
- """QAbstractTableModel method to inform the view whether data
- is editable or not.
- """
- row = index.row()
- column = index.column()
- pos = (row << 4) + column
- if column != 0x10 and pos >= len(self.__connector):
- return qt.Qt.NoItemFlags
- return qt.QAbstractTableModel.flags(self, index)
-
- def setArrayData(self, data):
- """Set the data array.
-
- :param data: A numpy object or a dataset.
- """
- if qt.qVersion() > "4.6":
- self.beginResetModel()
-
- self.__connector = None
- self.__data = data
- if self.__data is not None:
- if silx.io.utils.is_dataset(self.__data):
- data = data[()]
- elif isinstance(self.__data, numpy.ndarray):
- data = data[()]
- self.__connector = _VoidConnector(data)
-
- if qt.qVersion() > "4.6":
- self.endResetModel()
- else:
- self.reset()
-
- def arrayData(self):
- """Returns the internal data.
-
- :rtype: numpy.ndarray of h5py.Dataset
- """
- return self.__data
-
-
-class HexaTableView(qt.QTableView):
- """TableView using HexaTableModel as default model.
-
- It customs the column size to provide a better layout.
- """
- def __init__(self, parent=None):
- """
- Constructor
-
- :param qt.QWidget parent: parent QWidget
- """
- qt.QTableView.__init__(self, parent)
-
- model = HexaTableModel(self)
- self.setModel(model)
- self._copyAction = CopySelectedCellsAction(self)
- self.addAction(self._copyAction)
-
- def copy(self):
- self._copyAction.trigger()
-
- def setArrayData(self, data):
- """Set the data array.
-
- :param data: A numpy object or a dataset.
- """
- self.model().setArrayData(data)
- self.__fixHeader()
-
- def __fixHeader(self):
- """Update the view according to the state of the auto-resize"""
- header = self.horizontalHeader()
- if qt.qVersion() < "5.0":
- setResizeMode = header.setResizeMode
- else:
- setResizeMode = header.setSectionResizeMode
-
- header.setDefaultSectionSize(30)
- header.setStretchLastSection(True)
- for i in range(0x10):
- setResizeMode(i, qt.QHeaderView.Fixed)
- setResizeMode(0x10, qt.QHeaderView.Stretch)
diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py
deleted file mode 100644
index be7d0e3..0000000
--- a/silx/gui/data/NXdataWidgets.py
+++ /dev/null
@@ -1,1081 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 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 defines widgets used by _NXdataView.
-"""
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__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
- and a 1D axis array.
-
- The signal array can have an arbitrary number of dimensions, the only
- limitation being that the last dimension must have the same length as
- the axis array.
-
- The widget provides sliders to select indices on the first (n - 1)
- dimensions of the signal array, and buttons to add/replace selected
- curves to the plot.
-
- This widget also handles simple 2D or 3D scatter plots (third dimension
- displayed as colour of points).
- """
- def __init__(self, parent=None):
- """
-
- :param parent: Parent QWidget
- """
- super(ArrayCurvePlot, self).__init__(parent)
-
- self.__signals = None
- self.__signals_names = None
- self.__signal_errors = None
- self.__axis = None
- self.__axis_name = None
- self.__x_axis_errors = None
- self.__values = None
-
- self._plot = Plot1D(self)
-
- self._selector = NumpyAxesSelector(self)
- self._selector.setNamedAxesSelectorVisibility(False)
- self.__selector_is_connected = False
-
- self._plot.sigActiveCurveChanged.connect(self._setYLabelFromActiveLegend)
-
- layout = qt.QVBoxLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- layout.addWidget(self._plot)
- layout.addWidget(self._selector)
-
- self.setLayout(layout)
-
- def getPlot(self):
- """Returns the plot used for the display
-
- :rtype: Plot1D
- """
- return self._plot
-
- def setCurvesData(self, ys, x=None,
- yerror=None, xerror=None,
- ylabels=None, xlabel=None, title=None,
- xscale=None, yscale=None):
- """
-
- :param List[ndarray] ys: List of arrays to be represented by the y (vertical) axis.
- It can be multiple n-D array whose last dimension must
- have the same length as x (and values must be None)
- :param ndarray x: 1-D dataset used as the curve's x values. If provided,
- its lengths must be equal to the length of the last dimension of
- ``y`` (and equal to the length of ``value``, for a scatter plot).
- :param ndarray yerror: Single array of errors for y (same shape), or None.
- There can only be one array, and it applies to the first/main y
- (no y errors for auxiliary_signals curves).
- :param ndarray xerror: 1-D dataset of errors for x, or None
- :param str ylabels: Labels for each curve's Y axis
- :param str xlabel: Label for X axis
- :param str title: Graph title
- :param str xscale: Scale of X axis in (None, 'linear', 'log')
- :param str yscale: Scale of Y axis in (None, 'linear', 'log')
- """
- self.__signals = ys
- self.__signals_names = ylabels or (["Y"] * len(ys))
- self.__signal_errors = yerror
- self.__axis = x
- self.__axis_name = xlabel
- self.__x_axis_errors = xerror
-
- if self.__selector_is_connected:
- self._selector.selectionChanged.disconnect(self._updateCurve)
- self.__selector_is_connected = False
- self._selector.setData(ys[0])
- self._selector.setAxisNames(["Y"])
-
- if len(ys[0].shape) < 2:
- self._selector.hide()
- else:
- self._selector.show()
-
- self._plot.setGraphTitle(title or "")
- if xscale is not None:
- self._plot.getXAxis().setScale(
- 'log' if xscale == 'log' else 'linear')
- if yscale is not None:
- self._plot.getYAxis().setScale(
- 'log' if yscale == 'log' else 'linear')
- self._updateCurve()
-
- if not self.__selector_is_connected:
- self._selector.selectionChanged.connect(self._updateCurve)
- self.__selector_is_connected = True
-
- def _updateCurve(self):
- selection = self._selector.selection()
- ys = [sig[selection] for sig in self.__signals]
- y0 = ys[0]
- len_y = len(y0)
- x = self.__axis
- if x is None:
- x = numpy.arange(len_y)
- elif numpy.isscalar(x) or len(x) == 1:
- # constant axis
- x = x * numpy.ones_like(y0)
- elif len(x) == 2 and len_y != 2:
- # linear calibration a + b * x
- x = x[0] + x[1] * numpy.arange(len_y)
-
- self._plot.remove(kind=("curve",))
-
- for i in range(len(self.__signals)):
- legend = self.__signals_names[i]
-
- # errors only supported for primary signal in NXdata
- y_errors = None
- if i == 0 and self.__signal_errors is not None:
- y_errors = self.__signal_errors[self._selector.selection()]
- self._plot.addCurve(x, ys[i], legend=legend,
- xerror=self.__x_axis_errors,
- yerror=y_errors)
- if i == 0:
- self._plot.setActiveCurve(legend)
-
- self._plot.resetZoom()
- self._plot.getXAxis().setLabel(self.__axis_name)
- self._plot.getYAxis().setLabel(self.__signals_names[0])
-
- def _setYLabelFromActiveLegend(self, previous_legend, new_legend):
- for ylabel in self.__signals_names:
- if new_legend is not None and new_legend == ylabel:
- self._plot.getYAxis().setLabel(ylabel)
- break
-
- def clear(self):
- old = self._selector.blockSignals(True)
- self._selector.clear()
- self._selector.blockSignals(old)
- self._plot.clear()
-
-
-class XYVScatterPlot(qt.QWidget):
- """
- Widget for plotting one or more scatters
- (with identical x, y coordinates).
- """
- def __init__(self, parent=None):
- """
-
- :param parent: Parent QWidget
- """
- super(XYVScatterPlot, self).__init__(parent)
-
- self.__y_axis = None
- """1D array"""
- self.__y_axis_name = None
- self.__values = None
- """List of 1D arrays (for multiple scatters with identical
- x, y coordinates)"""
-
- self.__x_axis = None
- self.__x_axis_name = None
- self.__x_axis_errors = None
- self.__y_axis = None
- self.__y_axis_name = None
- self.__y_axis_errors = None
-
- self._plot = ScatterView(self)
- self._plot.setColormap(Colormap(name="viridis",
- vmin=None, vmax=None,
- normalization=Colormap.LINEAR))
-
- self._slider = HorizontalSliderWithBrowser(parent=self)
- self._slider.setMinimum(0)
- self._slider.setValue(0)
- self._slider.valueChanged[int].connect(self._sliderIdxChanged)
- self._slider.setToolTip("Select auxiliary signals")
-
- layout = qt.QGridLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- layout.addWidget(self._plot, 0, 0)
- layout.addWidget(self._slider, 1, 0)
-
- self.setLayout(layout)
-
- def _sliderIdxChanged(self, value):
- self._updateScatter()
-
- def getScatterView(self):
- """Returns the :class:`ScatterView` used for the display
-
- :rtype: ScatterView
- """
- return self._plot
-
- def getPlot(self):
- """Returns the plot used for the display
-
- :rtype: PlotWidget
- """
- return self._plot.getPlotWidget()
-
- def setScattersData(self, y, x, values,
- yerror=None, xerror=None,
- ylabel=None, xlabel=None,
- title="", scatter_titles=None,
- xscale=None, yscale=None):
- """
-
- :param ndarray y: 1D array for y (vertical) coordinates.
- :param ndarray x: 1D array for x coordinates.
- :param List[ndarray] values: List of 1D arrays of values.
- This will be used to compute the color map and assign colors
- to the points. There should be as many arrays in the list as
- scatters to be represented.
- :param ndarray yerror: 1D array of errors for y (same shape), or None.
- :param ndarray xerror: 1D array of errors for x, or None
- :param str ylabel: Label for Y axis
- :param str xlabel: Label for X axis
- :param str title: Main graph title
- :param List[str] scatter_titles: Subtitles (one per scatter)
- :param str xscale: Scale of X axis in (None, 'linear', 'log')
- :param str yscale: Scale of Y axis in (None, 'linear', 'log')
- """
- self.__y_axis = y
- self.__x_axis = x
- self.__x_axis_name = xlabel or "X"
- self.__y_axis_name = ylabel or "Y"
- self.__x_axis_errors = xerror
- self.__y_axis_errors = yerror
- self.__values = values
-
- self.__graph_title = title or ""
- self.__scatter_titles = scatter_titles
-
- self._slider.valueChanged[int].disconnect(self._sliderIdxChanged)
- self._slider.setMaximum(len(values) - 1)
- if len(values) > 1:
- self._slider.show()
- else:
- self._slider.hide()
- self._slider.setValue(0)
- self._slider.valueChanged[int].connect(self._sliderIdxChanged)
-
- if xscale is not None:
- self._plot.getXAxis().setScale(
- 'log' if xscale == 'log' else 'linear')
- if yscale is not None:
- self._plot.getYAxis().setScale(
- 'log' if yscale == 'log' else 'linear')
-
- self._updateScatter()
-
- def _updateScatter(self):
- x = self.__x_axis
- y = self.__y_axis
-
- idx = self._slider.value()
-
- if self.__graph_title:
- title = self.__graph_title # main NXdata @title
- if len(self.__scatter_titles) > 1:
- # Append dataset name only when there is many datasets
- title += '\n' + self.__scatter_titles[idx]
- else:
- title = self.__scatter_titles[idx] # scatter dataset name
-
- self._plot.setGraphTitle(title)
- self._plot.setData(x, y, self.__values[idx],
- xerror=self.__x_axis_errors,
- yerror=self.__y_axis_errors)
- self._plot.resetZoom()
- self._plot.getXAxis().setLabel(self.__x_axis_name)
- self._plot.getYAxis().setLabel(self.__y_axis_name)
-
- def clear(self):
- self._plot.getPlotWidget().clear()
-
-
-class ArrayImagePlot(qt.QWidget):
- """
- Widget for plotting an image 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):
- """
-
- :param parent: Parent QWidget
- """
- super(ArrayImagePlot, 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 = Plot2D(self)
- self._plot.setDefaultColormap(Colormap(name="viridis",
- vmin=None, vmax=None,
- normalization=Colormap.LINEAR))
- self._plot.getIntensityHistogramAction().setVisible(True)
- self._plot.setKeepDataAspectRatio(True)
- maskToolWidget = self._plot.getMaskToolsDockWidget().widget()
- maskToolWidget.setItemMaskUpdated(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: Plot2D
- """
- return self._plot
-
- def setImageData(self, signals,
- x_axis=None, y_axis=None,
- signals_names=None,
- xlabel=None, ylabel=None,
- title=None, isRgba=False,
- xscale=None, yscale=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
- :param isRgba: True if data is a 3D RGBA image
- :param str xscale: Scale of X axis in (None, 'linear', 'log')
- :param str yscale: Scale of Y axis in (None, 'linear', 'log')
- """
- 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()
- if not isRgba:
- self._selector.setAxisNames(["Y", "X"])
- img_ndim = 2
- else:
- self._selector.setAxisNames(["Y", "X", "RGB(A) channel"])
- img_ndim = 3
- self._selector.setData(signals[0])
-
- if len(signals[0].shape) <= img_ndim:
- 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._axis_scales = xscale, yscale
- self._updateImage()
- self._plot.resetZoom()
-
- self._selector.selectionChanged.connect(self._updateImage)
- self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged)
-
- def _updateImage(self):
- selection = self._selector.selection()
- auxSigIdx = self._auxSigSlider.value()
-
- legend = self.__signals_names[auxSigIdx]
-
- 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.remove(kind=("scatter", "image",))
- if xcalib.is_affine() and ycalib.is_affine():
- # regular image
- xorigin, xscale = xcalib(0), xcalib.get_slope()
- yorigin, yscale = ycalib(0), ycalib.get_slope()
- origin = (xorigin, yorigin)
- scale = (xscale, yscale)
-
- self._plot.getXAxis().setScale('linear')
- self._plot.getYAxis().setScale('linear')
- self._plot.addImage(image, legend=legend,
- origin=origin, scale=scale,
- replace=True, resetzoom=False)
- else:
- xaxisscale, yaxisscale = self._axis_scales
-
- if xaxisscale is not None:
- self._plot.getXAxis().setScale(
- 'log' if xaxisscale == 'log' else 'linear')
- if yaxisscale is not None:
- self._plot.getYAxis().setScale(
- 'log' if yaxisscale == 'log' else 'linear')
-
- scatterx, scattery = numpy.meshgrid(x_axis, y_axis)
- # fixme: i don't think this can handle "irregular" RGBA images
- self._plot.addScatter(numpy.ravel(scatterx),
- numpy.ravel(scattery),
- numpy.ravel(image),
- legend=legend)
-
- if self.__title:
- title = self.__title
- if len(self.__signals_names) > 1:
- # Append dataset name only when there is many datasets
- title += '\n' + self.__signals_names[auxSigIdx]
- else:
- title = 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.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)
- maskToolWidget = self._plot.getPlot().getMaskToolsDockWidget().widget()
- maskToolWidget.setItemMaskUpdated(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))
-
- if self.__title:
- title = self.__title
- if len(self.__signals_names) > 1:
- # Append dataset name only when there is many datasets
- title += '\n' + self.__signals_names[auxSigIdx]
- else:
- title = 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.
- 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(ArrayStackPlot, 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
-
- self._stack_view = StackView(self)
- maskToolWidget = self._stack_view.getPlotWidget().getMaskToolsDockWidget().widget()
- maskToolWidget.setItemMaskUpdated(True)
-
- 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._stack_view)
- layout.addWidget(self._hline)
- layout.addWidget(self._legend)
- layout.addWidget(self._selector)
-
- self.setLayout(layout)
-
- def getStackView(self):
- """Returns the plot used for the display
-
- :rtype: StackView
- """
- return self._stack_view
-
- def setStackData(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._updateStack)
- 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._stack_view.setGraphTitle(title or "")
- # by default, the z axis is the image position (dimension not plotted)
- self._stack_view.getPlotWidget().getXAxis().setLabel(self.__x_axis_name or "X")
- self._stack_view.getPlotWidget().getYAxis().setLabel(self.__y_axis_name or "Y")
-
- self._updateStack()
-
- ndims = len(signal.shape)
- self._stack_view.setFirstStackDimension(ndims - 3)
-
- # the legend label shows the selection slice producing the volume
- # (only interesting for ndim > 3)
- if ndims > 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._updateStack)
- self.__selector_is_connected = True
-
- @staticmethod
- def _get_origin_scale(axis):
- """Assuming axis is a regularly spaced 1D array,
- return a tuple (origin, scale) where:
- - origin = axis[0]
- - scale = (axis[n-1] - axis[0]) / (n -1)
- :param axis: 1D numpy array
- :return: Tuple (axis[0], (axis[-1] - axis[0]) / (len(axis) - 1))
- """
- return axis[0], (axis[-1] - axis[0]) / (len(axis) - 1)
-
- def _updateStack(self):
- """Update displayed stack according to the current axes selector
- data."""
- stk = self._selector.selectedData()
- x_axis = self.__x_axis
- y_axis = self.__y_axis
- z_axis = self.__z_axis
-
- calibrations = []
- for axis in [z_axis, y_axis, x_axis]:
-
- if axis is None:
- calibrations.append(NoCalibration())
- elif len(axis) == 2:
- calibrations.append(
- LinearCalibration(y_intercept=axis[0],
- slope=axis[1]))
- else:
- calibrations.append(ArrayCalibration(axis))
-
- 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)
-
- self._stack_view.setStack(stk, calibrations=calibrations)
- self._stack_view.setLabels(
- labels=[self.__z_axis_name,
- self.__y_axis_name,
- 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
deleted file mode 100644
index e6da0d4..0000000
--- a/silx/gui/data/NumpyAxesSelector.py
+++ /dev/null
@@ -1,578 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""This module defines a widget able to convert a numpy array from n-dimensions
-to a numpy array with less dimensions.
-"""
-from __future__ import division
-
-__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.
-
- It allows to display and scroll in the axis, and provide a widget to
- map the axis with a named axis (the one from the view).
- """
-
- valueChanged = qt.Signal(int)
- """Emitted when the location on the axis change."""
-
- axisNameChanged = qt.Signal(object)
- """Emitted when the user change the name of the axis."""
-
- def __init__(self, parent=None):
- """Constructor
-
- :param parent: Parent of the widget
- """
- super(_Axis, self).__init__(parent)
- self.__axisNumber = None
- self.__customAxisNames = set([])
- self.__label = qt.QLabel(self)
- self.__axes = qt.QComboBox(self)
- self.__axes.currentIndexChanged[int].connect(self.__axisMappingChanged)
- self.__slider = HorizontalSliderWithBrowser(self)
- self.__slider.valueChanged[int].connect(self.__sliderValueChanged)
- layout = qt.QHBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.addWidget(self.__label)
- layout.addWidget(self.__axes)
- layout.addWidget(self.__slider, 10000)
- layout.addStretch(1)
- self.setLayout(layout)
-
- def slider(self):
- """Returns the slider used to display axes location.
-
- :rtype: HorizontalSliderWithBrowser
- """
- return self.__slider
-
- def setAxis(self, number, position, size):
- """Set axis information.
-
- :param int number: The number of the axis (from the original numpy
- array)
- :param int position: The current position in the axis (for a slicing)
- :param int size: The size of this axis (0..n)
- """
- self.__label.setText("Dimension %s" % number)
- self.__axisNumber = number
- self.__slider.setMaximum(size - 1)
-
- def axisNumber(self):
- """Returns the axis number.
-
- :rtype: int
- """
- return self.__axisNumber
-
- def setAxisName(self, axisName):
- """Set the current used axis name.
-
- If this name is not available an exception is raised. An empty string
- means that no name is selected.
-
- :param str axisName: The new name of the axis
- :raise ValueError: When the name is not available
- """
- 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:
- self.__axes.setCurrentIndex(index)
- self.__updateSliderVisibility()
- return
- raise ValueError("Axis name '%s' not found", axisName)
-
- def axisName(self):
- """Returns the selected axis name.
-
- If no name is selected, an empty string is returned.
-
- :rtype: str
- """
- index = self.__axes.currentIndex()
- if index == -1:
- return ""
- return self.__axes.itemData(index)
-
- def setAxisNames(self, axesNames):
- """Set the available list of names for the axis.
-
- :param List[str] axesNames: List of available names
- """
- self.__axes.clear()
- with blockSignals(self.__axes):
- self.__axes.addItem(" ", "")
- for axis in axesNames:
- self.__axes.addItem(axis, axis)
-
- self.__updateSliderVisibility()
-
- def setCustomAxis(self, axesNames):
- """Set the available list of named axis which can be set to a value.
-
- :param List[str] axesNames: List of customable axis names
- """
- self.__customAxisNames = set(axesNames)
- self.__updateSliderVisibility()
-
- def __axisMappingChanged(self, index):
- """Called when the selected name change.
-
- :param int index: Selected index
- """
- self.__updateSliderVisibility()
- name = self.axisName()
- self.axisNameChanged.emit(name)
-
- def __updateSliderVisibility(self):
- """Update the visibility of the slider according to axis names and
- customable axis names."""
- name = self.axisName()
- isVisible = name == "" or name in self.__customAxisNames
- self.__slider.setVisible(isVisible)
-
- def value(self):
- """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.
-
- :param int value: Position of the axis
- """
- self.valueChanged.emit(value)
-
- def setNamedAxisSelectorVisibility(self, visible):
- """Hide or show the named axis combobox.
-
- If both the selector and the slider are hidden, hide the entire widget.
-
- :param visible: boolean
- """
- self.__axes.setVisible(visible)
- name = self.axisName()
- self.setVisible(visible or name == "")
-
-
-class NumpyAxesSelector(qt.QWidget):
- """Widget to select a view from a numpy array.
-
- .. image:: img/NumpyAxesSelector.png
-
- The widget is set with an input data using :meth:`setData`, and a requested
- output dimension using :meth:`setAxisNames`.
-
- Widgets are provided to selected expected input axis, and a slice on the
- non-selected axis.
-
- The final selected array can be reached using the getter
- :meth:`selectedData`, and the event `selectionChanged`.
-
- If the input data is a HDF5 Dataset, the selected output data will be a
- new numpy array.
- """
-
- dataChanged = qt.Signal()
- """Emitted when the input data change"""
-
- selectedAxisChanged = qt.Signal()
- """Emitted when the selected axis change"""
-
- selectionChanged = qt.Signal()
- """Emitted when the selected data change"""
-
- customAxisChanged = qt.Signal(str, int)
- """Emitted when a custom axis change"""
-
- def __init__(self, parent=None):
- """Constructor
-
- :param parent: Parent of the widget
- """
- super(NumpyAxesSelector, self).__init__(parent)
-
- self.__data = None
- self.__selectedData = None
- self.__axis = []
- self.__axisNames = []
- self.__customAxisNames = set([])
- self.__namedAxesVisibility = True
- layout = qt.QVBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
- self.setLayout(layout)
-
- def clear(self):
- """Clear the widget."""
- self.setData(None)
-
- def setAxisNames(self, axesNames):
- """Set the axis names of the output selected data.
-
- Axis names are defined from slower to faster axis.
-
- The size of the list will constrain the dimension of the resulting
- array.
-
- :param List[str] axesNames: List of distinct strings identifying axis names
- """
- self.__axisNames = list(axesNames)
- assert len(set(self.__axisNames)) == len(self.__axisNames),\
- "Non-unique axes names: %s" % self.__axisNames
-
- delta = len(self.__axis) - len(self.__axisNames)
- if delta < 0:
- delta = 0
- for index, axis in enumerate(self.__axis):
- 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):
- """Set the available list of named axis which can be set to a value.
-
- :param List[str] axesNames: List of customable axis names
- """
- self.__customAxisNames = set(axesNames)
- for axis in self.__axis:
- axis.setCustomAxis(self.__customAxisNames)
-
- def setData(self, data):
- """Set the input data unsed by the widget.
-
- :param numpy.ndarray data: The input data
- """
- if self.__data is not None:
- # clean up
- for widget in self.__axis:
- self.layout().removeWidget(widget)
- widget.deleteLater()
- self.__axis = []
-
- self.__data = data
-
- if data is not None:
- # create expected axes
- dimensionNumber = len(data.shape)
- delta = dimensionNumber - len(self.__axisNames)
- for index in range(dimensionNumber):
- axis = _Axis(self)
- axis.setAxis(index, 0, data.shape[index])
- axis.setAxisNames(self.__axisNames)
- axis.setCustomAxis(self.__customAxisNames)
- if index >= delta and index - delta < len(self.__axisNames):
- axis.setAxisName(self.__axisNames[index - delta])
- # this weak method was expected to be able to delete sub widget
- callback = functools.partial(silx.utils.weakref.WeakMethodProxy(self.__axisValueChanged), axis)
- axis.valueChanged.connect(callback)
- # this weak method was expected to be able to delete sub widget
- callback = functools.partial(silx.utils.weakref.WeakMethodProxy(self.__axisNameChanged), axis)
- axis.axisNameChanged.connect(callback)
- axis.setNamedAxisSelectorVisibility(self.__namedAxesVisibility)
- self.layout().addWidget(axis)
- self.__axis.append(axis)
- self.__normalizeAxisGeometry()
-
- self.dataChanged.emit()
- self.__updateSelectedData()
-
- def __normalizeAxisGeometry(self):
- """Update axes geometry to align all axes components together."""
- if len(self.__axis) <= 0:
- return
- lineEditWidth = max([a.slider().lineEdit().minimumSize().width() for a in self.__axis])
- limitWidth = max([a.slider().limitWidget().minimumSizeHint().width() for a in self.__axis])
- for a in self.__axis:
- a.slider().lineEdit().setFixedWidth(lineEditWidth)
- a.slider().limitWidget().setFixedWidth(limitWidth)
-
- def __axisValueChanged(self, axis, value):
- name = axis.axisName()
- if name in self.__customAxisNames:
- self.customAxisChanged.emit(name, value)
- else:
- self.__updateSelectedData()
-
- def __axisNameChanged(self, axis, name):
- """Called when an axis name change.
-
- :param _Axis axis: The changed axis
- :param str name: The new name of the axis
- """
- names = [x.axisName() for x in self.__axis]
- missingName = set(self.__axisNames) - set(names) - set("")
- if len(missingName) == 0:
- missingName = None
- elif len(missingName) == 1:
- missingName = list(missingName)[0]
- else:
- raise Exception("Unexpected state")
-
- axisChanged = True
-
- if axis.axisName() == "":
- # set the removed label to another widget if it is possible
- availableWidget = None
- for widget in self.__axis:
- if widget is axis:
- continue
- if widget.axisName() == "":
- availableWidget = widget
- break
- if availableWidget is None:
- # If there is no other solution we set the name at the same place
- axisChanged = False
- availableWidget = axis
- with blockSignals(availableWidget):
- availableWidget.setAxisName(missingName)
- else:
- # there is a duplicated name somewhere
- # we swap it with the missing name or with nothing
- dupWidget = None
- for widget in self.__axis:
- if widget is axis:
- continue
- if widget.axisName() == axis.axisName():
- dupWidget = widget
- break
- if missingName is None:
- missingName = ""
- with blockSignals(dupWidget):
- dupWidget.setAxisName(missingName)
-
- if self.__data is None:
- return
- if axisChanged:
- self.selectedAxisChanged.emit()
- self.__updateSelectedData()
-
- def __updateSelectedData(self):
- """Update the selected data according to the state of the widget.
-
- It fires a `selectionChanged` event.
- """
- 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.selectionChanged.emit()
- return
-
- # 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
- self.__selectedData = numpy.transpose(self.__data[self.selection()], permutation)
- self.selectionChanged.emit()
-
- def data(self):
- """Returns the input data.
-
- :rtype: Union[numpy.ndarray,None]
- """
- if self.__data is None:
- return None
- else:
- return numpy.array(self.__data, copy=False)
-
- def selectedData(self):
- """Returns the output data.
-
- This is equivalent to::
-
- numpy.transpose(self.data()[self.selection()], self.permutation())
-
- :rtype: Union[numpy.ndarray,None]
- """
- 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
- """
- 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
- to the data dimension.
-
- :param visible: Boolean
- """
- self.__namedAxesVisibility = visible
- for axis in self.__axis:
- axis.setNamedAxisSelectorVisibility(visible)
diff --git a/silx/gui/data/RecordTableView.py b/silx/gui/data/RecordTableView.py
deleted file mode 100644
index 2c0011a..0000000
--- a/silx/gui/data/RecordTableView.py
+++ /dev/null
@@ -1,447 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 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 define model and widget to display 1D slices from numpy
-array using compound data types or hdf5 databases.
-"""
-from __future__ import division
-
-import itertools
-import numpy
-from silx.gui import qt
-import silx.io
-from .TextFormatter import TextFormatter
-from silx.gui.widgets.TableWidget import CopySelectedCellsAction
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "29/08/2018"
-
-
-class _MultiLineItem(qt.QItemDelegate):
- """Draw a multiline text without hiding anything.
-
- The paint method display a cell without any wrap. And an editor is
- available to scroll into the selected cell.
- """
-
- def __init__(self, parent=None):
- """
- Constructor
-
- :param qt.QWidget parent: Parent of the widget
- """
- qt.QItemDelegate.__init__(self, parent)
- self.__textOptions = qt.QTextOption()
- self.__textOptions.setFlags(qt.QTextOption.IncludeTrailingSpaces |
- qt.QTextOption.ShowTabsAndSpaces)
- self.__textOptions.setWrapMode(qt.QTextOption.NoWrap)
- self.__textOptions.setAlignment(qt.Qt.AlignTop | qt.Qt.AlignLeft)
-
- def paint(self, painter, option, index):
- """
- Write multiline text without using any wrap or any alignment according
- to the cell size.
-
- :param qt.QPainter painter: Painter context used to displayed the cell
- :param qt.QStyleOptionViewItem option: Control how the editor is shown
- :param qt.QIndex index: Index of the data to display
- """
- painter.save()
-
- # set colors
- painter.setPen(qt.QPen(qt.Qt.NoPen))
- if option.state & qt.QStyle.State_Selected:
- brush = option.palette.highlight()
- painter.setBrush(brush)
- else:
- brush = index.data(qt.Qt.BackgroundRole)
- if brush is None:
- # default background color for a cell
- brush = qt.Qt.white
- painter.setBrush(brush)
- painter.drawRect(option.rect)
-
- if index.isValid():
- if option.state & qt.QStyle.State_Selected:
- brush = option.palette.highlightedText()
- else:
- brush = index.data(qt.Qt.ForegroundRole)
- if brush is None:
- brush = option.palette.text()
- painter.setPen(qt.QPen(brush.color()))
- text = index.data(qt.Qt.DisplayRole)
- painter.drawText(qt.QRectF(option.rect), text, self.__textOptions)
-
- painter.restore()
-
- def createEditor(self, parent, option, index):
- """
- Returns the widget used to edit the item specified by index for editing.
-
- We use it not to edit the content but to show the content with a
- convenient scroll bar.
-
- :param qt.QWidget parent: Parent of the widget
- :param qt.QStyleOptionViewItem option: Control how the editor is shown
- :param qt.QIndex index: Index of the data to display
- """
- if not index.isValid():
- return super(_MultiLineItem, self).createEditor(parent, option, index)
-
- editor = qt.QTextEdit(parent)
- editor.setReadOnly(True)
- return editor
-
- def setEditorData(self, editor, index):
- """
- Read data from the model and feed the editor.
-
- :param qt.QWidget editor: Editor widget
- :param qt.QIndex index: Index of the data to display
- """
- text = index.model().data(index, qt.Qt.EditRole)
- editor.setText(text)
-
- def updateEditorGeometry(self, editor, option, index):
- """
- Update the geometry of the editor according to the changes of the view.
-
- :param qt.QWidget editor: Editor widget
- :param qt.QStyleOptionViewItem option: Control how the editor is shown
- :param qt.QIndex index: Index of the data to display
- """
- editor.setGeometry(option.rect)
-
-
-class RecordTableModel(qt.QAbstractTableModel):
- """This data model provides access to 1D slices from numpy array using
- compound data types or hdf5 databases.
-
- Each entries are displayed in a single row, and each columns contain a
- specific field of the compound type.
-
- It also allows to display 1D arrays of simple data types.
- array.
-
- :param qt.QObject parent: Parent object
- :param numpy.ndarray data: A numpy array or a h5py dataset
- """
-
- MAX_NUMBER_OF_ROWS = 10e6
- """Maximum number of display values of the dataset"""
-
- def __init__(self, parent=None, data=None):
- qt.QAbstractTableModel.__init__(self, parent)
-
- self.__data = None
- self.__is_array = False
- self.__fields = None
- self.__formatter = None
- self.__editFormatter = None
- self.setFormatter(TextFormatter(self))
-
- # set _data
- self.setArrayData(data)
-
- # Methods to be implemented to subclass QAbstractTableModel
- def rowCount(self, parent_idx=None):
- """Returns number of rows to be displayed in table"""
- if self.__data is None:
- return 0
- elif not self.__is_array:
- return 1
- else:
- return min(len(self.__data), self.MAX_NUMBER_OF_ROWS)
-
- def columnCount(self, parent_idx=None):
- """Returns number of columns to be displayed in table"""
- if self.__fields is None:
- return 1
- else:
- return len(self.__fields)
-
- def __clippedData(self, role=qt.Qt.DisplayRole):
- """Return data for cells representing clipped data"""
- if role == qt.Qt.DisplayRole:
- return "..."
- elif role == qt.Qt.ToolTipRole:
- return "Dataset is too large: display is clipped"
- else:
- return None
-
- def data(self, index, role=qt.Qt.DisplayRole):
- """QAbstractTableModel method to access data values
- in the format ready to be displayed"""
- if not index.isValid():
- return None
-
- if self.__data is None:
- return None
-
- # Special display of one before last data for clipped table
- if self.__isClipped() and index.row() == self.rowCount() - 2:
- return self.__clippedData(role)
-
- if self.__is_array:
- row = index.row()
- if row >= self.rowCount():
- return None
- elif self.__isClipped() and row == self.rowCount() - 1:
- # Clipped array, display last value at the end
- data = self.__data[-1]
- else:
- data = self.__data[row]
- else:
- if index.row() > 0:
- return None
- data = self.__data
-
- if self.__fields is not None:
- if index.column() >= len(self.__fields):
- return None
- key = self.__fields[index.column()][1]
- data = data[key[0]]
- if len(key) > 1:
- data = data[key[1]]
-
- # no dtype in case of 1D array of unicode objects (#2093)
- dtype = getattr(data, "dtype", None)
-
- if role == qt.Qt.DisplayRole:
- return self.__formatter.toString(data, dtype=dtype)
- elif role == qt.Qt.EditRole:
- return self.__editFormatter.toString(data, dtype=dtype)
- return None
-
- def headerData(self, section, orientation, role=qt.Qt.DisplayRole):
- """Returns the 0-based row or column index, for display in the
- horizontal and vertical headers"""
- if section == -1:
- # PyQt4 send -1 when there is columns but no rows
- return None
-
- # Handle clipping of huge tables
- if (self.__isClipped() and
- orientation == qt.Qt.Vertical and
- section == self.rowCount() - 2):
- return self.__clippedData(role)
-
- if role == qt.Qt.DisplayRole:
- if orientation == qt.Qt.Vertical:
- if not self.__is_array:
- return "Scalar"
- elif section == self.MAX_NUMBER_OF_ROWS - 1:
- return str(len(self.__data) - 1)
- else:
- return str(section)
- if orientation == qt.Qt.Horizontal:
- if self.__fields is None:
- if section == 0:
- return "Data"
- else:
- return None
- else:
- if section < len(self.__fields):
- return self.__fields[section][0]
- else:
- return None
- return None
-
- def flags(self, index):
- """QAbstractTableModel method to inform the view whether data
- is editable or not.
- """
- return qt.QAbstractTableModel.flags(self, index)
-
- def __isClipped(self) -> bool:
- """Returns whether the displayed array is clipped or not"""
- return self.__data is not None and self.__is_array and len(self.__data) > self.MAX_NUMBER_OF_ROWS
-
- def setArrayData(self, data):
- """Set the data array and the viewing perspective.
-
- You can set ``copy=False`` if you need more performances, when dealing
- with a large numpy array. In this case, a simple reference to the data
- is used to access the data, rather than a copy of the array.
-
- .. warning::
-
- Any change to the data model will affect your original data
- array, when using a reference rather than a copy..
-
- :param data: 1D numpy array, or any object that can be
- converted to a numpy array using ``numpy.array(data)`` (e.g.
- a nested sequence).
- """
- if qt.qVersion() > "4.6":
- self.beginResetModel()
-
- self.__data = data
- if isinstance(data, numpy.ndarray):
- self.__is_array = True
- elif silx.io.is_dataset(data) and data.shape != tuple():
- self.__is_array = True
- else:
- self.__is_array = False
-
- self.__fields = []
- if data is not None:
- if data.dtype.fields is not None:
- fields = sorted(data.dtype.fields.items(), key=lambda e: e[1][1])
- for name, (dtype, _index) in fields:
- if dtype.shape != tuple():
- keys = itertools.product(*[range(x) for x in dtype.shape])
- for key in keys:
- label = "%s%s" % (name, list(key))
- array_key = (name, key)
- self.__fields.append((label, array_key))
- else:
- self.__fields.append((name, (name,)))
- else:
- self.__fields = None
-
- if qt.qVersion() > "4.6":
- self.endResetModel()
- else:
- self.reset()
-
- def arrayData(self):
- """Returns the internal data.
-
- :rtype: numpy.ndarray of h5py.Dataset
- """
- return self.__data
-
- def setFormatter(self, formatter):
- """Set the formatter object to be used to display data from the model
-
- :param TextFormatter formatter: Formatter to use
- """
- if formatter is self.__formatter:
- return
-
- if qt.qVersion() > "4.6":
- self.beginResetModel()
-
- if self.__formatter is not None:
- self.__formatter.formatChanged.disconnect(self.__formatChanged)
-
- self.__formatter = formatter
- self.__editFormatter = TextFormatter(formatter)
- self.__editFormatter.setUseQuoteForText(False)
-
- if self.__formatter is not None:
- self.__formatter.formatChanged.connect(self.__formatChanged)
-
- if qt.qVersion() > "4.6":
- self.endResetModel()
- else:
- self.reset()
-
- def getFormatter(self):
- """Returns the text formatter used.
-
- :rtype: TextFormatter
- """
- return self.__formatter
-
- def __formatChanged(self):
- """Called when the format changed.
- """
- self.__editFormatter = TextFormatter(self, self.getFormatter())
- self.__editFormatter.setUseQuoteForText(False)
- self.reset()
-
-
-class _ShowEditorProxyModel(qt.QIdentityProxyModel):
- """
- Allow to custom the flag edit of the model
- """
-
- def __init__(self, parent=None):
- """
- Constructor
-
- :param qt.QObject arent: parent object
- """
- super(_ShowEditorProxyModel, self).__init__(parent)
- self.__forceEditable = False
-
- def flags(self, index):
- flag = qt.QIdentityProxyModel.flags(self, index)
- if self.__forceEditable:
- flag = flag | qt.Qt.ItemIsEditable
- return flag
-
- def forceCellEditor(self, show):
- """
- Enable the editable flag to allow to display cell editor.
- """
- if self.__forceEditable == show:
- return
- self.beginResetModel()
- self.__forceEditable = show
- self.endResetModel()
-
-
-class RecordTableView(qt.QTableView):
- """TableView using DatabaseTableModel as default model.
- """
- def __init__(self, parent=None):
- """
- Constructor
-
- :param qt.QWidget parent: parent QWidget
- """
- qt.QTableView.__init__(self, parent)
-
- model = _ShowEditorProxyModel(self)
- self._model = RecordTableModel()
- model.setSourceModel(self._model)
- self.setModel(model)
-
- self.__multilineView = _MultiLineItem(self)
- self.setEditTriggers(qt.QAbstractItemView.AllEditTriggers)
- self._copyAction = CopySelectedCellsAction(self)
- self.addAction(self._copyAction)
-
- def copy(self):
- self._copyAction.trigger()
-
- def setArrayData(self, data):
- model = self.model()
- sourceModel = model.sourceModel()
- sourceModel.setArrayData(data)
-
- if data is not None:
- if issubclass(data.dtype.type, (numpy.string_, numpy.unicode_)):
- # TODO it would be nice to also fix fields
- # but using it only for string array is already very useful
- self.setItemDelegateForColumn(0, self.__multilineView)
- model.forceCellEditor(True)
- else:
- self.setItemDelegateForColumn(0, None)
- model.forceCellEditor(False)
diff --git a/silx/gui/data/TextFormatter.py b/silx/gui/data/TextFormatter.py
deleted file mode 100644
index 8fd7c7c..0000000
--- a/silx/gui/data/TextFormatter.py
+++ /dev/null
@@ -1,395 +0,0 @@
-# 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 package provides a class sharred by widget from the
-data module to format data as text in the same way."""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "24/07/2018"
-
-import logging
-import numbers
-
-import numpy
-import six
-
-from silx.gui import qt
-
-import h5py
-
-
-_logger = logging.getLogger(__name__)
-
-
-class TextFormatter(qt.QObject):
- """Formatter to convert data to string.
-
- The method :meth:`toString` returns a formatted string from an input data
- using parameters set to this object.
-
- It support most python and numpy data, expecting dictionary. Unsupported
- data are displayed using the string representation of the object (`str`).
-
- It provides a set of parameters to custom the formatting of integer and
- float values (:meth:`setIntegerFormat`, :meth:`setFloatFormat`).
-
- It also allows to custom the use of quotes to display text data
- (:meth:`setUseQuoteForText`), and custom unit used to display imaginary
- numbers (:meth:`setImaginaryUnit`).
-
- The object emit an event `formatChanged` every time a parametter is
- changed.
- """
-
- formatChanged = qt.Signal()
- """Emitted when properties of the formatter change."""
-
- def __init__(self, parent=None, formatter=None):
- """
- Constructor
-
- :param qt.QObject parent: Owner of the object
- :param TextFormatter formatter: Instantiate this object from the
- formatter
- """
- qt.QObject.__init__(self, parent)
- if formatter is not None:
- self.__integerFormat = formatter.integerFormat()
- self.__floatFormat = formatter.floatFormat()
- self.__useQuoteForText = formatter.useQuoteForText()
- self.__imaginaryUnit = formatter.imaginaryUnit()
- self.__enumFormat = formatter.enumFormat()
- else:
- self.__integerFormat = "%d"
- self.__floatFormat = "%g"
- self.__useQuoteForText = True
- self.__imaginaryUnit = u"j"
- self.__enumFormat = u"%(name)s(%(value)d)"
-
- def integerFormat(self):
- """Returns the format string controlling how the integer data
- are formated by this object.
-
- This is the C-style format string used by python when formatting
- strings with the modulus operator.
-
- :rtype: str
- """
- return self.__integerFormat
-
- def setIntegerFormat(self, value):
- """Set format string controlling how the integer data are
- formated by this object.
-
- :param str value: Format string (e.g. "%d", "%i", "%08i").
- This is the C-style format string used by python when formatting
- strings with the modulus operator.
- """
- if self.__integerFormat == value:
- return
- self.__integerFormat = value
- self.formatChanged.emit()
-
- def floatFormat(self):
- """Returns the format string controlling how the floating-point data
- are formated by this object.
-
- This is the C-style format string used by python when formatting
- strings with the modulus operator.
-
- :rtype: str
- """
- return self.__floatFormat
-
- def setFloatFormat(self, value):
- """Set format string controlling how the floating-point data are
- formated by this object.
-
- :param str value: Format string (e.g. "%.3f", "%d", "%-10.2f",
- "%10.3e").
- This is the C-style format string used by python when formatting
- strings with the modulus operator.
- """
- if self.__floatFormat == value:
- return
- self.__floatFormat = value
- self.formatChanged.emit()
-
- def useQuoteForText(self):
- """Returns true if the string data are formatted using double quotes.
-
- Else, no quotes are used.
- """
- return self.__integerFormat
-
- def setUseQuoteForText(self, useQuote):
- """Set the use of quotes to delimit string data.
-
- :param bool useQuote: True to use quotes.
- """
- if self.__useQuoteForText == useQuote:
- return
- self.__useQuoteForText = useQuote
- self.formatChanged.emit()
-
- def imaginaryUnit(self):
- """Returns the unit display for imaginary numbers.
-
- :rtype: str
- """
- return self.__imaginaryUnit
-
- def setImaginaryUnit(self, imaginaryUnit):
- """Set the unit display for imaginary numbers.
-
- :param str imaginaryUnit: Unit displayed after imaginary numbers
- """
- if self.__imaginaryUnit == imaginaryUnit:
- return
- self.__imaginaryUnit = imaginaryUnit
- self.formatChanged.emit()
-
- def setEnumFormat(self, value):
- """Set format string controlling how the enum data are
- formated by this object.
-
- :param str value: Format string (e.g. "%(name)s(%(value)d)").
- This is the C-style format string used by python when formatting
- strings with the modulus operator.
- """
- if self.__enumFormat == value:
- return
- self.__enumFormat = value
- self.formatChanged.emit()
-
- def enumFormat(self):
- """Returns the format string controlling how the enum data
- are formated by this object.
-
- This is the C-style format string used by python when formatting
- strings with the modulus operator.
-
- :rtype: str
- """
- return self.__enumFormat
-
- def __formatText(self, text):
- if self.__useQuoteForText:
- text = "\"%s\"" % text.replace("\\", "\\\\").replace("\"", "\\\"")
- return text
-
- def __formatBinary(self, data):
- if isinstance(data, numpy.void):
- if six.PY2:
- data = [ord(d) for d in data.data]
- else:
- data = data.item()
- if isinstance(data, numpy.ndarray):
- # Before numpy 1.15.0 the item API was returning a numpy array
- data = data.astype(numpy.uint8)
- else:
- # Now it is supposed to be a bytes type
- pass
- elif six.PY2:
- data = [ord(d) for d in data]
- # In python3 data is already a bytes array
- data = ["\\x%02X" % d for d in data]
- if self.__useQuoteForText:
- return "b\"%s\"" % "".join(data)
- else:
- return "".join(data)
-
- def __formatSafeAscii(self, data):
- if six.PY2:
- data = [ord(d) for d in data]
- data = [chr(d) if (d > 0x20 and d < 0x7F) else "\\x%02X" % d for d in data]
- if self.__useQuoteForText:
- data = [c if c != '"' else "\\" + c for c in data]
- return "b\"%s\"" % "".join(data)
- else:
- return "".join(data)
-
- def __formatCharString(self, data):
- """Format text of char.
-
- From the specifications we expect to have ASCII, but we also allow
- CP1252 in some ceases as fallback.
-
- If no encoding fits, it will display a readable ASCII chars, with
- escaped chars (using the python syntax) for non decoded characters.
-
- :param data: A binary string of char expected in ASCII
- :rtype: str
- """
- try:
- text = "%s" % data.decode("ascii")
- return self.__formatText(text)
- except UnicodeDecodeError:
- # Here we can spam errors, this is definitly a badly
- # generated file
- _logger.error("Invalid ASCII string %s.", data)
- if data == b"\xB0":
- _logger.error("Fallback using cp1252 encoding")
- return self.__formatText(u"\u00B0")
- return self.__formatSafeAscii(data)
-
- def __formatH5pyObject(self, data, dtype):
- # That's an HDF5 object
- ref = h5py.check_dtype(ref=dtype)
- if ref is not None:
- if bool(data):
- return "REF"
- else:
- return "NULL_REF"
- vlen = h5py.check_dtype(vlen=dtype)
- if vlen is not None:
- if vlen == six.text_type:
- # HDF5 UTF8
- # With h5py>=3 reading dataset returns bytes
- if isinstance(data, (bytes, numpy.bytes_)):
- try:
- data = data.decode("utf-8")
- except UnicodeDecodeError:
- self.__formatSafeAscii(data)
- return self.__formatText(data)
- elif vlen == six.binary_type:
- # HDF5 ASCII
- return self.__formatCharString(data)
- elif isinstance(vlen, numpy.dtype):
- return self.toString(data, vlen)
- return None
-
- def toString(self, data, dtype=None):
- """Format a data into a string using formatter options
-
- :param object data: Data to render
- :param dtype: enforce a dtype (mostly used to remember the h5py dtype,
- special h5py dtypes are not propagated from array to items)
- :rtype: str
- """
- if isinstance(data, tuple):
- text = [self.toString(d) for d in data]
- return "(" + " ".join(text) + ")"
- elif isinstance(data, list):
- text = [self.toString(d) for d in data]
- return "[" + " ".join(text) + "]"
- elif isinstance(data, numpy.ndarray):
- if dtype is None:
- dtype = data.dtype
- if data.shape == ():
- # it is a scaler
- return self.toString(data[()], dtype)
- else:
- text = [self.toString(d, dtype) for d in data]
- return "[" + " ".join(text) + "]"
- if dtype is not None and dtype.kind == 'O':
- text = self.__formatH5pyObject(data, dtype)
- if text is not None:
- return text
- elif isinstance(data, numpy.void):
- if dtype is None:
- dtype = data.dtype
- if dtype.fields is not None:
- text = []
- for index, field in enumerate(dtype.fields.items()):
- text.append(field[0] + ":" + self.toString(data[index], field[1][0]))
- return "(" + " ".join(text) + ")"
- return self.__formatBinary(data)
- elif isinstance(data, (numpy.unicode_, six.text_type)):
- return self.__formatText(data)
- elif isinstance(data, (numpy.string_, six.binary_type)):
- if dtype is None and hasattr(data, "dtype"):
- dtype = data.dtype
- if dtype is not None:
- # Maybe a sub item from HDF5
- if dtype.kind == 'S':
- return self.__formatCharString(data)
- elif dtype.kind == 'O':
- text = self.__formatH5pyObject(data, dtype)
- if text is not None:
- return text
- try:
- # Try ascii/utf-8
- text = "%s" % data.decode("utf-8")
- return self.__formatText(text)
- except UnicodeDecodeError:
- pass
- return self.__formatBinary(data)
- elif isinstance(data, six.string_types):
- text = "%s" % data
- return self.__formatText(text)
- elif isinstance(data, (numpy.integer)):
- if dtype is None:
- dtype = data.dtype
- 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
- elif isinstance(data, (numbers.Real, numpy.floating)):
- # It have to be done before complex checking
- return self.__floatFormat % data
- elif isinstance(data, (numpy.complexfloating, numbers.Complex)):
- text = ""
- if data.real != 0:
- text += self.__floatFormat % data.real
- if data.real != 0 and data.imag != 0:
- if data.imag < 0:
- template = self.__floatFormat + " - " + self.__floatFormat + self.__imaginaryUnit
- params = (data.real, -data.imag)
- else:
- template = self.__floatFormat + " + " + self.__floatFormat + self.__imaginaryUnit
- params = (data.real, data.imag)
- else:
- if data.imag != 0:
- template = self.__floatFormat + self.__imaginaryUnit
- params = (data.imag)
- else:
- template = self.__floatFormat
- params = (data.real)
- return template % params
- elif isinstance(data, h5py.h5r.Reference):
- dtype = h5py.special_dtype(ref=h5py.Reference)
- text = self.__formatH5pyObject(data, dtype)
- return text
- 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
- 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/_RecordPlot.py b/silx/gui/data/_RecordPlot.py
deleted file mode 100644
index 5be792f..0000000
--- a/silx/gui/data/_RecordPlot.py
+++ /dev/null
@@ -1,92 +0,0 @@
-from silx.gui.plot.PlotWindow import PlotWindow
-from silx.gui.plot.PlotWidget import PlotWidget
-from .. import qt
-
-
-class RecordPlot(PlotWindow):
- def __init__(self, parent=None, backend=None):
- super(RecordPlot, self).__init__(parent=parent, backend=backend,
- resetzoom=True, autoScale=True,
- logScale=True, grid=True,
- curveStyle=True, colormap=False,
- aspectRatio=False, yInverted=False,
- copy=True, save=True, print_=True,
- control=True, position=True,
- roi=True, mask=False, fit=True)
- if parent is None:
- self.setWindowTitle('RecordPlot')
- self._axesSelectionToolBar = AxesSelectionToolBar(parent=self, plot=self)
- self.addToolBar(qt.Qt.BottomToolBarArea, self._axesSelectionToolBar)
-
- def setXAxisFieldName(self, value):
- """Set the current selected field for the X axis.
-
- :param Union[str,None] value:
- """
- label = '' if value is None else value
- index = self._axesSelectionToolBar.getXAxisDropDown().findData(value)
-
- if index >= 0:
- self.getXAxis().setLabel(label)
- self._axesSelectionToolBar.getXAxisDropDown().setCurrentIndex(index)
-
- def getXAxisFieldName(self):
- """Returns currently selected field for the X axis or None.
-
- rtype: Union[str,None]
- """
- return self._axesSelectionToolBar.getXAxisDropDown().currentData()
-
- def setYAxisFieldName(self, value):
- self.getYAxis().setLabel(value)
- index = self._axesSelectionToolBar.getYAxisDropDown().findText(value)
- if index >= 0:
- self._axesSelectionToolBar.getYAxisDropDown().setCurrentIndex(index)
-
- def getYAxisFieldName(self):
- return self._axesSelectionToolBar.getYAxisDropDown().currentText()
-
- def setSelectableXAxisFieldNames(self, fieldNames):
- """Add list of field names to X axis
-
- :param List[str] fieldNames:
- """
- comboBox = self._axesSelectionToolBar.getXAxisDropDown()
- comboBox.clear()
- comboBox.addItem('-', None)
- comboBox.insertSeparator(1)
- for name in fieldNames:
- comboBox.addItem(name, name)
-
- def setSelectableYAxisFieldNames(self, fieldNames):
- self._axesSelectionToolBar.getYAxisDropDown().clear()
- self._axesSelectionToolBar.getYAxisDropDown().addItems(fieldNames)
-
- def getAxesSelectionToolBar(self):
- return self._axesSelectionToolBar
-
-class AxesSelectionToolBar(qt.QToolBar):
- def __init__(self, parent=None, plot=None, title='Plot Axes Selection'):
- super(AxesSelectionToolBar, self).__init__(title, parent)
-
- assert isinstance(plot, PlotWidget)
-
- self.addWidget(qt.QLabel("Field selection: "))
-
- self._labelXAxis = qt.QLabel(" X: ")
- self.addWidget(self._labelXAxis)
-
- self._selectXAxisDropDown = qt.QComboBox()
- self.addWidget(self._selectXAxisDropDown)
-
- self._labelYAxis = qt.QLabel(" Y: ")
- self.addWidget(self._labelYAxis)
-
- self._selectYAxisDropDown = qt.QComboBox()
- self.addWidget(self._selectYAxisDropDown)
-
- def getXAxisDropDown(self):
- return self._selectXAxisDropDown
-
- def getYAxisDropDown(self):
- return self._selectYAxisDropDown \ No newline at end of file
diff --git a/silx/gui/data/_VolumeWindow.py b/silx/gui/data/_VolumeWindow.py
deleted file mode 100644
index 03b6876..0000000
--- a/silx/gui/data/_VolumeWindow.py
+++ /dev/null
@@ -1,148 +0,0 @@
-# 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/__init__.py b/silx/gui/data/__init__.py
deleted file mode 100644
index 560062d..0000000
--- a/silx/gui/data/__init__.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# 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 package provides a set of Qt widgets for displaying data arrays using
-table views and plot widgets.
-
-.. note::
-
- Widgets in this package may rely on additional dependencies that are
- not mandatory for *silx*.
- :class:`DataViewer.DataViewer` relies on :mod:`silx.gui.plot` which
- depends on *matplotlib*. It also optionally depends on *PyOpenGL* for 3D
- visualization.
-"""
diff --git a/silx/gui/data/setup.py b/silx/gui/data/setup.py
deleted file mode 100644
index 23ccbdd..0000000
--- a/silx/gui/data/setup.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "16/01/2017"
-
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('data', parent_package, top_path)
- config.add_subpackage('test')
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
- setup(configuration=configuration)
diff --git a/silx/gui/data/test/__init__.py b/silx/gui/data/test/__init__.py
deleted file mode 100644
index 08c044b..0000000
--- a/silx/gui/data/test/__init__.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-import unittest
-
-from . import test_arraywidget
-from . import test_numpyaxesselector
-from . import test_dataviewer
-from . import test_textformatter
-
-__authors__ = ["V. Valls", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "24/01/2017"
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTests(
- [test_arraywidget.suite(),
- test_numpyaxesselector.suite(),
- test_dataviewer.suite(),
- test_textformatter.suite(),
- ])
- return test_suite
diff --git a/silx/gui/data/test/test_arraywidget.py b/silx/gui/data/test/test_arraywidget.py
deleted file mode 100644
index 87081ed..0000000
--- a/silx/gui/data/test/test_arraywidget.py
+++ /dev/null
@@ -1,329 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2021 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.
-#
-# ###########################################################################*/
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "05/12/2016"
-
-import os
-import tempfile
-import unittest
-
-import numpy
-
-from silx.gui import qt
-from silx.gui.data import ArrayTableWidget
-from silx.gui.data.ArrayTableModel import ArrayTableModel
-from silx.gui.utils.testutils import TestCaseQt
-
-import h5py
-
-
-class TestArrayWidget(TestCaseQt):
- """Basic test for ArrayTableWidget with a numpy array"""
- def setUp(self):
- super(TestArrayWidget, self).setUp()
- self.aw = ArrayTableWidget.ArrayTableWidget()
-
- def tearDown(self):
- del self.aw
- super(TestArrayWidget, self).tearDown()
-
- def testShow(self):
- """test for errors"""
- self.aw.show()
- self.qWaitForWindowExposed(self.aw)
-
- def testSetData0D(self):
- a = 1
- self.aw.setArrayData(a)
- b = self.aw.getData(copy=True)
-
- self.assertTrue(numpy.array_equal(a, b))
-
- # scalar/0D data has no frame index
- self.assertEqual(len(self.aw.model._index), 0)
- # and no perspective
- self.assertEqual(len(self.aw.model._perspective), 0)
-
- def testSetData1D(self):
- a = [1, 2]
- self.aw.setArrayData(a)
- b = self.aw.getData(copy=True)
-
- self.assertTrue(numpy.array_equal(a, b))
-
- # 1D data has no frame index
- self.assertEqual(len(self.aw.model._index), 0)
- # and no perspective
- self.assertEqual(len(self.aw.model._perspective), 0)
-
- def testSetData4D(self):
- a = numpy.reshape(numpy.linspace(0.213, 1.234, 1250),
- (5, 5, 5, 10))
- self.aw.setArrayData(a)
-
- # default perspective (0, 1)
- self.assertEqual(list(self.aw.model._perspective),
- [0, 1])
- self.aw.setPerspective((1, 3))
- self.assertEqual(list(self.aw.model._perspective),
- [1, 3])
-
- b = self.aw.getData(copy=True)
- self.assertTrue(numpy.array_equal(a, b))
-
- # 4D data has a 2-tuple as frame index
- self.assertEqual(len(self.aw.model._index), 2)
- # default index is (0, 0)
- self.assertEqual(list(self.aw.model._index),
- [0, 0])
- self.aw.setFrameIndex((3, 1))
-
- self.assertEqual(list(self.aw.model._index),
- [3, 1])
-
- def testColors(self):
- a = numpy.arange(256, dtype=numpy.uint8)
- self.aw.setArrayData(a)
-
- bgcolor = numpy.empty(a.shape + (3,), dtype=numpy.uint8)
- # Black & white palette
- bgcolor[..., 0] = a
- bgcolor[..., 1] = a
- bgcolor[..., 2] = a
-
- fgcolor = numpy.bitwise_xor(bgcolor, 255)
-
- self.aw.setArrayColors(bgcolor, fgcolor)
-
- # test colors are as expected in model
- for i in range(256):
- # all RGB channels for BG equal to data value
- self.assertEqual(
- self.aw.model.data(self.aw.model.index(0, i),
- role=qt.Qt.BackgroundRole),
- qt.QColor(i, i, i),
- "Unexpected background color"
- )
-
- # all RGB channels for FG equal to XOR(data value, 255)
- self.assertEqual(
- self.aw.model.data(self.aw.model.index(0, i),
- role=qt.Qt.ForegroundRole),
- qt.QColor(i ^ 255, i ^ 255, i ^ 255),
- "Unexpected text color"
- )
-
- # test colors are reset to None when a new data array is loaded
- # with different shape
- self.aw.setArrayData(numpy.arange(300))
-
- for i in range(300):
- # all RGB channels for BG equal to data value
- self.assertIsNone(
- self.aw.model.data(self.aw.model.index(0, i),
- role=qt.Qt.BackgroundRole))
-
- def testDefaultFlagNotEditable(self):
- """editable should be False by default, in setArrayData"""
- self.aw.setArrayData([[0]])
- idx = self.aw.model.createIndex(0, 0)
- # model is editable
- self.assertFalse(
- self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
-
- def testFlagEditable(self):
- self.aw.setArrayData([[0]], editable=True)
- idx = self.aw.model.createIndex(0, 0)
- # model is editable
- self.assertTrue(
- self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
-
- def testFlagNotEditable(self):
- self.aw.setArrayData([[0]], editable=False)
- idx = self.aw.model.createIndex(0, 0)
- # model is editable
- self.assertFalse(
- self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
-
- def testReferenceReturned(self):
- """when setting the data with copy=False and
- retrieving it with getData(copy=False), we should recover
- the same original object.
- """
- # n-D (n >=2)
- a0 = numpy.reshape(numpy.linspace(0.213, 1.234, 1000),
- (10, 10, 10))
- self.aw.setArrayData(a0, copy=False)
- a1 = self.aw.getData(copy=False)
-
- self.assertIs(a0, a1)
-
- # 1D
- b0 = numpy.linspace(0.213, 1.234, 1000)
- self.aw.setArrayData(b0, copy=False)
- b1 = self.aw.getData(copy=False)
- self.assertIs(b0, b1)
-
- def testClipping(self):
- """Test clipping of large arrays"""
- self.aw.show()
- self.qWaitForWindowExposed(self.aw)
-
- data = numpy.arange(ArrayTableModel.MAX_NUMBER_OF_SECTIONS + 10)
-
- for shape in [(1, -1), (-1, 1)]:
- with self.subTest(shape=shape):
- self.aw.setArrayData(data.reshape(shape), editable=True)
- self.qapp.processEvents()
-
-
-class TestH5pyArrayWidget(TestCaseQt):
- """Basic test for ArrayTableWidget with a dataset.
-
- Test flags, for dataset open in read-only or read-write modes"""
- def setUp(self):
- super(TestH5pyArrayWidget, self).setUp()
- self.aw = ArrayTableWidget.ArrayTableWidget()
- self.data = numpy.reshape(numpy.linspace(0.213, 1.234, 1000),
- (10, 10, 10))
- # create an h5py file with a dataset
- self.tempdir = tempfile.mkdtemp()
- self.h5_fname = os.path.join(self.tempdir, "array.h5")
- h5f = h5py.File(self.h5_fname, mode='w')
- h5f["my_array"] = self.data
- h5f["my_scalar"] = 3.14
- h5f["my_1D_array"] = numpy.array(numpy.arange(1000))
- h5f.close()
-
- def tearDown(self):
- del self.aw
- os.unlink(self.h5_fname)
- os.rmdir(self.tempdir)
- super(TestH5pyArrayWidget, self).tearDown()
-
- def testShow(self):
- self.aw.show()
- self.qWaitForWindowExposed(self.aw)
-
- def testReadOnly(self):
- """Open H5 dataset in read-only mode, ensure the model is not editable."""
- h5f = h5py.File(self.h5_fname, "r")
- a = h5f["my_array"]
- # ArrayTableModel relies on following condition
- self.assertTrue(a.file.mode == "r")
-
- self.aw.setArrayData(a, copy=False, editable=True)
-
- self.assertIsInstance(a, h5py.Dataset) # simple sanity check
- # internal representation must be a reference to original data (copy=False)
- self.assertIsInstance(self.aw.model._array, h5py.Dataset)
- self.assertTrue(self.aw.model._array.file.mode == "r")
-
- b = self.aw.getData()
- self.assertTrue(numpy.array_equal(self.data, b))
-
- # model must have detected read-only dataset and disabled editing
- self.assertFalse(self.aw.model._editable)
- idx = self.aw.model.createIndex(0, 0)
- self.assertFalse(
- self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
-
- # force editing read-only datasets raises IOError
- self.assertRaises(IOError, self.aw.model.setData,
- idx, 123.4, role=qt.Qt.EditRole)
- h5f.close()
-
- def testReadWrite(self):
- h5f = h5py.File(self.h5_fname, "r+")
- a = h5f["my_array"]
- self.assertTrue(a.file.mode == "r+")
-
- self.aw.setArrayData(a, copy=False, editable=True)
- b = self.aw.getData(copy=False)
- self.assertTrue(numpy.array_equal(self.data, b))
-
- idx = self.aw.model.createIndex(0, 0)
- # model is editable
- self.assertTrue(
- self.aw.model.flags(idx) & qt.Qt.ItemIsEditable)
- h5f.close()
-
- def testSetData0D(self):
- h5f = h5py.File(self.h5_fname, "r+")
- a = h5f["my_scalar"]
- self.aw.setArrayData(a)
- b = self.aw.getData(copy=True)
-
- self.assertTrue(numpy.array_equal(a, b))
-
- h5f.close()
-
- def testSetData1D(self):
- h5f = h5py.File(self.h5_fname, "r+")
- a = h5f["my_1D_array"]
- self.aw.setArrayData(a)
- b = self.aw.getData(copy=True)
-
- self.assertTrue(numpy.array_equal(a, b))
-
- h5f.close()
-
- def testReferenceReturned(self):
- """when setting the data with copy=False and
- retrieving it with getData(copy=False), we should recover
- the same original object.
-
- This only works for array with at least 2D. For 1D and 0D
- arrays, a view is created at some point, which in the case
- of an hdf5 dataset creates a copy."""
- h5f = h5py.File(self.h5_fname, "r+")
-
- # n-D
- a0 = h5f["my_array"]
- self.aw.setArrayData(a0, copy=False)
- a1 = self.aw.getData(copy=False)
- self.assertIs(a0, a1)
-
- # 1D
- b0 = h5f["my_1D_array"]
- self.aw.setArrayData(b0, copy=False)
- b1 = self.aw.getData(copy=False)
- self.assertIs(b0, b1)
-
- h5f.close()
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestArrayWidget))
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestH5pyArrayWidget))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py
deleted file mode 100644
index dd01dd6..0000000
--- a/silx/gui/data/test/test_dataviewer.py
+++ /dev/null
@@ -1,314 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 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.
-#
-# ###########################################################################*/
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "19/02/2019"
-
-import os
-import tempfile
-import unittest
-from contextlib import contextmanager
-
-import numpy
-from ..DataViewer import DataViewer
-from ..DataViews import DataView
-from .. import DataViews
-
-from silx.gui import qt
-
-from silx.gui.data.DataViewerFrame import DataViewerFrame
-from silx.gui.utils.testutils import SignalListener
-from silx.gui.utils.testutils import TestCaseQt
-
-import h5py
-
-
-class _DataViewMock(DataView):
- """Dummy view to display nothing"""
-
- def __init__(self, parent):
- DataView.__init__(self, parent)
-
- def axesNames(self, data, info):
- return []
-
- def createWidget(self, parent):
- return qt.QLabel(parent)
-
- def getDataPriority(self, data, info):
- return 0
-
-
-class AbstractDataViewerTests(TestCaseQt):
-
- def create_widget(self):
- # Avoid to raise an error when testing the full module
- self.skipTest("Not implemented")
-
- @contextmanager
- def h5_temporary_file(self):
- # create tmp file
- fd, tmp_name = tempfile.mkstemp(suffix=".h5")
- os.close(fd)
- data = numpy.arange(3 * 3 * 3)
- data.shape = 3, 3, 3
- # create h5 data
- h5file = h5py.File(tmp_name, "w")
- h5file["data"] = data
- yield h5file
- # clean up
- h5file.close()
- os.unlink(tmp_name)
-
- def test_text_data(self):
- data_list = ["aaa", int, 8, self]
- widget = self.create_widget()
- for data in data_list:
- widget.setData(data)
- self.assertEqual(DataViews.RAW_MODE, widget.displayMode())
-
- def test_plot_1d_data(self):
- data = numpy.arange(3 ** 1)
- data.shape = [3] * 1
- widget = self.create_widget()
- widget.setData(data)
- availableModes = set([v.modeId() for v in widget.currentAvailableViews()])
- self.assertEqual(DataViews.RAW_MODE, widget.displayMode())
- self.assertIn(DataViews.PLOT1D_MODE, availableModes)
-
- def test_image_data(self):
- data = numpy.arange(3 ** 2)
- data.shape = [3] * 2
- widget = self.create_widget()
- widget.setData(data)
- availableModes = set([v.modeId() for v in widget.currentAvailableViews()])
- self.assertEqual(DataViews.RAW_MODE, widget.displayMode())
- self.assertIn(DataViews.IMAGE_MODE, availableModes)
-
- def test_image_bool(self):
- data = numpy.zeros((10, 10), dtype=bool)
- data[::2, ::2] = True
- widget = self.create_widget()
- widget.setData(data)
- availableModes = set([v.modeId() for v in widget.currentAvailableViews()])
- self.assertEqual(DataViews.RAW_MODE, widget.displayMode())
- self.assertIn(DataViews.IMAGE_MODE, availableModes)
-
- def test_image_complex_data(self):
- data = numpy.arange(3 ** 2, dtype=numpy.complex64)
- data.shape = [3] * 2
- widget = self.create_widget()
- widget.setData(data)
- availableModes = set([v.modeId() for v in widget.currentAvailableViews()])
- self.assertEqual(DataViews.RAW_MODE, widget.displayMode())
- self.assertIn(DataViews.IMAGE_MODE, availableModes)
-
- def test_plot_3d_data(self):
- data = numpy.arange(3 ** 3)
- data.shape = [3] * 3
- widget = self.create_widget()
- widget.setData(data)
- availableModes = set([v.modeId() for v in widget.currentAvailableViews()])
- try:
- import silx.gui.plot3d # noqa
- self.assertIn(DataViews.PLOT3D_MODE, availableModes)
- except ImportError:
- self.assertIn(DataViews.STACK_MODE, availableModes)
- self.assertEqual(DataViews.RAW_MODE, widget.displayMode())
-
- def test_array_1d_data(self):
- data = numpy.array(["aaa"] * (3 ** 1))
- data.shape = [3] * 1
- widget = self.create_widget()
- widget.setData(data)
- self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId())
-
- def test_array_2d_data(self):
- data = numpy.array(["aaa"] * (3 ** 2))
- data.shape = [3] * 2
- widget = self.create_widget()
- widget.setData(data)
- self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId())
-
- def test_array_4d_data(self):
- data = numpy.array(["aaa"] * (3 ** 4))
- data.shape = [3] * 4
- widget = self.create_widget()
- widget.setData(data)
- self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId())
-
- def test_record_4d_data(self):
- data = numpy.zeros(3 ** 4, dtype='3int8, float32, (2,3)float64')
- data.shape = [3] * 4
- widget = self.create_widget()
- widget.setData(data)
- self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId())
-
- def test_3d_h5_dataset(self):
- with self.h5_temporary_file() as h5file:
- dataset = h5file["data"]
- widget = self.create_widget()
- widget.setData(dataset)
-
- def test_data_event(self):
- listener = SignalListener()
- widget = self.create_widget()
- widget.dataChanged.connect(listener)
- widget.setData(10)
- widget.setData(None)
- self.assertEqual(listener.callCount(), 2)
-
- def test_display_mode_event(self):
- listener = SignalListener()
- widget = self.create_widget()
- widget.displayedViewChanged.connect(listener)
- widget.setData(10)
- widget.setData(None)
- modes = [v.modeId() for v in listener.arguments(argumentIndex=0)]
- self.assertEqual(modes, [DataViews.RAW_MODE, DataViews.EMPTY_MODE])
- listener.clear()
-
- def test_change_display_mode(self):
- data = numpy.arange(10 ** 4)
- data.shape = [10] * 4
- widget = self.create_widget()
- widget.setData(data)
- widget.setDisplayMode(DataViews.PLOT1D_MODE)
- self.assertEqual(widget.displayedView().modeId(), DataViews.PLOT1D_MODE)
- widget.setDisplayMode(DataViews.IMAGE_MODE)
- self.assertEqual(widget.displayedView().modeId(), DataViews.IMAGE_MODE)
- widget.setDisplayMode(DataViews.RAW_MODE)
- self.assertEqual(widget.displayedView().modeId(), DataViews.RAW_MODE)
- widget.setDisplayMode(DataViews.EMPTY_MODE)
- self.assertEqual(widget.displayedView().modeId(), DataViews.EMPTY_MODE)
-
- def test_create_default_views(self):
- widget = self.create_widget()
- views = widget.createDefaultViews()
- self.assertTrue(len(views) > 0)
-
- def test_add_view(self):
- widget = self.create_widget()
- view = _DataViewMock(widget)
- widget.addView(view)
- self.assertTrue(view in widget.availableViews())
- self.assertTrue(view in widget.currentAvailableViews())
-
- def test_remove_view(self):
- widget = self.create_widget()
- widget.setData("foobar")
- view = widget.currentAvailableViews()[0]
- widget.removeView(view)
- self.assertTrue(view not in widget.availableViews())
- self.assertTrue(view not in widget.currentAvailableViews())
-
- def test_replace_view(self):
- widget = self.create_widget()
- view = _DataViewMock(widget)
- widget.replaceView(DataViews.RAW_MODE,
- view)
- self.assertIsNone(widget.getViewFromModeId(DataViews.RAW_MODE))
- self.assertTrue(view in widget.availableViews())
- self.assertTrue(view in widget.currentAvailableViews())
-
- def test_replace_view_in_composite(self):
- # replace a view that is a child of a composite view
- widget = self.create_widget()
- view = _DataViewMock(widget)
- 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.getViews()])
- self.assertTrue(view in nxdata_view.getViews())
-
-
-class TestDataViewer(AbstractDataViewerTests):
- def create_widget(self):
- return DataViewer()
-
-
-class TestDataViewerFrame(AbstractDataViewerTests):
- def create_widget(self):
- return DataViewerFrame()
-
-
-class TestDataView(TestCaseQt):
-
- def createComplexData(self):
- line = [1, 2j, 3 + 3j, 4]
- image = [line, line, line, line]
- cube = [image, image, image, image]
- data = numpy.array(cube, dtype=numpy.complex64)
- return data
-
- def createDataViewWithData(self, dataViewClass, data):
- viewer = dataViewClass(None)
- widget = viewer.getWidget()
- viewer.setData(data)
- return widget
-
- def testCurveWithComplex(self):
- data = self.createComplexData()
- dataViewClass = DataViews._Plot1dView
- widget = self.createDataViewWithData(dataViewClass, data[0, 0])
- self.qWaitForWindowExposed(widget)
-
- def testImageWithComplex(self):
- data = self.createComplexData()
- dataViewClass = DataViews._Plot2dView
- widget = self.createDataViewWithData(dataViewClass, data[0])
- self.qWaitForWindowExposed(widget)
-
- def testCubeWithComplex(self):
- self.skipTest("OpenGL widget not yet tested")
- try:
- import silx.gui.plot3d # noqa
- except ImportError:
- self.skipTest("OpenGL not available")
- data = self.createComplexData()
- dataViewClass = DataViews._Plot3dView
- widget = self.createDataViewWithData(dataViewClass, data)
- self.qWaitForWindowExposed(widget)
-
- def testImageStackWithComplex(self):
- data = self.createComplexData()
- dataViewClass = DataViews._StackView
- widget = self.createDataViewWithData(dataViewClass, data)
- self.qWaitForWindowExposed(widget)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loadTestsFromTestCase(TestDataViewer))
- test_suite.addTest(loadTestsFromTestCase(TestDataViewerFrame))
- test_suite.addTest(loadTestsFromTestCase(TestDataView))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/data/test/test_numpyaxesselector.py b/silx/gui/data/test/test_numpyaxesselector.py
deleted file mode 100644
index d37cff7..0000000
--- a/silx/gui/data/test/test_numpyaxesselector.py
+++ /dev/null
@@ -1,161 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "29/01/2018"
-
-import os
-import tempfile
-import unittest
-from contextlib import contextmanager
-
-import numpy
-
-from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector
-from silx.gui.utils.testutils import SignalListener
-from silx.gui.utils.testutils import TestCaseQt
-
-import h5py
-
-
-class TestNumpyAxesSelector(TestCaseQt):
-
- def test_creation(self):
- data = numpy.arange(3 * 3 * 3)
- data.shape = 3, 3, 3
- widget = NumpyAxesSelector()
- widget.setVisible(True)
-
- def test_none(self):
- data = numpy.arange(3 * 3 * 3)
- widget = NumpyAxesSelector()
- widget.setData(data)
- widget.setData(None)
- result = widget.selectedData()
- self.assertIsNone(result)
-
- def test_output_samedim(self):
- data = numpy.arange(3 * 3 * 3)
- data.shape = 3, 3, 3
- expectedResult = data
-
- widget = NumpyAxesSelector()
- widget.setAxisNames(["x", "y", "z"])
- widget.setData(data)
- result = widget.selectedData()
- self.assertTrue(numpy.array_equal(result, expectedResult))
-
- def test_output_moredim(self):
- data = numpy.arange(3 * 3 * 3 * 3)
- data.shape = 3, 3, 3, 3
- expectedResult = data
-
- widget = NumpyAxesSelector()
- widget.setAxisNames(["x", "y", "z", "boum"])
- widget.setData(data[0])
- result = widget.selectedData()
- self.assertIsNone(result)
- widget.setData(data)
- result = widget.selectedData()
- self.assertTrue(numpy.array_equal(result, expectedResult))
-
- def test_output_lessdim(self):
- data = numpy.arange(3 * 3 * 3)
- data.shape = 3, 3, 3
- expectedResult = data[0]
-
- widget = NumpyAxesSelector()
- widget.setAxisNames(["y", "x"])
- widget.setData(data)
- result = widget.selectedData()
- self.assertTrue(numpy.array_equal(result, expectedResult))
-
- def test_output_1dim(self):
- data = numpy.arange(3 * 3 * 3)
- data.shape = 3, 3, 3
- expectedResult = data[0, 0, 0]
-
- widget = NumpyAxesSelector()
- widget.setData(data)
- result = widget.selectedData()
- self.assertTrue(numpy.array_equal(result, expectedResult))
-
- @contextmanager
- def h5_temporary_file(self):
- # create tmp file
- fd, tmp_name = tempfile.mkstemp(suffix=".h5")
- os.close(fd)
- data = numpy.arange(3 * 3 * 3)
- data.shape = 3, 3, 3
- # create h5 data
- h5file = h5py.File(tmp_name, "w")
- h5file["data"] = data
- yield h5file
- # clean up
- h5file.close()
- os.unlink(tmp_name)
-
- def test_h5py_dataset(self):
- with self.h5_temporary_file() as h5file:
- dataset = h5file["data"]
- expectedResult = dataset[0]
-
- widget = NumpyAxesSelector()
- widget.setData(dataset)
- widget.setAxisNames(["y", "x"])
- result = widget.selectedData()
- self.assertTrue(numpy.array_equal(result, expectedResult))
-
- def test_data_event(self):
- data = numpy.arange(3 * 3 * 3)
- widget = NumpyAxesSelector()
- listener = SignalListener()
- widget.dataChanged.connect(listener)
- widget.setData(data)
- widget.setData(None)
- self.assertEqual(listener.callCount(), 2)
-
- def test_selected_data_event(self):
- data = numpy.arange(3 * 3 * 3)
- data.shape = 3, 3, 3
- widget = NumpyAxesSelector()
- listener = SignalListener()
- widget.selectionChanged.connect(listener)
- widget.setData(data)
- widget.setAxisNames(["x"])
- widget.setData(None)
- self.assertEqual(listener.callCount(), 3)
- listener.clear()
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestNumpyAxesSelector))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/data/test/test_textformatter.py b/silx/gui/data/test/test_textformatter.py
deleted file mode 100644
index d3050bf..0000000
--- a/silx/gui/data/test/test_textformatter.py
+++ /dev/null
@@ -1,212 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__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.io.utils import h5py_read_dataset
-
-import h5py
-
-
-class TestTextFormatter(TestCaseQt):
-
- def test_copy(self):
- formatter = TextFormatter()
- copy = TextFormatter(formatter=formatter)
- self.assertIsNot(formatter, copy)
- copy.setFloatFormat("%.3f")
- self.assertEqual(formatter.integerFormat(), copy.integerFormat())
- self.assertNotEqual(formatter.floatFormat(), copy.floatFormat())
- self.assertEqual(formatter.useQuoteForText(), copy.useQuoteForText())
- self.assertEqual(formatter.imaginaryUnit(), copy.imaginaryUnit())
-
- def test_event(self):
- listener = SignalListener()
- formatter = TextFormatter()
- formatter.formatChanged.connect(listener)
- formatter.setFloatFormat("%.3f")
- formatter.setIntegerFormat("%03i")
- formatter.setUseQuoteForText(False)
- formatter.setImaginaryUnit("z")
- self.assertEqual(listener.callCount(), 4)
-
- def test_int(self):
- formatter = TextFormatter()
- formatter.setIntegerFormat("%05i")
- result = formatter.toString(512)
- self.assertEqual(result, "00512")
-
- def test_float(self):
- formatter = TextFormatter()
- formatter.setFloatFormat("%.3f")
- result = formatter.toString(1.3)
- self.assertEqual(result, "1.300")
-
- def test_complex(self):
- formatter = TextFormatter()
- formatter.setFloatFormat("%.1f")
- formatter.setImaginaryUnit("i")
- result = formatter.toString(1.0 + 5j)
- result = result.replace(" ", "")
- self.assertEqual(result, "1.0+5.0i")
-
- def test_string(self):
- formatter = TextFormatter()
- formatter.setIntegerFormat("%.1f")
- formatter.setImaginaryUnit("z")
- result = formatter.toString("toto")
- self.assertEqual(result, '"toto"')
-
- def test_numpy_void(self):
- formatter = TextFormatter()
- result = formatter.toString(numpy.void(b"\xFF"))
- self.assertEqual(result, 'b"\\xFF"')
-
- def test_char_cp1252(self):
- # degree character in cp1252
- formatter = TextFormatter()
- result = formatter.toString(numpy.bytes_(b"\xB0"))
- self.assertEqual(result, u'"\u00B0"')
-
-
-class TestTextFormatterWithH5py(TestCaseQt):
-
- @classmethod
- def setUpClass(cls):
- super(TestTextFormatterWithH5py, cls).setUpClass()
-
- cls.tmpDirectory = tempfile.mkdtemp()
- cls.h5File = h5py.File("%s/formatter.h5" % cls.tmpDirectory, mode="w")
- cls.formatter = TextFormatter()
-
- @classmethod
- def tearDownClass(cls):
- super(TestTextFormatterWithH5py, cls).tearDownClass()
- cls.h5File.close()
- cls.h5File = None
- shutil.rmtree(cls.tmpDirectory)
-
- def create_dataset(self, data, dtype=None):
- testName = "%s" % self.id()
- dataset = self.h5File.create_dataset(testName, data=data, dtype=dtype)
- return dataset
-
- def read_dataset(self, d):
- return self.formatter.toString(d[()], dtype=d.dtype)
-
- def testAscii(self):
- d = self.create_dataset(data=b"abc")
- result = self.read_dataset(d)
- self.assertEqual(result, '"abc"')
-
- def testUnicode(self):
- d = self.create_dataset(data=u"i\u2661cookies")
- result = self.read_dataset(d)
- self.assertEqual(len(result), 11)
- self.assertEqual(result, u'"i\u2661cookies"')
-
- def testBadAscii(self):
- d = self.create_dataset(data=b"\xF0\x9F\x92\x94")
- result = self.read_dataset(d)
- self.assertEqual(result, 'b"\\xF0\\x9F\\x92\\x94"')
-
- def testVoid(self):
- d = self.create_dataset(data=numpy.void(b"abc\xF0"))
- result = self.read_dataset(d)
- self.assertEqual(result, 'b"\\x61\\x62\\x63\\xF0"')
-
- def testEnum(self):
- dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42}))
- d = numpy.array(42, dtype=dtype)
- d = self.create_dataset(data=d)
- result = self.read_dataset(d)
- self.assertEqual(result, 'BLUE(42)')
-
- def testRef(self):
- dtype = h5py.special_dtype(ref=h5py.Reference)
- d = numpy.array(self.h5File.ref, dtype=dtype)
- d = self.create_dataset(data=d)
- result = self.read_dataset(d)
- self.assertEqual(result, 'REF')
-
- def testArrayAscii(self):
- d = self.create_dataset(data=[b"abc"])
- result = self.read_dataset(d)
- self.assertEqual(result, '["abc"]')
-
- def testArrayUnicode(self):
- dtype = h5py.special_dtype(vlen=six.text_type)
- d = numpy.array([u"i\u2661cookies"], dtype=dtype)
- d = self.create_dataset(data=d)
- result = self.read_dataset(d)
- self.assertEqual(len(result), 13)
- self.assertEqual(result, u'["i\u2661cookies"]')
-
- def testArrayBadAscii(self):
- d = self.create_dataset(data=[b"\xF0\x9F\x92\x94"])
- result = self.read_dataset(d)
- self.assertEqual(result, '[b"\\xF0\\x9F\\x92\\x94"]')
-
- def testArrayVoid(self):
- d = self.create_dataset(data=numpy.void([b"abc\xF0"]))
- result = self.read_dataset(d)
- self.assertEqual(result, '[b"\\x61\\x62\\x63\\xF0"]')
-
- def testArrayEnum(self):
- dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42}))
- d = numpy.array([42, 1, 100], dtype=dtype)
- d = self.create_dataset(data=d)
- result = self.read_dataset(d)
- self.assertEqual(result, '[BLUE(42) GREEN(1) 100]')
-
- def testArrayRef(self):
- dtype = h5py.special_dtype(ref=h5py.Reference)
- d = numpy.array([self.h5File.ref, None], dtype=dtype)
- d = self.create_dataset(data=d)
- result = self.read_dataset(d)
- self.assertEqual(result, '[REF NULL_REF]')
-
-
-def suite():
- loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite = unittest.TestSuite()
- test_suite.addTest(loadTests(TestTextFormatter))
- test_suite.addTest(loadTests(TestTextFormatterWithH5py))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/dialog/AbstractDataFileDialog.py b/silx/gui/dialog/AbstractDataFileDialog.py
deleted file mode 100644
index 29e7bb5..0000000
--- a/silx/gui/dialog/AbstractDataFileDialog.py
+++ /dev/null
@@ -1,1742 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# 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
-# 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 contains an :class:`AbstractDataFileDialog`.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "05/03/2019"
-
-
-import sys
-import os
-import logging
-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 .FileTypeComboBox import FileTypeComboBox
-
-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
-
- FileDialogToParentFile = qt.QStyle.SP_CustomBase + 2
-
- def __init__(self):
- self.__iconFileDialogToParentDir = None
- self.__iconFileDialogToParentFile = None
-
- def _createIconToParent(self, standardPixmap):
- """
-
- FIXME: It have to be tested for some OS (arrow icon do not have always
- the same direction)
- """
- style = qt.QApplication.style()
- baseIcon = style.standardIcon(qt.QStyle.SP_FileDialogToParent)
- backgroundIcon = style.standardIcon(standardPixmap)
- icon = qt.QIcon()
-
- sizes = baseIcon.availableSizes()
- sizes = sorted(sizes, key=lambda s: s.height())
- sizes = filter(lambda s: s.height() < 100, sizes)
- sizes = list(sizes)
- if len(sizes) > 0:
- baseSize = sizes[-1]
- else:
- baseSize = baseIcon.availableSizes()[0]
- size = qt.QSize(baseSize.width(), baseSize.height() * 3 // 2)
-
- modes = [qt.QIcon.Normal, qt.QIcon.Disabled]
- for mode in modes:
- pixmap = qt.QPixmap(size)
- pixmap.fill(qt.Qt.transparent)
- painter = qt.QPainter(pixmap)
- painter.drawPixmap(0, 0, backgroundIcon.pixmap(baseSize, mode=mode))
- painter.drawPixmap(0, size.height() // 3, baseIcon.pixmap(baseSize, mode=mode))
- painter.end()
- icon.addPixmap(pixmap, mode=mode)
-
- return icon
-
- def getFileDialogToParentDir(self):
- if self.__iconFileDialogToParentDir is None:
- self.__iconFileDialogToParentDir = self._createIconToParent(qt.QStyle.SP_DirIcon)
- return self.__iconFileDialogToParentDir
-
- def getFileDialogToParentFile(self):
- if self.__iconFileDialogToParentFile is None:
- self.__iconFileDialogToParentFile = self._createIconToParent(qt.QStyle.SP_FileIcon)
- return self.__iconFileDialogToParentFile
-
- def icon(self, kind):
- if kind == self.FileDialogToParentDir:
- return self.getFileDialogToParentDir()
- elif kind == self.FileDialogToParentFile:
- return self.getFileDialogToParentFile()
- else:
- style = qt.QApplication.style()
- icon = style.standardIcon(kind)
- return icon
-
-
-class _SideBar(qt.QListView):
- """Sidebar containing shortcuts for common directories"""
-
- def __init__(self, parent=None):
- super(_SideBar, self).__init__(parent)
- self.__iconProvider = qt.QFileIconProvider()
- self.setUniformItemSizes(True)
- model = qt.QStandardItemModel(self)
- self.setModel(model)
- self._initModel()
- self.setEditTriggers(qt.QAbstractItemView.NoEditTriggers)
-
- def iconProvider(self):
- return self.__iconProvider
-
- def _initModel(self):
- urls = self._getDefaultUrls()
- self.setUrls(urls)
-
- def _getDefaultUrls(self):
- """Returns the default shortcuts.
-
- It uses the default QFileDialog shortcuts if it is possible, else
- provides a link to the computer's root and the user's home.
-
- :rtype: List[str]
- """
- urls = []
- 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)")
- feed_sidebar = False
-
- if feed_sidebar:
- # Get default shortcut
- # There is no other way
- d = qt.QFileDialog(self)
- # Needed to be able to reach the sidebar urls
- d.setOption(qt.QFileDialog.DontUseNativeDialog, True)
- urls = d.sidebarUrls()
- d.deleteLater()
- d = None
-
- if len(urls) == 0:
- urls.append(qt.QUrl("file://"))
- urls.append(qt.QUrl.fromLocalFile(qt.QDir.homePath()))
-
- return urls
-
- def setSelectedPath(self, path):
- selected = None
- model = self.model()
- for i in range(model.rowCount()):
- index = model.index(i, 0)
- url = model.data(index, qt.Qt.UserRole)
- urlPath = url.toLocalFile()
- if path == urlPath:
- selected = index
-
- selectionModel = self.selectionModel()
- if selected is not None:
- selectionModel.setCurrentIndex(selected, qt.QItemSelectionModel.ClearAndSelect)
- else:
- selectionModel.clear()
-
- def setUrls(self, urls):
- model = self.model()
- model.clear()
-
- names = {}
- names[qt.QDir.rootPath()] = "Computer"
- names[qt.QDir.homePath()] = "Home"
-
- style = qt.QApplication.style()
- iconProvider = self.iconProvider()
- for url in urls:
- path = url.toLocalFile()
- if path == "":
- if sys.platform != "win32":
- url = qt.QUrl(qt.QDir.rootPath())
- name = "Computer"
- icon = style.standardIcon(qt.QStyle.SP_ComputerIcon)
- else:
- fileInfo = qt.QFileInfo(path)
- name = names.get(path, fileInfo.fileName())
- icon = iconProvider.icon(fileInfo)
-
- if icon.isNull():
- icon = style.standardIcon(qt.QStyle.SP_MessageBoxCritical)
-
- item = qt.QStandardItem()
- item.setText(name)
- item.setIcon(icon)
- item.setData(url, role=qt.Qt.UserRole)
- model.appendRow(item)
-
- def urls(self):
- result = []
- model = self.model()
- for i in range(model.rowCount()):
- index = model.index(i, 0)
- url = model.data(index, qt.Qt.UserRole)
- result.append(url)
- return result
-
- def sizeHint(self):
- index = self.model().index(0, 0)
- return self.sizeHintForIndex(index) + qt.QSize(2 * self.frameWidth(), 2 * self.frameWidth())
-
-
-class _Browser(qt.QStackedWidget):
-
- activated = qt.Signal(qt.QModelIndex)
- selected = qt.Signal(qt.QModelIndex)
- rootIndexChanged = qt.Signal(qt.QModelIndex)
-
- def __init__(self, parent, listView, detailView):
- qt.QStackedWidget.__init__(self, parent)
- self.__listView = listView
- self.__detailView = detailView
- self.insertWidget(0, self.__listView)
- self.insertWidget(1, self.__detailView)
-
- self.__listView.activated.connect(self.__emitActivated)
- self.__detailView.activated.connect(self.__emitActivated)
-
- def __emitActivated(self, index):
- self.activated.emit(index)
-
- def __emitSelected(self, selected, deselected):
- index = self.selectedIndex()
- if index is not None:
- self.selected.emit(index)
-
- def selectedIndex(self):
- if self.currentIndex() == 0:
- selectionModel = self.__listView.selectionModel()
- else:
- selectionModel = self.__detailView.selectionModel()
-
- if selectionModel is None:
- return None
-
- indexes = selectionModel.selectedIndexes()
- # Filter non-main columns
- indexes = [i for i in indexes if i.column() == 0]
- if len(indexes) == 1:
- index = indexes[0]
- return index
- return None
-
- def model(self):
- """Returns the current model."""
- if self.currentIndex() == 0:
- return self.__listView.model()
- else:
- return self.__detailView.model()
-
- def selectIndex(self, index):
- if self.currentIndex() == 0:
- selectionModel = self.__listView.selectionModel()
- else:
- selectionModel = self.__detailView.selectionModel()
- if selectionModel is None:
- return
- selectionModel.setCurrentIndex(index, qt.QItemSelectionModel.ClearAndSelect)
-
- def viewMode(self):
- """Returns the current view mode.
-
- :rtype: qt.QFileDialog.ViewMode
- """
- if self.currentIndex() == 0:
- return qt.QFileDialog.List
- elif self.currentIndex() == 1:
- return qt.QFileDialog.Detail
- else:
- assert(False)
-
- def setViewMode(self, mode):
- """Set the current view mode.
-
- :param qt.QFileDialog.ViewMode mode: The new view mode
- """
- if mode == qt.QFileDialog.Detail:
- self.showDetails()
- elif mode == qt.QFileDialog.List:
- self.showList()
- else:
- assert(False)
-
- def showList(self):
- self.__listView.show()
- self.__detailView.hide()
- self.setCurrentIndex(0)
-
- def showDetails(self):
- self.__listView.hide()
- self.__detailView.show()
- self.setCurrentIndex(1)
- self.__detailView.updateGeometry()
-
- def clear(self):
- self.__listView.setRootIndex(qt.QModelIndex())
- self.__detailView.setRootIndex(qt.QModelIndex())
- selectionModel = self.__listView.selectionModel()
- if selectionModel is not None:
- selectionModel.selectionChanged.disconnect()
- selectionModel.clear()
- selectionModel = self.__detailView.selectionModel()
- if selectionModel is not None:
- selectionModel.selectionChanged.disconnect()
- selectionModel.clear()
- self.__listView.setModel(None)
- self.__detailView.setModel(None)
-
- def setRootIndex(self, index, model=None):
- """Sets the root item to the item at the given index.
- """
- rootIndex = self.__listView.rootIndex()
- newModel = model or index.model()
- assert(newModel is not None)
-
- if rootIndex is None or rootIndex.model() is not newModel:
- # update the model
- selectionModel = self.__listView.selectionModel()
- if selectionModel is not None:
- selectionModel.selectionChanged.disconnect()
- selectionModel.clear()
- selectionModel = self.__detailView.selectionModel()
- if selectionModel is not None:
- selectionModel.selectionChanged.disconnect()
- selectionModel.clear()
- pIndex = qt.QPersistentModelIndex(index)
- self.__listView.setModel(newModel)
- # changing the model of the tree view change the index mapping
- # that is why we are using a persistance model index
- self.__detailView.setModel(newModel)
- index = newModel.index(pIndex.row(), pIndex.column(), pIndex.parent())
- selectionModel = self.__listView.selectionModel()
- selectionModel.selectionChanged.connect(self.__emitSelected)
- selectionModel = self.__detailView.selectionModel()
- selectionModel.selectionChanged.connect(self.__emitSelected)
-
- self.__listView.setRootIndex(index)
- self.__detailView.setRootIndex(index)
- self.rootIndexChanged.emit(index)
-
- def rootIndex(self):
- """Returns the model index of the model's root item. The root item is
- the parent item to the view's toplevel items. The root can be invalid.
- """
- return self.__listView.rootIndex()
-
- __serialVersion = 1
- """Store the current version of the serialized data"""
-
- def visualRect(self, index):
- """Returns the rectangle on the viewport occupied by the item at index.
-
- :param qt.QModelIndex index: An index
- :rtype: QRect
- """
- if self.currentIndex() == 0:
- return self.__listView.visualRect(index)
- else:
- return self.__detailView.visualRect(index)
-
- def viewport(self):
- """Returns the viewport widget.
-
- :param qt.QModelIndex index: An index
- :rtype: QRect
- """
- if self.currentIndex() == 0:
- return self.__listView.viewport()
- else:
- return self.__detailView.viewport()
-
- def restoreState(self, state):
- """Restores the dialogs's layout, history and current directory to the
- state specified.
-
- :param qt.QByeArray state: Stream containing the new state
- :rtype: bool
- """
- stream = qt.QDataStream(state, qt.QIODevice.ReadOnly)
-
- nameId = stream.readQString()
- if nameId != "Browser":
- _logger.warning("Stored state contains an invalid name id. Browser restoration cancelled.")
- return False
-
- version = stream.readInt32()
- if version != self.__serialVersion:
- _logger.warning("Stored state contains an invalid version. Browser restoration cancelled.")
- return False
-
- headerData = stream.readQVariant()
- self.__detailView.header().restoreState(headerData)
-
- viewMode = stream.readInt32()
- self.setViewMode(viewMode)
- return True
-
- def saveState(self):
- """Saves the state of the dialog's layout.
-
- :rtype: qt.QByteArray
- """
- data = qt.QByteArray()
- stream = qt.QDataStream(data, qt.QIODevice.WriteOnly)
-
- nameId = u"Browser"
- stream.writeQString(nameId)
- stream.writeInt32(self.__serialVersion)
- stream.writeQVariant(self.__detailView.header().saveState())
- stream.writeInt32(self.viewMode())
-
- return data
-
-
-class _FabioData(object):
-
- def __init__(self, fabioFile):
- self.__fabioFile = fabioFile
-
- @property
- def dtype(self):
- # Let say it is a valid type
- return numpy.dtype("float")
-
- @property
- 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]
-
- if isinstance(selector, six.integer_types):
- if 0 <= selector < self.__fabioFile.nframes:
- if self.__fabioFile.nframes == 1:
- return self.__fabioFile.data
- else:
- frame = self.__fabioFile.getframe(selector)
- return frame.data
- else:
- raise ValueError("Invalid selector %s" % selector)
- else:
- raise TypeError("Unsupported selector type %s" % type(selector))
-
-
-class _PathEdit(qt.QLineEdit):
- pass
-
-
-class _CatchResizeEvent(qt.QObject):
-
- resized = qt.Signal(qt.QResizeEvent)
-
- def __init__(self, parent, target):
- super(_CatchResizeEvent, self).__init__(parent)
- self.__target = target
- self.__target_oldResizeEvent = self.__target.resizeEvent
- self.__target.resizeEvent = self.__resizeEvent
-
- def __resizeEvent(self, event):
- result = self.__target_oldResizeEvent(event)
- self.resized.emit(event)
- return result
-
-
-class AbstractDataFileDialog(qt.QDialog):
- """The `AbstractFileDialog` provides a generic GUI to create a custom dialog
- allowing to access to file resources like HDF5 files or HDF5 datasets.
-
- .. image:: img/abstractdatafiledialog.png
-
- The dialog contains:
-
- - Shortcuts: It provides few links to have a fast access of browsing
- locations.
- - Browser: It provides a display to browse throw the file system and inside
- HDF5 files or fabio files. A file format selector is provided.
- - URL: Display the URL available to reach the data using
- :meth:`silx.io.get_data`, :meth:`silx.io.open`.
- - Data selector: A widget to apply a sub selection of the browsed dataset.
- This widget can be provided, else nothing will be used.
- - Data preview: A widget to preview the selected data, which is the result
- of the filter from the data selector.
- This widget can be provided, else nothing will be used.
- - Preview's toolbar: Provides tools used to custom data preview or data
- selector.
- This widget can be provided, else nothing will be used.
- - Buttons to validate the dialog
- """
-
- _defaultIconProvider = None
- """Lazy loaded default icon provider"""
-
- def __init__(self, parent=None):
- super(AbstractDataFileDialog, self).__init__(parent)
- self._init()
-
- 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
- self.__selectedFile = None
- self.__selectedData = None
- self.__currentHistory = []
- """Store history of URLs, last index one is the latest one"""
- self.__currentHistoryLocation = -1
- """Store the location in the history. Bigger is older"""
-
- self.__processing = 0
- """Number of asynchronous processing tasks"""
- self.__h5 = None
- self.__fabio = None
-
- if qt.qVersion() < "5.0":
- # On Qt4 it is needed to provide a safe file system model
- _logger.debug("Uses SafeFileSystemModel")
- from .SafeFileSystemModel import SafeFileSystemModel
- self.__fileModel = SafeFileSystemModel(self)
- else:
- # On Qt5 a safe icon provider is still needed to avoid freeze
- _logger.debug("Uses default QFileSystemModel with a SafeFileIconProvider")
- self.__fileModel = qt.QFileSystemModel(self)
- from .SafeFileIconProvider import SafeFileIconProvider
- iconProvider = SafeFileIconProvider()
- self.__fileModel.setIconProvider(iconProvider)
-
- # The common file dialog filter only on Mac OS X
- self.__fileModel.setNameFilterDisables(sys.platform == "darwin")
- self.__fileModel.setReadOnly(True)
- self.__fileModel.directoryLoaded.connect(self.__directoryLoaded)
-
- self.__dataModel = Hdf5TreeModel(self)
-
- self.__createWidgets()
- self.__initLayout()
- self.__showAsListView()
-
- path = os.getcwd()
- self.__fileModel_setRootPath(path)
-
- self.__clearData()
- self.__updatePath()
-
- # Update the file model filter
- self.__fileTypeCombo.setCurrentIndex(0)
- self.__filterSelected(0)
-
- # 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,
- # while the QObject still exists.
- # We use a static method plus explicit references to objects to
- # release. The callback do not use any ref to self.
- onDestroy = functools.partial(self._closeFileList, self.__openedFiles)
- self.destroyed.connect(onDestroy)
-
- @staticmethod
- def _closeFileList(fileList):
- """Static method to close explicit references to internal objects."""
- _logger.debug("Clear AbstractDataFileDialog")
- for obj in fileList:
- _logger.debug("Close file %s", obj.filename)
- obj.close()
- fileList[:] = []
-
- def done(self, result):
- self._clear()
- super(AbstractDataFileDialog, self).done(result)
-
- def _clear(self):
- """Explicit method to clear data stored in the dialog.
- After this call it is not anymore possible to use the widget.
-
- This method is triggered by the destruction of the object and the
- QDialog :meth:`done`. Then it can be triggered more than once.
- """
- _logger.debug("Clear dialog")
- self.__errorWhileLoadingFile = None
- self.__clearData()
- if self.__fileModel is not None:
- # Cache the directory before cleaning the model
- self.__directory = self.directory()
- self.__browser.clear()
- self.__closeFile()
- self.__fileModel = None
- self.__dataModel = None
-
- def hasPendingEvents(self):
- """Returns true if the dialog have asynchronous tasks working on the
- background."""
- return self.__processing > 0
-
- # User interface
-
- def __createWidgets(self):
- self.__sidebar = self._createSideBar()
- if self.__sidebar is not None:
- sideBarModel = self.__sidebar.selectionModel()
- sideBarModel.selectionChanged.connect(self.__shortcutSelected)
- self.__sidebar.setSelectionMode(qt.QAbstractItemView.SingleSelection)
-
- listView = qt.QListView(self)
- listView.setSelectionBehavior(qt.QAbstractItemView.SelectRows)
- listView.setSelectionMode(qt.QAbstractItemView.SingleSelection)
- listView.setResizeMode(qt.QListView.Adjust)
- listView.setWrapping(True)
- listView.setEditTriggers(qt.QAbstractItemView.NoEditTriggers)
- listView.setContextMenuPolicy(qt.Qt.CustomContextMenu)
- utils.patchToConsumeReturnKey(listView)
-
- treeView = qt.QTreeView(self)
- treeView.setSelectionBehavior(qt.QAbstractItemView.SelectRows)
- treeView.setSelectionMode(qt.QAbstractItemView.SingleSelection)
- treeView.setRootIsDecorated(False)
- treeView.setItemsExpandable(False)
- treeView.setSortingEnabled(True)
- treeView.header().setSortIndicator(0, qt.Qt.AscendingOrder)
- treeView.header().setStretchLastSection(False)
- treeView.setTextElideMode(qt.Qt.ElideMiddle)
- treeView.setEditTriggers(qt.QAbstractItemView.NoEditTriggers)
- treeView.setContextMenuPolicy(qt.Qt.CustomContextMenu)
- treeView.setDragDropMode(qt.QAbstractItemView.InternalMove)
- utils.patchToConsumeReturnKey(treeView)
-
- self.__browser = _Browser(self, listView, treeView)
- self.__browser.activated.connect(self.__browsedItemActivated)
- self.__browser.selected.connect(self.__browsedItemSelected)
- self.__browser.rootIndexChanged.connect(self.__rootIndexChanged)
- self.__browser.setObjectName("browser")
-
- self.__previewWidget = self._createPreviewWidget(self)
-
- self.__fileTypeCombo = FileTypeComboBox(self)
- self.__fileTypeCombo.setObjectName("fileTypeCombo")
- self.__fileTypeCombo.setDuplicatesEnabled(False)
- self.__fileTypeCombo.setSizeAdjustPolicy(qt.QComboBox.AdjustToMinimumContentsLength)
- self.__fileTypeCombo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
- self.__fileTypeCombo.activated[int].connect(self.__filterSelected)
- self.__fileTypeCombo.setFabioUrlSupproted(self._isFabioFilesSupported())
-
- self.__pathEdit = _PathEdit(self)
- self.__pathEdit.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
- self.__pathEdit.textChanged.connect(self.__textChanged)
- self.__pathEdit.setObjectName("url")
- utils.patchToConsumeReturnKey(self.__pathEdit)
-
- self.__buttons = qt.QDialogButtonBox(self)
- self.__buttons.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
- types = qt.QDialogButtonBox.Open | qt.QDialogButtonBox.Cancel
- self.__buttons.setStandardButtons(types)
- self.__buttons.button(qt.QDialogButtonBox.Cancel).setObjectName("cancel")
- self.__buttons.button(qt.QDialogButtonBox.Open).setObjectName("open")
-
- self.__buttons.accepted.connect(self.accept)
- self.__buttons.rejected.connect(self.reject)
-
- self.__browseToolBar = self._createBrowseToolBar()
- self.__backwardAction.setEnabled(False)
- self.__forwardAction.setEnabled(False)
- self.__fileDirectoryAction.setEnabled(False)
- self.__parentFileDirectoryAction.setEnabled(False)
-
- self.__selectorWidget = self._createSelectorWidget(self)
- if self.__selectorWidget is not None:
- self.__selectorWidget.selectionChanged.connect(self.__selectorWidgetChanged)
-
- self.__previewToolBar = self._createPreviewToolbar(self, self.__previewWidget, self.__selectorWidget)
-
- self.__dataIcon = qt.QLabel(self)
- self.__dataIcon.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
- self.__dataIcon.setScaledContents(True)
- self.__dataIcon.setMargin(2)
- self.__dataIcon.setAlignment(qt.Qt.AlignCenter)
-
- self.__dataInfo = qt.QLabel(self)
- self.__dataInfo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
-
- def _createSideBar(self):
- sidebar = _SideBar(self)
- sidebar.setObjectName("sidebar")
- return sidebar
-
- def iconProvider(self):
- iconProvider = self.__class__._defaultIconProvider
- if iconProvider is None:
- iconProvider = _IconProvider()
- self.__class__._defaultIconProvider = iconProvider
- return iconProvider
-
- def _createBrowseToolBar(self):
- toolbar = qt.QToolBar(self)
- toolbar.setIconSize(qt.QSize(16, 16))
- iconProvider = self.iconProvider()
-
- backward = qt.QAction(toolbar)
- backward.setText("Back")
- backward.setObjectName("backwardAction")
- backward.setIcon(iconProvider.icon(qt.QStyle.SP_ArrowBack))
- backward.triggered.connect(self.__navigateBackward)
- self.__backwardAction = backward
-
- forward = qt.QAction(toolbar)
- forward.setText("Forward")
- forward.setObjectName("forwardAction")
- forward.setIcon(iconProvider.icon(qt.QStyle.SP_ArrowForward))
- forward.triggered.connect(self.__navigateForward)
- self.__forwardAction = forward
-
- parentDirectory = qt.QAction(toolbar)
- parentDirectory.setText("Go to parent")
- parentDirectory.setObjectName("toParentAction")
- parentDirectory.setIcon(iconProvider.icon(qt.QStyle.SP_FileDialogToParent))
- parentDirectory.triggered.connect(self.__navigateToParent)
- self.__toParentAction = parentDirectory
-
- fileDirectory = qt.QAction(toolbar)
- fileDirectory.setText("Root of the file")
- fileDirectory.setObjectName("toRootFileAction")
- fileDirectory.setIcon(iconProvider.icon(iconProvider.FileDialogToParentFile))
- fileDirectory.triggered.connect(self.__navigateToParentFile)
- self.__fileDirectoryAction = fileDirectory
-
- parentFileDirectory = qt.QAction(toolbar)
- parentFileDirectory.setText("Parent directory of the file")
- parentFileDirectory.setObjectName("toDirectoryAction")
- parentFileDirectory.setIcon(iconProvider.icon(iconProvider.FileDialogToParentDir))
- parentFileDirectory.triggered.connect(self.__navigateToParentDir)
- self.__parentFileDirectoryAction = parentFileDirectory
-
- listView = qt.QAction(toolbar)
- listView.setText("List view")
- listView.setObjectName("listModeAction")
- listView.setIcon(iconProvider.icon(qt.QStyle.SP_FileDialogListView))
- listView.triggered.connect(self.__showAsListView)
- listView.setCheckable(True)
-
- detailView = qt.QAction(toolbar)
- detailView.setText("Detail view")
- detailView.setObjectName("detailModeAction")
- detailView.setIcon(iconProvider.icon(qt.QStyle.SP_FileDialogDetailedView))
- detailView.triggered.connect(self.__showAsDetailedView)
- detailView.setCheckable(True)
-
- self.__listViewAction = listView
- self.__detailViewAction = detailView
-
- toolbar.addAction(backward)
- toolbar.addAction(forward)
- toolbar.addSeparator()
- toolbar.addAction(parentDirectory)
- toolbar.addAction(fileDirectory)
- toolbar.addAction(parentFileDirectory)
- toolbar.addSeparator()
- toolbar.addAction(listView)
- toolbar.addAction(detailView)
-
- toolbar.setStyleSheet("QToolBar { border: 0px }")
-
- return toolbar
-
- def __initLayout(self):
- sideBarLayout = qt.QVBoxLayout()
- sideBarLayout.setContentsMargins(0, 0, 0, 0)
- dummyToolBar = qt.QWidget(self)
- dummyToolBar.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
- dummyCombo = qt.QWidget(self)
- dummyCombo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
- sideBarLayout.addWidget(dummyToolBar)
- if self.__sidebar is not None:
- sideBarLayout.addWidget(self.__sidebar)
- sideBarLayout.addWidget(dummyCombo)
- sideBarWidget = qt.QWidget(self)
- sideBarWidget.setLayout(sideBarLayout)
-
- dummyCombo.setFixedHeight(self.__fileTypeCombo.height())
- self.__resizeCombo = _CatchResizeEvent(self, self.__fileTypeCombo)
- self.__resizeCombo.resized.connect(lambda e: dummyCombo.setFixedHeight(e.size().height()))
-
- dummyToolBar.setFixedHeight(self.__browseToolBar.height())
- self.__resizeToolbar = _CatchResizeEvent(self, self.__browseToolBar)
- self.__resizeToolbar.resized.connect(lambda e: dummyToolBar.setFixedHeight(e.size().height()))
-
- datasetSelection = qt.QWidget(self)
- layoutLeft = qt.QVBoxLayout()
- layoutLeft.setContentsMargins(0, 0, 0, 0)
- layoutLeft.addWidget(self.__browseToolBar)
- layoutLeft.addWidget(self.__browser)
- layoutLeft.addWidget(self.__fileTypeCombo)
- datasetSelection.setLayout(layoutLeft)
- datasetSelection.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Expanding)
-
- infoLayout = qt.QHBoxLayout()
- infoLayout.setContentsMargins(0, 0, 0, 0)
- infoLayout.addWidget(self.__dataIcon)
- infoLayout.addWidget(self.__dataInfo)
-
- dataFrame = qt.QFrame(self)
- dataFrame.setFrameShape(qt.QFrame.StyledPanel)
- layout = qt.QVBoxLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
- layout.addWidget(self.__previewWidget)
- layout.addLayout(infoLayout)
- dataFrame.setLayout(layout)
-
- dataSelection = qt.QWidget(self)
- dataLayout = qt.QVBoxLayout()
- dataLayout.setContentsMargins(0, 0, 0, 0)
- if self.__previewToolBar is not None:
- dataLayout.addWidget(self.__previewToolBar)
- else:
- # Add dummy space
- dummyToolbar2 = qt.QWidget(self)
- dummyToolbar2.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
- dummyToolbar2.setFixedHeight(self.__browseToolBar.height())
- self.__resizeToolbar = _CatchResizeEvent(self, self.__browseToolBar)
- self.__resizeToolbar.resized.connect(lambda e: dummyToolbar2.setFixedHeight(e.size().height()))
- dataLayout.addWidget(dummyToolbar2)
-
- dataLayout.addWidget(dataFrame)
- if self.__selectorWidget is not None:
- dataLayout.addWidget(self.__selectorWidget)
- else:
- # Add dummy space
- dummyCombo2 = qt.QWidget(self)
- dummyCombo2.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
- dummyCombo2.setFixedHeight(self.__fileTypeCombo.height())
- self.__resizeToolbar = _CatchResizeEvent(self, self.__fileTypeCombo)
- self.__resizeToolbar.resized.connect(lambda e: dummyCombo2.setFixedHeight(e.size().height()))
- dataLayout.addWidget(dummyCombo2)
- dataSelection.setLayout(dataLayout)
-
- self.__splitter = qt.QSplitter(self)
- self.__splitter.setContentsMargins(0, 0, 0, 0)
- self.__splitter.addWidget(sideBarWidget)
- self.__splitter.addWidget(datasetSelection)
- self.__splitter.addWidget(dataSelection)
- self.__splitter.setStretchFactor(1, 10)
-
- bottomLayout = qt.QHBoxLayout()
- bottomLayout.setContentsMargins(0, 0, 0, 0)
- bottomLayout.addWidget(self.__pathEdit)
- bottomLayout.addWidget(self.__buttons)
-
- layout = qt.QVBoxLayout(self)
- layout.addWidget(self.__splitter)
- layout.addLayout(bottomLayout)
-
- self.setLayout(layout)
- self.updateGeometry()
-
- # Logic
-
- def __navigateBackward(self):
- """Navigate through the history one step backward."""
- if len(self.__currentHistory) > 0 and self.__currentHistoryLocation > 0:
- self.__currentHistoryLocation -= 1
- url = self.__currentHistory[self.__currentHistoryLocation]
- self.selectUrl(url)
-
- def __navigateForward(self):
- """Navigate through the history one step forward."""
- if len(self.__currentHistory) > 0 and self.__currentHistoryLocation < len(self.__currentHistory) - 1:
- self.__currentHistoryLocation += 1
- url = self.__currentHistory[self.__currentHistoryLocation]
- self.selectUrl(url)
-
- def __navigateToParent(self):
- index = self.__browser.rootIndex()
- if index.model() is self.__fileModel:
- # browse throw the file system
- index = index.parent()
- path = self.__fileModel.filePath(index)
- self.__fileModel_setRootPath(path)
- self.__browser.selectIndex(qt.QModelIndex())
- self.__updatePath()
- elif index.model() is self.__dataModel:
- index = index.parent()
- if index.isValid():
- # browse throw the hdf5
- self.__browser.setRootIndex(index)
- self.__browser.selectIndex(qt.QModelIndex())
- self.__updatePath()
- else:
- # go back to the file system
- self.__navigateToParentDir()
- else:
- # Root of the file system (my computer)
- pass
-
- def __navigateToParentFile(self):
- index = self.__browser.rootIndex()
- if index.model() is self.__dataModel:
- index = self.__dataModel.indexFromH5Object(self.__h5)
- self.__browser.setRootIndex(index)
- self.__browser.selectIndex(qt.QModelIndex())
- self.__updatePath()
-
- def __navigateToParentDir(self):
- index = self.__browser.rootIndex()
- if index.model() is self.__dataModel:
- path = os.path.dirname(self.__h5.file.filename)
- index = self.__fileModel.index(path)
- self.__browser.setRootIndex(index)
- self.__browser.selectIndex(qt.QModelIndex())
- self.__closeFile()
- self.__updatePath()
-
- def viewMode(self):
- """Returns the current view mode.
-
- :rtype: qt.QFileDialog.ViewMode
- """
- return self.__browser.viewMode()
-
- def setViewMode(self, mode):
- """Set the current view mode.
-
- :param qt.QFileDialog.ViewMode mode: The new view mode
- """
- if mode == qt.QFileDialog.Detail:
- self.__browser.showDetails()
- self.__listViewAction.setChecked(False)
- self.__detailViewAction.setChecked(True)
- elif mode == qt.QFileDialog.List:
- self.__browser.showList()
- self.__listViewAction.setChecked(True)
- self.__detailViewAction.setChecked(False)
- else:
- assert(False)
-
- def __showAsListView(self):
- self.setViewMode(qt.QFileDialog.List)
-
- def __showAsDetailedView(self):
- self.setViewMode(qt.QFileDialog.Detail)
-
- def __shortcutSelected(self):
- self.__browser.selectIndex(qt.QModelIndex())
- self.__clearData()
- self.__updatePath()
- selectionModel = self.__sidebar.selectionModel()
- indexes = selectionModel.selectedIndexes()
- if len(indexes) == 1:
- index = indexes[0]
- url = self.__sidebar.model().data(index, role=qt.Qt.UserRole)
- path = url.toLocalFile()
- self.__fileModel_setRootPath(path)
-
- def __browsedItemActivated(self, index):
- if not index.isValid():
- return
- if index.model() is self.__fileModel:
- path = self.__fileModel.filePath(index)
- if self.__fileModel.isDir(index):
- self.__fileModel_setRootPath(path)
- if os.path.isfile(path):
- self.__fileActivated(index)
- elif index.model() is self.__dataModel:
- obj = self.__dataModel.data(index, role=Hdf5TreeModel.H5PY_OBJECT_ROLE)
- if silx.io.is_group(obj):
- self.__browser.setRootIndex(index)
- else:
- assert(False)
-
- def __browsedItemSelected(self, index):
- self.__dataSelected(index)
- self.__updatePath()
-
- def __fileModel_setRootPath(self, path):
- """Set the root path of the fileModel with a filter on the
- directoryLoaded event.
-
- Without this filter an extra event is received (at least with PyQt4)
- when we use for the first time the sidebar.
-
- :param str path: Path to load
- """
- assert(path is not None)
- if path != "" and not os.path.exists(path):
- return
- if self.hasPendingEvents():
- # Make sure the asynchronous fileModel setRootPath is finished
- qt.QApplication.instance().processEvents()
-
- if self.__directoryLoadedFilter is not None:
- if utils.samefile(self.__directoryLoadedFilter, path):
- 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()
-
- def __directoryLoaded(self, path):
- if self.__directoryLoadedFilter is not None:
- if not utils.samefile(self.__directoryLoadedFilter, path):
- # Filter event which should not arrive in PyQt4
- # 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()
- self.__processing -= 1
-
- def __closeFile(self):
- self.__openedFiles[:] = []
- self.__fileDirectoryAction.setEnabled(False)
- self.__parentFileDirectoryAction.setEnabled(False)
- if self.__h5 is not None:
- self.__dataModel.removeH5pyObject(self.__h5)
- self.__h5.close()
- self.__h5 = None
- if self.__fabio is not None:
- if hasattr(self.__fabio, "close"):
- self.__fabio.close()
- self.__fabio = None
-
- def __openFabioFile(self, filename):
- self.__closeFile()
- try:
- self.__fabio = fabio.open(filename)
- self.__openedFiles.append(self.__fabio)
- self.__selectedFile = filename
- except Exception as e:
- _logger.error("Error while loading file %s: %s", filename, e.args[0])
- _logger.debug("Backtrace", exc_info=True)
- self.__errorWhileLoadingFile = filename, e.args[0]
- return False
- else:
- return True
-
- def __openSilxFile(self, filename):
- self.__closeFile()
- try:
- self.__h5 = silx.io.open(filename)
- self.__openedFiles.append(self.__h5)
- self.__selectedFile = filename
- except IOError as e:
- _logger.error("Error while loading file %s: %s", filename, e.args[0])
- _logger.debug("Backtrace", exc_info=True)
- self.__errorWhileLoadingFile = filename, e.args[0]
- return False
- else:
- self.__fileDirectoryAction.setEnabled(True)
- self.__parentFileDirectoryAction.setEnabled(True)
- self.__dataModel.insertH5pyObject(self.__h5)
- return True
-
- def __isSilxHavePriority(self, filename):
- """Silx have priority when there is a specific decoder
- """
- _, ext = os.path.splitext(filename)
- ext = "*%s" % ext
- formats = silx.io.supported_extensions(flat_formats=False)
- for extensions in formats.values():
- if ext in extensions:
- return True
- return False
-
- def __openFile(self, filename):
- codec = self.__fileTypeCombo.currentCodec()
- openners = []
- if codec.is_autodetect():
- if self.__isSilxHavePriority(filename):
- openners.append(self.__openSilxFile)
- if self._isFabioFilesSupported():
- openners.append(self.__openFabioFile)
- else:
- if self._isFabioFilesSupported():
- openners.append(self.__openFabioFile)
- openners.append(self.__openSilxFile)
- elif codec.is_silx_codec():
- openners.append(self.__openSilxFile)
- elif self._isFabioFilesSupported() and codec.is_fabio_codec():
- # It is requested to use fabio, anyway fabio is here or not
- openners.append(self.__openFabioFile)
-
- for openner in openners:
- ref = openner(filename)
- if ref is not None:
- return True
- return False
-
- def __fileActivated(self, index):
- self.__selectedFile = None
- path = self.__fileModel.filePath(index)
- if os.path.isfile(path):
- loaded = self.__openFile(path)
- if loaded:
- if self.__h5 is not None:
- index = self.__dataModel.indexFromH5Object(self.__h5)
- self.__browser.setRootIndex(index)
- elif self.__fabio is not None:
- data = _FabioData(self.__fabio)
- self.__setData(data)
- self.__updatePath()
- else:
- self.__clearData()
-
- def __dataSelected(self, index):
- selectedData = None
- if index is not None:
- if index.model() is self.__dataModel:
- obj = self.__dataModel.data(index, self.__dataModel.H5PY_OBJECT_ROLE)
- if self._isDataSupportable(obj):
- selectedData = obj
- elif index.model() is self.__fileModel:
- self.__closeFile()
- if self._isFabioFilesSupported():
- path = self.__fileModel.filePath(index)
- if os.path.isfile(path):
- codec = self.__fileTypeCombo.currentCodec()
- is_fabio_decoder = codec.is_fabio_codec()
- 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
- self.__openFabioFile(path)
- if self.__fabio is not None:
- selectedData = _FabioData(self.__fabio)
- else:
- assert(False)
-
- self.__setData(selectedData)
-
- def __filterSelected(self, index):
- filters = self.__fileTypeCombo.itemExtensions(index)
- self.__fileModel.setNameFilters(list(filters))
-
- def __setData(self, data):
- self.__data = data
-
- if data is not None and self._isDataSupportable(data):
- if self.__selectorWidget is not None:
- self.__selectorWidget.setData(data)
- if not self.__selectorWidget.isUsed():
- # Needed to fake the fact we have to reset the zoom in preview
- self.__selectedData = None
- self.__setSelectedData(data)
- self.__selectorWidget.hide()
- else:
- self.__selectorWidget.setVisible(self.__selectorWidget.hasVisibleSelectors())
- # Needed to fake the fact we have to reset the zoom in preview
- self.__selectedData = None
- self.__selectorWidget.selectionChanged.emit()
- else:
- # Needed to fake the fact we have to reset the zoom in preview
- self.__selectedData = None
- self.__setSelectedData(data)
- else:
- self.__clearData()
- self.__updatePath()
-
- def _isDataSupported(self, data):
- """Check if the data can be returned by the dialog.
-
- If true, this data can be returned by the dialog and the open button
- while be enabled. If false the button will be disabled.
-
- :rtype: bool
- """
- raise NotImplementedError()
-
- def _isDataSupportable(self, data):
- """Check if the selected data can be supported at one point.
-
- If true, the data selector will be checked and it will update the data
- preview. Else the selecting is disabled.
-
- :rtype: bool
- """
- raise NotImplementedError()
-
- def __clearData(self):
- """Clear the data part of the GUI"""
- 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
- self.__updateDataInfo()
- button = self.__buttons.button(qt.QDialogButtonBox.Open)
- button.setEnabled(False)
-
- def __selectorWidgetChanged(self):
- data = self.__selectorWidget.getSelectedData(self.__data)
- self.__setSelectedData(data)
-
- def __setSelectedData(self, data):
- """Set the data selected by the dialog.
-
- 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)
- if self._isDataSupported(data):
- self.__selectedData = data
- else:
- self.__clearData()
- return
- self.__updateDataInfo()
- self.__updatePath()
- button = self.__buttons.button(qt.QDialogButtonBox.Open)
- button.setEnabled(True)
-
- def __updateDataInfo(self):
- if self.__errorWhileLoadingFile is not None:
- filename, message = self.__errorWhileLoadingFile
- message = "<b>Error while loading file '%s'</b><hr/>%s" % (filename, message)
- size = self.__dataInfo.height()
- icon = self.style().standardIcon(qt.QStyle.SP_MessageBoxCritical)
- pixmap = icon.pixmap(size, size)
-
- self.__dataInfo.setText("Error while loading file")
- self.__dataInfo.setToolTip(message)
- self.__dataIcon.setToolTip(message)
- self.__dataIcon.setVisible(True)
- self.__dataIcon.setPixmap(pixmap)
-
- self.__errorWhileLoadingFile = None
- return
-
- self.__dataIcon.setVisible(False)
- self.__dataInfo.setToolTip("")
- if self.__selectedData is None:
- self.__dataInfo.setText("No data selected")
- else:
- text = self._displayedDataInfo(self.__data, self.__selectedData)
- self.__dataInfo.setVisible(text is not None)
- if text is not None:
- self.__dataInfo.setText(text)
-
- def _displayedDataInfo(self, dataBeforeSelection, dataAfterSelection):
- """Returns the text displayed under the data preview.
-
- This zone is used to display error in case or problem of data selection
- or problems with IO.
-
- :param numpy.ndarray dataAfterSelection: Data as it is after the
- selection widget (basically the data from the preview widget)
- :param numpy.ndarray dataAfterSelection: Data as it is before the
- selection widget (basically the data from the browsing widget)
- :rtype: bool
- """
- return None
-
- def __createUrlFromIndex(self, index, useSelectorWidget=True):
- if index.model() is self.__fileModel:
- filename = self.__fileModel.filePath(index)
- dataPath = None
- elif index.model() is self.__dataModel:
- obj = self.__dataModel.data(index, role=Hdf5TreeModel.H5PY_OBJECT_ROLE)
- filename = obj.file.filename
- dataPath = obj.name
- else:
- # root of the computer
- filename = ""
- dataPath = None
-
- if useSelectorWidget and self.__selectorWidget is not None and self.__selectorWidget.isUsed():
- slicing = self.__selectorWidget.slicing()
- if slicing == tuple():
- slicing = None
- else:
- slicing = None
-
- if self.__fabio is not None:
- scheme = "fabio"
- elif self.__h5 is not None:
- scheme = "silx"
- else:
- if os.path.isfile(filename):
- codec = self.__fileTypeCombo.currentCodec()
- if codec.is_fabio_codec():
- scheme = "fabio"
- elif codec.is_silx_codec():
- scheme = "silx"
- else:
- scheme = None
- else:
- scheme = None
-
- url = silx.io.url.DataUrl(file_path=filename, data_path=dataPath, data_slice=slicing, scheme=scheme)
- return url
-
- def __updatePath(self):
- index = self.__browser.selectedIndex()
- if index is None:
- index = self.__browser.rootIndex()
- url = self.__createUrlFromIndex(index)
- if url.path() != self.__pathEdit.text():
- old = self.__pathEdit.blockSignals(True)
- self.__pathEdit.setText(url.path())
- self.__pathEdit.blockSignals(old)
-
- def __rootIndexChanged(self, index):
- url = self.__createUrlFromIndex(index, useSelectorWidget=False)
-
- currentUrl = None
- if 0 <= self.__currentHistoryLocation < len(self.__currentHistory):
- currentUrl = self.__currentHistory[self.__currentHistoryLocation]
-
- if currentUrl is None or currentUrl != url.path():
- # clean up the forward history
- self.__currentHistory = self.__currentHistory[0:self.__currentHistoryLocation + 1]
- self.__currentHistory.append(url.path())
- self.__currentHistoryLocation += 1
-
- if index.model() != self.__dataModel:
- if sys.platform == "win32":
- # path == ""
- isRoot = not index.isValid()
- else:
- # path in ["", "/"]
- isRoot = not index.isValid() or not index.parent().isValid()
- else:
- isRoot = False
-
- if index.isValid():
- self.__dataSelected(index)
- self.__toParentAction.setEnabled(not isRoot)
- self.__updateActionHistory()
- self.__updateSidebar()
-
- def __updateSidebar(self):
- """Called when the current directory location change"""
- if self.__sidebar is None:
- return
- selectionModel = self.__sidebar.selectionModel()
- selectionModel.selectionChanged.disconnect(self.__shortcutSelected)
- index = self.__browser.rootIndex()
- if index.model() == self.__fileModel:
- path = self.__fileModel.filePath(index)
- self.__sidebar.setSelectedPath(path)
- elif index.model() is None:
- path = ""
- self.__sidebar.setSelectedPath(path)
- else:
- selectionModel.clear()
- selectionModel.selectionChanged.connect(self.__shortcutSelected)
-
- def __updateActionHistory(self):
- self.__forwardAction.setEnabled(len(self.__currentHistory) - 1 > self.__currentHistoryLocation)
- self.__backwardAction.setEnabled(self.__currentHistoryLocation > 0)
-
- def __textChanged(self, text):
- self.__pathChanged()
-
- def _isFabioFilesSupported(self):
- """Returns true fabio files can be loaded.
- """
- return True
-
- def _isLoadableUrl(self, url):
- """Returns true if the URL is loadable by this dialog.
-
- :param DataUrl url: The requested URL
- """
- return True
-
- def __pathChanged(self):
- url = silx.io.url.DataUrl(path=self.__pathEdit.text())
- if url.is_valid() or url.path() == "":
- if url.path() in ["", "/"] or url.file_path() in ["", "/"]:
- self.__fileModel_setRootPath(qt.QDir.rootPath())
- elif os.path.exists(url.file_path()):
- rootIndex = None
- if os.path.isdir(url.file_path()):
- self.__fileModel_setRootPath(url.file_path())
- index = self.__fileModel.index(url.file_path())
- elif os.path.isfile(url.file_path()):
- if self._isLoadableUrl(url):
- if url.scheme() == "silx":
- loaded = self.__openSilxFile(url.file_path())
- elif url.scheme() == "fabio" and self._isFabioFilesSupported():
- loaded = self.__openFabioFile(url.file_path())
- else:
- loaded = self.__openFile(url.file_path())
- else:
- loaded = False
- if loaded:
- if self.__h5 is not None:
- rootIndex = self.__dataModel.indexFromH5Object(self.__h5)
- elif self.__fabio is not None:
- index = self.__fileModel.index(url.file_path())
- rootIndex = index
- if rootIndex is None:
- index = self.__fileModel.index(url.file_path())
- index = index.parent()
-
- if rootIndex is not None:
- if rootIndex.model() == self.__dataModel:
- if url.data_path() is not None:
- dataPath = url.data_path()
- if dataPath in self.__h5:
- obj = self.__h5[dataPath]
- else:
- path = utils.findClosestSubPath(self.__h5, dataPath)
- if path is None:
- path = "/"
- obj = self.__h5[path]
-
- if silx.io.is_file(obj):
- self.__browser.setRootIndex(rootIndex)
- elif silx.io.is_group(obj):
- index = self.__dataModel.indexFromH5Object(obj)
- self.__browser.setRootIndex(index)
- else:
- index = self.__dataModel.indexFromH5Object(obj)
- self.__browser.setRootIndex(index.parent())
- self.__browser.selectIndex(index)
- else:
- self.__browser.setRootIndex(rootIndex)
- self.__clearData()
- elif rootIndex.model() == self.__fileModel:
- # that's a fabio file
- self.__browser.setRootIndex(rootIndex.parent())
- self.__browser.selectIndex(rootIndex)
- # data = _FabioData(self.__fabio)
- # self.__setData(data)
- else:
- assert(False)
- else:
- self.__browser.setRootIndex(index, model=self.__fileModel)
- self.__clearData()
-
- if self.__selectorWidget is not None:
- self.__selectorWidget.selectSlicing(url.data_slice())
- else:
- self.__errorWhileLoadingFile = (url.file_path(), "File not found")
- self.__clearData()
- else:
- self.__errorWhileLoadingFile = (url.file_path(), "Path invalid")
- self.__clearData()
-
- def previewToolbar(self):
- return self.__previewToolbar
-
- def previewWidget(self):
- return self.__previewWidget
-
- def selectorWidget(self):
- return self.__selectorWidget
-
- def _createPreviewToolbar(self, parent, dataPreviewWidget, dataSelectorWidget):
- return None
-
- def _createPreviewWidget(self, parent):
- return None
-
- def _createSelectorWidget(self, parent):
- return None
-
- # Selected file
-
- def setDirectory(self, path):
- """Sets the data dialog's current directory."""
- self.__fileModel_setRootPath(path)
-
- def selectedFile(self):
- """Returns the file path containing the selected data.
-
- :rtype: str
- """
- return self.__selectedFile
-
- def selectFile(self, filename):
- """Sets the data dialog's current file."""
- self.__directoryLoadedFilter = ""
- old = self.__pathEdit.blockSignals(True)
- try:
- self.__pathEdit.setText(filename)
- finally:
- self.__pathEdit.blockSignals(old)
- self.__pathChanged()
-
- # Selected data
-
- def selectUrl(self, url):
- """Sets the data dialog's current data url.
-
- :param Union[str,DataUrl] url: URL identifying a data (it can be a
- `DataUrl` object)
- """
- if isinstance(url, silx.io.url.DataUrl):
- url = url.path()
- self.__directoryLoadedFilter = ""
- old = self.__pathEdit.blockSignals(True)
- try:
- self.__pathEdit.setText(url)
- finally:
- self.__pathEdit.blockSignals(old)
- self.__pathChanged()
-
- def selectedUrl(self):
- """Returns the URL from the file system to the data.
-
- If the dialog is not validated, the path can be an intermediat
- selected path, or an invalid path.
-
- :rtype: str
- """
- return self.__pathEdit.text()
-
- def selectedDataUrl(self):
- """Returns the URL as a :class:`DataUrl` from the file system to the
- data.
-
- If the dialog is not validated, the path can be an intermediat
- selected path, or an invalid path.
-
- :rtype: DataUrl
- """
- url = self.selectedUrl()
- return silx.io.url.DataUrl(url)
-
- def directory(self):
- """Returns the path from the current browsed directory.
-
- :rtype: str
- """
- if self.__directory is not None:
- # At post execution, returns the cache
- return self.__directory
-
- index = self.__browser.rootIndex()
- if index.model() is self.__fileModel:
- path = self.__fileModel.filePath(index)
- return path
- elif index.model() is self.__dataModel:
- path = os.path.dirname(self.__h5.file.filename)
- return path
- else:
- return ""
-
- def _selectedData(self):
- """Returns the internal selected data
-
- :rtype: numpy.ndarray
- """
- return self.__selectedData
-
- # Filters
-
- def selectedNameFilter(self):
- """Returns the filter that the user selected in the file dialog."""
- return self.__fileTypeCombo.currentText()
-
- # History
-
- def history(self):
- """Returns the browsing history of the filedialog as a list of paths.
-
- :rtype: List<str>
- """
- if len(self.__currentHistory) <= 1:
- return []
- history = self.__currentHistory[0:self.__currentHistoryLocation]
- return list(history)
-
- def setHistory(self, history):
- self.__currentHistory = []
- self.__currentHistory.extend(history)
- self.__currentHistoryLocation = len(self.__currentHistory) - 1
- self.__updateActionHistory()
-
- # Colormap
-
- def colormap(self):
- if self.__previewWidget is None:
- return None
- return self.__previewWidget.colormap()
-
- def setColormap(self, colormap):
- if self.__previewWidget is None:
- raise RuntimeError("No preview widget defined")
- self.__previewWidget.setColormap(colormap)
-
- # Sidebar
-
- def setSidebarUrls(self, urls):
- """Sets the urls that are located in the sidebar."""
- if self.__sidebar is None:
- return
- self.__sidebar.setUrls(urls)
-
- def sidebarUrls(self):
- """Returns a list of urls that are currently in the sidebar."""
- if self.__sidebar is None:
- return []
- return self.__sidebar.urls()
-
- # State
-
- __serialVersion = 1
- """Store the current version of the serialized data"""
-
- @classmethod
- def qualifiedName(cls):
- return "%s.%s" % (cls.__module__, cls.__name__)
-
- def restoreState(self, state):
- """Restores the dialogs's layout, history and current directory to the
- state specified.
-
- :param qt.QByteArray state: Stream containing the new state
- :rtype: bool
- """
- stream = qt.QDataStream(state, qt.QIODevice.ReadOnly)
-
- qualifiedName = stream.readQString()
- if qualifiedName != self.qualifiedName():
- _logger.warning("Stored state contains an invalid qualified name. %s restoration cancelled.", self.__class__.__name__)
- return False
-
- version = stream.readInt32()
- if version != self.__serialVersion:
- _logger.warning("Stored state contains an invalid version. %s restoration cancelled.", self.__class__.__name__)
- return False
-
- result = True
-
- splitterData = stream.readQVariant()
- sidebarUrls = stream.readQStringList()
- history = stream.readQStringList()
- workingDirectory = stream.readQString()
- browserData = stream.readQVariant()
- viewMode = stream.readInt32()
- colormapData = stream.readQVariant()
-
- result &= self.__splitter.restoreState(splitterData)
- sidebarUrls = [qt.QUrl(s) for s in sidebarUrls]
- self.setSidebarUrls(list(sidebarUrls))
- history = [s for s in history]
- self.setHistory(list(history))
- if workingDirectory is not None:
- self.setDirectory(workingDirectory)
- result &= self.__browser.restoreState(browserData)
- self.setViewMode(viewMode)
- colormap = self.colormap()
- if colormap is not None:
- result &= self.colormap().restoreState(colormapData)
-
- return result
-
- def saveState(self):
- """Saves the state of the dialog's layout, history and current
- directory.
-
- :rtype: qt.QByteArray
- """
- data = qt.QByteArray()
- stream = qt.QDataStream(data, qt.QIODevice.WriteOnly)
-
- s = self.qualifiedName()
- stream.writeQString(u"%s" % s)
- stream.writeInt32(self.__serialVersion)
- stream.writeQVariant(self.__splitter.saveState())
- strings = [u"%s" % s.toString() for s in self.sidebarUrls()]
- stream.writeQStringList(strings)
- strings = [u"%s" % s for s in self.history()]
- stream.writeQStringList(strings)
- stream.writeQString(u"%s" % self.directory())
- stream.writeQVariant(self.__browser.saveState())
- stream.writeInt32(self.viewMode())
- colormap = self.colormap()
- if colormap is not None:
- stream.writeQVariant(self.colormap().saveState())
- else:
- stream.writeQVariant(None)
-
- return data
diff --git a/silx/gui/dialog/ColormapDialog.py b/silx/gui/dialog/ColormapDialog.py
deleted file mode 100644
index ca7ee97..0000000
--- a/silx/gui/dialog/ColormapDialog.py
+++ /dev/null
@@ -1,1771 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2020 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.
-#
-# ###########################################################################*/
-"""A QDialog widget to set-up the colormap.
-
-It uses a description of colormaps as dict compatible with :class:`Plot`.
-
-To run the following sample code, a QApplication must be initialized.
-
-Create the colormap dialog and set the colormap description and data range:
-
->>> from silx.gui.dialog.ColormapDialog import ColormapDialog
->>> from silx.gui.colors import Colormap
-
->>> dialog = ColormapDialog()
->>> colormap = Colormap(name='red', normalization='log',
-... vmin=1., vmax=2.)
-
->>> dialog.setColormap(colormap)
->>> colormap.setVRange(1., 100.) # This scale the width of the plot area
->>> dialog.show()
-
-Get the colormap description (compatible with :class:`Plot`) from the dialog:
-
->>> cmap = dialog.getColormap()
->>> cmap.getName()
-'red'
-
-It is also possible to display an histogram of the image in the dialog.
-This updates the data range with the range of the bins.
-
->>> import numpy
->>> image = numpy.random.normal(size=512 * 512).reshape(512, -1)
->>> hist, bin_edges = numpy.histogram(image, bins=10)
->>> dialog.setHistogram(hist, bin_edges)
-
-The updates of the colormap description are also available through the signal:
-:attr:`ColormapDialog.sigColormapChanged`.
-""" # noqa
-
-__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
-__license__ = "MIT"
-__date__ = "08/12/2020"
-
-import enum
-import logging
-
-import numpy
-
-from .. import qt
-from .. import utils
-from ..colors import Colormap, cursorColorForColormap
-from ..plot import PlotWidget
-from ..plot.items.axis import Axis
-from ..plot.items import BoundingRect
-from silx.gui.widgets.FloatEdit import FloatEdit
-import weakref
-from silx.math.combo import min_max
-from silx.gui.plot import items
-from silx.gui import icons
-from silx.gui.qt import inspect as qtinspect
-from silx.gui.widgets.ColormapNameComboBox import ColormapNameComboBox
-from silx.gui.widgets.WaitingPushButton import WaitingPushButton
-from silx.math.histogram import Histogramnd
-from silx.utils import deprecation
-from silx.gui.plot.items.roi import RectangleROI
-from silx.gui.plot.tools.roi import RegionOfInterestManager
-
-_logger = logging.getLogger(__name__)
-
-_colormapIconPreview = {}
-
-
-class _DataRefHolder(items.Item, items.ColormapMixIn):
- """Holder for a weakref of a numpy array.
-
- It provides features from `ColormapMixIn`.
- """
-
- def __init__(self, dataRef):
- items.Item.__init__(self)
- items.ColormapMixIn.__init__(self)
- self.__dataRef = dataRef
- self._updated(items.ItemChangedType.DATA)
-
- def getColormappedData(self, copy=True):
- return self.__dataRef()
-
-
-class _BoundaryWidget(qt.QWidget):
- """Widget to edit a boundary of the colormap (vmin or vmax)"""
-
- sigAutoScaleChanged = qt.Signal(object)
- """Signal emitted when the autoscale was changed
-
- True is sent as an argument if autoscale is set to true.
- """
-
- sigValueChanged = qt.Signal(object)
- """Signal emitted when value is changed
-
- The new value is sent as an argument.
- """
-
- def __init__(self, parent=None, value=0.0):
- qt.QWidget.__init__(self, parent=parent)
- self.setLayout(qt.QHBoxLayout())
- self.layout().setContentsMargins(0, 0, 0, 0)
- self._numVal = FloatEdit(parent=self, value=value)
- self.layout().addWidget(self._numVal)
- self._autoCB = qt.QCheckBox('auto', parent=self)
- self.layout().addWidget(self._autoCB)
- self._autoCB.setChecked(False)
- self._autoCB.setVisible(False)
-
- self._autoCB.toggled.connect(self._autoToggled)
- self._numVal.textEdited.connect(self.__textEdited)
- self._numVal.editingFinished.connect(self.__editingFinished)
- self.setFocusProxy(self._numVal)
-
- self.__textWasEdited = False
- """True if the text was edited, in order to send an event
- at the end of the user interaction"""
-
- self.__realValue = None
- """Store the real value set by setValue, to avoid
- rounding of the widget"""
-
- def __textEdited(self):
- self.__textWasEdited = True
-
- def __editingFinished(self):
- if self.__textWasEdited:
- value = self._numVal.value()
- self.__realValue = value
- with utils.blockSignals(self._numVal):
- # Fix the formatting
- self._numVal.setValue(self.__realValue)
- self.sigValueChanged.emit(value)
- self.__textWasEdited = False
-
- def isAutoChecked(self):
- return self._autoCB.isChecked()
-
- def getValue(self):
- """Returns the stored range. If autoscale is
- enabled, this returns None.
- """
- if self._autoCB.isChecked():
- return None
- if self.__realValue is not None:
- return self.__realValue
- return self._numVal.value()
-
- def _autoToggled(self, enabled):
- self._numVal.setEnabled(not enabled)
- self._updateDisplayedText()
- self.sigAutoScaleChanged.emit(enabled)
-
- def _updateDisplayedText(self):
- self.__textWasEdited = False
- if self._autoCB.isChecked() and self.__realValue is not None:
- with utils.blockSignals(self._numVal):
- self._numVal.setValue(self.__realValue)
-
- def setValue(self, value, isAuto=False):
- """Set the value of the boundary.
-
- :param float value: A finite value for the boundary
- :param bool isAuto: If true, the finite value was automatically computed
- from the data, else it is a fixed custom value.
- """
- assert value is not None
- self._autoCB.setChecked(isAuto)
- with utils.blockSignals(self._numVal):
- if isAuto or self.__realValue != value:
- if not self.__textWasEdited:
- self._numVal.setValue(value)
- self.__realValue = value
- self._numVal.setEnabled(not isAuto)
-
-
-class _AutoscaleModeComboBox(qt.QComboBox):
-
- DATA = {
- Colormap.MINMAX: ("Min/max", "Use the data min/max"),
- Colormap.STDDEV3: ("Mean ± 3 × stddev", "Use the data mean ± 3 × standard deviation"),
- }
-
- def __init__(self, parent: qt.QWidget):
- super(_AutoscaleModeComboBox, self).__init__(parent=parent)
- self.currentIndexChanged.connect(self.__updateTooltip)
- self._init()
-
- def _init(self):
- for mode in Colormap.AUTOSCALE_MODES:
- label, tooltip = self.DATA.get(mode, (mode, None))
- self.addItem(label, mode)
- if tooltip is not None:
- self.setItemData(self.count() - 1, tooltip, qt.Qt.ToolTipRole)
-
- def setCurrentIndex(self, index):
- self.__updateTooltip(index)
- super(_AutoscaleModeComboBox, self).setCurrentIndex(index)
-
- def __updateTooltip(self, index):
- if index > -1:
- tooltip = self.itemData(index, qt.Qt.ToolTipRole)
- else:
- tooltip = ""
- self.setToolTip(tooltip)
-
- def currentMode(self):
- index = self.currentIndex()
- return self.itemData(index)
-
- def setCurrentMode(self, mode):
- for index in range(self.count()):
- if mode == self.itemData(index):
- self.setCurrentIndex(index)
- return
- if mode is None:
- # If None was not a value
- self.setCurrentIndex(-1)
- return
- self.addItem(mode, mode)
- self.setCurrentIndex(self.count() - 1)
-
-
-class _AutoScaleButtons(qt.QWidget):
-
- autoRangeChanged = qt.Signal(object)
-
- def __init__(self, parent=None):
- qt.QWidget.__init__(self, parent=parent)
- layout = qt.QHBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
-
- self.setFocusPolicy(qt.Qt.NoFocus)
-
- self._bothAuto = qt.QPushButton(self)
- self._bothAuto.setText("Autoscale")
- self._bothAuto.setToolTip("Enable/disable the autoscale for both min and max")
- self._bothAuto.setCheckable(True)
- self._bothAuto.toggled[bool].connect(self.__bothToggled)
- self._bothAuto.setFocusPolicy(qt.Qt.TabFocus)
-
- self._minAuto = qt.QCheckBox(self)
- self._minAuto.setText("")
- self._minAuto.setToolTip("Enable/disable the autoscale for min")
- self._minAuto.toggled[bool].connect(self.__minToggled)
- self._minAuto.setFocusPolicy(qt.Qt.TabFocus)
-
- self._maxAuto = qt.QCheckBox(self)
- self._maxAuto.setText("")
- self._maxAuto.setToolTip("Enable/disable the autoscale for max")
- self._maxAuto.toggled[bool].connect(self.__maxToggled)
- self._maxAuto.setFocusPolicy(qt.Qt.TabFocus)
-
- layout.addStretch(1)
- layout.addWidget(self._minAuto)
- layout.addSpacing(20)
- layout.addWidget(self._bothAuto)
- layout.addSpacing(20)
- layout.addWidget(self._maxAuto)
- layout.addStretch(1)
-
- def __bothToggled(self, checked):
- autoRange = checked, checked
- self.setAutoRange(autoRange)
- self.autoRangeChanged.emit(autoRange)
-
- def __minToggled(self, checked):
- autoRange = self.getAutoRange()
- self.setAutoRange(autoRange)
- self.autoRangeChanged.emit(autoRange)
-
- def __maxToggled(self, checked):
- autoRange = self.getAutoRange()
- self.setAutoRange(autoRange)
- self.autoRangeChanged.emit(autoRange)
-
- def setAutoRangeFromColormap(self, colormap):
- vRange = colormap.getVRange()
- autoRange = vRange[0] is None, vRange[1] is None
- self.setAutoRange(autoRange)
-
- def setAutoRange(self, autoRange):
- if autoRange[0] == autoRange[1]:
- with utils.blockSignals(self._bothAuto):
- self._bothAuto.setChecked(autoRange[0])
- else:
- with utils.blockSignals(self._bothAuto):
- self._bothAuto.setChecked(False)
- with utils.blockSignals(self._minAuto):
- self._minAuto.setChecked(autoRange[0])
- with utils.blockSignals(self._maxAuto):
- self._maxAuto.setChecked(autoRange[1])
-
- def getAutoRange(self):
- return self._minAuto.isChecked(), self._maxAuto.isChecked()
-
-
-@enum.unique
-class _DataInPlotMode(enum.Enum):
- """Enum for each mode of display of the data in the plot."""
- RANGE = 'range'
- HISTOGRAM = 'histogram'
-
-
-class _ColormapHistogram(qt.QWidget):
- """Display the colormap and the data as a plot."""
-
- sigRangeMoving = qt.Signal(object, object)
- """Emitted when a mouse interaction moves the location
- of the colormap range in the plot.
-
- This signal contains 2 elements:
-
- - vmin: A float value if this range was moved, else None
- - vmax: A float value if this range was moved, else None
- """
-
- sigRangeMoved = qt.Signal(object, object)
- """Emitted when a mouse interaction stop.
-
- This signal contains 2 elements:
-
- - vmin: A float value if this range was moved, else None
- - vmax: A float value if this range was moved, else None
- """
-
- def __init__(self, parent):
- qt.QWidget.__init__(self, parent=parent)
- self._dataInPlotMode = _DataInPlotMode.RANGE
- self._finiteRange = None, None
- self._initPlot()
-
- self._histogramData = {}
- """Histogram displayed in the plot"""
-
- self._dragging = False, False
- """True, if the min or the max handle is dragging"""
-
- self._dataRange = {}
- """Histogram displayed in the plot"""
-
- self._invalidated = False
-
- def paintEvent(self, event):
- if self._invalidated:
- self._updateDataInPlot()
- self._invalidated = False
- self._updateMarkerPosition()
- return super(_ColormapHistogram, self).paintEvent(event)
-
- def getFiniteRange(self):
- """Returns the colormap range as displayed in the plot."""
- return self._finiteRange
-
- def setFiniteRange(self, vRange):
- """Set the colormap range to use in the plot.
-
- Here there is no concept of auto. The values should
- not be None, except if there is no range or marker
- to display.
- """
- # Do not reset the limit for handle about to be dragged
- if self._dragging[0]:
- vRange = self._finiteRange[0], vRange[1]
- if self._dragging[1]:
- vRange = vRange[0], self._finiteRange[1]
-
- if vRange == self._finiteRange:
- return
-
- self._finiteRange = vRange
- self.update()
-
- def getColormap(self):
- return self.parent().getColormap()
-
- def _getNormalizedHistogram(self):
- """Return an histogram already normalized according to the colormap
- normalization.
-
- Returns a tuple edges, counts
- """
- norm = self._getNorm()
- histogram = self._histogramData.get(norm, None)
- if histogram is None:
- histogram = self._computeNormalizedHistogram()
- self._histogramData[norm] = histogram
- return histogram
-
- def _computeNormalizedHistogram(self):
- colormap = self.getColormap()
- if colormap is None:
- norm = Colormap.LINEAR
- else:
- norm = colormap.getNormalization()
-
- # Try to use the histogram defined in the dialog
- histo = self.parent()._getHistogram()
- if histo is not None:
- counts, edges = histo
- normalizer = Colormap(normalization=norm)._getNormalizer()
- mask = normalizer.isValid(edges[:-1]) # Check lower bin edges only
- firstValid = numpy.argmax(mask) # edges increases monotonically
- if firstValid == 0: # Mask is all False or all True
- return (counts, edges) if mask[0] else (None, None)
- else: # Clip to valid values
- return counts[firstValid:], edges[firstValid:]
-
- data = self.parent()._getArray()
- if data is None:
- return None, None
- dataRange = self._getNormalizedDataRange()
- if dataRange[0] is None or dataRange[1] is None:
- return None, None
- counts, edges = self.parent().computeHistogram(data, scale=norm, dataRange=dataRange)
- return counts, edges
-
- def _getNormalizedDataRange(self):
- """Return a data range already normalized according to the colormap
- normalization.
-
- Returns a tuple with min and max
- """
- norm = self._getNorm()
- dataRange = self._dataRange.get(norm, None)
- if dataRange is None:
- dataRange = self._computeNormalizedDataRange()
- self._dataRange[norm] = dataRange
- return dataRange
-
- def _computeNormalizedDataRange(self):
- colormap = self.getColormap()
- if colormap is None:
- norm = Colormap.LINEAR
- else:
- norm = colormap.getNormalization()
-
- # Try to use the one defined in the dialog
- dataRange = self.parent()._getDataRange()
- if dataRange is not None:
- if norm in (Colormap.LINEAR, Colormap.GAMMA, Colormap.ARCSINH):
- return dataRange[0], dataRange[2]
- elif norm == Colormap.LOGARITHM:
- return dataRange[1], dataRange[2]
- elif norm == Colormap.SQRT:
- return dataRange[1], dataRange[2]
- else:
- _logger.error("Undefined %s normalization", norm)
-
- # Try to use the histogram defined in the dialog
- histo = self.parent()._getHistogram()
- if histo is not None:
- _histo, edges = histo
- normalizer = Colormap(normalization=norm)._getNormalizer()
- edges = edges[normalizer.isValid(edges)]
- if edges.size == 0:
- return None, None
- else:
- dataRange = min_max(edges, finite=True)
- return dataRange.minimum, dataRange.maximum
-
- item = self.parent()._getItem()
- if item is not None:
- # Trick to reach data range using colormap cache
- cm = Colormap()
- cm.setVRange(None, None)
- cm.setNormalization(norm)
- dataRange = item._getColormapAutoscaleRange(cm)
- return dataRange
-
- # If there is no item, there is no data
- return None, None
-
- def _getDisplayableRange(self):
- """Returns the selected min/max range to apply to the data,
- according to the used scale.
-
- One or both limits can be None in case it is not displayable in the
- current axes scale.
-
- :returns: Tuple{float, float}
- """
- scale = self._plot.getXAxis().getScale()
-
- def isDisplayable(pos):
- if pos is None:
- return False
- if scale == Axis.LOGARITHMIC:
- return pos > 0.0
- return True
-
- posMin, posMax = self.getFiniteRange()
- if not isDisplayable(posMin):
- posMin = None
- if not isDisplayable(posMax):
- posMax = None
-
- return posMin, posMax
-
- def _initPlot(self):
- """Init the plot to display the range and the values"""
- self._plot = PlotWidget(self)
- self._plot.setDataMargins(0.125, 0.125, 0.125, 0.125)
- self._plot.getXAxis().setLabel("Data Values")
- self._plot.getYAxis().setLabel("")
- self._plot.setInteractiveMode('select', zoomOnWheel=False)
- self._plot.setActiveCurveHandling(False)
- self._plot.setMinimumSize(qt.QSize(250, 200))
- self._plot.sigPlotSignal.connect(self._plotEventReceived)
- palette = self.palette()
- color = palette.color(qt.QPalette.Normal, qt.QPalette.Window)
- self._plot.setBackgroundColor(color)
- self._plot.setDataBackgroundColor("white")
-
- lut = numpy.arange(256)
- lut.shape = 1, -1
- self._plot.addImage(lut, legend='lut')
- self._lutItem = self._plot._getItem("image", "lut")
- self._lutItem.setVisible(False)
-
- self._plot.addScatter(x=[], y=[], value=[], legend='lut2')
- self._lutItem2 = self._plot._getItem("scatter", "lut2")
- self._lutItem2.setVisible(False)
- self.__lutY = numpy.array([-0.05] * 256)
- self.__lutV = numpy.arange(256)
-
- self._bound = BoundingRect()
- self._plot.addItem(self._bound)
- self._bound.setVisible(True)
-
- # Add plot for histogram
- self._plotToolbar = qt.QToolBar(self)
- self._plotToolbar.setFloatable(False)
- self._plotToolbar.setMovable(False)
- self._plotToolbar.setIconSize(qt.QSize(8, 8))
- self._plotToolbar.setStyleSheet("QToolBar { border: 0px }")
- self._plotToolbar.setOrientation(qt.Qt.Vertical)
-
- group = qt.QActionGroup(self._plotToolbar)
- group.setExclusive(True)
-
- action = qt.QAction("Data range", self)
- action.setToolTip("Display the data range within the colormap range. A fast data processing have to be done.")
- action.setIcon(icons.getQIcon('colormap-range'))
- action.setCheckable(True)
- action.setData(_DataInPlotMode.RANGE)
- action.setChecked(action.data() == self._dataInPlotMode)
- self._plotToolbar.addAction(action)
- group.addAction(action)
- action = qt.QAction("Histogram", self)
- action.setToolTip("Display the data histogram within the colormap range. A slow data processing have to be done. ")
- action.setIcon(icons.getQIcon('colormap-histogram'))
- action.setCheckable(True)
- action.setData(_DataInPlotMode.HISTOGRAM)
- action.setChecked(action.data() == self._dataInPlotMode)
- self._plotToolbar.addAction(action)
- group.addAction(action)
- group.triggered.connect(self._displayDataInPlotModeChanged)
-
- plotBoxLayout = qt.QHBoxLayout()
- plotBoxLayout.setContentsMargins(0, 0, 0, 0)
- plotBoxLayout.setSpacing(2)
- plotBoxLayout.addWidget(self._plotToolbar)
- plotBoxLayout.addWidget(self._plot)
- plotBoxLayout.setSizeConstraint(qt.QLayout.SetMinimumSize)
- self.setLayout(plotBoxLayout)
-
- def _plotEventReceived(self, event):
- """Handle events from the plot"""
- kind = event['event']
-
- if kind == 'markerMoving':
- value = event['xdata']
- if event['label'] == 'Min':
- self._dragging = True, False
- self._finiteRange = value, self._finiteRange[1]
- self._last = value, None
- self.sigRangeMoving.emit(*self._last)
- elif event['label'] == 'Max':
- self._dragging = False, True
- self._finiteRange = self._finiteRange[0], value
- self._last = None, value
- self.sigRangeMoving.emit(*self._last)
- self._updateLutItem(self._finiteRange)
- elif kind == 'markerMoved':
- self.sigRangeMoved.emit(*self._last)
- self._plot.resetZoom()
- self._dragging = False, False
- else:
- pass
-
- def _updateMarkerPosition(self):
- colormap = self.getColormap()
- posMin, posMax = self._getDisplayableRange()
-
- if colormap is None:
- isDraggable = False
- else:
- isDraggable = colormap.isEditable()
-
- with utils.blockSignals(self):
- if posMin is not None and not self._dragging[0]:
- self._plot.addXMarker(
- posMin,
- legend='Min',
- text='Min',
- draggable=isDraggable,
- color="blue",
- constraint=self._plotMinMarkerConstraint)
- if posMax is not None and not self._dragging[1]:
- self._plot.addXMarker(
- posMax,
- legend='Max',
- text='Max',
- draggable=isDraggable,
- color="blue",
- constraint=self._plotMaxMarkerConstraint)
-
- self._updateLutItem((posMin, posMax))
- self._plot.resetZoom()
-
- def _updateLutItem(self, vRange):
- colormap = self.getColormap()
- if colormap is None:
- return
-
- if vRange is None:
- posMin, posMax = self._getDisplayableRange()
- else:
- posMin, posMax = vRange
- if posMin is None or posMax is None:
- self._lutItem.setVisible(False)
- pos = posMax if posMin is None else posMin
- if pos is not None:
- self._bound.setBounds((pos, pos, -0.1, 0))
- else:
- self._bound.setBounds((0, 0, -0.1, 0))
- else:
- norm = colormap.getNormalization()
- normColormap = colormap.copy()
- normColormap.setVRange(0, 255)
- normColormap.setNormalization(Colormap.LINEAR)
- if norm == Colormap.LINEAR:
- scale = (posMax - posMin) / 256
- self._lutItem.setColormap(normColormap)
- self._lutItem.setOrigin((posMin, -0.09))
- self._lutItem.setScale((scale, 0.08))
- self._lutItem.setVisible(True)
- self._lutItem2.setVisible(False)
- elif norm == Colormap.LOGARITHM:
- self._lutItem2.setVisible(False)
- self._lutItem2.setColormap(normColormap)
- xx = numpy.geomspace(posMin, posMax, 256)
- self._lutItem2.setData(x=xx,
- y=self.__lutY,
- value=self.__lutV,
- copy=False)
- self._lutItem2.setSymbol("|")
- self._lutItem2.setVisible(True)
- self._lutItem.setVisible(False)
- else:
- # Fallback: Display with linear axis and applied normalization
- self._lutItem2.setVisible(False)
- normColormap.setNormalization(norm)
- self._lutItem2.setColormap(normColormap)
- xx = numpy.linspace(posMin, posMax, 256, endpoint=True)
- self._lutItem2.setData(
- x=xx,
- y=self.__lutY,
- value=self.__lutV,
- copy=False)
- self._lutItem2.setSymbol("|")
- self._lutItem2.setVisible(True)
- self._lutItem.setVisible(False)
-
- self._bound.setBounds((posMin, posMax, -0.1, 1))
-
- def _plotMinMarkerConstraint(self, x, y):
- """Constraint of the min marker"""
- _vmin, vmax = self.getFiniteRange()
- if vmax is None:
- return x, y
- return min(x, vmax), y
-
- def _plotMaxMarkerConstraint(self, x, y):
- """Constraint of the max marker"""
- vmin, _vmax = self.getFiniteRange()
- if vmin is None:
- return x, y
- return max(x, vmin), y
-
- def _setDataInPlotMode(self, mode):
- if self._dataInPlotMode == mode:
- return
- self._dataInPlotMode = mode
- self._updateDataInPlot()
-
- def _displayDataInPlotModeChanged(self, action):
- mode = action.data()
- self._setDataInPlotMode(mode)
-
- def invalidateData(self):
- self._histogramData = {}
- self._dataRange = {}
- self._invalidated = True
- self.update()
-
- def _updateDataInPlot(self):
- mode = self._dataInPlotMode
-
- norm = self._getNorm()
- if norm == Colormap.LINEAR:
- scale = Axis.LINEAR
- elif norm == Colormap.LOGARITHM: