pycforge 0.15.2__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 (221) hide show
  1. pycforge/__init__.py +8 -0
  2. pycforge/__main__.py +2 -0
  3. pycforge/_version.py +1 -0
  4. pycforge/converter/__init__.py +0 -0
  5. pycforge/converter/analysis/__init__.py +5 -0
  6. pycforge/converter/analysis/functions.py +723 -0
  7. pycforge/converter/analysis/model.py +158 -0
  8. pycforge/converter/analysis/planning.py +464 -0
  9. pycforge/converter/analysis/stage.py +1976 -0
  10. pycforge/converter/analysis/symbols.py +237 -0
  11. pycforge/converter/analysis/validation.py +2166 -0
  12. pycforge/converter/c_output/__init__.py +2 -0
  13. pycforge/converter/c_output/conformance.py +297 -0
  14. pycforge/converter/c_output/renderer.py +220 -0
  15. pycforge/converter/conditional_regions/__init__.py +71 -0
  16. pycforge/converter/conditional_regions/analysis.py +481 -0
  17. pycforge/converter/conditional_regions/lowering.py +579 -0
  18. pycforge/converter/conditional_regions/model.py +243 -0
  19. pycforge/converter/conditional_regions/validation.py +690 -0
  20. pycforge/converter/containers/__init__.py +19 -0
  21. pycforge/converter/containers/analysis.py +542 -0
  22. pycforge/converter/containers/lowering.py +290 -0
  23. pycforge/converter/containers/model.py +146 -0
  24. pycforge/converter/contracts/__init__.py +0 -0
  25. pycforge/converter/contracts/configuration.py +178 -0
  26. pycforge/converter/contracts/identifiers.py +68 -0
  27. pycforge/converter/contracts/versions.py +71 -0
  28. pycforge/converter/core/__init__.py +0 -0
  29. pycforge/converter/core/artifact_io.py +87 -0
  30. pycforge/converter/core/cancellation.py +7 -0
  31. pycforge/converter/core/canonicalization.py +254 -0
  32. pycforge/converter/core/context.py +13 -0
  33. pycforge/converter/core/diagnostics.py +95 -0
  34. pycforge/converter/core/enums.py +23 -0
  35. pycforge/converter/core/fingerprint.py +22 -0
  36. pycforge/converter/core/pipeline.py +8 -0
  37. pycforge/converter/core/progress.py +25 -0
  38. pycforge/converter/core/request.py +50 -0
  39. pycforge/converter/core/resource_policy.py +31 -0
  40. pycforge/converter/core/result.py +23 -0
  41. pycforge/converter/core/serialization.py +36 -0
  42. pycforge/converter/core/stage.py +10 -0
  43. pycforge/converter/core/stage_artifact.py +80 -0
  44. pycforge/converter/core/stage_outcome.py +15 -0
  45. pycforge/converter/decision_trace/__init__.py +0 -0
  46. pycforge/converter/decision_trace/recorder.py +52 -0
  47. pycforge/converter/facade.py +459 -0
  48. pycforge/converter/frontend/__init__.py +4 -0
  49. pycforge/converter/frontend/normalizer.py +59 -0
  50. pycforge/converter/frontend/parser.py +20 -0
  51. pycforge/converter/frontend/source_document.py +93 -0
  52. pycforge/converter/frontend/stages.py +518 -0
  53. pycforge/converter/frontend/validation.py +76 -0
  54. pycforge/converter/io/__init__.py +0 -0
  55. pycforge/converter/io/atomic_writer.py +33 -0
  56. pycforge/converter/ir/__init__.py +1 -0
  57. pycforge/converter/ir/c_ir/__init__.py +4 -0
  58. pycforge/converter/ir/c_ir/builder.py +26 -0
  59. pycforge/converter/ir/c_ir/model.py +318 -0
  60. pycforge/converter/ir/c_ir/serialization.py +88 -0
  61. pycforge/converter/ir/c_ir/validation.py +834 -0
  62. pycforge/converter/ir/python_ir/__init__.py +17 -0
  63. pycforge/converter/ir/python_ir/nodes.py +86 -0
  64. pycforge/converter/keyword_calls/__init__.py +68 -0
  65. pycforge/converter/keyword_calls/analysis.py +458 -0
  66. pycforge/converter/keyword_calls/lowering.py +271 -0
  67. pycforge/converter/keyword_calls/model.py +304 -0
  68. pycforge/converter/keyword_calls/validation.py +802 -0
  69. pycforge/converter/keyword_only_calls/__init__.py +68 -0
  70. pycforge/converter/keyword_only_calls/analysis.py +640 -0
  71. pycforge/converter/keyword_only_calls/lowering.py +376 -0
  72. pycforge/converter/keyword_only_calls/model.py +328 -0
  73. pycforge/converter/keyword_only_calls/validation.py +1357 -0
  74. pycforge/converter/lowering.py +1000 -0
  75. pycforge/converter/modules/__init__.py +13 -0
  76. pycforge/converter/modules/analysis.py +1250 -0
  77. pycforge/converter/modules/lowering.py +200 -0
  78. pycforge/converter/modules/model.py +73 -0
  79. pycforge/converter/modules/stage.py +192 -0
  80. pycforge/converter/numeric_semantics/__init__.py +23 -0
  81. pycforge/converter/numeric_semantics/analysis.py +420 -0
  82. pycforge/converter/numeric_semantics/lowering.py +153 -0
  83. pycforge/converter/numeric_semantics/model.py +123 -0
  84. pycforge/converter/records/__init__.py +34 -0
  85. pycforge/converter/records/analysis.py +1394 -0
  86. pycforge/converter/records/lowering.py +393 -0
  87. pycforge/converter/records/model.py +319 -0
  88. pycforge/converter/rules/__init__.py +3 -0
  89. pycforge/converter/support_templates/__init__.py +49 -0
  90. pycforge/converter/support_templates/assembly.py +106 -0
  91. pycforge/converter/support_templates/factories.py +348 -0
  92. pycforge/converter/support_templates/model.py +159 -0
  93. pycforge/converter/support_templates/registry.py +388 -0
  94. pycforge/converter/telemetry/__init__.py +0 -0
  95. pycforge/converter/telemetry/sink.py +39 -0
  96. pycforge/ide/__init__.py +19 -0
  97. pycforge/ide/__main__.py +5 -0
  98. pycforge/ide/_worker_event_codec.py +276 -0
  99. pycforge/ide/_worker_protocol_json.py +197 -0
  100. pycforge/ide/_worker_protocol_types.py +90 -0
  101. pycforge/ide/_worker_request_codec.py +200 -0
  102. pycforge/ide/_worker_request_data.py +260 -0
  103. pycforge/ide/_worker_result_data.py +343 -0
  104. pycforge/ide/action_contract.py +469 -0
  105. pycforge/ide/action_validation.py +229 -0
  106. pycforge/ide/command_palette.py +223 -0
  107. pycforge/ide/controller.py +904 -0
  108. pycforge/ide/controller_conversion.py +546 -0
  109. pycforge/ide/controller_io.py +205 -0
  110. pycforge/ide/editor.py +951 -0
  111. pycforge/ide/editor_commands_qt.py +274 -0
  112. pycforge/ide/editor_lexical.py +43 -0
  113. pycforge/ide/editor_sidebars.py +236 -0
  114. pycforge/ide/editor_syntax.py +243 -0
  115. pycforge/ide/find_replace.py +535 -0
  116. pycforge/ide/icons.py +91 -0
  117. pycforge/ide/io_service.py +196 -0
  118. pycforge/ide/model.py +164 -0
  119. pycforge/ide/panels.py +903 -0
  120. pycforge/ide/positions.py +220 -0
  121. pycforge/ide/process_worker.py +130 -0
  122. pycforge/ide/qt.py +230 -0
  123. pycforge/ide/qt_actions.py +418 -0
  124. pycforge/ide/qt_close.py +169 -0
  125. pycforge/ide/qt_command_palette.py +297 -0
  126. pycforge/ide/qt_contract.py +220 -0
  127. pycforge/ide/qt_documents.py +590 -0
  128. pycforge/ide/qt_editor_buffers.py +113 -0
  129. pycforge/ide/qt_editor_surfaces.py +112 -0
  130. pycforge/ide/qt_menus.py +324 -0
  131. pycforge/ide/qt_projection.py +500 -0
  132. pycforge/ide/qt_shell.py +348 -0
  133. pycforge/ide/qt_shell_interactions.py +320 -0
  134. pycforge/ide/qt_state.py +393 -0
  135. pycforge/ide/qt_workspace_features.py +398 -0
  136. pycforge/ide/qt_workspace_navigation.py +208 -0
  137. pycforge/ide/qt_workspace_observers.py +173 -0
  138. pycforge/ide/qt_workspace_panels.py +545 -0
  139. pycforge/ide/qt_workspace_widgets.py +228 -0
  140. pycforge/ide/resources/icons/about.svg +1 -0
  141. pycforge/ide/resources/icons/add-document.svg +1 -0
  142. pycforge/ide/resources/icons/brand-mark.svg +1 -0
  143. pycforge/ide/resources/icons/bundle-search.svg +1 -0
  144. pycforge/ide/resources/icons/cancel.svg +1 -0
  145. pycforge/ide/resources/icons/check.svg +1 -0
  146. pycforge/ide/resources/icons/chevron-right.svg +1 -0
  147. pycforge/ide/resources/icons/close.svg +1 -0
  148. pycforge/ide/resources/icons/collapse-all.svg +1 -0
  149. pycforge/ide/resources/icons/command-palette.svg +1 -0
  150. pycforge/ide/resources/icons/convert.svg +1 -0
  151. pycforge/ide/resources/icons/copy.svg +1 -0
  152. pycforge/ide/resources/icons/cut.svg +1 -0
  153. pycforge/ide/resources/icons/decision-trace.svg +1 -0
  154. pycforge/ide/resources/icons/details.svg +1 -0
  155. pycforge/ide/resources/icons/diagnostics.svg +1 -0
  156. pycforge/ide/resources/icons/duplicate-line.svg +1 -0
  157. pycforge/ide/resources/icons/exit.svg +1 -0
  158. pycforge/ide/resources/icons/expand-all.svg +1 -0
  159. pycforge/ide/resources/icons/export.svg +1 -0
  160. pycforge/ide/resources/icons/find.svg +1 -0
  161. pycforge/ide/resources/icons/go-to-line.svg +1 -0
  162. pycforge/ide/resources/icons/go-to-output.svg +1 -0
  163. pycforge/ide/resources/icons/go-to-source.svg +1 -0
  164. pycforge/ide/resources/icons/history.svg +1 -0
  165. pycforge/ide/resources/icons/indent.svg +1 -0
  166. pycforge/ide/resources/icons/link-c.svg +1 -0
  167. pycforge/ide/resources/icons/mappings.svg +1 -0
  168. pycforge/ide/resources/icons/module.svg +1 -0
  169. pycforge/ide/resources/icons/move-down.svg +1 -0
  170. pycforge/ide/resources/icons/move-line-down.svg +1 -0
  171. pycforge/ide/resources/icons/move-line-up.svg +1 -0
  172. pycforge/ide/resources/icons/move-up.svg +1 -0
  173. pycforge/ide/resources/icons/next.svg +1 -0
  174. pycforge/ide/resources/icons/open.svg +1 -0
  175. pycforge/ide/resources/icons/outdent.svg +1 -0
  176. pycforge/ide/resources/icons/outline.svg +1 -0
  177. pycforge/ide/resources/icons/paste.svg +1 -0
  178. pycforge/ide/resources/icons/previous.svg +1 -0
  179. pycforge/ide/resources/icons/primary-module.svg +1 -0
  180. pycforge/ide/resources/icons/redo.svg +1 -0
  181. pycforge/ide/resources/icons/remove-document.svg +1 -0
  182. pycforge/ide/resources/icons/replace.svg +1 -0
  183. pycforge/ide/resources/icons/save-as.svg +1 -0
  184. pycforge/ide/resources/icons/save.svg +1 -0
  185. pycforge/ide/resources/icons/select-all.svg +1 -0
  186. pycforge/ide/resources/icons/settings.svg +1 -0
  187. pycforge/ide/resources/icons/show-c.svg +1 -0
  188. pycforge/ide/resources/icons/split-view.svg +1 -0
  189. pycforge/ide/resources/icons/summary.svg +1 -0
  190. pycforge/ide/resources/icons/telemetry.svg +1 -0
  191. pycforge/ide/resources/icons/toggle-comment.svg +1 -0
  192. pycforge/ide/resources/icons/toggle-fold.svg +1 -0
  193. pycforge/ide/resources/icons/undo.svg +1 -0
  194. pycforge/ide/resources/icons/whitespace.svg +1 -0
  195. pycforge/ide/revisions.py +400 -0
  196. pycforge/ide/search_service.py +280 -0
  197. pycforge/ide/session_history.py +150 -0
  198. pycforge/ide/source_editing.py +417 -0
  199. pycforge/ide/source_structure.py +558 -0
  200. pycforge/ide/source_structure_async.py +191 -0
  201. pycforge/ide/supervisor.py +503 -0
  202. pycforge/ide/theme.py +119 -0
  203. pycforge/ide/theme_stylesheet.py +598 -0
  204. pycforge/ide/theme_workspace_stylesheet.py +155 -0
  205. pycforge/ide/visual_tokens.py +142 -0
  206. pycforge/ide/worker_protocol.py +80 -0
  207. pycforge/ide/workspace_search.py +462 -0
  208. pycforge/ide/workspace_session.py +256 -0
  209. pycforge/laboratory/__init__.py +6 -0
  210. pycforge/laboratory/audits.py +2244 -0
  211. pycforge/laboratory/checkpoint_e.py +2685 -0
  212. pycforge/laboratory/cli.py +119 -0
  213. pycforge/laboratory/exit_codes.py +11 -0
  214. pycforge/laboratory/keyword_audit.py +661 -0
  215. pycforge/laboratory/keyword_only_audit.py +481 -0
  216. pycforge-0.15.2.dist-info/METADATA +209 -0
  217. pycforge-0.15.2.dist-info/RECORD +221 -0
  218. pycforge-0.15.2.dist-info/WHEEL +5 -0
  219. pycforge-0.15.2.dist-info/entry_points.txt +5 -0
  220. pycforge-0.15.2.dist-info/licenses/LICENSE +674 -0
  221. pycforge-0.15.2.dist-info/top_level.txt +1 -0
