phasor-handler 2.2.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.
- phasor_handler/__init__.py +9 -0
- phasor_handler/app.py +249 -0
- phasor_handler/img/icons/chevron-down.svg +3 -0
- phasor_handler/img/icons/chevron-up.svg +3 -0
- phasor_handler/img/logo.ico +0 -0
- phasor_handler/models/dir_manager.py +100 -0
- phasor_handler/scripts/contrast.py +131 -0
- phasor_handler/scripts/convert.py +155 -0
- phasor_handler/scripts/meta_reader.py +467 -0
- phasor_handler/scripts/plot.py +110 -0
- phasor_handler/scripts/register.py +86 -0
- phasor_handler/themes/__init__.py +8 -0
- phasor_handler/themes/dark_theme.py +330 -0
- phasor_handler/tools/__init__.py +1 -0
- phasor_handler/tools/check_stylesheet.py +15 -0
- phasor_handler/tools/misc.py +20 -0
- phasor_handler/widgets/__init__.py +5 -0
- phasor_handler/widgets/analysis/components/__init__.py +9 -0
- phasor_handler/widgets/analysis/components/bnc.py +426 -0
- phasor_handler/widgets/analysis/components/circle_roi.py +850 -0
- phasor_handler/widgets/analysis/components/image_view.py +667 -0
- phasor_handler/widgets/analysis/components/meta_info.py +481 -0
- phasor_handler/widgets/analysis/components/roi_list.py +659 -0
- phasor_handler/widgets/analysis/components/trace_plot.py +621 -0
- phasor_handler/widgets/analysis/view.py +1735 -0
- phasor_handler/widgets/conversion/view.py +83 -0
- phasor_handler/widgets/registration/view.py +110 -0
- phasor_handler/workers/__init__.py +2 -0
- phasor_handler/workers/analysis_worker.py +0 -0
- phasor_handler/workers/histogram_worker.py +55 -0
- phasor_handler/workers/registration_worker.py +242 -0
- phasor_handler-2.2.1.dist-info/METADATA +134 -0
- phasor_handler-2.2.1.dist-info/RECORD +37 -0
- phasor_handler-2.2.1.dist-info/WHEEL +5 -0
- phasor_handler-2.2.1.dist-info/entry_points.txt +5 -0
- phasor_handler-2.2.1.dist-info/licenses/LICENSE.md +21 -0
- phasor_handler-2.2.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Metadata Information Viewer Component
|
|
3
|
+
|
|
4
|
+
This module contains the MetadataViewer dialog that displays experiment metadata
|
|
5
|
+
in a formatted, user-friendly way.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from PyQt6.QtWidgets import (
|
|
9
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QTextEdit, QPushButton,
|
|
10
|
+
QScrollArea, QWidget, QFrame, QGridLayout, QTabWidget, QSplitter,
|
|
11
|
+
QGroupBox, QTreeWidget, QTreeWidgetItem, QHeaderView
|
|
12
|
+
)
|
|
13
|
+
from PyQt6.QtCore import Qt, QSize
|
|
14
|
+
from PyQt6.QtGui import QFont, QTextOption
|
|
15
|
+
import json
|
|
16
|
+
import datetime
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MetadataViewer(QDialog):
|
|
20
|
+
"""
|
|
21
|
+
Dialog window that displays experiment metadata in a user-friendly format.
|
|
22
|
+
|
|
23
|
+
Handles both dictionary and object-based metadata formats and provides
|
|
24
|
+
multiple views (Overview, Raw JSON, Tree View) for comprehensive data inspection.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, parent=None):
|
|
28
|
+
super().__init__(parent)
|
|
29
|
+
self.setWindowTitle("Experiment Metadata Viewer")
|
|
30
|
+
self.setModal(False) # Allow interaction with main window while open
|
|
31
|
+
self.resize(400, 500)
|
|
32
|
+
|
|
33
|
+
self.metadata = None
|
|
34
|
+
self.directory_path = None
|
|
35
|
+
|
|
36
|
+
self.setupUI()
|
|
37
|
+
|
|
38
|
+
def setupUI(self):
|
|
39
|
+
"""Set up the user interface."""
|
|
40
|
+
layout = QVBoxLayout()
|
|
41
|
+
|
|
42
|
+
# Header with directory path
|
|
43
|
+
self.header_label = QLabel("No experiment data loaded")
|
|
44
|
+
self.header_label.setStyleSheet("""
|
|
45
|
+
QLabel {
|
|
46
|
+
font-size: 14px;
|
|
47
|
+
font-weight: bold;
|
|
48
|
+
padding: 10px;
|
|
49
|
+
background-color: #2E4A67;
|
|
50
|
+
border: 1px solid #ddd;
|
|
51
|
+
border-radius: 5px;
|
|
52
|
+
}
|
|
53
|
+
""")
|
|
54
|
+
layout.addWidget(self.header_label)
|
|
55
|
+
|
|
56
|
+
# Create tab widget for different views
|
|
57
|
+
self.tab_widget = QTabWidget()
|
|
58
|
+
|
|
59
|
+
# Overview tab
|
|
60
|
+
self.overview_tab = self.create_overview_tab()
|
|
61
|
+
self.tab_widget.addTab(self.overview_tab, "Overview")
|
|
62
|
+
|
|
63
|
+
# Detailed tree view tab
|
|
64
|
+
self.tree_tab = self.create_tree_tab()
|
|
65
|
+
self.tab_widget.addTab(self.tree_tab, "Tree View")
|
|
66
|
+
|
|
67
|
+
# Raw JSON tab
|
|
68
|
+
self.raw_tab = self.create_raw_tab()
|
|
69
|
+
self.tab_widget.addTab(self.raw_tab, "Raw JSON")
|
|
70
|
+
|
|
71
|
+
layout.addWidget(self.tab_widget)
|
|
72
|
+
|
|
73
|
+
# Button bar
|
|
74
|
+
button_layout = QHBoxLayout()
|
|
75
|
+
|
|
76
|
+
self.refresh_button = QPushButton("Refresh")
|
|
77
|
+
self.refresh_button.clicked.connect(self.refresh_metadata)
|
|
78
|
+
button_layout.addWidget(self.refresh_button)
|
|
79
|
+
|
|
80
|
+
button_layout.addStretch()
|
|
81
|
+
|
|
82
|
+
self.close_button = QPushButton("Close")
|
|
83
|
+
self.close_button.clicked.connect(self.close)
|
|
84
|
+
button_layout.addWidget(self.close_button)
|
|
85
|
+
|
|
86
|
+
layout.addLayout(button_layout)
|
|
87
|
+
|
|
88
|
+
self.setLayout(layout)
|
|
89
|
+
|
|
90
|
+
def create_overview_tab(self):
|
|
91
|
+
"""Create the overview tab with key experiment information."""
|
|
92
|
+
scroll_area = QScrollArea()
|
|
93
|
+
scroll_widget = QWidget()
|
|
94
|
+
layout = QVBoxLayout()
|
|
95
|
+
|
|
96
|
+
# Experiment Summary Group
|
|
97
|
+
self.exp_summary_group = QGroupBox("Experiment Summary")
|
|
98
|
+
self.exp_summary_layout = QGridLayout()
|
|
99
|
+
self.exp_summary_group.setLayout(self.exp_summary_layout)
|
|
100
|
+
layout.addWidget(self.exp_summary_group)
|
|
101
|
+
|
|
102
|
+
# Timing Information Group
|
|
103
|
+
self.timing_group = QGroupBox("Timing Information")
|
|
104
|
+
self.timing_layout = QGridLayout()
|
|
105
|
+
self.timing_group.setLayout(self.timing_layout)
|
|
106
|
+
layout.addWidget(self.timing_group)
|
|
107
|
+
|
|
108
|
+
# Stimulation Information Group
|
|
109
|
+
self.stim_group = QGroupBox("Stimulation Information")
|
|
110
|
+
self.stim_layout = QGridLayout()
|
|
111
|
+
self.stim_group.setLayout(self.stim_layout)
|
|
112
|
+
layout.addWidget(self.stim_group)
|
|
113
|
+
|
|
114
|
+
# Image Information Group
|
|
115
|
+
self.image_group = QGroupBox("Image Information")
|
|
116
|
+
self.image_layout = QGridLayout()
|
|
117
|
+
self.image_group.setLayout(self.image_layout)
|
|
118
|
+
layout.addWidget(self.image_group)
|
|
119
|
+
|
|
120
|
+
layout.addStretch()
|
|
121
|
+
scroll_widget.setLayout(layout)
|
|
122
|
+
scroll_area.setWidget(scroll_widget)
|
|
123
|
+
scroll_area.setWidgetResizable(True)
|
|
124
|
+
|
|
125
|
+
return scroll_area
|
|
126
|
+
|
|
127
|
+
def create_tree_tab(self):
|
|
128
|
+
"""Create the tree view tab for hierarchical data exploration."""
|
|
129
|
+
self.tree_widget = QTreeWidget()
|
|
130
|
+
self.tree_widget.setHeaderLabels(["Key", "Value", "Type"])
|
|
131
|
+
|
|
132
|
+
# Set column widths
|
|
133
|
+
header = self.tree_widget.header()
|
|
134
|
+
header.setStretchLastSection(False)
|
|
135
|
+
header.setSectionResizeMode(0, QHeaderView.ResizeMode.Interactive)
|
|
136
|
+
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
|
137
|
+
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
|
138
|
+
|
|
139
|
+
return self.tree_widget
|
|
140
|
+
|
|
141
|
+
def create_raw_tab(self):
|
|
142
|
+
"""Create the raw JSON tab for viewing unformatted data."""
|
|
143
|
+
self.raw_text_edit = QTextEdit()
|
|
144
|
+
self.raw_text_edit.setReadOnly(True)
|
|
145
|
+
|
|
146
|
+
# Set monospace font for better readability
|
|
147
|
+
font = QFont("Consolas", 10)
|
|
148
|
+
if not font.exactMatch():
|
|
149
|
+
font = QFont("Courier New", 10)
|
|
150
|
+
self.raw_text_edit.setFont(font)
|
|
151
|
+
|
|
152
|
+
return self.raw_text_edit
|
|
153
|
+
|
|
154
|
+
def set_metadata(self, metadata, directory_path=None):
|
|
155
|
+
"""
|
|
156
|
+
Set the metadata to display.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
metadata: The experiment metadata (dict or object)
|
|
160
|
+
directory_path: Path to the experiment directory
|
|
161
|
+
"""
|
|
162
|
+
self.metadata = metadata
|
|
163
|
+
self.directory_path = directory_path
|
|
164
|
+
|
|
165
|
+
if directory_path:
|
|
166
|
+
self.header_label.setText(f"Experiment Metadata: {directory_path}")
|
|
167
|
+
else:
|
|
168
|
+
self.header_label.setText("Experiment Metadata")
|
|
169
|
+
|
|
170
|
+
self.update_display()
|
|
171
|
+
|
|
172
|
+
def update_display(self):
|
|
173
|
+
"""Update all tabs with current metadata."""
|
|
174
|
+
if self.metadata is None:
|
|
175
|
+
self.clear_display()
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
self.update_overview_tab()
|
|
179
|
+
self.update_tree_tab()
|
|
180
|
+
self.update_raw_tab()
|
|
181
|
+
|
|
182
|
+
def clear_display(self):
|
|
183
|
+
"""Clear all displayed content."""
|
|
184
|
+
# Clear overview tab
|
|
185
|
+
self.clear_group_layout(self.exp_summary_layout)
|
|
186
|
+
self.clear_group_layout(self.timing_layout)
|
|
187
|
+
self.clear_group_layout(self.stim_layout)
|
|
188
|
+
self.clear_group_layout(self.image_layout)
|
|
189
|
+
|
|
190
|
+
# Clear tree tab
|
|
191
|
+
self.tree_widget.clear()
|
|
192
|
+
|
|
193
|
+
# Clear raw tab
|
|
194
|
+
self.raw_text_edit.clear()
|
|
195
|
+
|
|
196
|
+
self.header_label.setText("No experiment data loaded")
|
|
197
|
+
|
|
198
|
+
def clear_group_layout(self, layout):
|
|
199
|
+
"""Clear all widgets from a group layout."""
|
|
200
|
+
while layout.count():
|
|
201
|
+
child = layout.takeAt(0)
|
|
202
|
+
if child.widget():
|
|
203
|
+
child.widget().deleteLater()
|
|
204
|
+
|
|
205
|
+
def update_overview_tab(self):
|
|
206
|
+
"""Update the overview tab with key information."""
|
|
207
|
+
# Clear existing content
|
|
208
|
+
self.clear_group_layout(self.exp_summary_layout)
|
|
209
|
+
self.clear_group_layout(self.timing_layout)
|
|
210
|
+
self.clear_group_layout(self.stim_layout)
|
|
211
|
+
self.clear_group_layout(self.image_layout)
|
|
212
|
+
|
|
213
|
+
if isinstance(self.metadata, dict):
|
|
214
|
+
self.update_overview_from_dict()
|
|
215
|
+
else:
|
|
216
|
+
self.update_overview_from_object()
|
|
217
|
+
|
|
218
|
+
def update_overview_from_dict(self):
|
|
219
|
+
"""Update overview from dictionary metadata."""
|
|
220
|
+
# Experiment Summary
|
|
221
|
+
row = 0
|
|
222
|
+
for key in ['device_name', 'n_frames']:
|
|
223
|
+
if key in self.metadata:
|
|
224
|
+
value = self.metadata[key]
|
|
225
|
+
if key == "device_name": key = "Device Name"
|
|
226
|
+
elif key == "n_frames": key = "Number of Frames"
|
|
227
|
+
|
|
228
|
+
self.add_info_row(self.exp_summary_layout, row, key.replace('_', ' '),
|
|
229
|
+
str(value), self.format_value_with_unit(key, value))
|
|
230
|
+
row += 1
|
|
231
|
+
|
|
232
|
+
# Timing Information
|
|
233
|
+
row = 0
|
|
234
|
+
time_keys = ['time_stamps', 'elapsed_times', 'acquisition_start_time', 'year', 'hour']
|
|
235
|
+
for key in time_keys:
|
|
236
|
+
if key in self.metadata:
|
|
237
|
+
value = self.metadata[key]
|
|
238
|
+
if key == "year":
|
|
239
|
+
year = self.metadata["year"]
|
|
240
|
+
month = self.metadata["month"]
|
|
241
|
+
day = self.metadata["day"]
|
|
242
|
+
display_value = f"{day:02d}/{month:02d}/{year:04d}"
|
|
243
|
+
key = "Date (DDMMYYYY)"
|
|
244
|
+
elif key == "hour":
|
|
245
|
+
hour = self.metadata["hour"]
|
|
246
|
+
minute = self.metadata["minute"]
|
|
247
|
+
second = self.metadata["second"]
|
|
248
|
+
display_value = f"{hour:02d}:{minute:02d}:{second:02d}"
|
|
249
|
+
key = "Local Time"
|
|
250
|
+
elif isinstance(value, list) and len(value) > 0:
|
|
251
|
+
interval = (sum([value[i+1] - value[i] for i in range(len(value)-1)])/(len(value)-1))/1000
|
|
252
|
+
start_display = "0" if value[0] == 0 else f"{value[0]/1000:.3f}"
|
|
253
|
+
display_value = f"From {start_display} to {value[-1]/1000:.3f} seconds with an average interval of {interval:.3f} seconds"
|
|
254
|
+
else:
|
|
255
|
+
display_value = str(value)
|
|
256
|
+
self.add_info_row(self.timing_layout, row, key.replace('_', ' ').title(),
|
|
257
|
+
display_value) if key != "Date (DDMMYYYY)" else self.add_info_row(self.timing_layout, row, key, display_value)
|
|
258
|
+
row += 1
|
|
259
|
+
|
|
260
|
+
# Stimulation Information
|
|
261
|
+
row = 0
|
|
262
|
+
stim_keys = ['stimulation_timeframes', 'stimulation_ms', 'duty_cycle', 'stimulated_roi_location', 'stimulated_rois']
|
|
263
|
+
for key in stim_keys:
|
|
264
|
+
if key in self.metadata:
|
|
265
|
+
value = self.metadata[key]
|
|
266
|
+
if isinstance(value, list):
|
|
267
|
+
if key == 'stimulation_timeframes':
|
|
268
|
+
display_value = ', '.join(map(str, value))
|
|
269
|
+
key = "Stimulation Timeframes"
|
|
270
|
+
elif key == 'stimulation_ms':
|
|
271
|
+
display_value = ', '.join(map(str, [int(v/1000) for v in value]))
|
|
272
|
+
key = "Stimulation Time (s)"
|
|
273
|
+
elif key == 'duty_cycle':
|
|
274
|
+
duty_cycle_counts = {val: value.count(val) for val in set(value)}
|
|
275
|
+
display_value = ' | '.join([f'{val}: {count}X' for val, count in duty_cycle_counts.items()])
|
|
276
|
+
key = "Duty Cycle"
|
|
277
|
+
elif key == 'stimulated_roi_location':
|
|
278
|
+
display_value = ', '.join(map(str, [len(x) for x in value]))
|
|
279
|
+
key = "Number of stimulated ROIs"
|
|
280
|
+
elif key == 'stimulated_rois':
|
|
281
|
+
if not value or all(not sublist for sublist in value):
|
|
282
|
+
display_value = "NA"
|
|
283
|
+
else:
|
|
284
|
+
display_value = ', '.join([', '.join(map(str, x)) for x in value])
|
|
285
|
+
key = "Stimulated ROIs"
|
|
286
|
+
else:
|
|
287
|
+
display_value = str(value)
|
|
288
|
+
self.add_info_row(self.stim_layout, row, key.replace('_', ' '),
|
|
289
|
+
display_value)
|
|
290
|
+
row += 1
|
|
291
|
+
|
|
292
|
+
# Image Information
|
|
293
|
+
row = 0
|
|
294
|
+
image_keys = ['pixel_size', 'FOV_size']
|
|
295
|
+
for key in image_keys:
|
|
296
|
+
if key in self.metadata:
|
|
297
|
+
value = self.metadata[key]
|
|
298
|
+
if key == "pixel_size": key = "Pixel Size (µm)"
|
|
299
|
+
elif key == "FOV_size":
|
|
300
|
+
key = "FOV Size (µm)"
|
|
301
|
+
value = value[:-7]
|
|
302
|
+
|
|
303
|
+
self.add_info_row(self.image_layout, row, key.replace('_', ' '),
|
|
304
|
+
str(value))
|
|
305
|
+
row += 1
|
|
306
|
+
|
|
307
|
+
def update_overview_from_object(self):
|
|
308
|
+
"""Update overview from object metadata."""
|
|
309
|
+
# Get all non-private attributes
|
|
310
|
+
attrs = [attr for attr in dir(self.metadata) if not attr.startswith('_')]
|
|
311
|
+
|
|
312
|
+
# Categorize attributes
|
|
313
|
+
exp_attrs = []
|
|
314
|
+
time_attrs = []
|
|
315
|
+
stim_attrs = []
|
|
316
|
+
image_attrs = []
|
|
317
|
+
|
|
318
|
+
for attr in attrs:
|
|
319
|
+
if any(keyword in attr.lower() for keyword in ['frame', 'duration', 'rate']):
|
|
320
|
+
exp_attrs.append(attr)
|
|
321
|
+
elif any(keyword in attr.lower() for keyword in ['time', 'elapsed']):
|
|
322
|
+
time_attrs.append(attr)
|
|
323
|
+
elif any(keyword in attr.lower() for keyword in ['stim', 'duty']):
|
|
324
|
+
stim_attrs.append(attr)
|
|
325
|
+
elif any(keyword in attr.lower() for keyword in ['image', 'width', 'height', 'channel', 'bit']):
|
|
326
|
+
image_attrs.append(attr)
|
|
327
|
+
else:
|
|
328
|
+
exp_attrs.append(attr) # Default to experiment summary
|
|
329
|
+
|
|
330
|
+
# Populate each section
|
|
331
|
+
self.populate_section_from_attrs(self.exp_summary_layout, exp_attrs)
|
|
332
|
+
self.populate_section_from_attrs(self.timing_layout, time_attrs)
|
|
333
|
+
self.populate_section_from_attrs(self.stim_layout, stim_attrs)
|
|
334
|
+
self.populate_section_from_attrs(self.image_layout, image_attrs)
|
|
335
|
+
|
|
336
|
+
def populate_section_from_attrs(self, layout, attrs):
|
|
337
|
+
"""Populate a section with attributes from the metadata object."""
|
|
338
|
+
for row, attr in enumerate(attrs):
|
|
339
|
+
try:
|
|
340
|
+
value = getattr(self.metadata, attr)
|
|
341
|
+
if isinstance(value, list) and len(value) > 0:
|
|
342
|
+
display_value = f"Array with {len(value)} entries"
|
|
343
|
+
if len(value) <= 10:
|
|
344
|
+
display_value += f": {value}"
|
|
345
|
+
else:
|
|
346
|
+
display_value += f" (first few: {value[:3]}...)"
|
|
347
|
+
else:
|
|
348
|
+
display_value = str(value)
|
|
349
|
+
self.add_info_row(layout, row, attr.replace('_', ' ').title(), display_value)
|
|
350
|
+
except Exception as e:
|
|
351
|
+
self.add_info_row(layout, row, attr.replace('_', ' ').title(), f"Error: {e}")
|
|
352
|
+
|
|
353
|
+
def add_info_row(self, layout, row, label, value, tooltip=None):
|
|
354
|
+
"""Add an information row to a layout."""
|
|
355
|
+
label_widget = QLabel(f"{label}:")
|
|
356
|
+
label_widget.setStyleSheet("font-weight: bold;")
|
|
357
|
+
|
|
358
|
+
value_widget = QLabel(str(value))
|
|
359
|
+
value_widget.setWordWrap(True)
|
|
360
|
+
value_widget.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
|
|
361
|
+
|
|
362
|
+
if tooltip:
|
|
363
|
+
value_widget.setToolTip(tooltip)
|
|
364
|
+
|
|
365
|
+
layout.addWidget(label_widget, row, 0)
|
|
366
|
+
layout.addWidget(value_widget, row, 1)
|
|
367
|
+
|
|
368
|
+
def format_value_with_unit(self, key, value):
|
|
369
|
+
"""Format values with appropriate units for tooltips."""
|
|
370
|
+
if 'ms' in key.lower():
|
|
371
|
+
return f"{value} milliseconds"
|
|
372
|
+
elif 'um' in key.lower():
|
|
373
|
+
return f"{value} micrometers"
|
|
374
|
+
elif 'rate' in key.lower():
|
|
375
|
+
return f"{value} Hz"
|
|
376
|
+
else:
|
|
377
|
+
return str(value)
|
|
378
|
+
|
|
379
|
+
def update_tree_tab(self):
|
|
380
|
+
"""Update the tree view tab."""
|
|
381
|
+
self.tree_widget.clear()
|
|
382
|
+
|
|
383
|
+
if self.metadata is None:
|
|
384
|
+
return
|
|
385
|
+
|
|
386
|
+
if isinstance(self.metadata, dict):
|
|
387
|
+
self.add_dict_to_tree(self.metadata, self.tree_widget.invisibleRootItem())
|
|
388
|
+
else:
|
|
389
|
+
self.add_object_to_tree(self.metadata, self.tree_widget.invisibleRootItem())
|
|
390
|
+
|
|
391
|
+
# Expand first level
|
|
392
|
+
for i in range(self.tree_widget.topLevelItemCount()):
|
|
393
|
+
item = self.tree_widget.topLevelItem(i)
|
|
394
|
+
if item.childCount() < 20: # Don't auto-expand large sections
|
|
395
|
+
item.setExpanded(True)
|
|
396
|
+
|
|
397
|
+
def add_dict_to_tree(self, data, parent_item, max_depth=5, current_depth=0):
|
|
398
|
+
"""Recursively add dictionary data to tree widget."""
|
|
399
|
+
if current_depth >= max_depth:
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
for key, value in data.items():
|
|
403
|
+
self.add_value_to_tree(key, value, parent_item, current_depth)
|
|
404
|
+
|
|
405
|
+
def add_object_to_tree(self, obj, parent_item, max_depth=5, current_depth=0):
|
|
406
|
+
"""Add object attributes to tree widget."""
|
|
407
|
+
if current_depth >= max_depth:
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
attrs = [attr for attr in dir(obj) if not attr.startswith('_')]
|
|
411
|
+
for attr in attrs:
|
|
412
|
+
try:
|
|
413
|
+
value = getattr(obj, attr)
|
|
414
|
+
if not callable(value): # Skip methods
|
|
415
|
+
self.add_value_to_tree(attr, value, parent_item, current_depth)
|
|
416
|
+
except Exception:
|
|
417
|
+
pass # Skip attributes that can't be accessed
|
|
418
|
+
|
|
419
|
+
def add_value_to_tree(self, key, value, parent_item, current_depth):
|
|
420
|
+
"""Add a key-value pair to the tree."""
|
|
421
|
+
item = QTreeWidgetItem(parent_item)
|
|
422
|
+
item.setText(0, str(key))
|
|
423
|
+
|
|
424
|
+
value_type = type(value).__name__
|
|
425
|
+
item.setText(2, value_type)
|
|
426
|
+
|
|
427
|
+
if isinstance(value, (dict, list, tuple)) and len(value) > 0:
|
|
428
|
+
if isinstance(value, dict):
|
|
429
|
+
item.setText(1, f"Dictionary with {len(value)} items")
|
|
430
|
+
if current_depth < 4: # Prevent infinite recursion
|
|
431
|
+
self.add_dict_to_tree(value, item, current_depth=current_depth+1)
|
|
432
|
+
elif isinstance(value, (list, tuple)):
|
|
433
|
+
if len(value) <= 5:
|
|
434
|
+
item.setText(1, str(value))
|
|
435
|
+
else:
|
|
436
|
+
item.setText(1, f"{value_type} with {len(value)} items: {value[:3]}...")
|
|
437
|
+
# Add first few items for lists
|
|
438
|
+
if current_depth < 3:
|
|
439
|
+
for i, list_val in enumerate(value[:10]): # Limit to first 10 items
|
|
440
|
+
self.add_value_to_tree(f"[{i}]", list_val, item, current_depth+1)
|
|
441
|
+
else:
|
|
442
|
+
# Truncate very long strings
|
|
443
|
+
str_value = str(value)
|
|
444
|
+
if len(str_value) > 200:
|
|
445
|
+
str_value = str_value[:200] + "..."
|
|
446
|
+
item.setText(1, str_value)
|
|
447
|
+
|
|
448
|
+
def update_raw_tab(self):
|
|
449
|
+
"""Update the raw JSON tab."""
|
|
450
|
+
if self.metadata is None:
|
|
451
|
+
self.raw_text_edit.clear()
|
|
452
|
+
return
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
if isinstance(self.metadata, dict):
|
|
456
|
+
json_text = json.dumps(self.metadata, indent=2, default=str)
|
|
457
|
+
else:
|
|
458
|
+
# Convert object to dict for JSON serialization
|
|
459
|
+
obj_dict = {}
|
|
460
|
+
attrs = [attr for attr in dir(self.metadata) if not attr.startswith('_')]
|
|
461
|
+
for attr in attrs:
|
|
462
|
+
try:
|
|
463
|
+
value = getattr(self.metadata, attr)
|
|
464
|
+
if not callable(value):
|
|
465
|
+
obj_dict[attr] = value
|
|
466
|
+
except Exception:
|
|
467
|
+
obj_dict[attr] = f"<Error accessing {attr}>"
|
|
468
|
+
|
|
469
|
+
json_text = json.dumps(obj_dict, indent=2, default=str)
|
|
470
|
+
|
|
471
|
+
self.raw_text_edit.setPlainText(json_text)
|
|
472
|
+
except Exception as e:
|
|
473
|
+
self.raw_text_edit.setPlainText(f"Error displaying metadata: {e}")
|
|
474
|
+
|
|
475
|
+
def refresh_metadata(self):
|
|
476
|
+
"""Refresh the metadata display."""
|
|
477
|
+
self.update_display()
|
|
478
|
+
|
|
479
|
+
def sizeHint(self):
|
|
480
|
+
"""Provide size hint for the dialog."""
|
|
481
|
+
return QSize(900, 700)
|