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
@@ -0,0 +1,348 @@
1
+ """Widget construction and action wiring for the PyCForge Qt workspace."""
2
+
3
+ from __future__ import annotations
4
+ from PyQt5.QtCore import QSize, Qt
5
+ from PyQt5.QtGui import QIcon
6
+ from PyQt5.QtWidgets import (
7
+ QFrame,
8
+ QHBoxLayout,
9
+ QLabel,
10
+ QProgressBar,
11
+ QSizePolicy,
12
+ QSplitter,
13
+ QTabWidget,
14
+ QToolBar,
15
+ QToolButton,
16
+ QVBoxLayout,
17
+ QWidget,
18
+ )
19
+
20
+ from .find_replace import FindReplaceBar
21
+ from .icons import pycforge_icon_path
22
+ from .qt_editor_surfaces import (
23
+ build_generated_c_surface,
24
+ build_source_editor_surface,
25
+ )
26
+ from .qt_workspace_widgets import BreadcrumbBar, DocumentTabBar
27
+ from .qt_workspace_panels import (
28
+ BundleSearchView,
29
+ OutlineView,
30
+ SessionHistoryView,
31
+ )
32
+ from .panels import (
33
+ DiagnosticsView,
34
+ DocumentNavigator,
35
+ InspectorTree,
36
+ MappingsView,
37
+ ToastBanner,
38
+ )
39
+ from .qt_actions import QtActionRegistry
40
+ from .qt_menus import QtMenuFactory
41
+ from .qt_shell_interactions import QtShellInteractionMixin
42
+ from .visual_tokens import PYCFORGE_METRICS
43
+
44
+
45
+ class QtShellMixin(QtShellInteractionMixin):
46
+ """Construct and connect the fixed professional workspace shell."""
47
+
48
+ def _build_workspace(self) -> None:
49
+ self.setWindowIcon(
50
+ QIcon(str(pycforge_icon_path("brand-mark")))
51
+ )
52
+ shell = QWidget()
53
+ shell.setObjectName("workspaceShell")
54
+ shell.setProperty("role", "workspace-shell")
55
+ shell_layout = QVBoxLayout(shell)
56
+ shell_layout.setContentsMargins(10, 9, 10, 8)
57
+ shell_layout.setSpacing(7)
58
+
59
+ self.toast = ToastBanner()
60
+ shell_layout.addWidget(self.toast)
61
+
62
+ self.main_splitter = QSplitter(Qt.Vertical)
63
+ self.main_splitter.setObjectName("MainWorkspaceSplitter")
64
+ self.main_splitter.setChildrenCollapsible(False)
65
+ shell_layout.addWidget(self.main_splitter, 1)
66
+ self.setCentralWidget(shell)
67
+
68
+ self.workspace_splitter = QSplitter(Qt.Horizontal)
69
+ self.workspace_splitter.setObjectName("SourceBundleSplitter")
70
+ self.workspace_splitter.setChildrenCollapsible(False)
71
+ self.main_splitter.addWidget(self.workspace_splitter)
72
+
73
+ self.navigator = DocumentNavigator()
74
+ self.navigator.add_button.setIcon(
75
+ QIcon(str(pycforge_icon_path("add-document")))
76
+ )
77
+ self.navigator.remove_button.setIcon(
78
+ QIcon(str(pycforge_icon_path("remove-document")))
79
+ )
80
+ self.navigator.add_button.setText("")
81
+ self.navigator.remove_button.setText("")
82
+ self.workspace_splitter.addWidget(self.navigator)
83
+
84
+ editor_panel = QFrame()
85
+ editor_panel.setObjectName("editorPanel")
86
+ editor_panel.setProperty("role", "panel")
87
+ editor_layout = QVBoxLayout(editor_panel)
88
+ editor_layout.setContentsMargins(8, 8, 8, 8)
89
+ editor_layout.setSpacing(6)
90
+
91
+ header = QHBoxLayout()
92
+ title = QLabel("PYTHON SOURCE")
93
+ title.setProperty("role", "panel-title")
94
+ self.active_module_label = QLabel("main · main.py")
95
+ self.active_module_label.setObjectName("ActiveModuleLabel")
96
+ self.active_path_label = QLabel("Unsaved document")
97
+ self.active_path_label.setObjectName("PathLabel")
98
+ self.active_path_label.setToolTip(
99
+ "Active Python file linkage"
100
+ )
101
+ self.active_path_label.setMinimumWidth(0)
102
+ self.active_path_label.setSizePolicy(
103
+ QSizePolicy.Ignored, QSizePolicy.Preferred
104
+ )
105
+ self.state_chip = QLabel("EMPTY")
106
+ self.state_chip.setProperty("role", "status-chip")
107
+ self.state_chip.setProperty("status", "neutral")
108
+ header.addWidget(title)
109
+ header.addWidget(self.active_module_label)
110
+ header.addWidget(self.active_path_label, 1)
111
+ header.addWidget(self.state_chip)
112
+ editor_layout.addLayout(header)
113
+
114
+ self.document_tabs = DocumentTabBar()
115
+ editor_layout.addWidget(self.document_tabs)
116
+ self.breadcrumbs = BreadcrumbBar()
117
+ editor_layout.addWidget(self.breadcrumbs)
118
+
119
+ self.find_bar = FindReplaceBar()
120
+ editor_layout.addWidget(self.find_bar)
121
+
122
+ self.editor_splitter = QSplitter(Qt.Horizontal)
123
+ self.editor_splitter.setObjectName("PythonCEditorSplitter")
124
+ self.editor_splitter.setChildrenCollapsible(False)
125
+ editor_layout.addWidget(self.editor_splitter, 1)
126
+
127
+ source_panel = build_source_editor_surface(self)
128
+ self.editor_splitter.addWidget(source_panel)
129
+
130
+ self.output_panel = build_generated_c_surface(self)
131
+ self.editor_splitter.addWidget(self.output_panel)
132
+ self.editor_splitter.setStretchFactor(0, 1)
133
+ self.editor_splitter.setStretchFactor(1, 1)
134
+ self.workspace_splitter.addWidget(editor_panel)
135
+ self.workspace_splitter.setStretchFactor(0, 0)
136
+ self.workspace_splitter.setStretchFactor(1, 1)
137
+
138
+ self.tabs = QTabWidget()
139
+ self.tabs.setObjectName("detailsPanel")
140
+ self.tabs.setAccessibleName("Transpilation details")
141
+ self.diags = DiagnosticsView()
142
+ self.summary = InspectorTree("Transpilation summary")
143
+ self.trace = InspectorTree("Decision trace")
144
+ self.mappings = MappingsView()
145
+ self.telemetry = InspectorTree("Telemetry")
146
+ self.outline = OutlineView()
147
+ self.bundle_search = BundleSearchView()
148
+ self.session_history = SessionHistoryView()
149
+ self.tabs.addTab(self.diags, "Diagnostics")
150
+ self.tabs.addTab(self.summary, "Summary")
151
+ self.tabs.addTab(self.trace, "Decision Trace")
152
+ self.tabs.addTab(self.mappings, "Mappings")
153
+ self.tabs.addTab(self.telemetry, "Telemetry")
154
+ self.tabs.addTab(self.outline, "Outline")
155
+ self.tabs.addTab(self.bundle_search, "Bundle Search")
156
+ self.tabs.addTab(self.session_history, "History")
157
+ self.tabs.setVisible(False)
158
+ self.main_splitter.addWidget(self.tabs)
159
+ self.main_splitter.setStretchFactor(0, 4)
160
+ self.main_splitter.setStretchFactor(1, 2)
161
+
162
+ self.progress = QProgressBar()
163
+ self.progress.setAccessibleName("Transpilation progress")
164
+ self.progress.setMinimumWidth(170)
165
+ self.progress.setMaximumWidth(250)
166
+ self.progress.setTextVisible(True)
167
+ self.progress.hide()
168
+ self.status_document = QLabel("main.py")
169
+ self.status_document.setObjectName("MutedLabel")
170
+ self.statusBar().addPermanentWidget(self.status_document)
171
+ self.statusBar().addPermanentWidget(self.progress)
172
+
173
+ def _build_actions(self) -> None:
174
+ handlers = {
175
+ "file.open_python": self.open_file,
176
+ "bundle.new_module": self.add_document,
177
+ "bundle.remove_module": self.remove_active_document,
178
+ "bundle.move_up": self._move_document_up,
179
+ "bundle.move_down": self._move_document_down,
180
+ "bundle.make_primary": self._make_selected_primary,
181
+ "file.save_python": self.save_source,
182
+ "file.save_python_as": self.save_source_as,
183
+ "output.set_destination": self.link_c_file,
184
+ "output.save_c": self.save_c,
185
+ "conversion.convert": self.convert,
186
+ "conversion.cancel": self.controller.cancel,
187
+ "edit.undo": lambda: self._invoke_target_method("undo"),
188
+ "edit.redo": lambda: self._invoke_target_method("redo"),
189
+ "edit.cut": lambda: self._invoke_target_method("cut"),
190
+ "edit.copy": self._copy_action_selection,
191
+ "edit.paste": lambda: self._invoke_target_method("paste"),
192
+ "edit.select_all": (
193
+ lambda: self._invoke_target_method("selectAll")
194
+ ),
195
+ "edit.duplicate_line": self._duplicate_source,
196
+ "edit.move_line_up": self._move_source_up,
197
+ "edit.move_line_down": self._move_source_down,
198
+ "edit.indent": self._indent_source,
199
+ "edit.outdent": self._outdent_source,
200
+ "edit.toggle_comment": self._toggle_source_comment,
201
+ "search.find": lambda: self._open_find(False),
202
+ "search.replace": lambda: self._open_find(True),
203
+ "search.bundle": self._show_bundle_search,
204
+ "search.next_match": self.find_bar.next_match,
205
+ "search.previous_match": self.find_bar.previous_match,
206
+ "search.replace_current": self.find_bar.replace_current,
207
+ "search.replace_all": self.find_bar.replace_all,
208
+ "search.close": self.find_bar.close_bar,
209
+ "view.source_bundle": self._set_navigator_visible,
210
+ "view.generated_c": self._set_output_visible,
211
+ "view.conversion_details": self._set_details_visible,
212
+ "view.outline": self._show_outline,
213
+ "view.conversion_history": self._show_conversion_history,
214
+ "view.whitespace": self._set_whitespace_visible,
215
+ "view.split_source": self._set_source_split_visible,
216
+ "editor.toggle_fold": self._toggle_source_fold,
217
+ "navigation.go_to_line": self._go_to_source_line,
218
+ "workspace.command_palette": self._open_command_palette,
219
+ "tree.expand_all": (
220
+ lambda: self._invoke_target_method("expandAll")
221
+ ),
222
+ "tree.collapse_all": (
223
+ lambda: self._invoke_target_method("collapseAll")
224
+ ),
225
+ "diagnostics.reveal_source": (
226
+ self._reveal_selected_diagnostic
227
+ ),
228
+ "mappings.reveal_output": self._reveal_selected_mapping,
229
+ "mappings.reveal_source": (
230
+ self._reveal_selected_mapping_source
231
+ ),
232
+ }
233
+ self.action_registry = QtActionRegistry(
234
+ self,
235
+ handlers,
236
+ state_provider=self._action_states,
237
+ )
238
+ self.menu_factory = QtMenuFactory(self.action_registry, self)
239
+ compatibility = {
240
+ "open_action": "file.open_python",
241
+ "add_document_action": "bundle.new_module",
242
+ "remove_document_action": "bundle.remove_module",
243
+ "save_python_action": "file.save_python",
244
+ "save_python_as_action": "file.save_python_as",
245
+ "convert_action": "conversion.convert",
246
+ "cancel_action": "conversion.cancel",
247
+ "find_action": "search.find",
248
+ "replace_action": "search.replace",
249
+ "link_c_action": "output.set_destination",
250
+ "save_c_action": "output.save_c",
251
+ "show_c_action": "view.generated_c",
252
+ "show_details_action": "view.conversion_details",
253
+ "show_navigator_action": "view.source_bundle",
254
+ }
255
+ for attribute, action_id in compatibility.items():
256
+ setattr(self, attribute, self.action_registry.action(action_id))
257
+ self.action_registry.set_checked("view.source_bundle", True)
258
+ self.navigator.bind_action_registry(self.action_registry)
259
+ self.find_bar.bind_action_registry(self.action_registry)
260
+
261
+ def _build_toolbar(self) -> None:
262
+ toolbar = QToolBar("PyCForge Workspace")
263
+ toolbar.setObjectName("PyCForgeToolbar")
264
+ toolbar.setMovable(False)
265
+ toolbar.setFloatable(False)
266
+ toolbar.setToolButtonStyle(Qt.ToolButtonIconOnly)
267
+ size = PYCFORGE_METRICS.icon_toolbar
268
+ toolbar.setIconSize(QSize(size, size))
269
+ self.addToolBar(toolbar)
270
+ self.menu_factory.populate_toolbar(toolbar)
271
+ self.workspace_toolbar = toolbar
272
+ for action in (
273
+ self.open_action,
274
+ self.save_python_action,
275
+ self.convert_action,
276
+ self.cancel_action,
277
+ ):
278
+ button = toolbar.widgetForAction(action)
279
+ if isinstance(button, QToolButton):
280
+ button.setToolButtonStyle(
281
+ Qt.ToolButtonTextBesideIcon
282
+ )
283
+ convert_button = toolbar.widgetForAction(
284
+ self.convert_action
285
+ )
286
+ if isinstance(convert_button, QToolButton):
287
+ convert_button.setProperty("role", "primary")
288
+ convert_button.setAccessibleName(
289
+ "Transpile source bundle to C source"
290
+ )
291
+ cancel_button = toolbar.widgetForAction(
292
+ self.cancel_action
293
+ )
294
+ if isinstance(cancel_button, QToolButton):
295
+ cancel_button.setProperty("role", "danger")
296
+
297
+ def _build_menus(self) -> None:
298
+ self.main_menus = self.menu_factory.install_main_menus(self.menuBar())
299
+ self.recent_menu = self.menu_factory.menu("menu.open_recent")
300
+ self._rebuild_recent_menu()
301
+ required_contexts = (
302
+ (self.source, "context.python_source"),
303
+ (self.source_secondary, "context.python_source"),
304
+ (self.output, "context.generated_c"),
305
+ (self.navigator.documents, "context.source_bundle"),
306
+ (self.document_tabs, "context.document_tabs"),
307
+ (self.diags.tree, "context.diagnostics"),
308
+ (self.mappings.tree, "context.mappings"),
309
+ (self.summary.tree, "context.inspector"),
310
+ (self.trace.tree, "context.inspector"),
311
+ (self.telemetry.tree, "context.inspector"),
312
+ (self.outline.tree, "context.inspector"),
313
+ (self.bundle_search.tree, "context.bundle_search"),
314
+ (
315
+ self.session_history.tree,
316
+ "context.conversion_history",
317
+ ),
318
+ )
319
+ for widget, surface_id in required_contexts:
320
+ self.menu_factory.install_context_menu(widget, surface_id)
321
+ text_inputs = (
322
+ self.navigator.filter_edit,
323
+ self.navigator.module_edit,
324
+ self.navigator.logical_edit,
325
+ self.find_bar.find_edit,
326
+ self.find_bar.replace_edit,
327
+ self.diags.filter_edit,
328
+ self.summary.filter_edit,
329
+ self.trace.filter_edit,
330
+ self.mappings.filter_edit,
331
+ self.telemetry.filter_edit,
332
+ self.outline.filter_edit,
333
+ self.bundle_search.query_edit,
334
+ )
335
+ for widget in text_inputs:
336
+ self.menu_factory.install_context_menu(
337
+ widget, "context.text_input"
338
+ )
339
+ self.menu_factory.install_context_menu(
340
+ self.diags.details, "context.read_only_text"
341
+ )
342
+
343
+
344
+ # Executable wiring is separated into ``QtShellInteractionMixin``. These two
345
+ # seam markers retain the predecessor's static integration audit vocabulary:
346
+ # self.navigator.identity_pending_changed.connect(
347
+ # and not self.navigator.identity_pending
348
+ __all__ = ["QtShellMixin"]
@@ -0,0 +1,320 @@
1
+ """Action-state projection and signal wiring for the optional Qt shell."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from PyQt5.QtCore import QEvent, Qt
6
+ from PyQt5.QtWidgets import QApplication, QLineEdit
7
+
8
+ from .action_contract import ActionState
9
+
10
+
11
+ class QtShellInteractionMixin:
12
+ """Keep action targeting and widget wiring separate from shell layout."""
13
+
14
+ def _resolved_action_target(self, target=None):
15
+ registry = getattr(self, "action_registry", None)
16
+ return target or getattr(registry, "active_target", None) or (
17
+ QApplication.focusWidget()
18
+ )
19
+
20
+ def _invoke_target_method(self, method_name: str) -> None:
21
+ target = self._resolved_action_target()
22
+ method = getattr(target, method_name, None)
23
+ if callable(method):
24
+ method()
25
+ self.action_registry.refresh()
26
+
27
+ def _copy_action_selection(self) -> None:
28
+ target = self._resolved_action_target()
29
+ method = getattr(target, "copy", None)
30
+ if callable(method):
31
+ method()
32
+ return
33
+ current = getattr(target, "currentItem", lambda: None)()
34
+ if current is None:
35
+ return
36
+ columns = getattr(target, "columnCount", lambda: 1)()
37
+ text = "\t".join(
38
+ current.text(column) for column in range(max(1, columns))
39
+ ).strip()
40
+ if text:
41
+ QApplication.clipboard().setText(text)
42
+
43
+ def _make_selected_primary(self) -> None:
44
+ document_id = self.navigator.current_document_id
45
+ if document_id is not None:
46
+ self._set_primary_document(document_id)
47
+
48
+ def _reveal_selected_diagnostic(self) -> None:
49
+ item = self.diags.tree.currentItem()
50
+ if item is not None:
51
+ self._activate_diagnostic(item.data(0, Qt.UserRole))
52
+
53
+ def _reveal_selected_mapping(self) -> None:
54
+ item = self.mappings.tree.currentItem()
55
+ if item is not None:
56
+ self._activate_mapping(item.data(0, Qt.UserRole))
57
+
58
+ def _action_states(self, _surface_id, target):
59
+ snapshot = self.controller.snapshot
60
+ target = self._resolved_action_target(target)
61
+ source_target = target in (self.source, self.source_secondary)
62
+ current_row = self.navigator.documents.currentRow()
63
+ document_count = self.navigator.documents.count()
64
+ has_selection = self._target_has_selection(target)
65
+ writable = self._target_is_writable(target)
66
+ can_undo = self._target_history_available(
67
+ target, "isUndoAvailable"
68
+ )
69
+ can_redo = self._target_history_available(
70
+ target, "isRedoAvailable"
71
+ )
72
+ clipboard = QApplication.clipboard()
73
+ mime_data = (
74
+ clipboard.mimeData()
75
+ if clipboard is not None
76
+ else None
77
+ )
78
+ can_paste = (
79
+ writable
80
+ and callable(getattr(target, "paste", None))
81
+ and mime_data is not None
82
+ and mime_data.hasText()
83
+ )
84
+ source_action = ActionState(source_target and writable)
85
+ states = {
86
+ "conversion.convert": ActionState(snapshot.can_convert),
87
+ "conversion.cancel": ActionState(snapshot.can_cancel),
88
+ "file.save_python": ActionState(snapshot.can_save_python),
89
+ "file.save_python_as": ActionState(snapshot.can_save_python),
90
+ "output.save_c": ActionState(
91
+ snapshot.can_save_c
92
+ and not self._source_sync_pending
93
+ and not self.navigator.identity_pending
94
+ ),
95
+ "output.set_destination": ActionState(bool(snapshot.documents)),
96
+ "bundle.new_module": ActionState(snapshot.can_add_document),
97
+ "bundle.remove_module": ActionState(snapshot.can_remove_document),
98
+ "bundle.move_up": ActionState(current_row > 0),
99
+ "bundle.move_down": ActionState(
100
+ 0 <= current_row < document_count - 1
101
+ ),
102
+ "bundle.make_primary": ActionState(
103
+ self.navigator.current_document_id is not None
104
+ and self.navigator.primary_check.isEnabled()
105
+ ),
106
+ "edit.undo": ActionState(writable and can_undo),
107
+ "edit.redo": ActionState(writable and can_redo),
108
+ "edit.cut": ActionState(writable and has_selection),
109
+ "edit.copy": ActionState(has_selection),
110
+ "edit.paste": ActionState(can_paste),
111
+ "edit.select_all": ActionState(
112
+ callable(getattr(target, "selectAll", None))
113
+ ),
114
+ "edit.duplicate_line": source_action,
115
+ "edit.move_line_up": source_action,
116
+ "edit.move_line_down": source_action,
117
+ "edit.indent": source_action,
118
+ "edit.outdent": source_action,
119
+ "edit.toggle_comment": source_action,
120
+ "search.find": ActionState(True),
121
+ "search.replace": ActionState(True),
122
+ "search.bundle": ActionState(bool(snapshot.documents)),
123
+ "search.next_match": ActionState(
124
+ self.find_bar.next_button.isEnabled()
125
+ ),
126
+ "search.previous_match": ActionState(
127
+ self.find_bar.previous_button.isEnabled()
128
+ ),
129
+ "search.replace_current": ActionState(
130
+ self.find_bar.replace_button.isEnabled()
131
+ ),
132
+ "search.replace_all": ActionState(
133
+ self.find_bar.replace_all_button.isEnabled()
134
+ ),
135
+ "search.close": ActionState(self.find_bar.isVisible()),
136
+ "view.source_bundle": ActionState(
137
+ checked=self.navigator.isVisible()
138
+ ),
139
+ "view.generated_c": ActionState(
140
+ checked=self.output_panel.isVisible()
141
+ ),
142
+ "view.conversion_details": ActionState(
143
+ checked=self.tabs.isVisible()
144
+ ),
145
+ "view.outline": ActionState(bool(snapshot.documents)),
146
+ "view.conversion_history": ActionState(True),
147
+ "view.whitespace": ActionState(
148
+ checked=self.source.whitespace_visible
149
+ ),
150
+ "view.split_source": ActionState(
151
+ checked=self.source_secondary.isVisible()
152
+ ),
153
+ "editor.toggle_fold": ActionState(source_target),
154
+ "navigation.go_to_line": ActionState(bool(snapshot.documents)),
155
+ "workspace.command_palette": ActionState(
156
+ not getattr(self, "_command_palette_open", False)
157
+ ),
158
+ "tree.expand_all": ActionState(
159
+ callable(getattr(target, "expandAll", None))
160
+ ),
161
+ "tree.collapse_all": ActionState(
162
+ callable(getattr(target, "collapseAll", None))
163
+ ),
164
+ "diagnostics.reveal_source": ActionState(
165
+ self.diags.tree.currentItem() is not None
166
+ ),
167
+ "mappings.reveal_output": ActionState(
168
+ self.mappings.tree.currentItem() is not None
169
+ and bool(snapshot.generated_c)
170
+ ),
171
+ "mappings.reveal_source": ActionState(
172
+ self.mappings.tree.currentItem() is not None
173
+ ),
174
+ }
175
+ return states
176
+
177
+ @staticmethod
178
+ def _target_has_selection(target) -> bool:
179
+ if target is None:
180
+ return False
181
+ cursor_getter = getattr(target, "textCursor", None)
182
+ if callable(cursor_getter):
183
+ return bool(cursor_getter().hasSelection())
184
+ selected_text = getattr(target, "hasSelectedText", None)
185
+ if callable(selected_text):
186
+ return bool(selected_text())
187
+ selected_items = getattr(target, "selectedItems", None)
188
+ return bool(selected_items()) if callable(selected_items) else False
189
+
190
+ def _target_is_writable(self, target) -> bool:
191
+ if target is None or target is self.output:
192
+ return False
193
+ if not all(
194
+ callable(getattr(target, method_name, None))
195
+ for method_name in ("cut", "paste")
196
+ ):
197
+ return False
198
+ read_only = getattr(target, "isReadOnly", None)
199
+ return not (callable(read_only) and read_only())
200
+
201
+ @staticmethod
202
+ def _target_history_available(target, method_name: str) -> bool:
203
+ if target is None:
204
+ return False
205
+ method = getattr(target, method_name, None)
206
+ if callable(method):
207
+ return bool(method())
208
+ document = getattr(target, "document", None)
209
+ if callable(document):
210
+ method = getattr(document(), method_name, None)
211
+ return bool(method()) if callable(method) else False
212
+ return False
213
+
214
+ def _refresh_action_projection(self, *_args) -> None:
215
+ self.action_registry.refresh()
216
+
217
+ def eventFilter(self, watched, event) -> bool: # noqa: N802 - Qt API
218
+ if (
219
+ event.type() == QEvent.KeyPress
220
+ and self.action_registry.is_disabled_window_shortcut(
221
+ event.key(),
222
+ event.modifiers(),
223
+ )
224
+ ):
225
+ return True
226
+ if event.type() == QEvent.FocusIn and watched in (
227
+ self.source,
228
+ self.source_secondary,
229
+ self.output,
230
+ ):
231
+ self.find_bar.attach_editor(watched)
232
+ return super().eventFilter(watched, event)
233
+
234
+ def _wire_workspace(self) -> None:
235
+ for line_edit in self.findChildren(QLineEdit):
236
+ line_edit.installEventFilter(self)
237
+ for editor in (self.source, self.source_secondary):
238
+ editor.textChanged.connect(self._source_changed)
239
+ editor.installEventFilter(self)
240
+ editor.markerActivated.connect(self._editor_marker_activated)
241
+ editor.findRequested.connect(self._open_find)
242
+ editor.cursorPositionChanged.connect(self._update_breadcrumbs)
243
+ self.output.installEventFilter(self)
244
+ self.output.markerActivated.connect(self._editor_marker_activated)
245
+ self.output.findRequested.connect(self._open_find)
246
+ self.navigator.document_selected.connect(self._select_document)
247
+ self.navigator.add_requested.connect(self.add_document)
248
+ self.navigator.remove_requested.connect(self._remove_document)
249
+ self.navigator.move_up_requested.connect(self._move_document_up)
250
+ self.navigator.move_down_requested.connect(self._move_document_down)
251
+ self.navigator.identity_changed.connect(
252
+ self._change_document_identity
253
+ )
254
+ self.navigator.identity_pending_changed.connect(
255
+ self._present_pending_identity_edit
256
+ )
257
+ self.navigator.primary_requested.connect(
258
+ self._set_primary_document
259
+ )
260
+ self.document_tabs.document_selected.connect(self._select_document)
261
+ self.document_tabs.close_requested.connect(self._remove_document)
262
+ self.document_tabs.order_requested.connect(
263
+ self._reorder_document_tabs
264
+ )
265
+ self.breadcrumbs.location_requested.connect(
266
+ self._navigate_breadcrumb
267
+ )
268
+ self.diags.diagnostic_activated.connect(self._activate_diagnostic)
269
+ self.mappings.mapping_activated.connect(self._activate_mapping)
270
+ self.outline.symbolActivated.connect(self._activate_outline_symbol)
271
+ self.bundle_search.matchActivated.connect(
272
+ self._activate_bundle_search_match
273
+ )
274
+ self.session_history.historyActivated.connect(
275
+ self._activate_history_entry
276
+ )
277
+ self.find_bar.attach_editor(self.source)
278
+ app = QApplication.instance()
279
+ if app is not None:
280
+ app.focusChanged.connect(self._refresh_action_projection)
281
+ app.clipboard().dataChanged.connect(
282
+ self._refresh_action_projection
283
+ )
284
+ for editor in (self.source, self.source_secondary, self.output):
285
+ for signal in (
286
+ editor.copyAvailable,
287
+ editor.undoAvailable,
288
+ editor.redoAvailable,
289
+ ):
290
+ signal.connect(self._refresh_action_projection)
291
+ selection_views = (
292
+ self.navigator.documents,
293
+ self.diags.tree,
294
+ self.mappings.tree,
295
+ self.summary.tree,
296
+ self.trace.tree,
297
+ self.telemetry.tree,
298
+ self.outline.tree,
299
+ self.bundle_search.tree,
300
+ self.session_history.tree,
301
+ )
302
+ for view in selection_views:
303
+ view.itemSelectionChanged.connect(
304
+ self._refresh_action_projection
305
+ )
306
+ source_actions = (
307
+ "edit.duplicate_line",
308
+ "edit.move_line_up",
309
+ "edit.move_line_down",
310
+ "edit.indent",
311
+ "edit.outdent",
312
+ "edit.toggle_comment",
313
+ "editor.toggle_fold",
314
+ )
315
+ for action_id in source_actions:
316
+ for editor in (self.source, self.source_secondary):
317
+ self.action_registry.attach_to_widget(action_id, editor)
318
+
319
+
320
+ __all__ = ["QtShellInteractionMixin"]