dcscope 2.22.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. dcscope/__init__.py +17 -0
  2. dcscope/__main__.py +44 -0
  3. dcscope/_version.py +21 -0
  4. dcscope/extensions.py +250 -0
  5. dcscope/gui/__init__.py +2 -0
  6. dcscope/gui/analysis/__init__.py +10 -0
  7. dcscope/gui/analysis/ana_basins.py +110 -0
  8. dcscope/gui/analysis/ana_basins.ui +143 -0
  9. dcscope/gui/analysis/ana_filter.py +341 -0
  10. dcscope/gui/analysis/ana_filter.ui +364 -0
  11. dcscope/gui/analysis/ana_log.py +122 -0
  12. dcscope/gui/analysis/ana_log.ui +100 -0
  13. dcscope/gui/analysis/ana_meta.py +207 -0
  14. dcscope/gui/analysis/ana_meta.ui +141 -0
  15. dcscope/gui/analysis/ana_plot.py +638 -0
  16. dcscope/gui/analysis/ana_plot.ui +1060 -0
  17. dcscope/gui/analysis/ana_slot.py +504 -0
  18. dcscope/gui/analysis/ana_slot.ui +684 -0
  19. dcscope/gui/analysis/ana_tables.py +227 -0
  20. dcscope/gui/analysis/ana_tables.ui +195 -0
  21. dcscope/gui/analysis/ana_view.py +69 -0
  22. dcscope/gui/analysis/ana_view.ui +269 -0
  23. dcscope/gui/analysis/dlg_slot_reorder.py +55 -0
  24. dcscope/gui/analysis/dlg_slot_reorder.ui +138 -0
  25. dcscope/gui/bulk/__init__.py +2 -0
  26. dcscope/gui/bulk/bulk_emodulus.py +200 -0
  27. dcscope/gui/bulk/bulk_emodulus.ui +245 -0
  28. dcscope/gui/compute/__init__.py +3 -0
  29. dcscope/gui/compute/comp_lme4.py +115 -0
  30. dcscope/gui/compute/comp_lme4.ui +194 -0
  31. dcscope/gui/compute/comp_lme4_dataset.py +51 -0
  32. dcscope/gui/compute/comp_lme4_dataset.ui +92 -0
  33. dcscope/gui/compute/comp_lme4_results.py +120 -0
  34. dcscope/gui/compute/comp_lme4_results.ui +268 -0
  35. dcscope/gui/compute/comp_stats.py +223 -0
  36. dcscope/gui/compute/comp_stats.ui +187 -0
  37. dcscope/gui/dcor/__init__.py +304 -0
  38. dcscope/gui/dcor/dcor.ui +118 -0
  39. dcscope/gui/export/__init__.py +3 -0
  40. dcscope/gui/export/e2data.py +394 -0
  41. dcscope/gui/export/e2data.ui +367 -0
  42. dcscope/gui/export/e2filter.py +76 -0
  43. dcscope/gui/export/e2filter.ui +111 -0
  44. dcscope/gui/export/e2plot.py +94 -0
  45. dcscope/gui/export/e2plot.ui +175 -0
  46. dcscope/gui/main.py +1089 -0
  47. dcscope/gui/main.ui +579 -0
  48. dcscope/gui/matrix/__init__.py +4 -0
  49. dcscope/gui/matrix/block_matrix.py +141 -0
  50. dcscope/gui/matrix/block_matrix.ui +207 -0
  51. dcscope/gui/matrix/data_matrix.py +574 -0
  52. dcscope/gui/matrix/dm_dataset.py +116 -0
  53. dcscope/gui/matrix/dm_dataset.ui +348 -0
  54. dcscope/gui/matrix/dm_element.py +111 -0
  55. dcscope/gui/matrix/dm_element.ui +169 -0
  56. dcscope/gui/matrix/dm_filter.py +115 -0
  57. dcscope/gui/matrix/dm_filter.ui +287 -0
  58. dcscope/gui/matrix/plot_matrix.py +316 -0
  59. dcscope/gui/matrix/pm_element.py +39 -0
  60. dcscope/gui/matrix/pm_plot.py +86 -0
  61. dcscope/gui/matrix/pm_plot.ui +249 -0
  62. dcscope/gui/pipeline_plot.py +797 -0
  63. dcscope/gui/pipeline_plot.ui +66 -0
  64. dcscope/gui/preferences.py +380 -0
  65. dcscope/gui/preferences.ui +554 -0
  66. dcscope/gui/quick_view/__init__.py +1 -0
  67. dcscope/gui/quick_view/qv_main.py +1072 -0
  68. dcscope/gui/quick_view/qv_main.ui +1297 -0
  69. dcscope/gui/quick_view/qv_scatter.py +284 -0
  70. dcscope/gui/quick_view/qv_style.css +10 -0
  71. dcscope/gui/update.py +90 -0
  72. dcscope/gui/widgets/__init__.py +15 -0
  73. dcscope/gui/widgets/bg_thread.py +29 -0
  74. dcscope/gui/widgets/bulk_list.py +79 -0
  75. dcscope/gui/widgets/bulk_list.ui +95 -0
  76. dcscope/gui/widgets/double_spin_box_nan.py +75 -0
  77. dcscope/gui/widgets/feature_combobox.py +140 -0
  78. dcscope/gui/widgets/get_path.py +42 -0
  79. dcscope/gui/widgets/key_value_table_widget.css +12 -0
  80. dcscope/gui/widgets/key_value_table_widget.py +75 -0
  81. dcscope/gui/widgets/mdi_subwindow_wo_close.py +12 -0
  82. dcscope/gui/widgets/qrangeslider.py +262 -0
  83. dcscope/gui/widgets/rangecontrol.py +308 -0
  84. dcscope/gui/widgets/rangecontrol.ui +136 -0
  85. dcscope/gui/widgets/simple_image_view.py +55 -0
  86. dcscope/gui/widgets/simple_plot_widget.py +113 -0
  87. dcscope/gui/widgets/so_colorbaritem.py +29 -0
  88. dcscope/gui/widgets/wait_cursor.py +33 -0
  89. dcscope/idiom.py +26 -0
  90. dcscope/img/__init__.py +2 -0
  91. dcscope/img/icon-theme/breeze/COPYING-ICONS +209 -0
  92. dcscope/img/icon-theme/breeze/COPYING.LIB +510 -0
  93. dcscope/img/icon-theme/breeze/README +5 -0
  94. dcscope/img/icon-theme/breeze/actions/16/application-exit.svg +17 -0
  95. dcscope/img/icon-theme/breeze/actions/16/code-context.svg +13 -0
  96. dcscope/img/icon-theme/breeze/actions/16/dialog-cancel.svg +13 -0
  97. dcscope/img/icon-theme/breeze/actions/16/dialog-close.svg +17 -0
  98. dcscope/img/icon-theme/breeze/actions/16/dialog-messages.svg +13 -0
  99. dcscope/img/icon-theme/breeze/actions/16/dialog-ok-apply.svg +13 -0
  100. dcscope/img/icon-theme/breeze/actions/16/dialog-ok.svg +13 -0
  101. dcscope/img/icon-theme/breeze/actions/16/document-open-folder.svg +13 -0
  102. dcscope/img/icon-theme/breeze/actions/16/document-open.svg +13 -0
  103. dcscope/img/icon-theme/breeze/actions/16/document-save.svg +13 -0
  104. dcscope/img/icon-theme/breeze/actions/16/documentinfo.svg +12 -0
  105. dcscope/img/icon-theme/breeze/actions/16/draw-watercolor.svg +13 -0
  106. dcscope/img/icon-theme/breeze/actions/16/edit-clear-all.svg +13 -0
  107. dcscope/img/icon-theme/breeze/actions/16/edit-clear.svg +13 -0
  108. dcscope/img/icon-theme/breeze/actions/16/edit-paste.svg +13 -0
  109. dcscope/img/icon-theme/breeze/actions/16/globe.svg +16 -0
  110. dcscope/img/icon-theme/breeze/actions/16/gtk-preferences.svg +13 -0
  111. dcscope/img/icon-theme/breeze/actions/16/list-add.svg +13 -0
  112. dcscope/img/icon-theme/breeze/actions/16/messagebox_warning.svg +20 -0
  113. dcscope/img/icon-theme/breeze/actions/16/object-columns.svg +13 -0
  114. dcscope/img/icon-theme/breeze/actions/16/object-order-lower.svg +13 -0
  115. dcscope/img/icon-theme/breeze/actions/16/object-rows.svg +13 -0
  116. dcscope/img/icon-theme/breeze/actions/16/office-chart-line-stacked.svg +13 -0
  117. dcscope/img/icon-theme/breeze/actions/16/office-chart-ring.svg +13 -0
  118. dcscope/img/icon-theme/breeze/actions/16/office-chart-scatter.svg +13 -0
  119. dcscope/img/icon-theme/breeze/actions/16/path-mode-polyline.svg +13 -0
  120. dcscope/img/icon-theme/breeze/actions/16/preferences-activities.svg +13 -0
  121. dcscope/img/icon-theme/breeze/actions/16/remove.svg +10 -0
  122. dcscope/img/icon-theme/breeze/actions/16/search.svg +13 -0
  123. dcscope/img/icon-theme/breeze/actions/16/show-grid.svg +13 -0
  124. dcscope/img/icon-theme/breeze/actions/16/special_paste.svg +13 -0
  125. dcscope/img/icon-theme/breeze/actions/16/tools-wizard.svg +13 -0
  126. dcscope/img/icon-theme/breeze/actions/16/view-calendar-list.svg +13 -0
  127. dcscope/img/icon-theme/breeze/actions/16/view-filter.svg +13 -0
  128. dcscope/img/icon-theme/breeze/actions/16/view-list-tree.svg +13 -0
  129. dcscope/img/icon-theme/breeze/actions/16/view-statistics.svg +13 -0
  130. dcscope/img/icon-theme/breeze/actions/16/visibility.svg +21 -0
  131. dcscope/img/icon-theme/breeze/actions/22/application-exit.svg +13 -0
  132. dcscope/img/icon-theme/breeze/actions/22/code-context.svg +14 -0
  133. dcscope/img/icon-theme/breeze/actions/22/dialog-cancel.svg +14 -0
  134. dcscope/img/icon-theme/breeze/actions/22/dialog-close.svg +10 -0
  135. dcscope/img/icon-theme/breeze/actions/22/dialog-messages.svg +14 -0
  136. dcscope/img/icon-theme/breeze/actions/22/dialog-ok-apply.svg +14 -0
  137. dcscope/img/icon-theme/breeze/actions/22/dialog-ok.svg +14 -0
  138. dcscope/img/icon-theme/breeze/actions/22/document-open-folder.svg +14 -0
  139. dcscope/img/icon-theme/breeze/actions/22/document-open.svg +14 -0
  140. dcscope/img/icon-theme/breeze/actions/22/document-save.svg +14 -0
  141. dcscope/img/icon-theme/breeze/actions/22/documentinfo.svg +14 -0
  142. dcscope/img/icon-theme/breeze/actions/22/draw-watercolor.svg +14 -0
  143. dcscope/img/icon-theme/breeze/actions/22/edit-clear-all.svg +14 -0
  144. dcscope/img/icon-theme/breeze/actions/22/edit-clear.svg +14 -0
  145. dcscope/img/icon-theme/breeze/actions/22/edit-paste.svg +14 -0
  146. dcscope/img/icon-theme/breeze/actions/22/globe.svg +12 -0
  147. dcscope/img/icon-theme/breeze/actions/22/gtk-preferences.svg +13 -0
  148. dcscope/img/icon-theme/breeze/actions/22/list-add.svg +14 -0
  149. dcscope/img/icon-theme/breeze/actions/22/messagebox_warning.svg +17 -0
  150. dcscope/img/icon-theme/breeze/actions/22/object-columns.svg +14 -0
  151. dcscope/img/icon-theme/breeze/actions/22/object-order-lower.svg +17 -0
  152. dcscope/img/icon-theme/breeze/actions/22/object-rows.svg +17 -0
  153. dcscope/img/icon-theme/breeze/actions/22/office-chart-line-stacked.svg +17 -0
  154. dcscope/img/icon-theme/breeze/actions/22/office-chart-ring.svg +17 -0
  155. dcscope/img/icon-theme/breeze/actions/22/office-chart-scatter.svg +17 -0
  156. dcscope/img/icon-theme/breeze/actions/22/path-mode-polyline.svg +17 -0
  157. dcscope/img/icon-theme/breeze/actions/22/preferences-activities.svg +14 -0
  158. dcscope/img/icon-theme/breeze/actions/22/remove.svg +10 -0
  159. dcscope/img/icon-theme/breeze/actions/22/special_paste.svg +14 -0
  160. dcscope/img/icon-theme/breeze/actions/22/tools-wizard.svg +14 -0
  161. dcscope/img/icon-theme/breeze/actions/22/view-calendar-list.svg +14 -0
  162. dcscope/img/icon-theme/breeze/actions/22/view-filter.svg +14 -0
  163. dcscope/img/icon-theme/breeze/actions/22/view-list-tree.svg +14 -0
  164. dcscope/img/icon-theme/breeze/actions/22/view-statistics.svg +14 -0
  165. dcscope/img/icon-theme/breeze/actions/22/visibility.svg +14 -0
  166. dcscope/img/icon-theme/breeze/actions/24/application-exit.svg +12 -0
  167. dcscope/img/icon-theme/breeze/actions/24/code-context.svg +12 -0
  168. dcscope/img/icon-theme/breeze/actions/24/dialog-cancel.svg +12 -0
  169. dcscope/img/icon-theme/breeze/actions/24/dialog-close.svg +10 -0
  170. dcscope/img/icon-theme/breeze/actions/24/dialog-messages.svg +12 -0
  171. dcscope/img/icon-theme/breeze/actions/24/dialog-ok-apply.svg +12 -0
  172. dcscope/img/icon-theme/breeze/actions/24/dialog-ok.svg +14 -0
  173. dcscope/img/icon-theme/breeze/actions/24/document-open-folder.svg +12 -0
  174. dcscope/img/icon-theme/breeze/actions/24/document-open.svg +12 -0
  175. dcscope/img/icon-theme/breeze/actions/24/document-save.svg +12 -0
  176. dcscope/img/icon-theme/breeze/actions/24/documentinfo.svg +14 -0
  177. dcscope/img/icon-theme/breeze/actions/24/draw-watercolor.svg +12 -0
  178. dcscope/img/icon-theme/breeze/actions/24/edit-clear-all.svg +14 -0
  179. dcscope/img/icon-theme/breeze/actions/24/edit-clear.svg +12 -0
  180. dcscope/img/icon-theme/breeze/actions/24/edit-paste.svg +12 -0
  181. dcscope/img/icon-theme/breeze/actions/24/globe.svg +14 -0
  182. dcscope/img/icon-theme/breeze/actions/24/gtk-preferences.svg +13 -0
  183. dcscope/img/icon-theme/breeze/actions/24/list-add.svg +12 -0
  184. dcscope/img/icon-theme/breeze/actions/24/messagebox_warning.svg +13 -0
  185. dcscope/img/icon-theme/breeze/actions/24/object-columns.svg +12 -0
  186. dcscope/img/icon-theme/breeze/actions/24/object-order-lower.svg +15 -0
  187. dcscope/img/icon-theme/breeze/actions/24/object-rows.svg +15 -0
  188. dcscope/img/icon-theme/breeze/actions/24/office-chart-line-stacked.svg +15 -0
  189. dcscope/img/icon-theme/breeze/actions/24/office-chart-ring.svg +15 -0
  190. dcscope/img/icon-theme/breeze/actions/24/office-chart-scatter.svg +15 -0
  191. dcscope/img/icon-theme/breeze/actions/24/path-mode-polyline.svg +15 -0
  192. dcscope/img/icon-theme/breeze/actions/24/preferences-activities.svg +13 -0
  193. dcscope/img/icon-theme/breeze/actions/24/remove.svg +10 -0
  194. dcscope/img/icon-theme/breeze/actions/24/special_paste.svg +12 -0
  195. dcscope/img/icon-theme/breeze/actions/24/tools-wizard.svg +12 -0
  196. dcscope/img/icon-theme/breeze/actions/24/view-calendar-list.svg +12 -0
  197. dcscope/img/icon-theme/breeze/actions/24/view-filter.svg +12 -0
  198. dcscope/img/icon-theme/breeze/actions/24/view-list-tree.svg +12 -0
  199. dcscope/img/icon-theme/breeze/actions/24/view-statistics.svg +12 -0
  200. dcscope/img/icon-theme/breeze/actions/24/visibility.svg +14 -0
  201. dcscope/img/icon-theme/breeze/actions/32/application-exit.svg +8 -0
  202. dcscope/img/icon-theme/breeze/actions/32/dialog-cancel.svg +13 -0
  203. dcscope/img/icon-theme/breeze/actions/32/dialog-messages.svg +13 -0
  204. dcscope/img/icon-theme/breeze/actions/32/dialog-ok-apply.svg +1 -0
  205. dcscope/img/icon-theme/breeze/actions/32/dialog-ok.svg +1 -0
  206. dcscope/img/icon-theme/breeze/actions/32/document-open-folder.svg +13 -0
  207. dcscope/img/icon-theme/breeze/actions/32/document-open.svg +13 -0
  208. dcscope/img/icon-theme/breeze/actions/32/document-save.svg +18 -0
  209. dcscope/img/icon-theme/breeze/actions/32/gtk-preferences.svg +17 -0
  210. dcscope/img/icon-theme/breeze/actions/32/object-order-lower.svg +18 -0
  211. dcscope/img/icon-theme/breeze/actions/32/view-filter.svg +18 -0
  212. dcscope/img/icon-theme/breeze/actions/32/view-list-tree.svg +18 -0
  213. dcscope/img/icon-theme/breeze/places/16/folder-cloud.svg +13 -0
  214. dcscope/img/icon-theme/breeze/places/16/folder.svg +13 -0
  215. dcscope/img/icon-theme/breeze/places/22/folder-cloud.svg +13 -0
  216. dcscope/img/icon-theme/breeze/places/22/folder.svg +13 -0
  217. dcscope/img/icon-theme/breeze/places/24/folder-cloud.svg +12 -0
  218. dcscope/img/icon-theme/breeze/places/24/folder.svg +12 -0
  219. dcscope/img/icon-theme/breeze/places/32/folder-cloud.svg +35 -0
  220. dcscope/img/icon-theme/breeze/places/32/folder.svg +30 -0
  221. dcscope/img/icon-theme/breeze/places/64/folder-cloud.svg +35 -0
  222. dcscope/img/icon-theme/breeze/places/64/folder.svg +30 -0
  223. dcscope/img/icon-theme/breeze/status/16/dialog-error.svg +9 -0
  224. dcscope/img/icon-theme/breeze/status/16/dialog-information.svg +9 -0
  225. dcscope/img/icon-theme/breeze/status/16/dialog-question.svg +7 -0
  226. dcscope/img/icon-theme/breeze/status/16/dialog-warning.svg +9 -0
  227. dcscope/img/icon-theme/breeze/status/22/dialog-error.svg +9 -0
  228. dcscope/img/icon-theme/breeze/status/22/dialog-information.svg +9 -0
  229. dcscope/img/icon-theme/breeze/status/22/dialog-question.svg +7 -0
  230. dcscope/img/icon-theme/breeze/status/22/dialog-warning.svg +9 -0
  231. dcscope/img/icon-theme/breeze/status/24/dialog-error.svg +13 -0
  232. dcscope/img/icon-theme/breeze/status/24/dialog-information.svg +13 -0
  233. dcscope/img/icon-theme/breeze/status/24/dialog-question.svg +9 -0
  234. dcscope/img/icon-theme/breeze/status/24/dialog-warning.svg +1 -0
  235. dcscope/img/icon-theme/breeze/status/64/dialog-error.svg +25 -0
  236. dcscope/img/icon-theme/breeze/status/64/dialog-information.svg +25 -0
  237. dcscope/img/icon-theme/breeze/status/64/dialog-question.svg +25 -0
  238. dcscope/img/icon-theme/breeze/status/64/dialog-warning.svg +25 -0
  239. dcscope/img/icon-theme/collect_icons.py +124 -0
  240. dcscope/img/icon-theme/dcscope/dcor.png +0 -0
  241. dcscope/img/icon-theme/dcscope/edit-copy-anew.svg +13 -0
  242. dcscope/img/icon-theme/dcscope/emodulus.svg +69 -0
  243. dcscope/img/icon-theme/dcscope/filter_ray.svg +54 -0
  244. dcscope/img/icon-theme/dcscope/region_channel.svg +86 -0
  245. dcscope/img/icon-theme/dcscope/region_reservoir.svg +89 -0
  246. dcscope/img/icon-theme/dcscope/rlang.svg +108 -0
  247. dcscope/img/icon-theme/dcscope/statistical_significance.svg +106 -0
  248. dcscope/img/icon-theme/index.theme +81 -0
  249. dcscope/img/icon.png +0 -0
  250. dcscope/img/icon.svg +73 -0
  251. dcscope/img/splash.png +0 -0
  252. dcscope/meta_tool.py +143 -0
  253. dcscope/pipeline/__init__.py +5 -0
  254. dcscope/pipeline/core.py +632 -0
  255. dcscope/pipeline/dataslot.py +270 -0
  256. dcscope/pipeline/filter.py +161 -0
  257. dcscope/pipeline/filter_ray.py +170 -0
  258. dcscope/pipeline/plot.py +187 -0
  259. dcscope/plot_cache.py +79 -0
  260. dcscope/session.py +379 -0
  261. dcscope/util.py +34 -0
  262. dcscope-2.22.1.dist-info/METADATA +95 -0
  263. dcscope-2.22.1.dist-info/RECORD +267 -0
  264. dcscope-2.22.1.dist-info/WHEEL +5 -0
  265. dcscope-2.22.1.dist-info/entry_points.txt +2 -0
  266. dcscope-2.22.1.dist-info/licenses/LICENSE +621 -0
  267. dcscope-2.22.1.dist-info/top_level.txt +1 -0
