gitpane 1.0.0__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 (353) hide show
  1. gitpane/__init__.py +0 -0
  2. gitpane/account/__init__.py +0 -0
  3. gitpane/account/account_choice.py +297 -0
  4. gitpane/account/address_owner.py +67 -0
  5. gitpane/account/authentication.py +1811 -0
  6. gitpane/account/authentication_worker.py +836 -0
  7. gitpane/account/credential_manager.py +480 -0
  8. gitpane/account/credential_manager_install.py +171 -0
  9. gitpane/account/keyring_reader.py +135 -0
  10. gitpane/account/keyring_state.py +158 -0
  11. gitpane/account/ssh.py +312 -0
  12. gitpane/account/ssh_askpass.py +216 -0
  13. gitpane/account/ssh_prompt.py +222 -0
  14. gitpane/application.py +684 -0
  15. gitpane/commit/__init__.py +0 -0
  16. gitpane/commit/commit_metadata.py +88 -0
  17. gitpane/commit/commit_push.py +276 -0
  18. gitpane/commit/commit_write.py +830 -0
  19. gitpane/config.py +1066 -0
  20. gitpane/controller/__init__.py +0 -0
  21. gitpane/controller/account_controller.py +677 -0
  22. gitpane/controller/clone_controller.py +776 -0
  23. gitpane/controller/close_readiness.py +212 -0
  24. gitpane/controller/commit_timing_controller.py +566 -0
  25. gitpane/controller/conflict_controller.py +885 -0
  26. gitpane/controller/console_registry.py +38 -0
  27. gitpane/controller/emoji_window_controller.py +95 -0
  28. gitpane/controller/file_action_controller.py +479 -0
  29. gitpane/controller/history_read_controller.py +671 -0
  30. gitpane/controller/history_rewrite_controller.py +739 -0
  31. gitpane/controller/navigation_controller.py +658 -0
  32. gitpane/controller/opening_controller.py +375 -0
  33. gitpane/controller/operation_policy.py +76 -0
  34. gitpane/controller/rebase_controller.py +797 -0
  35. gitpane/controller/rebase_editor_controller.py +309 -0
  36. gitpane/controller/reference_form_controller.py +1677 -0
  37. gitpane/controller/reference_read_controller.py +242 -0
  38. gitpane/controller/refresh_controller.py +477 -0
  39. gitpane/controller/replay_controller.py +805 -0
  40. gitpane/controller/session_registry.py +337 -0
  41. gitpane/controller/setting_controller.py +592 -0
  42. gitpane/controller/shutdown_controller.py +345 -0
  43. gitpane/controller/staging_controller.py +199 -0
  44. gitpane/controller/staging_session_registry.py +47 -0
  45. gitpane/controller/stash_controller.py +367 -0
  46. gitpane/controller/transport_controller.py +813 -0
  47. gitpane/controller/welcome_controller.py +102 -0
  48. gitpane/controller/window_activation.py +116 -0
  49. gitpane/controller/worktree_state_controller.py +169 -0
  50. gitpane/emoji/__init__.py +0 -0
  51. gitpane/emoji/emoji_catalogue.py +156 -0
  52. gitpane/git_config/__init__.py +0 -0
  53. gitpane/git_config/git_setting.py +441 -0
  54. gitpane/history/__init__.py +0 -0
  55. gitpane/history/avatar.py +241 -0
  56. gitpane/history/file_history.py +359 -0
  57. gitpane/history/graph.py +145 -0
  58. gitpane/history/history_filter.py +31 -0
  59. gitpane/history/history_parse.py +605 -0
  60. gitpane/history/repository.py +1600 -0
  61. gitpane/history/repository_value.py +341 -0
  62. gitpane/history/revision.py +180 -0
  63. gitpane/history/revision_file.py +182 -0
  64. gitpane/history_rewrite/__init__.py +0 -0
  65. gitpane/history_rewrite/graph.py +210 -0
  66. gitpane/history_rewrite/guard.py +288 -0
  67. gitpane/history_rewrite/index.py +125 -0
  68. gitpane/history_rewrite/prepare.py +378 -0
  69. gitpane/history_rewrite/protocol.py +320 -0
  70. gitpane/history_rewrite/publish.py +444 -0
  71. gitpane/history_rewrite/recovery.py +481 -0
  72. gitpane/history_rewrite/value.py +229 -0
  73. gitpane/operation/__init__.py +0 -0
  74. gitpane/operation/console_line.py +313 -0
  75. gitpane/operation/console_tone.py +76 -0
  76. gitpane/operation/diagnostic.py +954 -0
  77. gitpane/operation/launch_signature.py +254 -0
  78. gitpane/operation/operation_status.py +87 -0
  79. gitpane/operation/operation_trace.py +766 -0
  80. gitpane/operation/repository_operation.py +781 -0
  81. gitpane/preference/__init__.py +0 -0
  82. gitpane/preference/preference_location.py +40 -0
  83. gitpane/preference/preference_store.py +590 -0
  84. gitpane/process/__init__.py +0 -0
  85. gitpane/process/exact_arguments.py +77 -0
  86. gitpane/process/git_output_text.py +20 -0
  87. gitpane/process/process_group.py +851 -0
  88. gitpane/process/process_log.py +114 -0
  89. gitpane/process/process_runner.py +1033 -0
  90. gitpane/process/released_child_watch.py +138 -0
  91. gitpane/recent/__init__.py +0 -0
  92. gitpane/recent/mount_table.py +422 -0
  93. gitpane/recent/recent_availability.py +414 -0
  94. gitpane/recent/recent_name.py +11 -0
  95. gitpane/recent/recent_search.py +114 -0
  96. gitpane/reference/__init__.py +0 -0
  97. gitpane/reference/reference_edit.py +2030 -0
  98. gitpane/reference/reference_snapshot.py +196 -0
  99. gitpane/reference/stash.py +730 -0
  100. gitpane/reference/stash_list.py +164 -0
  101. gitpane/sequencer/__init__.py +0 -0
  102. gitpane/sequencer/commit_message_rule.py +154 -0
  103. gitpane/sequencer/conflict.py +506 -0
  104. gitpane/sequencer/merge_recovery.py +163 -0
  105. gitpane/sequencer/merge_state.py +166 -0
  106. gitpane/sequencer/mergetool.py +856 -0
  107. gitpane/sequencer/rebase.py +750 -0
  108. gitpane/sequencer/rebase_edit_file.py +224 -0
  109. gitpane/sequencer/rebase_state.py +523 -0
  110. gitpane/sequencer/replay.py +1435 -0
  111. gitpane/sequencer/replay_legacy.py +253 -0
  112. gitpane/sequencer/replay_state.py +747 -0
  113. gitpane/submodule/__init__.py +0 -0
  114. gitpane/submodule/checkout_refusal.py +102 -0
  115. gitpane/submodule/checkout_refusal_notice.py +111 -0
  116. gitpane/submodule/cloned_without_checkout.py +65 -0
  117. gitpane/submodule/submodule_discovery.py +1180 -0
  118. gitpane/submodule/submodule_loss.py +218 -0
  119. gitpane/system/__init__.py +0 -0
  120. gitpane/system/appearance_portal.py +125 -0
  121. gitpane/system/application_identity.py +48 -0
  122. gitpane/system/command_line.py +36 -0
  123. gitpane/system/desktop_icon_fact.py +128 -0
  124. gitpane/system/desktop_icon_support.py +270 -0
  125. gitpane/system/desktop_shortcut.py +536 -0
  126. gitpane/system/icns_image.py +35 -0
  127. gitpane/system/installed_launcher.py +136 -0
  128. gitpane/system/linux_distribution.py +151 -0
  129. gitpane/system/network_folder.py +46 -0
  130. gitpane/system/path_identity.py +95 -0
  131. gitpane/system/private_tree_removal.py +47 -0
  132. gitpane/system/process_program.py +63 -0
  133. gitpane/system/qt_folder.py +38 -0
  134. gitpane/system/ready_channel_watch.py +67 -0
  135. gitpane/system/startup_activation.py +65 -0
  136. gitpane/system/terminal_detach.py +384 -0
  137. gitpane/system/xdg_session.py +85 -0
  138. gitpane/text/__init__.py +0 -0
  139. gitpane/text/text_query.py +97 -0
  140. gitpane/text/typed_path.py +75 -0
  141. gitpane/text/typed_text.py +17 -0
  142. gitpane/thread/__init__.py +0 -0
  143. gitpane/thread/weak_signal.py +93 -0
  144. gitpane/tool/__init__.py +0 -0
  145. gitpane/tool/app_bundle.py +164 -0
  146. gitpane/tool/desktop_text_editor.py +156 -0
  147. gitpane/tool/difftool.py +429 -0
  148. gitpane/tool/editor.py +445 -0
  149. gitpane/tool/editor_bridge.py +193 -0
  150. gitpane/tool/editor_recipe.py +481 -0
  151. gitpane/tool/file_reveal.py +94 -0
  152. gitpane/tool/git_executable.py +264 -0
  153. gitpane/tool/git_version.py +62 -0
  154. gitpane/tool/kdiff3.py +41 -0
  155. gitpane/tool/kdiff3_install.py +162 -0
  156. gitpane/tool/launch_services_text_editor.py +104 -0
  157. gitpane/tool/mergetool_recipe.py +84 -0
  158. gitpane/tool/system_editor_default.py +80 -0
  159. gitpane/tool/system_text_editor.py +44 -0
  160. gitpane/tool/system_tool.py +816 -0
  161. gitpane/tool/terminal_recipe.py +390 -0
  162. gitpane/transport/__init__.py +0 -0
  163. gitpane/transport/branch_exchange.py +1690 -0
  164. gitpane/transport/clone.py +1273 -0
  165. gitpane/transport/credential_read_retry.py +35 -0
  166. gitpane/transport/git_refusal.py +207 -0
  167. gitpane/transport/push_tracking.py +121 -0
  168. gitpane/transport/refusal_notice.py +225 -0
  169. gitpane/transport/remote.py +747 -0
  170. gitpane/transport/remote_config.py +544 -0
  171. gitpane/ui/__init__.py +0 -0
  172. gitpane/ui/console/__init__.py +0 -0
  173. gitpane/ui/console/error_dialog.py +332 -0
  174. gitpane/ui/console/operation_dialog.py +408 -0
  175. gitpane/ui/console/operation_output.py +91 -0
  176. gitpane/ui/console/operation_output_view.py +373 -0
  177. gitpane/ui/emoji/__init__.py +0 -0
  178. gitpane/ui/emoji/emoji_font.py +257 -0
  179. gitpane/ui/emoji/emoji_grid.py +125 -0
  180. gitpane/ui/emoji/emoji_window.py +154 -0
  181. gitpane/ui/emoji/file_action_icon.py +131 -0
  182. gitpane/ui/emoji/opentype_family_name.py +182 -0
  183. gitpane/ui/history_rewrite/__init__.py +0 -0
  184. gitpane/ui/history_rewrite/details.py +78 -0
  185. gitpane/ui/history_rewrite/preview.py +565 -0
  186. gitpane/ui/history_rewrite/preview_model.py +291 -0
  187. gitpane/ui/history_rewrite/question.py +87 -0
  188. gitpane/ui/history_rewrite/recovery_dialog.py +262 -0
  189. gitpane/ui/look/__init__.py +0 -0
  190. gitpane/ui/look/action_icon.py +504 -0
  191. gitpane/ui/look/application_icon.py +78 -0
  192. gitpane/ui/look/application_style.py +317 -0
  193. gitpane/ui/look/avatar_label.py +53 -0
  194. gitpane/ui/look/color_contrast.py +43 -0
  195. gitpane/ui/look/desktop_appearance_follower.py +118 -0
  196. gitpane/ui/look/fixed_font.py +21 -0
  197. gitpane/ui/look/gtk_settings.py +116 -0
  198. gitpane/ui/look/icon_density.py +16 -0
  199. gitpane/ui/look/reference_font.py +19 -0
  200. gitpane/ui/look/theme_color.py +63 -0
  201. gitpane/ui/look/theme_look.py +95 -0
  202. gitpane/ui/look/theme_palette.py +173 -0
  203. gitpane/ui/look/warning_icon.py +86 -0
  204. gitpane/ui/look/windows_control_look.py +250 -0
  205. gitpane/ui/reader/__init__.py +0 -0
  206. gitpane/ui/reader/commit_message.py +129 -0
  207. gitpane/ui/reader/commit_panel.py +378 -0
  208. gitpane/ui/reader/diff_panel.py +626 -0
  209. gitpane/ui/reader/file_action.py +138 -0
  210. gitpane/ui/reader/file_tree_panel.py +235 -0
  211. gitpane/ui/reader/reader_toolbar.py +56 -0
  212. gitpane/ui/reader/revision_tree.py +80 -0
  213. gitpane/ui/reader/search_result.py +165 -0
  214. gitpane/ui/reader/source_text.py +134 -0
  215. gitpane/ui/reader/source_view.py +151 -0
  216. gitpane/ui/reader/syntax_span.py +67 -0
  217. gitpane/ui/reference/__init__.py +0 -0
  218. gitpane/ui/reference/branch_selector.py +106 -0
  219. gitpane/ui/reference/checkout_dialog.py +419 -0
  220. gitpane/ui/reference/checkout_revision_dialog.py +182 -0
  221. gitpane/ui/reference/commit_summary.py +339 -0
  222. gitpane/ui/reference/create_branch_dialog.py +346 -0
  223. gitpane/ui/reference/create_tag_dialog.py +283 -0
  224. gitpane/ui/reference/delete_local_branch_dialog.py +323 -0
  225. gitpane/ui/reference/delete_tag_dialog.py +394 -0
  226. gitpane/ui/reference/form_opening.py +53 -0
  227. gitpane/ui/reference/reference_dialog.py +417 -0
  228. gitpane/ui/reference/reference_feedback.py +62 -0
  229. gitpane/ui/reference/reference_form.py +139 -0
  230. gitpane/ui/reference/rename_branch_dialog.py +179 -0
  231. gitpane/ui/reference/reset_branch_dialog.py +430 -0
  232. gitpane/ui/reference/reset_dialog.py +263 -0
  233. gitpane/ui/reference/revision_dialog.py +121 -0
  234. gitpane/ui/reference/revision_picker.py +227 -0
  235. gitpane/ui/reference/select_branches_dialog.py +199 -0
  236. gitpane/ui/reference/stash_dialog.py +171 -0
  237. gitpane/ui/repository/__init__.py +0 -0
  238. gitpane/ui/repository/graph_delegate.py +298 -0
  239. gitpane/ui/repository/history_avatar.py +130 -0
  240. gitpane/ui/repository/history_model.py +179 -0
  241. gitpane/ui/repository/repository_list_popup.py +319 -0
  242. gitpane/ui/repository/repository_window.py +2580 -0
  243. gitpane/ui/resource/ArrowDown.png +0 -0
  244. gitpane/ui/resource/ArrowUp.png +0 -0
  245. gitpane/ui/resource/BranchCheckout.png +0 -0
  246. gitpane/ui/resource/BranchCreate.png +0 -0
  247. gitpane/ui/resource/BranchDelete.png +0 -0
  248. gitpane/ui/resource/CopyToClipboard.png +0 -0
  249. gitpane/ui/resource/GitPane-16.png +0 -0
  250. gitpane/ui/resource/GitPane-24.png +0 -0
  251. gitpane/ui/resource/GitPane-256.png +0 -0
  252. gitpane/ui/resource/GitPane-32.png +0 -0
  253. gitpane/ui/resource/GitPane-48.png +0 -0
  254. gitpane/ui/resource/GitPane-64.png +0 -0
  255. gitpane/ui/resource/GitPane.ico +0 -0
  256. gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  257. gitpane/ui/resource/NotoColorEmoji_WindowsCompatible.ttf +0 -0
  258. gitpane/ui/resource/Renamed.png +0 -0
  259. gitpane/ui/resource/ResetAnotherBranchToHere.png +0 -0
  260. gitpane/ui/resource/ResetCurrentBranchToHere.png +0 -0
  261. gitpane/ui/resource/Stash.png +0 -0
  262. gitpane/ui/resource/Tag.png +0 -0
  263. gitpane/ui/resource/TagCreate.png +0 -0
  264. gitpane/ui/resource/TagDelete.png +0 -0
  265. gitpane/ui/resource/Unmerged.png +0 -0
  266. gitpane/ui/resource/checkout.png +0 -0
  267. gitpane/ui/sequencer/__init__.py +0 -0
  268. gitpane/ui/sequencer/conflict_dialog.py +532 -0
  269. gitpane/ui/sequencer/merge_recovery_dialog.py +67 -0
  270. gitpane/ui/sequencer/rebase_dialog.py +461 -0
  271. gitpane/ui/sequencer/rebase_editor_window.py +220 -0
  272. gitpane/ui/sequencer/replay_conflict_prompt.py +105 -0
  273. gitpane/ui/sequencer/replay_dialog.py +501 -0
  274. gitpane/ui/setting/__init__.py +0 -0
  275. gitpane/ui/setting/account_page.py +724 -0
  276. gitpane/ui/setting/add_account_dialog.py +1005 -0
  277. gitpane/ui/setting/credential_manager_dialog.py +151 -0
  278. gitpane/ui/setting/git_setting_widget.py +388 -0
  279. gitpane/ui/setting/setting_block.py +19 -0
  280. gitpane/ui/setting/setting_dialog.py +750 -0
  281. gitpane/ui/setting/ssh_widget.py +232 -0
  282. gitpane/ui/setting/utility_page.py +240 -0
  283. gitpane/ui/staging/__init__.py +0 -0
  284. gitpane/ui/staging/staging_action.py +356 -0
  285. gitpane/ui/staging/staging_window.py +745 -0
  286. gitpane/ui/transport/__init__.py +0 -0
  287. gitpane/ui/transport/clone_dialog.py +879 -0
  288. gitpane/ui/transport/push_dialog.py +580 -0
  289. gitpane/ui/transport/push_rejection_prompt.py +100 -0
  290. gitpane/ui/transport/push_tracking_question.py +88 -0
  291. gitpane/ui/transport/remote_dialog.py +943 -0
  292. gitpane/ui/welcome/__init__.py +0 -0
  293. gitpane/ui/welcome/invalid_repository_prompt.py +114 -0
  294. gitpane/ui/welcome/welcome_window.py +1137 -0
  295. gitpane/ui/widget/__init__.py +0 -0
  296. gitpane/ui/widget/busy_indicator.py +106 -0
  297. gitpane/ui/widget/commit_date_text.py +55 -0
  298. gitpane/ui/widget/content_growth.py +262 -0
  299. gitpane/ui/widget/content_measure.py +99 -0
  300. gitpane/ui/widget/dialog_layout.py +668 -0
  301. gitpane/ui/widget/drop_down_wheel_filter.py +32 -0
  302. gitpane/ui/widget/flow_layout.py +110 -0
  303. gitpane/ui/widget/long_text.py +542 -0
  304. gitpane/ui/widget/mnemonic.py +13 -0
  305. gitpane/ui/widget/numbered_text_edit.py +127 -0
  306. gitpane/ui/widget/plain_label.py +15 -0
  307. gitpane/ui/widget/plain_tooltip.py +19 -0
  308. gitpane/ui/widget/refresh_advice.py +25 -0
  309. gitpane/ui/widget/result_label.py +227 -0
  310. gitpane/ui/widget/result_status_bar.py +53 -0
  311. gitpane/ui/widget/scrolled_text.py +69 -0
  312. gitpane/ui/widget/shortcut_text.py +31 -0
  313. gitpane/ui/widget/standard_icon_label.py +55 -0
  314. gitpane/ui/widget/system_shortcut.py +75 -0
  315. gitpane/ui/widget/unavailable_reason.py +29 -0
  316. gitpane/ui/widget/wheel_safe_drop_down.py +24 -0
  317. gitpane/ui/window_control/__init__.py +0 -0
  318. gitpane/ui/window_control/button_focus.py +42 -0
  319. gitpane/ui/window_control/compositor_answer.py +152 -0
  320. gitpane/ui/window_control/compositor_growth_cap.py +73 -0
  321. gitpane/ui/window_control/desktop_activation.py +203 -0
  322. gitpane/ui/window_control/dialog_modality.py +122 -0
  323. gitpane/ui/window_control/item_activation.py +42 -0
  324. gitpane/ui/window_control/modal_focus_relay.py +124 -0
  325. gitpane/ui/window_control/wayland_activation.py +695 -0
  326. gitpane/ui/window_control/window_placement.py +305 -0
  327. gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  328. gitpane/vendor/w32.exe +0 -0
  329. gitpane/vendor/w64-arm.exe +0 -0
  330. gitpane/vendor/w64.exe +0 -0
  331. gitpane/worktree/__init__.py +0 -0
  332. gitpane/worktree/command_sequence.py +110 -0
  333. gitpane/worktree/file_action_target.py +61 -0
  334. gitpane/worktree/file_menu_action.py +52 -0
  335. gitpane/worktree/historical_restore.py +361 -0
  336. gitpane/worktree/ignore_rule.py +266 -0
  337. gitpane/worktree/mutation.py +1539 -0
  338. gitpane/worktree/partial_patch.py +435 -0
  339. gitpane/worktree/staged_reset.py +447 -0
  340. gitpane/worktree/staging.py +1418 -0
  341. gitpane/worktree/trash.py +302 -0
  342. gitpane/worktree/worktree_file.py +406 -0
  343. gitpane/worktree/worktree_service.py +584 -0
  344. gitpane/worktree/worktree_snapshot.py +630 -0
  345. gitpane-1.0.0.dist-info/METADATA +141 -0
  346. gitpane-1.0.0.dist-info/RECORD +353 -0
  347. gitpane-1.0.0.dist-info/WHEEL +5 -0
  348. gitpane-1.0.0.dist-info/entry_points.txt +2 -0
  349. gitpane-1.0.0.dist-info/licenses/LICENSE +21 -0
  350. gitpane-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +109 -0
  351. gitpane-1.0.0.dist-info/licenses/src/gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  352. gitpane-1.0.0.dist-info/licenses/src/gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  353. gitpane-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,501 @@