pycforge/ide/panels.py ADDED
@@ -0,0 +1,903 @@
1
+ """PyCForge workspace navigation and inspection panels.
2
+
3
+ The module remains import-safe if the required PyQt5 dependency is missing from
4
+ a damaged installation. The headless controller remains the product boundary;
5
+ these widgets only project immutable workspace snapshots.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from itertools import islice
12
+ from typing import Any, Iterable
13
+
14
+ try:
15
+ from PyQt5.QtCore import Qt, pyqtSignal
16
+ from PyQt5.QtGui import QBrush, QColor, QIcon
17
+ from PyQt5.QtWidgets import (
18
+ QAbstractItemView,
19
+ QCheckBox,
20
+ QFrame,
21
+ QHBoxLayout,
22
+ QLabel,
23
+ QLineEdit,
24
+ QListWidget,
25
+ QListWidgetItem,
26
+ QSizePolicy,
27
+ QTextBrowser,
28
+ QToolButton,
29
+ QTreeWidget,
30
+ QTreeWidgetItem,
31
+ QVBoxLayout,
32
+ QWidget,
33
+ )
34
+ except (ImportError, ModuleNotFoundError) as exc:
35
+ QT_AVAILABLE = False
36
+ _QT_ERROR = exc
37
+ else:
38
+ QT_AVAILABLE = True
39
+ _QT_ERROR = None
40
+
41
+ from .theme import pycforge_icon_path
42
+ from .visual_tokens import PYCFORGE_COLORS
43
+
44
+
45
+ INSPECTOR_TREE_MAX_NODES = 1024
46
+ INSPECTOR_TREE_MAX_DEPTH = 16
47
+ INSPECTOR_TREE_MAX_CHILDREN = 256
48
+ INSPECTOR_TREE_MAX_TEXT_CHARS = 2048
49
+
50
+
51
+ @dataclass(frozen=True)
52
+ class InspectorNode:
53
+ """One bounded, parent-indexed row in a structured inspector projection."""
54
+
55
+ parent_index: int | None
56
+ key: str
57
+ value: str
58
+
59
+
60
+ def _bounded_inspector_text(value: Any, limit: int) -> str:
61
+ if value is None:
62
+ return "null"
63
+ rendered = value if isinstance(value, str) else str(value)
64
+ if len(rendered) <= limit:
65
+ return rendered
66
+ return rendered[: limit - 1] + "…"
67
+
68
+
69
+ def project_inspector_tree(
70
+ value: Any,
71
+ *,
72
+ max_nodes: int = INSPECTOR_TREE_MAX_NODES,
73
+ max_depth: int = INSPECTOR_TREE_MAX_DEPTH,
74
+ max_children: int = INSPECTOR_TREE_MAX_CHILDREN,
75
+ max_text_chars: int = INSPECTOR_TREE_MAX_TEXT_CHARS,
76
+ ) -> tuple[InspectorNode, ...]:
77
+ """Return a deterministic structured projection under absolute budgets."""
78
+
79
+ if max_nodes < 2:
80
+ raise ValueError("max_nodes must be at least 2")
81
+ if max_depth < 0:
82
+ raise ValueError("max_depth cannot be negative")
83
+ if max_children < 1:
84
+ raise ValueError("max_children must be positive")
85
+ if max_text_chars < 8:
86
+ raise ValueError("max_text_chars must be at least 8")
87
+
88
+ # Frames are parent, key, value, depth, and an optional pre-rendered value.
89
+ stack: list[tuple[int | None, str, Any, int, str | None]] = [
90
+ (None, "root", value, 0, None)
91
+ ]
92
+ nodes: list[InspectorNode] = []
93
+ seen_containers: dict[int, int] = {}
94
+ data_node_limit = max_nodes - 1
95
+
96
+ while stack and len(nodes) < data_node_limit:
97
+ parent_index, key, current, depth, notice = stack.pop()
98
+ bounded_key = _bounded_inspector_text(key, max_text_chars)
99
+ if notice is not None:
100
+ nodes.append(
101
+ InspectorNode(
102
+ parent_index,
103
+ bounded_key,
104
+ _bounded_inspector_text(notice, max_text_chars),
105
+ )
106
+ )
107
+ continue
108
+
109
+ is_mapping = isinstance(current, dict)
110
+ is_sequence = isinstance(current, (list, tuple))
111
+ if not (is_mapping or is_sequence):
112
+ nodes.append(
113
+ InspectorNode(
114
+ parent_index,
115
+ bounded_key,
116
+ _bounded_inspector_text(current, max_text_chars),
117
+ )
118
+ )
119
+ continue
120
+
121
+ prior_index = seen_containers.get(id(current))
122
+ if prior_index is not None:
123
+ nodes.append(
124
+ InspectorNode(
125
+ parent_index,
126
+ bounded_key,
127
+ _bounded_inspector_text(
128
+ f"reference to node {prior_index}",
129
+ max_text_chars,
130
+ ),
131
+ )
132
+ )
133
+ continue
134
+
135
+ node_index = len(nodes)
136
+ seen_containers[id(current)] = node_index
137
+ total = len(current)
138
+ noun = "fields" if is_mapping else "items"
139
+ depth_limited = depth >= max_depth and total > 0
140
+ child_limited = total > max_children
141
+ detail = f"{total} {noun}"
142
+ if depth_limited:
143
+ detail += " · depth limit reached"
144
+ elif child_limited:
145
+ detail += f" · first {max_children} shown"
146
+ nodes.append(
147
+ InspectorNode(
148
+ parent_index,
149
+ bounded_key,
150
+ _bounded_inspector_text(detail, max_text_chars),
151
+ )
152
+ )
153
+ if depth_limited or not total:
154
+ continue
155
+
156
+ if is_mapping:
157
+ selected = list(islice(current.items(), max_children))
158
+ children = [
159
+ (
160
+ node_index,
161
+ _bounded_inspector_text(child_key, max_text_chars),
162
+ child,
163
+ depth + 1,
164
+ None,
165
+ )
166
+ for child_key, child in selected
167
+ ]
168
+ else:
169
+ selected = current[:max_children]
170
+ children = [
171
+ (node_index, f"[{index}]", child, depth + 1, None)
172
+ for index, child in enumerate(selected)
173
+ ]
174
+ if child_limited:
175
+ stack.append(
176
+ (
177
+ node_index,
178
+ "…",
179
+ None,
180
+ depth + 1,
181
+ f"{total - max_children} additional {noun} not shown",
182
+ )
183
+ )
184
+ stack.extend(reversed(children))
185
+
186
+ if stack:
187
+ root_index = 0 if nodes else None
188
+ nodes.append(
189
+ InspectorNode(
190
+ root_index,
191
+ "…",
192
+ _bounded_inspector_text(
193
+ f"node limit reached · first {data_node_limit} shown",
194
+ max_text_chars,
195
+ ),
196
+ )
197
+ )
198
+ return tuple(nodes)
199
+
200
+
201
+ if QT_AVAILABLE:
202
+ class DocumentNavigator(QFrame):
203
+ """Bundle document list with exact module/source identity controls."""
204
+
205
+ document_selected = pyqtSignal(str)
206
+ add_requested = pyqtSignal()
207
+ remove_requested = pyqtSignal(str)
208
+ move_up_requested = pyqtSignal(str)
209
+ move_down_requested = pyqtSignal(str)
210
+ identity_changed = pyqtSignal(str, str, str)
211
+ identity_pending_changed = pyqtSignal(bool)
212
+ primary_requested = pyqtSignal(str)
213
+
214
+ def __init__(self, parent: QWidget | None = None) -> None:
215
+ super().__init__(parent)
216
+ self.setObjectName("DocumentNavigator")
217
+ self.setMinimumWidth(230)
218
+ self.setMaximumWidth(360)
219
+ self._document_id: str | None = None
220
+ self._updating = False
221
+ self._identity_baseline: tuple[str, str] | None = None
222
+ self._identity_pending = False
223
+
224
+ heading = QLabel("SOURCE BUNDLE")
225
+ heading.setObjectName("PanelEyebrow")
226
+ self.count_label = QLabel("1 / 64")
227
+ self.count_label.setObjectName("MutedLabel")
228
+
229
+ self.add_button = QToolButton()
230
+ self.add_button.setObjectName("IconButton")
231
+ self.add_button.setIcon(QIcon(str(pycforge_icon_path("add-document"))))
232
+ self.add_button.setText("")
233
+ self.add_button.setToolTip("Add module document")
234
+ self.add_button.setAccessibleName("Add module document")
235
+ self.remove_button = QToolButton()
236
+ self.remove_button.setObjectName("IconButton")
237
+ self.remove_button.setIcon(
238
+ QIcon(str(pycforge_icon_path("remove-document")))
239
+ )
240
+ self.remove_button.setText("")
241
+ self.remove_button.setToolTip("Remove selected module document")
242
+ self.remove_button.setAccessibleName("Remove selected module document")
243
+ self.move_up_button = QToolButton()
244
+ self.move_up_button.setObjectName("IconButton")
245
+ self.move_up_button.setIcon(QIcon(str(pycforge_icon_path("move-up"))))
246
+ self.move_up_button.setText("")
247
+ self.move_up_button.setToolTip("Move selected module up (Alt+Up)")
248
+ self.move_up_button.setAccessibleName("Move selected module up")
249
+ self.move_up_button.setAccessibleDescription(
250
+ "Moves the selected document one position earlier in the source bundle."
251
+ )
252
+ self.move_up_button.setFocusPolicy(Qt.StrongFocus)
253
+ self.move_down_button = QToolButton()
254
+ self.move_down_button.setObjectName("IconButton")
255
+ self.move_down_button.setIcon(QIcon(str(pycforge_icon_path("move-down"))))
256
+ self.move_down_button.setText("")
257
+ self.move_down_button.setToolTip("Move selected module down (Alt+Down)")
258
+ self.move_down_button.setAccessibleName("Move selected module down")
259
+ self.move_down_button.setAccessibleDescription(
260
+ "Moves the selected document one position later in the source bundle."
261
+ )
262
+ self.move_down_button.setFocusPolicy(Qt.StrongFocus)
263
+
264
+ title_row = QHBoxLayout()
265
+ title_row.addWidget(heading)
266
+ title_row.addStretch(1)
267
+ title_row.addWidget(self.count_label)
268
+ title_row.addWidget(self.add_button)
269
+ title_row.addWidget(self.remove_button)
270
+
271
+ self.filter_edit = QLineEdit()
272
+ self.filter_edit.setObjectName("NavigatorFilter")
273
+ self.filter_edit.setPlaceholderText("Filter modules…")
274
+ self.filter_edit.setClearButtonEnabled(True)
275
+ self.filter_edit.setAccessibleName("Filter source bundle documents")
276
+
277
+ self.documents = QListWidget()
278
+ self.documents.setObjectName("DocumentList")
279
+ self.documents.setSelectionMode(QAbstractItemView.SingleSelection)
280
+ self.documents.setAlternatingRowColors(False)
281
+ self.documents.setMinimumHeight(68)
282
+ self.documents.setAccessibleName("Source bundle documents")
283
+ self.documents.setAccessibleDescription(
284
+ "Select a module document. Use Alt+Up or Alt+Down to change bundle order."
285
+ )
286
+
287
+ self.module_edit = QLineEdit()
288
+ self.module_edit.setPlaceholderText("module.id")
289
+ self.module_edit.setAccessibleName("Logical module ID")
290
+ self.logical_edit = QLineEdit()
291
+ self.logical_edit.setPlaceholderText("relative/source.py")
292
+ self.logical_edit.setAccessibleName("Logical source name")
293
+ self.primary_check = QCheckBox("Primary document")
294
+ self.primary_check.setAccessibleName("Make selected document primary")
295
+ self.path_label = QLabel("Unsaved document")
296
+ self.path_label.setObjectName("PathLabel")
297
+ self.path_label.setMinimumWidth(0)
298
+ self.path_label.setSizePolicy(
299
+ QSizePolicy.Ignored, QSizePolicy.Preferred
300
+ )
301
+ self.path_label.setToolTip("Unsaved document")
302
+
303
+ navigation_row = QHBoxLayout()
304
+ navigation_row.addWidget(self.filter_edit, 1)
305
+ navigation_row.addWidget(self.move_up_button)
306
+ navigation_row.addWidget(self.move_down_button)
307
+
308
+ identity_row = QHBoxLayout()
309
+ identity_row.setSpacing(5)
310
+ id_label = QLabel("ID")
311
+ id_label.setObjectName("MutedLabel")
312
+ source_label = QLabel("FILE")
313
+ source_label.setObjectName("MutedLabel")
314
+ identity_row.addWidget(id_label)
315
+ identity_row.addWidget(self.module_edit, 3)
316
+ identity_row.addWidget(source_label)
317
+ identity_row.addWidget(self.logical_edit, 2)
318
+
319
+ primary_row = QHBoxLayout()
320
+ primary_row.addWidget(self.primary_check)
321
+ primary_row.addWidget(self.path_label, 1)
322
+
323
+ layout = QVBoxLayout(self)
324
+ layout.setContentsMargins(10, 10, 10, 9)
325
+ layout.setSpacing(6)
326
+ layout.addLayout(title_row)
327
+ layout.addLayout(navigation_row)
328
+ layout.addWidget(self.documents, 1)
329
+ layout.addLayout(identity_row)
330
+ layout.addLayout(primary_row)
331
+
332
+ self.add_button.clicked.connect(self._request_add)
333
+ self.remove_button.clicked.connect(self._request_remove)
334
+ self.move_up_button.clicked.connect(self._request_move_up)
335
+ self.move_down_button.clicked.connect(self._request_move_down)
336
+ self.documents.currentItemChanged.connect(self._select_item)
337
+ self.filter_edit.textChanged.connect(self._filter)
338
+ self.module_edit.textChanged.connect(
339
+ self._update_identity_pending
340
+ )
341
+ self.logical_edit.textChanged.connect(
342
+ self._update_identity_pending
343
+ )
344
+ self.module_edit.editingFinished.connect(self._emit_identity)
345
+ self.logical_edit.editingFinished.connect(self._emit_identity)
346
+ self.primary_check.clicked.connect(self._request_primary)
347
+
348
+ def set_documents(self, documents: Iterable[Any], active_document_id: str | None) -> None:
349
+ records = tuple(documents)
350
+ self._updating = True
351
+ self.documents.clear()
352
+ selected: QListWidgetItem | None = None
353
+ for document in records:
354
+ states = []
355
+ if document.is_primary:
356
+ states.append("primary")
357
+ if document.dirty:
358
+ states.append("modified")
359
+ state_text = (
360
+ " — " + ", ".join(states) if states else ""
361
+ )
362
+ item = QListWidgetItem(
363
+ f"{document.module_id}{state_text}\n"
364
+ f" {document.logical_name}"
365
+ )
366
+ item.setIcon(
367
+ QIcon(
368
+ str(
369
+ pycforge_icon_path(
370
+ "primary-module"
371
+ if document.is_primary
372
+ else "module"
373
+ )
374
+ )
375
+ )
376
+ )
377
+ item.setData(Qt.UserRole, document.document_id)
378
+ item.setData(Qt.UserRole + 1, document)
379
+ item.setData(
380
+ Qt.AccessibleTextRole,
381
+ (
382
+ f"{document.module_id}, {document.logical_name}"
383
+ + (
384
+ ", " + ", ".join(states)
385
+ if states else ""
386
+ )
387
+ ),
388
+ )
389
+ item.setToolTip(
390
+ f"{document.module_id}\n{document.logical_name}\n"
391
+ f"{document.path or 'Unsaved document'}"
392
+ )
393
+ self.documents.addItem(item)
394
+ if document.document_id == active_document_id:
395
+ selected = item
396
+ self.count_label.setText(f"{len(records)} / 64")
397
+ self.count_label.setAccessibleName(
398
+ f"{len(records)} source bundle document"
399
+ + ("" if len(records) == 1 else "s")
400
+ )
401
+ self.remove_button.setEnabled(len(records) > 1)
402
+ if selected is not None:
403
+ self.documents.setCurrentItem(selected)
404
+ self._show_document(selected.data(Qt.UserRole + 1))
405
+ self._updating = False
406
+ self._filter(self.filter_edit.text())
407
+ self._update_move_buttons()
408
+
409
+ def _show_document(self, document: Any) -> None:
410
+ was_updating = self._updating
411
+ self._updating = True
412
+ self._document_id = document.document_id
413
+ try:
414
+ self.module_edit.setText(document.module_id)
415
+ self.logical_edit.setText(document.logical_name)
416
+ self.primary_check.setChecked(bool(document.is_primary))
417
+ self.primary_check.setEnabled(not document.is_primary)
418
+ self.path_label.setText(document.path or "Unsaved")
419
+ self.path_label.setToolTip(
420
+ document.path or "Unsaved document"
421
+ )
422
+ finally:
423
+ self._updating = was_updating
424
+ self._identity_baseline = (
425
+ document.module_id,
426
+ document.logical_name,
427
+ )
428
+ self._set_identity_pending(False)
429
+
430
+ @property
431
+ def current_document_id(self) -> str | None:
432
+ return self._document_id
433
+
434
+ def pending_identity(self) -> tuple[str, str]:
435
+ return (
436
+ self.module_edit.text().strip(),
437
+ self.logical_edit.text().strip(),
438
+ )
439
+
440
+ @property
441
+ def identity_pending(self) -> bool:
442
+ return self._identity_pending
443
+
444
+ def bind_action_registry(self, registry: Any) -> None:
445
+ """Move button commands and widget shortcuts into *registry*."""
446
+
447
+ bindings = (
448
+ (
449
+ "bundle.new_module",
450
+ self.add_button,
451
+ self._request_add,
452
+ ),
453
+ (
454
+ "bundle.remove_module",
455
+ self.remove_button,
456
+ self._request_remove,
457
+ ),
458
+ (
459
+ "bundle.move_up",
460
+ self.move_up_button,
461
+ self._request_move_up,
462
+ ),
463
+ (
464
+ "bundle.move_down",
465
+ self.move_down_button,
466
+ self._request_move_down,
467
+ ),
468
+ )
469
+ for action_id, button, handler in bindings:
470
+ try:
471
+ button.clicked.disconnect(handler)
472
+ except (RuntimeError, TypeError):
473
+ pass
474
+ registry.register_handler(action_id, handler)
475
+ registry.bind_tool_button(action_id, button)
476
+ registry.attach_to_widget("bundle.move_up", self)
477
+ registry.attach_to_widget("bundle.move_down", self)
478
+
479
+ def _select_item(
480
+ self,
481
+ current: QListWidgetItem | None,
482
+ previous: QListWidgetItem | None,
483
+ ) -> None:
484
+ del previous
485
+ if current is None:
486
+ self._document_id = None
487
+ self._update_move_buttons()
488
+ return
489
+ self._show_document(current.data(Qt.UserRole + 1))
490
+ self._update_move_buttons()
491
+ if not self._updating:
492
+ self.document_selected.emit(str(current.data(Qt.UserRole)))
493
+
494
+ def _emit_identity(self) -> None:
495
+ if self._updating or self._document_id is None:
496
+ return
497
+ self.identity_changed.emit(
498
+ self._document_id,
499
+ self.module_edit.text().strip(),
500
+ self.logical_edit.text().strip(),
501
+ )
502
+
503
+ def _update_identity_pending(self, _value: str) -> None:
504
+ if self._updating:
505
+ return
506
+ self._set_identity_pending(
507
+ self._identity_baseline is not None
508
+ and self.pending_identity() != self._identity_baseline
509
+ )
510
+
511
+ def _set_identity_pending(self, pending: bool) -> None:
512
+ if pending == self._identity_pending:
513
+ return
514
+ self._identity_pending = pending
515
+ self.identity_pending_changed.emit(pending)
516
+
517
+ def _request_primary(self) -> None:
518
+ if not self._updating and self._document_id is not None:
519
+ self.primary_requested.emit(self._document_id)
520
+
521
+ def _request_add(self) -> None:
522
+ self.add_requested.emit()
523
+
524
+ def _request_remove(self) -> None:
525
+ if self._document_id is not None:
526
+ self.remove_requested.emit(self._document_id)
527
+
528
+ def _request_move_up(self) -> None:
529
+ if self._document_id is not None and self.move_up_button.isEnabled():
530
+ self.move_up_requested.emit(self._document_id)
531
+
532
+ def _request_move_down(self) -> None:
533
+ if self._document_id is not None and self.move_down_button.isEnabled():
534
+ self.move_down_requested.emit(self._document_id)
535
+
536
+ def _update_move_buttons(self) -> None:
537
+ row = self.documents.currentRow()
538
+ count = self.documents.count()
539
+ self.move_up_button.setEnabled(row > 0)
540
+ self.move_down_button.setEnabled(0 <= row < count - 1)
541
+
542
+ def _filter(self, value: str) -> None:
543
+ needle = value.casefold().strip()
544
+ for row in range(self.documents.count()):
545
+ item = self.documents.item(row)
546
+ item.setHidden(bool(needle and needle not in item.text().casefold()))
547
+
548
+
549
+ class DiagnosticsView(QWidget):
550
+ """Filterable, navigable diagnostic list with remediation details."""
551
+
552
+ diagnostic_activated = pyqtSignal(object)
553
+
554
+ _SEVERITY_COLORS = {
555
+ "InternalError": PYCFORGE_COLORS.error_bright,
556
+ "Error": PYCFORGE_COLORS.error,
557
+ "Warning": PYCFORGE_COLORS.warning,
558
+ "Approximation": PYCFORGE_COLORS.violet_bright,
559
+ "Information": PYCFORGE_COLORS.blue_bright,
560
+ "Info": PYCFORGE_COLORS.blue_bright,
561
+ }
562
+
563
+ def __init__(self, parent: QWidget | None = None) -> None:
564
+ super().__init__(parent)
565
+ self.setObjectName("DiagnosticsView")
566
+ self.filter_edit = QLineEdit()
567
+ self.filter_edit.setPlaceholderText("Filter diagnostics…")
568
+ self.filter_edit.setClearButtonEnabled(True)
569
+ self.filter_edit.setAccessibleName("Filter diagnostics")
570
+ self.count_label = QLabel("No diagnostics")
571
+ self.count_label.setObjectName("MutedLabel")
572
+ self.tree = QTreeWidget()
573
+ self.tree.setObjectName("DiagnosticsTree")
574
+ self.tree.setColumnCount(5)
575
+ self.tree.setHeaderLabels(("Severity", "Code", "Message", "Module", "Line"))
576
+ self.tree.setRootIsDecorated(False)
577
+ self.tree.setAlternatingRowColors(True)
578
+ self.tree.setUniformRowHeights(True)
579
+ self.tree.setAccessibleName("Conversion diagnostics")
580
+ self.tree.header().setStretchLastSection(False)
581
+ self.tree.header().resizeSection(0, 105)
582
+ self.tree.header().resizeSection(1, 90)
583
+ self.tree.header().resizeSection(2, 520)
584
+ self.tree.header().resizeSection(3, 160)
585
+ self.tree.header().resizeSection(4, 55)
586
+ self.details = QTextBrowser()
587
+ self.details.setObjectName("DiagnosticDetails")
588
+ self.details.setOpenExternalLinks(False)
589
+ self.details.setMaximumHeight(130)
590
+ self.details.setPlaceholderText("Select a diagnostic for explanation and remediation.")
591
+
592
+ top = QHBoxLayout()
593
+ top.addWidget(self.filter_edit, 1)
594
+ top.addWidget(self.count_label)
595
+ layout = QVBoxLayout(self)
596
+ layout.setContentsMargins(8, 8, 8, 8)
597
+ layout.setSpacing(7)
598
+ layout.addLayout(top)
599
+ layout.addWidget(self.tree, 1)
600
+ layout.addWidget(self.details)
601
+
602
+ self.filter_edit.textChanged.connect(self._filter)
603
+ self.tree.currentItemChanged.connect(self._show_details)
604
+ self.tree.itemActivated.connect(self._activate)
605
+
606
+ def set_diagnostics(self, diagnostics: Iterable[dict[str, Any]]) -> None:
607
+ records = tuple(diagnostics)
608
+ self.tree.clear()
609
+ for diagnostic in records:
610
+ span = diagnostic.get("source_span") or {}
611
+ start = span.get("start") or {}
612
+ line = start.get("line")
613
+ module = diagnostic.get("source_module_id") or diagnostic.get(
614
+ "source_logical_name"
615
+ ) or "—"
616
+ item = QTreeWidgetItem(
617
+ (
618
+ str(diagnostic.get("severity") or ""),
619
+ str(diagnostic.get("code") or ""),
620
+ str(diagnostic.get("message") or ""),
621
+ str(module),
622
+ str(line or "—"),
623
+ )
624
+ )
625
+ item.setData(0, Qt.UserRole, diagnostic)
626
+ color = self._SEVERITY_COLORS.get(
627
+ str(diagnostic.get("severity")),
628
+ PYCFORGE_COLORS.text_muted,
629
+ )
630
+ item.setForeground(0, QBrush(QColor(color)))
631
+ self.tree.addTopLevelItem(item)
632
+ self.count_label.setText(
633
+ "No diagnostics"
634
+ if not records
635
+ else f"{len(records)} diagnostic" + ("" if len(records) == 1 else "s")
636
+ )
637
+ self.details.clear()
638
+ self._filter(self.filter_edit.text())
639
+
640
+ def line_markers(self, logical_name: str) -> tuple[tuple[int, str], ...]:
641
+ result = []
642
+ for row in range(self.tree.topLevelItemCount()):
643
+ diagnostic = self.tree.topLevelItem(row).data(0, Qt.UserRole)
644
+ if diagnostic.get("source_logical_name") != logical_name:
645
+ continue
646
+ span = diagnostic.get("source_span") or {}
647
+ line = (span.get("start") or {}).get("line")
648
+ if isinstance(line, int):
649
+ result.append((line, str(diagnostic.get("severity") or "Error")))
650
+ return tuple(result)
651
+
652
+ def _show_details(
653
+ self,
654
+ current: QTreeWidgetItem | None,
655
+ previous: QTreeWidgetItem | None,
656
+ ) -> None:
657
+ del previous
658
+ if current is None:
659
+ self.details.clear()
660
+ return
661
+ diagnostic = current.data(0, Qt.UserRole)
662
+ blocks = [
663
+ f"{diagnostic.get('code', '')} — {diagnostic.get('message', '')}",
664
+ str(diagnostic.get("explanation") or ""),
665
+ ]
666
+ remediation = diagnostic.get("remediation")
667
+ if remediation:
668
+ blocks.append(f"Suggested action: {remediation}")
669
+ self.details.setPlainText("\n\n".join(item for item in blocks if item))
670
+
671
+ def _activate(self, item: QTreeWidgetItem, column: int) -> None:
672
+ del column
673
+ self.diagnostic_activated.emit(item.data(0, Qt.UserRole))
674
+
675
+ def _filter(self, value: str) -> None:
676
+ needle = value.casefold().strip()
677
+ for row in range(self.tree.topLevelItemCount()):
678
+ item = self.tree.topLevelItem(row)
679
+ haystack = " ".join(item.text(column) for column in range(5)).casefold()
680
+ item.setHidden(bool(needle and needle not in haystack))
681
+
682
+
683
+ class InspectorTree(QWidget):
684
+ """Searchable structured projection for summaries, traces, and telemetry."""
685
+
686
+ def __init__(self, label: str, parent: QWidget | None = None) -> None:
687
+ super().__init__(parent)
688
+ self.setObjectName("InspectorTree")
689
+ self.filter_edit = QLineEdit()
690
+ self.filter_edit.setPlaceholderText(f"Filter {label.casefold()}…")
691
+ self.filter_edit.setClearButtonEnabled(True)
692
+ self.filter_edit.setAccessibleName(f"Filter {label.casefold()}")
693
+ self.tree = QTreeWidget()
694
+ self.tree.setColumnCount(2)
695
+ self.tree.setHeaderLabels(("Field", "Value"))
696
+ self.tree.setAlternatingRowColors(True)
697
+ self.tree.setAccessibleName(label)
698
+ self.tree.header().resizeSection(0, 310)
699
+ layout = QVBoxLayout(self)
700
+ layout.setContentsMargins(8, 8, 8, 8)
701
+ layout.setSpacing(7)
702
+ layout.addWidget(self.filter_edit)
703
+ layout.addWidget(self.tree, 1)
704
+ self.filter_edit.textChanged.connect(self._filter)
705
+
706
+ def set_data(self, value: Any) -> None:
707
+ self.tree.clear()
708
+ items: list[QTreeWidgetItem] = []
709
+ root = self.tree.invisibleRootItem()
710
+ for node in project_inspector_tree(value):
711
+ parent = (
712
+ root
713
+ if node.parent_index is None
714
+ else items[node.parent_index]
715
+ )
716
+ item = QTreeWidgetItem((node.key, node.value))
717
+ parent.addChild(item)
718
+ items.append(item)
719
+ self.tree.expandToDepth(1)
720
+ self._filter(self.filter_edit.text())
721
+
722
+ def _filter(self, value: str) -> None:
723
+ needle = value.casefold().strip()
724
+
725
+ def visit(item: QTreeWidgetItem) -> bool:
726
+ own = needle in (item.text(0) + " " + item.text(1)).casefold()
727
+ child_match = False
728
+ for index in range(item.childCount()):
729
+ child_match = visit(item.child(index)) or child_match
730
+ visible = not needle or own or child_match
731
+ item.setHidden(not visible)
732
+ if child_match and needle:
733
+ item.setExpanded(True)
734
+ return visible and (own or child_match)
735
+
736
+ for index in range(self.tree.topLevelItemCount()):
737
+ visit(self.tree.topLevelItem(index))
738
+
739
+
740
+ class MappingsView(QWidget):
741
+ """Filterable source-to-output provenance with direct C navigation."""
742
+
743
+ mapping_activated = pyqtSignal(object)
744
+ DISPLAY_LIMIT = 5000
745
+
746
+ def __init__(self, parent: QWidget | None = None) -> None:
747
+ super().__init__(parent)
748
+ self.setObjectName("MappingsView")
749
+ self.filter_edit = QLineEdit()
750
+ self.filter_edit.setPlaceholderText("Filter source mappings…")
751
+ self.filter_edit.setClearButtonEnabled(True)
752
+ self.filter_edit.setAccessibleName("Filter source-to-output mappings")
753
+ self.count_label = QLabel("No mappings")
754
+ self.count_label.setObjectName("MutedLabel")
755
+ self.tree = QTreeWidget()
756
+ self.tree.setObjectName("MappingsTree")
757
+ self.tree.setColumnCount(5)
758
+ self.tree.setHeaderLabels(("Module", "Origin", "Rule", "C range", "C node"))
759
+ self.tree.setRootIsDecorated(False)
760
+ self.tree.setAlternatingRowColors(True)
761
+ self.tree.setUniformRowHeights(True)
762
+ self.tree.setAccessibleName("Source-to-output mappings")
763
+ self.tree.header().setStretchLastSection(True)
764
+ self.tree.header().resizeSection(0, 170)
765
+ self.tree.header().resizeSection(1, 190)
766
+ self.tree.header().resizeSection(2, 210)
767
+ self.tree.header().resizeSection(3, 130)
768
+
769
+ top = QHBoxLayout()
770
+ top.addWidget(self.filter_edit, 1)
771
+ top.addWidget(self.count_label)
772
+ layout = QVBoxLayout(self)
773
+ layout.setContentsMargins(8, 8, 8, 8)
774
+ layout.setSpacing(7)
775
+ layout.addLayout(top)
776
+ layout.addWidget(self.tree, 1)
777
+
778
+ self.filter_edit.textChanged.connect(self._filter)
779
+ self.tree.itemActivated.connect(self._activate)
780
+
781
+ def set_mappings(self, mappings: Iterable[dict[str, Any]]) -> None:
782
+ records = tuple(mappings)
783
+ self.tree.clear()
784
+ for mapping in records[: self.DISPLAY_LIMIT]:
785
+ positions = (
786
+ mapping.get("start_line"),
787
+ mapping.get("start_column"),
788
+ mapping.get("end_line"),
789
+ mapping.get("end_column"),
790
+ )
791
+ location = (
792
+ f"{positions[0]}:{positions[1]}–{positions[2]}:{positions[3]}"
793
+ if all(isinstance(value, int) for value in positions)
794
+ else "—"
795
+ )
796
+ module = mapping.get("module_id") or mapping.get(
797
+ "logical_source_name"
798
+ ) or "synthetic"
799
+ item = QTreeWidgetItem(
800
+ (
801
+ str(module),
802
+ str(mapping.get("origin_kind") or ""),
803
+ str(mapping.get("rule_plan_id") or "—"),
804
+ location,
805
+ str(mapping.get("c_node_id") or ""),
806
+ )
807
+ )
808
+ item.setData(0, Qt.UserRole, mapping)
809
+ self.tree.addTopLevelItem(item)
810
+ if not records:
811
+ label = "No mappings"
812
+ elif len(records) > self.DISPLAY_LIMIT:
813
+ label = f"{self.DISPLAY_LIMIT} of {len(records)} mappings"
814
+ else:
815
+ label = f"{len(records)} mapping" + ("" if len(records) == 1 else "s")
816
+ self.count_label.setText(label)
817
+ self._filter(self.filter_edit.text())
818
+
819
+ def _activate(self, item: QTreeWidgetItem, column: int) -> None:
820
+ del column
821
+ self.mapping_activated.emit(item.data(0, Qt.UserRole))
822
+
823
+ def _filter(self, value: str) -> None:
824
+ needle = value.casefold().strip()
825
+ for row in range(self.tree.topLevelItemCount()):
826
+ item = self.tree.topLevelItem(row)
827
+ haystack = " ".join(item.text(column) for column in range(5)).casefold()
828
+ item.setHidden(bool(needle and needle not in haystack))
829
+
830
+
831
+ class ToastBanner(QFrame):
832
+ dismissed = pyqtSignal()
833
+
834
+ def __init__(self, parent: QWidget | None = None) -> None:
835
+ super().__init__(parent)
836
+ self.setObjectName("ToastBanner")
837
+ self.setProperty("tone", "info")
838
+ self.label = QLabel()
839
+ self.label.setWordWrap(True)
840
+ self.close_button = QToolButton()
841
+ self.close_button.setObjectName("IconButton")
842
+ self.close_button.setIcon(
843
+ QIcon(str(pycforge_icon_path("close")))
844
+ )
845
+ self.close_button.setText("")
846
+ self.close_button.setToolTip("Dismiss notification")
847
+ self.close_button.setAccessibleName("Dismiss notification")
848
+ layout = QHBoxLayout(self)
849
+ layout.setContentsMargins(12, 8, 8, 8)
850
+ layout.addWidget(self.label, 1)
851
+ layout.addWidget(self.close_button)
852
+ self.close_button.clicked.connect(self.hide)
853
+ self.close_button.clicked.connect(self.dismissed.emit)
854
+ self.hide()
855
+
856
+ def show_message(self, message: str, tone: str = "info") -> None:
857
+ self.setProperty("tone", tone)
858
+ self.style().unpolish(self)
859
+ self.style().polish(self)
860
+ self.label.setText(message)
861
+ self.show()
862
+
863
+
864
+ else:
865
+ class _QtRequired:
866
+ def __init__(self, *args, **kwargs) -> None:
867
+ raise RuntimeError("PyQt5 is required for workspace panels") from _QT_ERROR
868
+
869
+
870
+ class DocumentNavigator(_QtRequired):
871
+ pass
872
+
873
+
874
+ class DiagnosticsView(_QtRequired):
875
+ pass
876
+
877
+
878
+ class InspectorTree(_QtRequired):
879
+ pass
880
+
881
+
882
+ class MappingsView(_QtRequired):
883
+ pass
884
+
885
+
886
+ class ToastBanner(_QtRequired):
887
+ pass
888
+
889
+
890
+ __all__ = [
891
+ "INSPECTOR_TREE_MAX_CHILDREN",
892
+ "INSPECTOR_TREE_MAX_DEPTH",
893
+ "INSPECTOR_TREE_MAX_NODES",
894
+ "INSPECTOR_TREE_MAX_TEXT_CHARS",
895
+ "QT_AVAILABLE",
896
+ "DocumentNavigator",
897
+ "DiagnosticsView",
898
+ "InspectorNode",
899
+ "InspectorTree",
900
+ "MappingsView",
901
+ "ToastBanner",
902
+ "project_inspector_tree",
903
+ ]