dcscope/gui/main.py ADDED
@@ -0,0 +1,1089 @@
1
+ import pathlib
2
+ import importlib.resources
3
+ import signal
4
+ import sys
5
+ import traceback
6
+ import warnings
7
+ import webbrowser
8
+
9
+ import dclab
10
+ from dclab.lme4 import rsetup
11
+ import h5py
12
+ import numpy
13
+ import scipy
14
+
15
+ from PyQt6 import uic, QtCore, QtGui, QtWidgets
16
+ from PyQt6.QtCore import QStandardPaths
17
+ from PyQt6.QtWidgets import QMessageBox
18
+ import pyqtgraph as pg
19
+
20
+ from . import analysis
21
+ from . import bulk
22
+ from . import compute
23
+ from . import dcor
24
+ from . import export
25
+ from . import pipeline_plot
26
+ from . import preferences
27
+ from . import quick_view
28
+ from . import update
29
+ from . import widgets
30
+
31
+ from ..extensions import ExtensionManager
32
+ from .. import pipeline
33
+ from .. import session
34
+
35
+ from .._version import version
36
+
37
+ # global plotting configuration parameters
38
+ pg.setConfigOption("background", None)
39
+ pg.setConfigOption("foreground", "k")
40
+ pg.setConfigOption("antialias", True)
41
+ pg.setConfigOption("imageAxisOrder", "row-major")
42
+
43
+ # set Qt icon theme search path
44
+ ref = importlib.resources.files("dcscope.img") / "icon.png"
45
+ with importlib.resources.as_file(ref) as icon_path:
46
+ theme_path = icon_path.with_name("icon-theme")
47
+ if theme_path.exists():
48
+ QtGui.QIcon.setThemeSearchPaths([str(theme_path)])
49
+ QtGui.QIcon.setThemeName(".")
50
+ else:
51
+ warnings.warn("DCscope theme path not available")
52
+
53
+
54
+ class DCscope(QtWidgets.QMainWindow):
55
+ plots_changed = QtCore.pyqtSignal()
56
+
57
+ def __init__(self, *arguments):
58
+ """Initialize DCscope
59
+
60
+ If you pass the "--version" command line argument, the
61
+ application will print the version after initialization
62
+ and exit.
63
+ """
64
+ super(DCscope, self).__init__()
65
+ ref = importlib.resources.files("dcscope.gui") / "main.ui"
66
+ with importlib.resources.as_file(ref) as path_ui:
67
+ uic.loadUi(path_ui, self)
68
+
69
+ # update check
70
+ self._update_thread = None
71
+ self._update_worker = None
72
+ # Settings are stored in the .ini file format. Even though
73
+ # `self.settings` may return integer/bool in the same session,
74
+ # in the next session, it will reliably return strings. Lists
75
+ # of strings (comma-separated) work nicely though.
76
+ QtCore.QCoreApplication.setOrganizationName("DC-analysis")
77
+ QtCore.QCoreApplication.setOrganizationDomain("dc-cosmos.org")
78
+ QtCore.QCoreApplication.setApplicationName("dcscope")
79
+ QtCore.QSettings.setDefaultFormat(QtCore.QSettings.Format.IniFormat)
80
+
81
+ #: DCscope settings
82
+ self.settings = QtCore.QSettings()
83
+ # Register custom DCOR CA bundle directory with dclab
84
+ ca_path = pathlib.Path(
85
+ QStandardPaths.writableLocation(
86
+ QStandardPaths.StandardLocation.AppDataLocation)
87
+ ) / "certificates"
88
+ ca_path.mkdir(exist_ok=True, parents=True)
89
+ dclab.rtdc_dataset.fmt_dcor.DCOR_CERTS_SEARCH_PATHS.append(ca_path)
90
+ # Register user-defined DCOR API Key in case the user wants to
91
+ # open a session with private data.
92
+ api_key = self.settings.value("dcor/api key", "")
93
+ dclab.rtdc_dataset.fmt_dcor.api.APIHandler.add_api_key(api_key)
94
+ # Register S3 access settings in dclab
95
+ s3_endpoint_url = self.settings.value("s3/endpoint url", "")
96
+ if s3_endpoint_url:
97
+ dclab.rtdc_dataset.fmt_s3.S3_ENDPOINT_URL = s3_endpoint_url
98
+ s3_access_key_id = self.settings.value("s3/access key id", "")
99
+ if s3_access_key_id:
100
+ dclab.rtdc_dataset.fmt_s3.S3_ACCESS_KEY_ID = s3_access_key_id
101
+ s3_secret_access_key = self.settings.value("s3/secret access key", "")
102
+ if s3_secret_access_key:
103
+ dclab.rtdc_dataset.fmt_s3.S3_SECRET_ACCESS_KEY = \
104
+ s3_secret_access_key
105
+
106
+ #: Analysis pipeline
107
+ self.pipeline = pipeline.Pipeline()
108
+ #: Extensions
109
+ store_path = pathlib.Path(
110
+ QStandardPaths.writableLocation(
111
+ QStandardPaths.StandardLocation.AppDataLocation)
112
+ ) / "extensions"
113
+ try:
114
+ self.extensions = ExtensionManager(store_path)
115
+ except BaseException:
116
+ QtWidgets.QMessageBox.warning(
117
+ self,
118
+ "Extensions automatically disabled",
119
+ "Some extensions could not be loaded and were disabled:\n\n"
120
+ + traceback.format_exc(),
121
+ )
122
+ # GUI
123
+ self.setWindowTitle(f"DCscope {version}")
124
+ # Disable native menu bar (e.g. on Mac)
125
+ self.menubar.setNativeMenuBar(False)
126
+ # File menu
127
+ self.actionLoadDataset.triggered.connect(self.add_dataslot)
128
+ self.actionLoadDCOR.triggered.connect(self.on_action_dcor)
129
+ self.actionClearDatasets.triggered.connect(
130
+ self.on_action_clear_datasets)
131
+ self.actionClearSession.triggered.connect(self.on_action_clear)
132
+ self.actionOpenSession.triggered.connect(self.on_action_open)
133
+ self.actionQuit.triggered.connect(self.on_action_quit)
134
+ self.actionSaveSession.triggered.connect(self.on_action_save)
135
+ # Edit menu
136
+ self.actionChangeDatasetOrder.triggered.connect(
137
+ self.on_action_change_dataset_order)
138
+ self.actionPreferences.triggered.connect(self.on_action_preferences)
139
+ # Bulk action menu
140
+ self.actionComputeEmodulus.triggered.connect(
141
+ self.on_action_compute_emodulus)
142
+ # Compute menu
143
+ self.actionComputeStatistics.triggered.connect(
144
+ self.on_action_compute_statistics)
145
+ self.actionComputeSignificance.triggered.connect(
146
+ self.on_action_compute_significance)
147
+ # Export menu
148
+ # data
149
+ self.actionExportData.triggered.connect(self.on_action_export_data)
150
+ # filters
151
+ self.action_export_filter_polygon.triggered.connect(
152
+ self.on_action_export_filter_polygon)
153
+ self.action_export_filter_pipeline.triggered.connect(
154
+ self.on_action_export_filter_pipeline)
155
+ self.action_export_filter_ray_dataset.triggered.connect(
156
+ self.on_action_export_filter_ray_dataset)
157
+ # plot
158
+ self.actionExportPlot.triggered.connect(self.on_action_export_plot)
159
+ # Import menu
160
+ self.actionImportFilter.triggered.connect(self.on_action_import_filter)
161
+ # Help menu
162
+ self.actionDocumentation.triggered.connect(self.on_action_docs)
163
+ self.actionSoftware.triggered.connect(self.on_action_software)
164
+ self.actionAbout.triggered.connect(self.on_action_about)
165
+ # Subwindows
166
+ self.subwindows = {}
167
+ # Subwindows for plots
168
+ self.subwindows_plots = {}
169
+ # Initialize a few things
170
+ self.init_quick_view()
171
+ self.init_analysis_view()
172
+ self.mdiArea.cascadeSubWindows()
173
+ # BLOCK MATRIX (wraps DataMatrix and PlotMatrix)
174
+ # BlockMatrix appearance
175
+ self.toolButton_dm.clicked.connect(self.on_data_matrix)
176
+ self.splitter.splitterMoved.connect(self.on_splitter)
177
+ # BlockMatrix Actions
178
+ self.actionNewFilter.triggered.connect(self.add_filter)
179
+ self.actionNewPlot.triggered.connect(self.add_plot)
180
+ self.block_matrix.toolButton_load_dataset.clicked.connect(
181
+ self.add_dataslot)
182
+ self.block_matrix.toolButton_new_filter.clicked.connect(
183
+ self.add_filter)
184
+ self.block_matrix.toolButton_new_plot.clicked.connect(self.add_plot)
185
+ # BlockMatrix default state
186
+ self.toolButton_new_plot.setEnabled(False)
187
+ self.block_matrix.toolButton_new_plot.setEnabled(False)
188
+ # BlockMatrix other signals
189
+ self.block_matrix.pipeline_changed.connect(self.adopt_pipeline)
190
+ self.block_matrix.slot_modify_clicked.connect(self.on_modify_slot)
191
+ self.block_matrix.filter_modify_clicked.connect(self.on_modify_filter)
192
+ self.block_matrix.plot_modify_clicked.connect(self.on_modify_plot)
193
+ # ANALYSIS VIEW
194
+ self.widget_ana_view.set_pipeline(self.pipeline)
195
+ # filter signals
196
+ self.widget_ana_view.filter_changed.connect(self.adopt_filter)
197
+ self.widget_ana_view.pipeline_changed.connect(self.adopt_pipeline)
198
+ # polygon filter creation
199
+ self.widget_ana_view.widget_filter.request_new_polygon_filter.connect(
200
+ self.on_new_polygon_filter)
201
+ self.widget_quick_view.polygon_filter_about_to_be_deleted.connect(
202
+ self.on_remove_polygon_filter_from_pipeline)
203
+ self.widget_quick_view.polygon_filter_created.connect(
204
+ self.widget_ana_view.widget_filter.update_polygon_filters)
205
+ self.widget_quick_view.polygon_filter_modified.connect(
206
+ self.widget_ana_view.widget_filter.update_polygon_filters)
207
+ self.widget_quick_view.polygon_filter_modified.connect(
208
+ self.on_quickview_refresh) # might be an active filter (#26)
209
+ self.widget_quick_view.polygon_filter_modified.connect(
210
+ self.plots_changed) # might be an active filter (#26)
211
+ # This is important, because if metadata such as emodulus recipe
212
+ # is changed, the QuickView must be updated as well.
213
+ self.plots_changed.connect(self.widget_quick_view.plot)
214
+ # plot signals
215
+ self.widget_ana_view.plot_changed.connect(self.adopt_plot)
216
+ # slot signals
217
+ self.widget_ana_view.slot_changed.connect(self.adopt_slot)
218
+ # if "--version" was specified, print the version and exit
219
+ if "--version" in arguments:
220
+ print(version)
221
+ QtWidgets.QApplication.processEvents(
222
+ QtCore.QEventLoop.ProcessEventsFlag.AllEvents, 300)
223
+ sys.exit(0)
224
+ else:
225
+ # deal with any other arguments that might have been passed
226
+ for arg in arguments:
227
+ apath = pathlib.Path(arg)
228
+ if apath.exists():
229
+ if apath.suffix == ".so2":
230
+ # load a session
231
+ self.on_action_open(path=apath)
232
+ elif apath.suffix == ".rtdc":
233
+ # add a dataslot
234
+ self.add_dataslot(paths=[apath])
235
+ elif apath.suffix in [".sof", ".poly"]:
236
+ # add a filter
237
+ self.on_action_import_filter(path=apath)
238
+
239
+ # check for updates
240
+ do_update = int(self.settings.value("check for updates", 1))
241
+ self.on_action_check_update(do_update)
242
+ # finalize
243
+ self.show()
244
+ self.raise_()
245
+ self.activateWindow()
246
+ self.showMaximized()
247
+ self.setWindowState(QtCore.Qt.WindowState.WindowActive)
248
+
249
+ @widgets.show_wait_cursor
250
+ @QtCore.pyqtSlot(dict)
251
+ def adopt_filter(self, filt_state):
252
+ filt_id = filt_state["identifier"]
253
+ state = self.pipeline.__getstate__()
254
+ for ii in range(len(state["filters"])):
255
+ if state["filters"][ii]["identifier"] == filt_id:
256
+ state["filters"][ii] = filt_state
257
+ # make sure filters are enabled/disabled
258
+ if (filt_state["filter used"]
259
+ and filt_id not in state["filters used"]):
260
+ state["filters used"].append(filt_id)
261
+ elif (not filt_state["filter used"]
262
+ and filt_id in state["filters used"]):
263
+ state["filters used"].remove(filt_id)
264
+ break
265
+ else:
266
+ raise ValueError("Filter not in pipeline: {}".format(filt_id))
267
+ self.adopt_pipeline(state)
268
+
269
+ @widgets.show_wait_cursor
270
+ @QtCore.pyqtSlot(dict)
271
+ def adopt_pipeline(self, pipeline_state):
272
+ # If the number of subplots within a plot changed, update the
273
+ # plot size accordingly.
274
+ for plot_index, plot_id in enumerate(self.pipeline.plot_ids):
275
+ old_ncol, old_nrow = self.pipeline.get_plot_col_row_count(plot_id)
276
+ try:
277
+ new_ncol, new_nrow = self.pipeline.get_plot_col_row_count(
278
+ plot_id, pipeline_state)
279
+ lay = pipeline_state["plots"][plot_index]["layout"]
280
+ except (KeyError, IndexError):
281
+ # the plot was removed
282
+ continue
283
+ else:
284
+ # we are aiming for a square plot aspect ratio
285
+ plot_width = lay["size x"] / new_ncol
286
+ plot_height = lay["size y"] / new_nrow
287
+ if plot_width < 200:
288
+ lay["size x"] += 200 * (new_ncol - old_ncol)
289
+ if plot_height < 200:
290
+ lay["size y"] += 200*(new_nrow-old_nrow)
291
+ # set the new state of the pipeline
292
+ self.pipeline.__setstate__(pipeline_state)
293
+ # update BlockMatrix
294
+ if self.sender() != self.block_matrix:
295
+ # Update BlockMatrix
296
+ self.setUpdatesEnabled(False)
297
+ self.block_matrix.adopt_pipeline(pipeline_state)
298
+ self.setUpdatesEnabled(True)
299
+ # trigger redraw
300
+ self.block_matrix.update()
301
+ # Invalidate block matrix elements that do not make sense due to
302
+ # filtering or plotting features.
303
+ invalid_dm = []
304
+ invalid_pm = []
305
+ for slot_index, slot in enumerate(self.pipeline.slots):
306
+ ds = self.pipeline.get_dataset(slot_index=slot_index,
307
+ filt_index=None)
308
+ for filt in self.pipeline.filters:
309
+ # box filters
310
+ for feat in filt.boxdict:
311
+ if feat not in ds.features:
312
+ invalid_dm.append((slot.identifier, filt.identifier))
313
+ break
314
+ else:
315
+ # polygon filters
316
+ for pid in filt.polylist:
317
+ pf = dclab.PolygonFilter.get_instance_from_id(pid)
318
+ if (pf.axes[0] not in ds.features
319
+ or pf.axes[1] not in ds.features):
320
+ invalid_dm.append((slot.identifier,
321
+ filt.identifier))
322
+ break
323
+ for plot in self.pipeline.plots:
324
+ plot_state = plot.__getstate__()
325
+ if (plot_state["general"]["axis x"] not in ds
326
+ or plot_state["general"]["axis x"] not in ds):
327
+ invalid_pm.append((slot.identifier, plot.identifier))
328
+ self.block_matrix.invalidate_elements(invalid_dm, invalid_pm)
329
+ # Update AnalysisView
330
+ self.widget_ana_view.set_pipeline(self.pipeline)
331
+ # Update QuickView choices
332
+ self.widget_quick_view.update_feature_choices()
333
+ # update list of polygon filters in Quick View
334
+ self.widget_quick_view.update_polygon_panel()
335
+ # Show Plot Windows
336
+ # create and show
337
+ for plot_state in pipeline_state["plots"]:
338
+ self.add_plot_window(plot_state["identifier"])
339
+ # remove old plot subwindows
340
+ plot_ids = [pp["identifier"] for pp in pipeline_state["plots"]]
341
+ for plot_id in list(self.subwindows_plots.keys()):
342
+ if plot_id not in plot_ids:
343
+ sub = self.subwindows_plots.pop(plot_id)
344
+ for child in sub.children():
345
+ # disconnect signals
346
+ if isinstance(child, pipeline_plot.PipelinePlot):
347
+ self.plots_changed.disconnect(child.update_content)
348
+ break
349
+ sub.deleteLater()
350
+ self.plots_changed.emit()
351
+ self.widget_ana_view.widget_plot.update_content()
352
+ # Remove zombie slots
353
+ for slot_id in list(pipeline.Dataslot._instances.keys()):
354
+ if slot_id not in self.pipeline.slot_ids:
355
+ pipeline.Dataslot.remove_slot(slot_id)
356
+ # enable buttons
357
+ if self.pipeline.slots:
358
+ self.toolButton_new_plot.setEnabled(True)
359
+ self.block_matrix.toolButton_new_plot.setEnabled(True)
360
+ else:
361
+ self.toolButton_new_plot.setEnabled(False)
362
+ self.block_matrix.toolButton_new_plot.setEnabled(False)
363
+ # redraw
364
+ self.block_matrix.update()
365
+ self.mdiArea.update()
366
+ self.subwindows["analysis_view"].update()
367
+
368
+ @widgets.show_wait_cursor
369
+ @QtCore.pyqtSlot(dict)
370
+ def adopt_plot(self, plot_state):
371
+ plot_id = plot_state["identifier"]
372
+ state = self.pipeline.__getstate__()
373
+ for ii in range(len(state["plots"])):
374
+ if state["plots"][ii]["identifier"] == plot_id:
375
+ state["plots"][ii] = plot_state
376
+ break
377
+ else:
378
+ raise ValueError("Plot not in pipeline: {}".format(plot_id))
379
+ self.adopt_pipeline(state)
380
+
381
+ @widgets.show_wait_cursor
382
+ @QtCore.pyqtSlot(dict)
383
+ def adopt_slot(self, slot_state):
384
+ slot_id = slot_state["identifier"]
385
+ state = self.pipeline.__getstate__()
386
+ for ii in range(len(state["slots"])):
387
+ if state["slots"][ii]["identifier"] == slot_id:
388
+ state["slots"][ii] = slot_state
389
+ # make sure filters are enabled/disabled
390
+ if (slot_state["slot used"]
391
+ and slot_id not in state["slots used"]):
392
+ state["slots used"].append(slot_id)
393
+ elif (not slot_state["slot used"]
394
+ and slot_id in state["slots used"]):
395
+ state["slots used"].remove(slot_id)
396
+ break
397
+ else:
398
+ raise ValueError("Slot not in pipeline: {}".format(slot_id))
399
+ self.adopt_pipeline(state)
400
+
401
+ @widgets.show_wait_cursor
402
+ @QtCore.pyqtSlot()
403
+ def add_dataslot(self, paths=None, is_dcor=False):
404
+ """Adds a dataslot to the pipeline
405
+
406
+ Parameters
407
+ ----------
408
+ paths: list of str or list of pathlib.Path
409
+ If specified, no file dialog is displayed and the files
410
+ specified are loaded. Can also be DCOR URL.
411
+ is_dcor: bool
412
+ If set to True, `paths` will be treated as a list of
413
+ DCOR URLs. Does not have any effect if `paths` is None.
414
+ """
415
+ if paths is None:
416
+ fnames, _ = QtWidgets.QFileDialog.getOpenFileNames(
417
+ parent=self,
418
+ caption="Select an RT-DC measurement",
419
+ directory=self.settings.value("paths/add dataset", ""),
420
+ filter="RT-DC Files (*.rtdc)")
421
+ else:
422
+ fnames = paths
423
+
424
+ if fnames:
425
+ self.toolButton_new_plot.setEnabled(True)
426
+ self.block_matrix.toolButton_new_plot.setEnabled(True)
427
+
428
+ failed_paths = []
429
+
430
+ slot_ids = []
431
+ # Create Dataslot instance and update block matrix
432
+ self.setUpdatesEnabled(False)
433
+ for fn in fnames:
434
+ if is_dcor:
435
+ path = fn
436
+ else:
437
+ path = pathlib.Path(fn)
438
+ self.settings.setValue("paths/add dataset", str(path.parent))
439
+ # add a filter if we don't have one already
440
+ if self.pipeline.num_filters == 0:
441
+ self.add_filter()
442
+ try:
443
+ slot_id = self.pipeline.add_slot(path=path)
444
+ except BaseException:
445
+ if len(fnames) == 1:
446
+ # Let the user know immediately
447
+ raise
448
+ else:
449
+ failed_paths.append(path)
450
+ continue
451
+
452
+ self.block_matrix.add_dataset(slot_id=slot_id)
453
+ slot_ids.append(slot_id)
454
+
455
+ self.setUpdatesEnabled(True)
456
+ self.repaint()
457
+
458
+ # Update box filter limits
459
+ self.widget_ana_view.widget_filter.update_box_ranges()
460
+ # Update dataslot in analysis view
461
+ self.widget_ana_view.widget_slot.update_content()
462
+ # redraw
463
+ self.block_matrix.update()
464
+
465
+ if failed_paths:
466
+ failed_text = ("The following files could not be loaded. You can "
467
+ "open them individually to see the corresponding "
468
+ "error message during loading.\n")
469
+ for path in failed_paths:
470
+ failed_text += f"- {path}\n"
471
+ QtWidgets.QMessageBox.warning(self,
472
+ "Failed to load some datasets",
473
+ failed_text)
474
+
475
+ return slot_ids
476
+
477
+ @QtCore.pyqtSlot()
478
+ def add_filter(self):
479
+ """Add a filter using tool buttons"""
480
+ filt_id = self.pipeline.add_filter()
481
+ self.block_matrix.add_filter(identifier=filt_id)
482
+ self.widget_ana_view.widget_filter.update_content()
483
+ # redraw
484
+ self.block_matrix.update()
485
+ return filt_id
486
+
487
+ @QtCore.pyqtSlot()
488
+ def add_plot(self):
489
+ plot_id = self.pipeline.add_plot()
490
+ self.block_matrix.add_plot(identifier=plot_id)
491
+ self.add_plot_window(plot_id)
492
+ # update UI contents
493
+ self.widget_ana_view.widget_plot.update_content()
494
+ # redraw
495
+ self.block_matrix.update()
496
+ return plot_id
497
+
498
+ @QtCore.pyqtSlot()
499
+ def add_plot_window(self, plot_id):
500
+ """Create a plot window if necessary and show it"""
501
+ if plot_id in self.subwindows_plots:
502
+ sub = self.subwindows_plots[plot_id]
503
+ else:
504
+ # create subwindow
505
+ sub = widgets.MDISubWindowWOButtons(self)
506
+ pw = pipeline_plot.PipelinePlot(parent=sub,
507
+ pipeline=self.pipeline,
508
+ plot_id=plot_id)
509
+ self.plots_changed.connect(pw.update_content)
510
+ sub.setWidget(pw)
511
+ pw.update_content()
512
+ self.mdiArea.addSubWindow(sub)
513
+ self.subwindows_plots[plot_id] = sub
514
+ sub.show()
515
+
516
+ def closeEvent(self, event):
517
+ """Determine what happens when the user wants to quit"""
518
+ if self.pipeline.slots or self.pipeline.filters:
519
+ if self.on_action_clear():
520
+ event.accept()
521
+ else:
522
+ event.ignore()
523
+ else:
524
+ event.accept()
525
+
526
+ def dragEnterEvent(self, e):
527
+ """Whether files are accepted"""
528
+ if e.mimeData().hasUrls():
529
+ e.accept()
530
+ else:
531
+ e.ignore()
532
+
533
+ @widgets.show_wait_cursor
534
+ def dropEvent(self, e):
535
+ """Add dropped files to view"""
536
+ urls = e.mimeData().urls()
537
+ if urls:
538
+ pathlist = []
539
+ is_dcor = bool(urls[0].host())
540
+ for ff in urls:
541
+ if is_dcor:
542
+ # DCOR data
543
+ pathlist.append(ff.toString())
544
+ else:
545
+ pp = pathlib.Path(ff.toLocalFile())
546
+ if pp.is_dir():
547
+ pathlist += list(pp.rglob("*.rtdc"))
548
+ elif pp.suffix == ".rtdc":
549
+ pathlist.append(pp)
550
+ pathlist = sorted(pathlist)
551
+ if pathlist:
552
+ self.add_dataslot(paths=pathlist, is_dcor=is_dcor)
553
+
554
+ def init_analysis_view(self):
555
+ sub = widgets.MDISubWindowWOButtons(self)
556
+ self.widget_ana_view = analysis.AnalysisView(parent=self)
557
+ sub.setWidget(self.widget_ana_view)
558
+ self.subwindows["analysis_view"] = sub
559
+ # signals
560
+ self.toolButton_ana_view.clicked.connect(sub.setVisible)
561
+ self.block_matrix.quickviewed.connect(
562
+ self.widget_ana_view.on_quickview)
563
+ # applying a new filter triggers updating QuickView
564
+ self.widget_ana_view.widget_filter.pushButton_apply.clicked.connect(
565
+ self.on_quickview_refresh)
566
+ sub.hide()
567
+ self.mdiArea.addSubWindow(sub)
568
+
569
+ def init_quick_view(self):
570
+ sub = widgets.MDISubWindowWOButtons(self)
571
+ self.widget_quick_view = quick_view.QuickView(parent=self)
572
+ sub.setWidget(self.widget_quick_view)
573
+ self.subwindows["quick_view"] = sub
574
+ # signals
575
+ self.toolButton_quick_view.clicked.connect(self.on_quickview)
576
+ self.block_matrix.quickviewed.connect(self.on_quickview_show_dataset)
577
+ sub.hide()
578
+ self.mdiArea.addSubWindow(sub)
579
+
580
+ @QtCore.pyqtSlot()
581
+ def on_action_about(self):
582
+ gh = "DC-analysis/DCscope"
583
+ rtd = "dcscope.readthedocs.io"
584
+ about_text = (
585
+ f"DCscope is a graphical user interface for the analysis "
586
+ f"and visualization of deformability cytometry data sets.<br><br>"
587
+ f"Author: Paul Müller<br>"
588
+ f"GitHub: <a href='https://github.com/{gh}'>{gh}</a><br>"
589
+ f"Documentation: <a href='https://{rtd}'>{rtd}</a><br>"
590
+ )
591
+ QtWidgets.QMessageBox.about(self, f"DCscope {version}", about_text)
592
+
593
+ @QtCore.pyqtSlot()
594
+ def on_action_change_dataset_order(self):
595
+ """Show dialog for changing dataset order"""
596
+ dlg = analysis.DlgSlotReorder(self.pipeline, self)
597
+ dlg.pipeline_changed.connect(self.adopt_pipeline)
598
+ dlg.exec()
599
+
600
+ @QtCore.pyqtSlot(bool)
601
+ def on_action_check_update(self, b):
602
+ self.settings.setValue("check for updates", int(b))
603
+ if b and self._update_thread is None:
604
+ self._update_thread = QtCore.QThread()
605
+ self._update_worker = update.UpdateWorker()
606
+ self._update_worker.moveToThread(self._update_thread)
607
+ self._update_worker.finished.connect(self._update_thread.quit)
608
+ self._update_worker.data_ready.connect(
609
+ self.on_action_check_update_finished)
610
+ self._update_thread.start()
611
+
612
+ ghrepo = "DC-analysis/DCscope"
613
+
614
+ QtCore.QMetaObject.invokeMethod(
615
+ self._update_worker,
616
+ 'processUpdate',
617
+ QtCore.Qt.ConnectionType.QueuedConnection,
618
+ QtCore.Q_ARG(str, version),
619
+ QtCore.Q_ARG(str, ghrepo),
620
+ )
621
+
622
+ @QtCore.pyqtSlot(dict)
623
+ def on_action_check_update_finished(self, mdict):
624
+ # cleanup
625
+ self._update_thread.quit()
626
+ self._update_thread.wait()
627
+ self._update_worker = None
628
+ self._update_thread = None
629
+ # display message box
630
+ ver = mdict["version"]
631
+ web = mdict["releases url"]
632
+ dlb = mdict["binary url"]
633
+ msg = QtWidgets.QMessageBox()
634
+ msg.setWindowTitle("DCscope {} available!".format(ver))
635
+ msg.setTextFormat(QtCore.Qt.TextFormat.RichText)
636
+ text = "You can install DCscope {} ".format(ver)
637
+ if dlb is not None:
638
+ text += 'from a <a href="{}">direct download</a>. '.format(dlb)
639
+ else:
640
+ text += 'by running `pip install --upgrade dcscope`. '
641
+ text += 'Visit the <a href="{}">official release page</a>!'.format(web)
642
+ msg.setText(text)
643
+ msg.exec()
644
+
645
+ @QtCore.pyqtSlot()
646
+ def on_action_compute_emodulus(self):
647
+ dlg = bulk.BulkActionEmodulus(self, pipeline=self.pipeline)
648
+ dlg.pipeline_changed.connect(self.adopt_pipeline)
649
+ dlg.exec()
650
+
651
+ @QtCore.pyqtSlot()
652
+ def on_action_compute_significance(self):
653
+ # check that R is available
654
+ if rsetup.has_r():
655
+ dlg = compute.ComputeSignificance(self, pipeline=self.pipeline)
656
+ dlg.exec()
657
+ else:
658
+ QtWidgets.QMessageBox.critical(
659
+ self, "R not found!",
660
+ "The R executable was not found. Please add it "
661
+ + "to the PATH variable or define it manually in the "
662
+ + "DCscope preferences.")
663
+
664
+ @QtCore.pyqtSlot()
665
+ def on_action_compute_statistics(self):
666
+ dlg = compute.ComputeStatistics(self, pipeline=self.pipeline)
667
+ dlg.exec()
668
+
669
+ @QtCore.pyqtSlot()
670
+ def on_action_clear(self):
671
+ """Clear the entire session"""
672
+ if bool(int(self.settings.value("advanced/user confirm clear", "1"))):
673
+ button_reply = QtWidgets.QMessageBox.question(
674
+ self, 'Clear Session', "All progress will be lost. Continue?",
675
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
676
+ QtWidgets.QMessageBox.StandardButton.No)
677
+ yes = button_reply == QtWidgets.QMessageBox.StandardButton.Yes
678
+ else:
679
+ yes = True
680
+ if yes:
681
+ session.clear_session(self.pipeline)
682
+ self.reload_pipeline()
683
+ self.setWindowTitle(f"DCscope {version}")
684
+ return yes
685
+
686
+ @QtCore.pyqtSlot()
687
+ def on_action_clear_datasets(self):
688
+ """Clear only the datasets"""
689
+ if bool(int(self.settings.value("advanced/user confirm clear", "1"))):
690
+ button_reply = QtWidgets.QMessageBox.question(
691
+ self, 'Clear Datasets',
692
+ "Remove all datasets from this session?",
693
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
694
+ QtWidgets.QMessageBox.StandardButton.No)
695
+ yes = button_reply == QtWidgets.QMessageBox.StandardButton.Yes
696
+ else:
697
+ yes = True
698
+ if yes:
699
+ for slot_id in self.pipeline.slot_ids:
700
+ self.pipeline.remove_slot(slot_id)
701
+ self.reload_pipeline()
702
+ return yes
703
+
704
+ @QtCore.pyqtSlot()
705
+ def on_action_dcor(self):
706
+ """Show the DCOR import dialog"""
707
+ dlg = dcor.DCORLoader(self)
708
+ dlg.exec()
709
+
710
+ @QtCore.pyqtSlot()
711
+ def on_action_docs(self):
712
+ webbrowser.open("https://dcscope.readthedocs.io")
713
+
714
+ @QtCore.pyqtSlot()
715
+ def on_action_export_data(self):
716
+ dlg = export.ExportData(self, pipeline=self.pipeline)
717
+ if dlg.path is not None: # user pressed cancel
718
+ dlg.exec()
719
+
720
+ @QtCore.pyqtSlot()
721
+ def on_action_export_filter_pipeline(self):
722
+ """Export the entire filter pipeline"""
723
+ dlg = export.ExportFilter(self,
724
+ pipeline=self.pipeline,
725
+ file_format="sof")
726
+ dlg.exec()
727
+ return dlg # for testing
728
+
729
+ @QtCore.pyqtSlot()
730
+ def on_action_export_filter_polygon(self, exec_dialog=True):
731
+ """Export only polygon filters of the current pipeline"""
732
+ dlg = export.ExportFilter(self,
733
+ pipeline=self.pipeline,
734
+ file_format="poly")
735
+ dlg.exec()
736
+ return dlg # for testing
737
+
738
+ @QtCore.pyqtSlot()
739
+ def on_action_export_filter_ray_dataset(self):
740
+ """Export the filter ray for each dataset to the dataset location"""
741
+ #: dictionary with paths as keys and filter ID list as values
742
+ filt_dict = {}
743
+ #: paths that already exist (user will be asked to override)
744
+ existing_paths = []
745
+ #: keeps track of datasets that are loaded twice (will notify user)
746
+ double_paths = []
747
+ #: keeps track of DCOR data
748
+ dcor_data = []
749
+ for slot in self.pipeline.slots:
750
+ if slot.format == "dcor":
751
+ dcor_data.append(slot.path)
752
+ continue
753
+ ray_path = slot.path.with_suffix(".sof")
754
+ if ray_path in filt_dict:
755
+ double_paths.append(ray_path)
756
+ continue
757
+ if ray_path.exists():
758
+ existing_paths.append(ray_path)
759
+ filters = self.pipeline.get_filters_for_slot(slot.identifier)
760
+ filt_dict[ray_path] = [ff.identifier for ff in filters]
761
+ if dcor_data:
762
+ QtWidgets.QMessageBox.warning(
763
+ self,
764
+ "Some datasets are DCOR data for which no filter rays ",
765
+ "will be exported:\n\n"
766
+ + "\n".join([str(p) for p in list(set(dcor_data))])
767
+ )
768
+ if double_paths:
769
+ QtWidgets.QMessageBox.warning(
770
+ self,
771
+ "Same datasets loaded in different slots",
772
+ "The following datasets are loaded twice. Only the first "
773
+ + "filter ray will be saved:\n\n"
774
+ + "\n".join([str(p) for p in list(set(double_paths))])
775
+ )
776
+ if existing_paths:
777
+ resp = QtWidgets.QMessageBox.question(
778
+ self,
779
+ "Override existing files?",
780
+ "The following files already exist, override?\n\n"
781
+ + "\n".join([str(p) for p in existing_paths])
782
+ )
783
+ if resp:
784
+ # if the user agrees, these files will be overridden
785
+ existing_paths.clear()
786
+ for path in filt_dict:
787
+ if path in existing_paths:
788
+ # not overriding this one
789
+ continue
790
+ else:
791
+ session.export_filters(path, self.pipeline, filt_dict[path])
792
+
793
+ @QtCore.pyqtSlot()
794
+ def on_action_export_plot(self):
795
+ dlg = export.ExportPlot(self, pipeline=self.pipeline)
796
+ dlg.exec()
797
+
798
+ @QtCore.pyqtSlot()
799
+ def on_action_import_filter(self, path=None):
800
+ """Import a filter from a .sof or .poly file
801
+
802
+ Parameters
803
+ ----------
804
+ path: str or pathlib.Path
805
+ path to the filter to impoert
806
+ """
807
+ if path is None:
808
+ path, _ = QtWidgets.QFileDialog.getOpenFileName(
809
+ self, 'Select Filter', '', 'Filters formats (*.poly *.sof)')
810
+ if path:
811
+ session.import_filters(path, self.pipeline)
812
+ # update UI
813
+ self.reload_pipeline()
814
+
815
+ @QtCore.pyqtSlot()
816
+ def on_action_open(self, path=None):
817
+ """Open a DCscope session"""
818
+ if self.pipeline.slots or self.pipeline.filters:
819
+ if not self.on_action_clear():
820
+ return
821
+ if path is None:
822
+ path, _ = QtWidgets.QFileDialog.getOpenFileName(
823
+ self, 'Open session', '', 'DCscope session (*.so2)',
824
+ 'DCscope session (*.so2)',
825
+ QtWidgets.QFileDialog.Option.DontUseNativeDialog)
826
+ if path:
827
+ search_paths = []
828
+ while True:
829
+ try:
830
+ with widgets.ShowWaitCursor():
831
+ session.open_session(path, self.pipeline, search_paths)
832
+ except session.DataFileNotFoundError as e:
833
+ missds = "\r".join([str(pp) for pp in e.missing_paths])
834
+ msg = QtWidgets.QMessageBox()
835
+ msg.setIcon(QtWidgets.QMessageBox.Icon.Warning)
836
+ msg.setText("Some datasets were not found! "
837
+ + "Please specify a search location.")
838
+ msg.setWindowTitle(
839
+ "Missing {} dataset(s)".format(len(e.missing_paths)))
840
+ msg.setDetailedText("Missing files: \n\n" + missds)
841
+ msg.exec()
842
+ spath = QtWidgets.QFileDialog.getExistingDirectory(
843
+ self, 'Data search path')
844
+ if spath:
845
+ search_paths.append(spath)
846
+ else:
847
+ break
848
+ else:
849
+ break
850
+ self.show()
851
+ self.reload_pipeline()
852
+ self.setWindowTitle(
853
+ f"{pathlib.Path(path).name} [DCscope {version}]")
854
+
855
+ @QtCore.pyqtSlot()
856
+ def on_action_preferences(self):
857
+ """Show the preferences dialog"""
858
+ dlg = preferences.Preferences(self)
859
+ dlg.setWindowTitle("DCscope Preferences")
860
+ dlg.feature_changed.connect(self.plots_changed)
861
+ dlg.feature_changed.connect(self.on_quickview_refresh)
862
+ dlg.exec()
863
+
864
+ @QtCore.pyqtSlot()
865
+ def on_action_quit(self):
866
+ """Determine what happens when the user wants to quit"""
867
+ if self.pipeline.slots or self.pipeline.filters:
868
+ if not self.on_action_clear():
869
+ return
870
+ QtCore.QCoreApplication.quit()
871
+
872
+ @QtCore.pyqtSlot()
873
+ def on_action_save(self):
874
+ path, _ = QtWidgets.QFileDialog.getSaveFileName(
875
+ self, 'Save session', '', 'DCscope session (*.so2)')
876
+ if path:
877
+ if not path.endswith(".so2"):
878
+ path += ".so2"
879
+ session.save_session(path, self.pipeline)
880
+ self.setWindowTitle(
881
+ f"{pathlib.Path(path).name} [DCscope {version}]")
882
+
883
+ @QtCore.pyqtSlot()
884
+ def on_action_software(self):
885
+ libs = [dclab,
886
+ h5py,
887
+ numpy,
888
+ pg,
889
+ scipy,
890
+ ]
891
+
892
+ sw_text = f"DCscope {version}\n\n"
893
+ sw_text += f"Python {sys.version}\n\n"
894
+ sw_text += "Modules:\n"
895
+ for lib in libs:
896
+ sw_text += f"- {lib.__name__} {lib.__version__}\n"
897
+ sw_text += f"- PyQt6 {QtCore.QT_VERSION_STR}\n" # Extrawurst
898
+ sw_text += "\n Breeze icon theme by the KDE Community (LGPL)."
899
+ if hasattr(sys, 'frozen'):
900
+ sw_text += "\nThis executable has been created using PyInstaller."
901
+ QtWidgets.QMessageBox.information(self, "Software", sw_text)
902
+
903
+ @QtCore.pyqtSlot(int)
904
+ def on_remove_polygon_filter_from_pipeline(self, pf_id):
905
+ """Remove a polygon filter from all filters in the pipeline"""
906
+ for filt in self.pipeline.filters:
907
+ if pf_id in filt.polylist:
908
+ filt.polylist.remove(pf_id)
909
+
910
+ @widgets.show_wait_cursor
911
+ @QtCore.pyqtSlot()
912
+ def on_data_matrix(self):
913
+ """Show/hide data matrix (User clicked Data Matrix button)"""
914
+ if self.toolButton_dm.isChecked():
915
+ self.splitter.setSizes([200, 1000])
916
+ else:
917
+ self.splitter.setSizes([0, 1])
918
+ # redraw
919
+ self.splitter.update()
920
+ self.mdiArea.update()
921
+ self.block_matrix.update()
922
+
923
+ @QtCore.pyqtSlot()
924
+ def on_new_polygon_filter(self):
925
+ if not self.pipeline.slots:
926
+ msg = QtWidgets.QMessageBox()
927
+ msg.setIcon(QtWidgets.QMessageBox.Icon.Critical)
928
+ msg.setText("A dataset is required for creating a polygon filter!")
929
+ msg.setWindowTitle("No dataset loaded")
930
+ msg.exec()
931
+ else:
932
+ slot_index, _ = self.block_matrix.get_quickview_indices()
933
+ if slot_index is None:
934
+ # show the first dataset
935
+ self.on_quickview_show_dataset(0, 0)
936
+ self.on_quickview(view=True)
937
+ self.widget_quick_view.on_poly_create()
938
+ # adjusts QuickView size correctly
939
+ self.widget_quick_view.on_tool()
940
+
941
+ @widgets.show_wait_cursor
942
+ @QtCore.pyqtSlot(str)
943
+ def on_modify_filter(self, filt_id):
944
+ self.widget_ana_view.tabWidget.setCurrentWidget(
945
+ self.widget_ana_view.tab_filter)
946
+ self.widget_ana_view.widget_filter.show_filter(filt_id)
947
+ # finally, check the button
948
+ self.toolButton_ana_view.setChecked(True)
949
+ self.subwindows["analysis_view"].setVisible(True)
950
+ # redraw
951
+ self.mdiArea.update()
952
+ self.subwindows["analysis_view"].update()
953
+
954
+ @widgets.show_wait_cursor
955
+ @QtCore.pyqtSlot(str)
956
+ def on_modify_plot(self, plot_id):
957
+ self.widget_ana_view.tabWidget.setCurrentWidget(
958
+ self.widget_ana_view.tab_plot)
959
+ self.widget_ana_view.widget_plot.show_plot(plot_id)
960
+ # finally, check the button
961
+ self.toolButton_ana_view.setChecked(True)
962
+ self.subwindows["analysis_view"].setVisible(True)
963
+ # redraw
964
+ self.mdiArea.update()
965
+ self.subwindows["analysis_view"].update()
966
+
967
+ @widgets.show_wait_cursor
968
+ @QtCore.pyqtSlot(str)
969
+ def on_modify_slot(self, slot_id):
970
+ self.widget_ana_view.tabWidget.setCurrentWidget(
971
+ self.widget_ana_view.tab_slot)
972
+ self.widget_ana_view.widget_slot.show_slot(slot_id)
973
+ # finally, check the button
974
+ self.toolButton_ana_view.setChecked(True)
975
+ self.subwindows["analysis_view"].setVisible(True)
976
+ # redraw
977
+ self.mdiArea.update()
978
+ self.subwindows["analysis_view"].update()
979
+
980
+ @widgets.show_wait_cursor
981
+ @QtCore.pyqtSlot(bool)
982
+ def on_quickview(self, view=True):
983
+ """Show/Hide QuickView (User clicked the QuickView button)"""
984
+ self.subwindows["quick_view"].setVisible(view)
985
+ self.block_matrix.enable_quickview(view)
986
+ # redraw
987
+ self.mdiArea.update()
988
+ if view:
989
+ self.subwindows["quick_view"].update()
990
+
991
+ @widgets.show_wait_cursor
992
+ @QtCore.pyqtSlot()
993
+ def on_quickview_refresh(self):
994
+ """Refresh quickview with the currently shown dataset"""
995
+ slot_index, filt_index = self.block_matrix.get_quickview_indices()
996
+ if slot_index is not None:
997
+ self.on_quickview_show_dataset(slot_index, filt_index,
998
+ update_ana_filter=False)
999
+
1000
+ @widgets.show_wait_cursor
1001
+ @QtCore.pyqtSlot(int, int)
1002
+ def on_quickview_show_dataset(self,
1003
+ slot_index: int,
1004
+ filt_index: int,
1005
+ update_ana_filter: bool = True):
1006
+ """Update QuickView dataset (User selected new dataset)
1007
+
1008
+ Parameters
1009
+ ----------
1010
+ slot_index: int
1011
+ Index of the slot in `self.pipeline`
1012
+ filt_index: int
1013
+ Index of the filter in `self.pipeline`
1014
+ update_ana_filter: bool
1015
+ Whether to update the filter panel in the analysis view.
1016
+ If True, matches the filter displayed in the panel to
1017
+ the one where QuickView is currently set active. If
1018
+ False, nothing is changed.
1019
+ """
1020
+ if slot_index < 0 or filt_index < 0:
1021
+ return
1022
+ try:
1023
+ ds = self.pipeline.get_dataset(slot_index=slot_index,
1024
+ filt_index=filt_index,
1025
+ apply_filter=True)
1026
+ except IndexError:
1027
+ # There is no such slot.
1028
+ return
1029
+
1030
+ slot = self.pipeline.slots[slot_index]
1031
+ # update quick view subwindow
1032
+ self.widget_quick_view.show_rtdc(ds, slot)
1033
+ # show quick view subwindow
1034
+ if not self.subwindows["quick_view"].isVisible():
1035
+ self.toolButton_quick_view.toggle()
1036
+ self.subwindows["quick_view"].setVisible(True)
1037
+ if update_ana_filter:
1038
+ # update FilterPanel
1039
+ filt_id = self.pipeline.filters[filt_index].identifier
1040
+ self.widget_ana_view.widget_filter.show_filter(filt_id=filt_id)
1041
+
1042
+ @QtCore.pyqtSlot()
1043
+ def on_splitter(self):
1044
+ if self.splitter.sizes()[0] == 0:
1045
+ self.toolButton_dm.setChecked(False)
1046
+ else:
1047
+ self.toolButton_dm.setChecked(True)
1048
+
1049
+ @QtCore.pyqtSlot()
1050
+ def reload_pipeline(self):
1051
+ """Convenience function for reloading the current pipeline"""
1052
+ self.adopt_pipeline(self.pipeline.__getstate__())
1053
+
1054
+
1055
+ def excepthook(etype, value, trace):
1056
+ """
1057
+ Handler for all unhandled exceptions.
1058
+
1059
+ :param `etype`: the exception type (`SyntaxError`,
1060
+ `ZeroDivisionError`, etc...);
1061
+ :type `etype`: `Exception`
1062
+ :param string `value`: the exception error message;
1063
+ :param string `trace`: the traceback header, if any (otherwise, it
1064
+ prints the standard Python header: ``Traceback (most recent
1065
+ call last)``.
1066
+ """
1067
+ vinfo = f"Unhandled exception in DCscope version {version}:\n"
1068
+ tmp = traceback.format_exception(etype, value, trace)
1069
+ exception = "".join([vinfo]+tmp)
1070
+
1071
+ errorbox = QtWidgets.QMessageBox()
1072
+ errorbox.setIcon(QtWidgets.QMessageBox.Icon.Critical)
1073
+ errorbox.addButton(QtWidgets.QPushButton('Close'),
1074
+ QtWidgets.QMessageBox.ButtonRole.YesRole)
1075
+ errorbox.addButton(QtWidgets.QPushButton(
1076
+ 'Copy text && Close'), QtWidgets.QMessageBox.ButtonRole.NoRole)
1077
+ errorbox.setText(exception)
1078
+ ret = errorbox.exec()
1079
+ print(exception)
1080
+ if ret == 1:
1081
+ cb = QtWidgets.QApplication.clipboard()
1082
+ cb.clear(mode=cb.Mode.Clipboard)
1083
+ cb.setText(exception)
1084
+
1085
+
1086
+ # Make Ctr+C close the app
1087
+ signal.signal(signal.SIGINT, signal.SIG_DFL)
1088
+ # Display exception hook in separate dialog instead of crashing
1089
+ sys.excepthook = excepthook