1
+ """Present the Cherry-pick or Revert form of a captured commit, from its metadata to its explicit replay choices."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import replace
6
+ from datetime import datetime
7
+ from typing import TYPE_CHECKING
8
+
9
+ from PySide6.QtCore import QEvent, QMargins, QSize, Qt, Signal
10
+ from PySide6.QtGui import QColor, QPainter, QPalette
11
+ from PySide6.QtWidgets import (
12
+ QAbstractItemView,
13
+ QApplication,
14
+ QCheckBox,
15
+ QDialog,
16
+ QHBoxLayout,
17
+ QHeaderView,
18
+ QLabel,
19
+ QPushButton,
20
+ QTableWidget,
21
+ QTableWidgetItem,
22
+ QVBoxLayout,
23
+ QWidget,
24
+ )
25
+
26
+ from gitpane.config import ICON_SIDE, REPLAY_DIALOG_HEIGHT, REPLAY_DIALOG_WIDTH, keep_selection_visible
27
+ from gitpane.sequencer.replay import MERGE_PARENT_MINIMUM
28
+ from gitpane.sequencer.replay_state import ReplayKind
29
+ from gitpane.ui.look.action_icon import action_icon
30
+ from gitpane.ui.look.theme_color import is_dark_palette, theme_line_color
31
+ from gitpane.ui.look.theme_look import follow_theme
32
+ from gitpane.ui.look.theme_palette import PALETTE_COLOR_GROUPS
33
+ from gitpane.ui.look.windows_control_look import WINDOWS_LIST_BORDER_LIGHT_COLOR, apply_windows_button_look
34
+ from gitpane.ui.reference.commit_summary import CommitSummary
35
+ from gitpane.ui.reference.revision_picker import REVISION_BUTTON_SIZE, REVISION_CHOICE_NAME
36
+ from gitpane.ui.widget.content_growth import ContentGrowthMixin, OpeningFit
37
+ from gitpane.ui.widget.content_measure import LayoutContentMeasure
38
+ from gitpane.ui.widget.dialog_layout import BandButtonSizing, fit_banded_form, fit_capture_control, install_banded_layout
39
+ from gitpane.ui.widget.long_text import WrappingLabel
40
+ from gitpane.ui.widget.scrolled_text import ScrolledText
41
+
42
+ if TYPE_CHECKING:
43
+ from collections.abc import Callable
44
+
45
+ from PySide6.QtGui import QCloseEvent, QKeyEvent, QPaintEvent
46
+ from PySide6.QtWidgets import QLayout
47
+
48
+ from gitpane.sequencer.replay import ReplayIntent, ReplayParent
49
+ from gitpane.ui.reference.revision_dialog import RevisionChoiceDialog
50
+
51
+ PARENT_NUMBER_COLUMN, PARENT_MESSAGE_COLUMN, PARENT_AUTHOR_COLUMN, PARENT_DATE_COLUMN = range(4)
52
+ PARENT_COLUMN_COUNT = 4
53
+ # Selected row color of the merge parent list in the Git Extensions capture (light theme).
54
+ PARENT_SELECTION_LIGHT_COLOR = "#cde8ff"
55
+ # The merge parent list of the captures: a fixed 78 px list (two parents and some empty space, more parents scroll),
56
+ # a 25 px header, 19 px rows, fixed number, author and date columns, and text starting a few pixels into each column.
57
+ PARENT_TABLE_HEIGHT = 78
58
+ PARENT_HEADER_HEIGHT = 25
59
+ PARENT_ROW_HEIGHT = 19
60
+ PARENT_FIXED_COLUMN_WIDTH_BY_COLUMN = {PARENT_NUMBER_COLUMN: 44, PARENT_AUTHOR_COLUMN: 120, PARENT_DATE_COLUMN: 81}
61
+ PARENT_HEADER_TEXT_PADDING = 7
62
+ PARENT_CELL_TEXT_PADDING = 3
63
+ # Qt centres header text one pixel lower than the Windows list header does.
64
+ PARENT_HEADER_TEXT_LIFT = 1
65
+ PARENT_TABLE_FRAME_WIDTH = 1
66
+ # Windows list colors sampled on the captures: list border, header column separators, and the faint accent-blue column
67
+ # lines of the rows (8 % opaque, so they read #ebf4fe on white and a little darker on the selected row).
68
+ PARENT_TABLE_FRAME_LIGHT_COLOR = WINDOWS_LIST_BORDER_LIGHT_COLOR
69
+ PARENT_HEADER_LINE_LIGHT_COLOR = "#e5e5e5"
70
+ PARENT_COLUMN_LINE_LIGHT_COLOR = "#140576f2"
71
+ # Body of the forms, measured on the captures in client-area pixels: check boxes 14 px from the left, heading 11 px from
72
+ # the top, commit frame ending 15 px from the right, 12 px under the last check box row.
73
+ FORM_BODY_MARGINS = QMargins(14, 11, 15, 12)
74
+ HEADING_INDENT = 3
75
+ HEADING_TO_FRAME_SPACE = 2
76
+ # The commit frame is indented about 10 px from the heading text.
77
+ FRAME_INDENT = 13
78
+ FRAME_TO_REVISION_SPACE = 11
79
+ REVISION_ROW_RIGHT_INDENT = 4
80
+ REVISION_CAPTION_TO_BUTTON_SPACE = 11
81
+ # Space before the next control (merge parent caption or check boxes), after the frame or after the revision row.
82
+ SPACE_AFTER_FRAME = 3
83
+ SPACE_AFTER_REVISION_ROW = 6
84
+ PARENT_CAPTION_INDENT = 3
85
+ PARENT_CAPTION_TO_TABLE_SPACE = 6
86
+ PARENT_TABLE_INDENT = 3
87
+ PARENT_TABLE_TO_CHECK_SPACE = 5
88
+ CHECK_BLOCK_TOP_SPACE = 1
89
+ CHECK_ROW_HEIGHT = 25
90
+ # Windows Forms button sizes of the captures: the submit buttons are wider than the standard Abort button.
91
+ SUBMIT_BUTTON_WIDTH_BY_KIND: dict[ReplayKind, int] = {ReplayKind.CHERRY_PICK: 107, ReplayKind.REVERT: 115}
92
+ CANCEL_BUTTON_WIDTH = 73
93
+ FORM_BUTTON_HEIGHT = 23
94
+
95
+
96
+ def indented(widget: QWidget, left: int, right: int = 0) -> QHBoxLayout:
97
+ """Wrap one control in a row that moves it in from the body edges, as the captured window positions it."""
98
+ row = QHBoxLayout()
99
+ row.setContentsMargins(left, 0, right, 0)
100
+ row.setSpacing(0)
101
+ row.addWidget(widget)
102
+ return row
103
+
104
+
105
+ class ParentTable(QTableWidget):
106
+ """Merge parent list drawn like the Windows list view of Git Extensions, with faint column lines down to its bottom."""
107
+
108
+ def __init__(self, rows: int, columns: int) -> None:
109
+ """Start without column lines; the form sets their color once it knows the theme."""
110
+ super().__init__(rows, columns)
111
+ self.column_line_color: QColor | None = None
112
+
113
+ def paintEvent(self, event: QPaintEvent) -> None:
114
+ """Paint the rows, then the column lines over the whole list height, empty area included."""
115
+ super().paintEvent(event)
116
+ if self.column_line_color is None:
117
+ return
118
+ painter = QPainter(self.viewport())
119
+ painter.setPen(self.column_line_color)
120
+ header = self.horizontalHeader()
121
+ last_column = self.columnCount() - 1
122
+ for column in range(last_column):
123
+ x = header.sectionViewportPosition(column) + header.sectionSize(column) - 1
124
+ painter.drawLine(x, 0, x, self.viewport().height())
125
+
126
+
127
+ class ReplayDialog(ContentGrowthMixin, QDialog):
128
+ """Collect one source and its explicit choices, then hand the launched operation over to the console.
129
+
130
+ The window opens at exactly the height of its content, and its height then follows that content both ways by the
131
+ common growth rule: an error or a merge's parent list grows it, and their leaving brings it back to its opening
132
+ height, or to the height the user set by hand.
133
+ """
134
+
135
+ replay_requested = Signal(object)
136
+ revision_requested = Signal()
137
+
138
+ def __init__(self, session_id: str, source_oid: str, kind: ReplayKind, parent: QWidget | None = None, *, clock: Callable[[], datetime] | None = None) -> None:
139
+ """Build the compact commit form with fresh defaults on each opening.
140
+
141
+ ``clock`` goes to every commit box the form builds, so the relative commit date can be reproduced, as the window
142
+ gallery needs; the commit boxes read the system time when it is absent.
143
+ """
144
+ super().__init__(parent)
145
+ self.clock = clock
146
+ self.session_id = session_id
147
+ self.source_oid = source_oid
148
+ self.kind = kind
149
+ self.intent: ReplayIntent | None = None
150
+ self.closed = False
151
+ self.busy = False
152
+ self.completed = False
153
+ self.metadata_ready = False
154
+ self.parents: tuple[ReplayParent, ...] = ()
155
+ self.source_generation = 0
156
+ self.revision_dialog: RevisionChoiceDialog | None = None
157
+ cherry_pick = kind == ReplayKind.CHERRY_PICK
158
+ self.setWindowTitle("Cherry pick commit" if cherry_pick else "Revert commit")
159
+ # The title bar keeps the GitPane icon every window shares, and no help button since there is no help to open.
160
+ self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False)
161
+ # The width follows the captures; the height is fitted to the content once the form is built, then follows it.
162
+ self.resize(REPLAY_DIALOG_WIDTH, REPLAY_DIALOG_HEIGHT)
163
+ # Every distance below is measured on the Git Extensions window of the same name, so the layout adds explicit
164
+ # spaces instead of a uniform spacing.
165
+ layout = QVBoxLayout()
166
+ layout.setSpacing(0)
167
+ self.body_layout = layout
168
+ self.heading_label = QLabel("Cherry pick this commit:" if cherry_pick else "Revert this commit:")
169
+ layout.addLayout(indented(self.heading_label, HEADING_INDENT))
170
+ layout.addSpacing(HEADING_TO_FRAME_SPACE)
171
+ # The commit box reproduces the Git Extensions one: short hash in the frame line, colored reference bands.
172
+ self.summary = CommitSummary(source_oid, git_extensions_look=True, clock=clock)
173
+ self.summary_row = indented(self.summary, FRAME_INDENT)
174
+ layout.addLayout(self.summary_row)
175
+ self.revision_label = None
176
+ self.revision_button = None
177
+ if cherry_pick:
178
+ layout.addSpacing(FRAME_TO_REVISION_SPACE)
179
+ layout.addLayout(self._revision_row())
180
+ layout.addSpacing(SPACE_AFTER_REVISION_ROW)
181
+ else:
182
+ layout.addSpacing(SPACE_AFTER_FRAME)
183
+ self.parent_label = QLabel("This commit is a merge, select parent:")
184
+ self.parent_table = ParentTable(0, PARENT_COLUMN_COUNT)
185
+ self.parent_table.setHorizontalHeaderLabels(["No.", "Message", "Author", "Date"])
186
+ self.parent_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
187
+ self.parent_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
188
+ self.parent_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
189
+ self.parent_table.setSortingEnabled(False)
190
+ self.parent_table.setAccessibleName("Merge parent")
191
+ self._use_capture_list_look()
192
+ self.apply_theme_look()
193
+ self.parent_section = QWidget()
194
+ parent_section_layout = QVBoxLayout(self.parent_section)
195
+ parent_section_layout.setContentsMargins(0, 0, 0, PARENT_TABLE_TO_CHECK_SPACE)
196
+ parent_section_layout.setSpacing(0)
197
+ parent_section_layout.addLayout(indented(self.parent_label, PARENT_CAPTION_INDENT))
198
+ parent_section_layout.addSpacing(PARENT_CAPTION_TO_TABLE_SPACE)
199
+ parent_section_layout.addLayout(indented(self.parent_table, PARENT_TABLE_INDENT, PARENT_TABLE_INDENT))
200
+ layout.addWidget(self.parent_section)
201
+ self.parent_label.hide()
202
+ self.parent_table.hide()
203
+ self.parent_section.hide()
204
+ self.parent_table.itemSelectionChanged.connect(self._parent_selected)
205
+ layout.addSpacing(CHECK_BLOCK_TOP_SPACE)
206
+ self.create_commit_check = QCheckBox("Automatically create a commit")
207
+ self.create_commit_check.setChecked(True)
208
+ self.add_reference_check = None
209
+ checks = [self.create_commit_check]
210
+ if cherry_pick:
211
+ self.add_reference_check = QCheckBox("Add commit reference to commit message")
212
+ checks.append(self.add_reference_check)
213
+ for check in checks:
214
+ # Windows Forms places these check boxes 25 px apart; a fixed row height keeps that pitch.
215
+ check.setFixedHeight(CHECK_ROW_HEIGHT)
216
+ layout.addWidget(check)
217
+ self.status_label = WrappingLabel("")
218
+ self.status_label.setWordWrap(True)
219
+ # A cause read from Git can be longer than the screen: it scrolls past the window's growth cap, so the buttons
220
+ # under it stay inside the window.
221
+ self.status_area = ScrolledText(self.status_label)
222
+ layout.addWidget(self.status_area)
223
+ self.status_area.hide()
224
+ self.submit_button = QPushButton("Cherry pick" if cherry_pick else "Revert this commit")
225
+ self.submit_button.setEnabled(False)
226
+ self.cancel_button = QPushButton("Abort")
227
+ # Only the submit button is the default one. A focused auto-default button would take that role over, and
228
+ # then Enter pressed from a check box would abort instead of running the replay; keyPressEvent below presses a
229
+ # focused button on Enter instead, as Windows does.
230
+ for button in (self.revision_button, self.cancel_button):
231
+ if button is not None:
232
+ button.setAutoDefault(False)
233
+ # Git Extensions shows these forms as a white body over a grey band holding the buttons; the form reproduces it.
234
+ self.button_band = install_banded_layout(self, layout, [self.submit_button, self.cancel_button], body_margins=FORM_BODY_MARGINS, button_sizing=BandButtonSizing.WINDOW)
235
+ # Sized once the band gave the buttons their look, which changes the room their text needs.
236
+ self._fit_band_buttons()
237
+ # Like Git Extensions, Enter runs the submit button; Qt ignores Enter while that default button is disabled,
238
+ # so a merge without a chosen parent cannot be submitted from the keyboard either. The role is set only now,
239
+ # once the button belongs to the dialog: Qt then records it as the dialog's main default and gives it back
240
+ # whenever the focus moves, instead of dropping it the first time the button loses the focus.
241
+ self.submit_button.setDefault(True)
242
+ self.submit_button.clicked.connect(self._submit)
243
+ self.cancel_button.clicked.connect(self.reject)
244
+ self.finished.connect(self._closed)
245
+ follow_theme(self, self.apply_theme_look)
246
+ fit_banded_form(self)
247
+ self.install_content_growth(LayoutContentMeasure(self), OpeningFit.EXACT, self.height())
248
+
249
+ def apply_theme_look(self) -> None:
250
+ """Lay everything of the form's own look that depends on the light or dark appearance; the one place that lays it again after a switch.
251
+
252
+ The revision button takes or loses its light sheet, and the merge parent list takes the frame, header, column
253
+ line and selection colors of the new appearance. The chosen parent, the check boxes and the commit stay as they
254
+ are; the commit frame already paints itself from the palette.
255
+ """
256
+ if self.revision_button is not None:
257
+ apply_windows_button_look(self.revision_button)
258
+ # Workaround: a list with a style sheet gets back, when its sheet changes, the palette it had before that sheet
259
+ # was laid, and the sheet copies the list's palette to its viewport. Changing the sheet alone would bring the
260
+ # old selection colors back, and laying them after the sheet would leave the old ones on the viewport. The old
261
+ # sheet is taken off first, then the selection colors are laid, then the new sheet copies them to the viewport.
262
+ self.parent_table.setStyleSheet("")
263
+ self._use_capture_selection_color()
264
+ self._apply_parent_table_colors()
265
+
266
+ def _fit_band_buttons(self) -> None:
267
+ """Give the submit and Abort buttons the capture's sizes, or more when their text needs it in the current font.
268
+
269
+ The band's height follows the buttons, so a taller button keeps the same space above and below it.
270
+ """
271
+ fit_capture_control(self.submit_button, QSize(SUBMIT_BUTTON_WIDTH_BY_KIND[self.kind], FORM_BUTTON_HEIGHT))
272
+ fit_capture_control(self.cancel_button, QSize(CANCEL_BUTTON_WIDTH, FORM_BUTTON_HEIGHT))
273
+
274
+ def changeEvent(self, event: QEvent) -> None:
275
+ """Size the band's buttons again when the text size or the style changes."""
276
+ super().changeEvent(event)
277
+ # A style or font change can arrive while the form is still being built, before its band holds the buttons.
278
+ if event.type() in (QEvent.Type.FontChange, QEvent.Type.StyleChange) and hasattr(self, "button_band"):
279
+ self._fit_band_buttons()
280
+
281
+ def _revision_row(self) -> QLayout:
282
+ """Build the right-aligned "Choose another revision:" caption followed by its small icon button."""
283
+ self.revision_label = QLabel(REVISION_CHOICE_NAME + ":")
284
+ self.revision_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
285
+ self.revision_button = QPushButton()
286
+ self.revision_button.setIcon(action_icon("choose_revision", self.devicePixelRatioF()))
287
+ self.revision_button.setIconSize(QSize(ICON_SIDE, ICON_SIDE))
288
+ self.revision_button.setFixedSize(REVISION_BUTTON_SIZE)
289
+ self.revision_button.setAccessibleName(REVISION_CHOICE_NAME)
290
+ self.revision_button.setToolTip(REVISION_CHOICE_NAME)
291
+ self.revision_label.setBuddy(self.revision_button)
292
+ self.revision_button.clicked.connect(self.revision_requested)
293
+ self.revision_button.setEnabled(False)
294
+ row = QHBoxLayout()
295
+ row.setContentsMargins(0, 0, REVISION_ROW_RIGHT_INDENT, 0)
296
+ row.setSpacing(REVISION_CAPTION_TO_BUTTON_SPACE)
297
+ row.addWidget(self.revision_label, 1)
298
+ row.addWidget(self.revision_button)
299
+ return row
300
+
301
+ def _use_capture_list_look(self) -> None:
302
+ """Give the merge parent list the layout of the Windows list view of the captures: no grid, fixed header and row heights, fixed columns."""
303
+ table = self.parent_table
304
+ table.setShowGrid(False)
305
+ # Long authors and messages are cut character by character ("Alexander Mordvi...") like the Windows list, not at a word.
306
+ table.setWordWrap(False)
307
+ table.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
308
+ table.setFixedHeight(PARENT_TABLE_HEIGHT)
309
+ rows = table.verticalHeader()
310
+ rows.hide()
311
+ rows.setSectionResizeMode(QHeaderView.ResizeMode.Fixed)
312
+ rows.setMinimumSectionSize(PARENT_ROW_HEIGHT)
313
+ rows.setDefaultSectionSize(PARENT_ROW_HEIGHT)
314
+ header = table.horizontalHeader()
315
+ header.setFixedHeight(PARENT_HEADER_HEIGHT)
316
+ header.setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
317
+ header.setHighlightSections(False)
318
+ header.setStretchLastSection(False)
319
+ header.setSectionResizeMode(PARENT_MESSAGE_COLUMN, QHeaderView.ResizeMode.Stretch)
320
+ for column, width in PARENT_FIXED_COLUMN_WIDTH_BY_COLUMN.items():
321
+ header.setSectionResizeMode(column, QHeaderView.ResizeMode.Fixed)
322
+ header.resizeSection(column, width)
323
+
324
+ def _apply_parent_table_colors(self) -> None:
325
+ """Give the merge parent list the colors of the captures in a light theme, lines in the theme's line grey in a dark one.
326
+
327
+ In a dark theme the frame, the header lines and the column lines all take the theme's line grey, worked out from
328
+ the application palette like every other line of the window, so the list's columns stay visible. Every color
329
+ comes from the application palette, which already holds the new colors when the appearance has just changed.
330
+ """
331
+ table = self.parent_table
332
+ palette = QApplication.palette()
333
+ base, text = palette.color(QPalette.ColorRole.Base), palette.color(QPalette.ColorRole.Text)
334
+ if is_dark_palette(palette):
335
+ frame = header_line = theme_line_color(palette)
336
+ table.column_line_color = frame
337
+ else:
338
+ frame, header_line = QColor(PARENT_TABLE_FRAME_LIGHT_COLOR), QColor(PARENT_HEADER_LINE_LIGHT_COLOR)
339
+ table.column_line_color = QColor(PARENT_COLUMN_LINE_LIGHT_COLOR)
340
+ # The sheet only styles the list frame and the header sections; rows keep the palette, including the selection
341
+ # colors set just after.
342
+ # No focus outline either: the Windows list shows none on an unselected row, and the selection already marks
343
+ # the chosen parent.
344
+ table.setStyleSheet(f"QTableWidget {{ border: {PARENT_TABLE_FRAME_WIDTH}px solid {frame.name()}; background-color: {base.name()}; outline: 0; }}"
345
+ f" QTableWidget::item {{ padding-left: {PARENT_CELL_TEXT_PADDING}px; }}"
346
+ f" QHeaderView::section {{ background-color: {base.name()}; color: {text.name()}; border: none;"
347
+ f" border-right: {PARENT_TABLE_FRAME_WIDTH}px solid {header_line.name()}; padding-left: {PARENT_HEADER_TEXT_PADDING}px;"
348
+ f" padding-bottom: {PARENT_HEADER_TEXT_LIFT}px; }}")
349
+
350
+ def _use_capture_selection_color(self) -> None:
351
+ """Select merge parents with the Git Extensions blue in a light theme, the system highlight in a dark one.
352
+
353
+ Fusion's light highlight is a saturated blue under white text; the captures use a pale blue under the normal
354
+ text color, in both active and inactive windows. A dark palette keeps its own readable highlight, kept visible
355
+ when the list does not have the focus. The selection colors of the application palette are laid first, so a
356
+ switch from one appearance to the other leaves none of the old ones behind.
357
+ """
358
+ application_palette = QApplication.palette()
359
+ palette = self.parent_table.palette()
360
+ for group in PALETTE_COLOR_GROUPS:
361
+ for role in (QPalette.ColorRole.Highlight, QPalette.ColorRole.HighlightedText):
362
+ palette.setColor(group, role, application_palette.color(group, role))
363
+ if is_dark_palette(application_palette):
364
+ self.parent_table.setPalette(palette)
365
+ keep_selection_visible(self.parent_table)
366
+ return
367
+ for group in (QPalette.ColorGroup.Active, QPalette.ColorGroup.Inactive):
368
+ palette.setColor(group, QPalette.ColorRole.Highlight, QColor(PARENT_SELECTION_LIGHT_COLOR))
369
+ palette.setColor(group, QPalette.ColorRole.HighlightedText, application_palette.color(group, QPalette.ColorRole.Text))
370
+ self.parent_table.setPalette(palette)
371
+
372
+ def _show_status(self, text: str) -> None:
373
+ """Show a cause under the choices, and give its space back when there is nothing to say."""
374
+ self.status_label.setText(text)
375
+ self.status_area.setVisible(bool(text))
376
+
377
+ def populate(self, intent: ReplayIntent, fields: tuple[str, ...], parents: tuple[ReplayParent, ...] = ()) -> None:
378
+ """Publish metadata only for the prepared source before submission."""
379
+ if self.closed or self.busy or self.completed:
380
+ return
381
+ self.intent = replace(intent, mainline_parent_number=None, mainline_parent_oid=None)
382
+ self.source_oid = intent.source_oid
383
+ if self.summary.target_oid != self.source_oid:
384
+ previous = self.summary
385
+ self.summary = CommitSummary(self.source_oid, git_extensions_look=True, clock=self.clock)
386
+ # replaceWidget would hand the layout item of the old commit box back to the caller: dropped, it stays
387
+ # allocated for as long as the application runs, one more at each change of commit, and deleting it from
388
+ # Python crashes once the form is deleted, since PySide still counts it as a child of the body layout.
389
+ # removeWidget lets Qt delete that item itself; the row holds no other widget, so the new box takes its place.
390
+ self.summary_row.removeWidget(previous)
391
+ self.summary_row.addWidget(self.summary)
392
+ previous.hide()
393
+ previous.deleteLater()
394
+ self.parents = parents
395
+ self.summary.show_commit(fields)
396
+ self.parent_table.setRowCount(0)
397
+ merge = len(parents) >= MERGE_PARENT_MINIMUM
398
+ if merge:
399
+ self.parent_table.setRowCount(len(parents))
400
+ for row, parent in enumerate(parents):
401
+ try:
402
+ date_text = datetime.fromisoformat(parent.commit_date).date().isoformat()
403
+ except ValueError:
404
+ date_text = parent.commit_date or "n/a"
405
+ for column, value in enumerate((str(parent.number), parent.subject or "n/a", parent.author or "n/a", date_text)):
406
+ item = QTableWidgetItem(value)
407
+ item.setToolTip(parent.commit_date if column == PARENT_DATE_COLUMN and parent.commit_date else value)
408
+ item.setData(Qt.ItemDataRole.UserRole, parent.oid)
409
+ self.parent_table.setItem(row, column, item)
410
+ self.parent_table.clearSelection()
411
+ self.parent_section.setVisible(merge)
412
+ self.parent_table.setVisible(merge)
413
+ self.parent_label.setVisible(merge)
414
+ self.metadata_ready = True
415
+ self.submit_button.setEnabled(not merge)
416
+ if self.revision_button is not None:
417
+ self.revision_button.setEnabled(True)
418
+ self._show_status("")
419
+ # Enter then runs Cherry pick or Revert this commit as in Git Extensions; a merge waits for a parent choice.
420
+ (self.parent_table if merge else self.submit_button).setFocus()
421
+
422
+ def _parent_selected(self) -> None:
423
+ """Tie the explicit row selection to its captured native parent identity."""
424
+ if self.intent is None or self.busy or self.completed:
425
+ return
426
+ rows = self.parent_table.selectionModel().selectedRows()
427
+ parent = self.parents[next(iter(rows)).row()] if rows else None
428
+ self.intent = replace(self.intent, mainline_parent_number=parent.number if parent else None, mainline_parent_oid=parent.oid if parent else None)
429
+ self.submit_button.setEnabled(self.metadata_ready and (len(self.parents) < MERGE_PARENT_MINIMUM or parent is not None))
430
+
431
+ def show_error(self, cause: str) -> None:
432
+ """Explain preparation failures without inventing a writable source."""
433
+ self._show_status(cause)
434
+ self.submit_button.setEnabled(False)
435
+
436
+ def _submit(self) -> None:
437
+ """Freeze choices before emitting one immutable write request."""
438
+ if self.intent is None or not self.metadata_ready or self.busy or self.completed or self.closed:
439
+ return
440
+ if len(self.parents) >= MERGE_PARENT_MINIMUM and self.intent.mainline_parent_oid is None:
441
+ return
442
+ self.intent = replace(self.intent, create_commit=self.create_commit_check.isChecked(),
443
+ add_reference=self.add_reference_check is not None and self.add_reference_check.isChecked())
444
+ self.busy = True
445
+ for widget in (self.create_commit_check, self.add_reference_check, self.revision_button, self.parent_table, self.submit_button, self.cancel_button):
446
+ if widget is not None:
447
+ widget.setEnabled(False)
448
+ self.replay_requested.emit(self.intent)
449
+
450
+ def close_after_submission(self) -> None:
451
+ """Close the form once its operation is accepted; the console and the repository window take over from here.
452
+
453
+ A form left open after launch would stay window-modal over the repository window and block every later
454
+ choice (conflict prompt, banner, progress), so the form never waits for Git's result.
455
+ """
456
+ self.busy = False
457
+ self.completed = True
458
+ self.accept()
459
+
460
+ def show_submission_refused(self, cause: str) -> None:
461
+ """Give the choices back when the request was refused before any console or Git command started."""
462
+ self.busy = False
463
+ for widget in (self.create_commit_check, self.add_reference_check, self.revision_button, self.parent_table, self.cancel_button):
464
+ if widget is not None:
465
+ widget.setEnabled(True)
466
+ self.submit_button.setEnabled(self.metadata_ready and (len(self.parents) < MERGE_PARENT_MINIMUM or self.intent is not None and self.intent.mainline_parent_oid is not None))
467
+ self._show_status(cause)
468
+
469
+ def reject(self) -> None:
470
+ """Treat closing the idle form as cancellation without abandoning native work."""
471
+ if not self.busy:
472
+ super().reject()
473
+
474
+ def accept(self) -> None:
475
+ """Ignore direct acceptance or keyboard submission while native work is active."""
476
+ if not self.busy:
477
+ super().accept()
478
+
479
+ def keyPressEvent(self, event: QKeyEvent) -> None:
480
+ """Press the focused button on Enter, as Windows does; anywhere else Enter runs the default submit button."""
481
+ focused = self.focusWidget()
482
+ other_buttons = (self.revision_button, self.cancel_button)
483
+ if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter) and focused is not None and focused in other_buttons:
484
+ if focused.isEnabled():
485
+ focused.click()
486
+ return
487
+ super().keyPressEvent(event)
488
+
489
+ def closeEvent(self, event: QCloseEvent) -> None:
490
+ """Keep the captured request visible while its native process is owned."""
491
+ if self.busy:
492
+ event.ignore()
493
+ return
494
+ super().closeEvent(event)
495
+
496
+ def _closed(self) -> None:
497
+ """Invalidate every delayed metadata callback for this form."""
498
+ self.closed = True
499
+ self.source_generation += 1
500
+ if self.revision_dialog is not None:
501
+ self.revision_dialog.reject()
File without changes