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,394 @@
1
+ """Reproduce the Git Extensions "Delete tag" window: a tag list, Delete, an optional deletion on one remote."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from typing import TYPE_CHECKING
7
+
8
+ from PySide6.QtCore import QMargins, QRect, QSignalBlocker, QSize, Qt, Signal
9
+ from PySide6.QtWidgets import QCheckBox, QComboBox, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget
10
+
11
+ from gitpane.config import DELETE_BUTTON_TEXT, SharedText
12
+ from gitpane.history.repository_value import ReferenceKind
13
+ from gitpane.operation.operation_status import OperationStatus
14
+ from gitpane.reference.reference_edit import DEFAULT_PUSH_REMOTE_NAME, DELETE_LOCAL_TAG_OPERATION, DELETE_REMOTE_TAG_OPERATION, ReferenceDeletionIntent
15
+ from gitpane.reference.reference_snapshot import short_reference_name
16
+ from gitpane.text.typed_text import typed_value
17
+ from gitpane.ui.look.theme_look import follow_theme
18
+ from gitpane.ui.look.windows_control_look import apply_windows_button_look, apply_windows_combo_look
19
+ from gitpane.ui.reference.reference_feedback import ReferenceSummaryLabel
20
+ from gitpane.ui.reference.reference_form import ReferenceFormDialog
21
+ from gitpane.ui.widget.content_growth import ContentGrowthMixin, OpeningFit
22
+ from gitpane.ui.widget.content_measure import LayoutContentMeasure
23
+ from gitpane.ui.widget.dialog_layout import fit_caption_column, fit_capture_control, install_plain_form_palette
24
+ from gitpane.ui.widget.scrolled_text import ScrolledText
25
+
26
+ if TYPE_CHECKING:
27
+ from gitpane.operation.repository_operation import OperationOutcome
28
+ from gitpane.reference.reference_snapshot import ReferenceSnapshot
29
+ from gitpane.transport.remote_config import RemoteSnapshot
30
+
31
+ DELETE_TAG_TITLE = "Delete tag"
32
+ SELECT_TAG_CAPTION = "Select tag"
33
+ LOCAL_DELETION_TEXT = "This will delete the selected tag from the (local) repository."
34
+ REMOTE_DELETION_CAPTION = "Delete tag also from the following remote(s):"
35
+ REMOTE_LIST_NAME = "Remote"
36
+ RETRY_BUTTON_TEXT = "Retry failed step"
37
+ NO_TAG_CAUSE = "No tags are available"
38
+ LOCAL_DESTINATION = "Local"
39
+ # Refusals of a remote deletion, written before anything is deleted, locally or on a server.
40
+ SEVERAL_PUSH_ADDRESSES_CAUSE = "Remote '{0}' has several push addresses. GitPane deletes a tag on one server only; the tag was not deleted."
41
+ NO_PUSH_ADDRESS_CAUSE = "Remote '{0}' has no push address; the tag was not deleted."
42
+ UNUSABLE_REMOTE_CAUSE = "Remote '{0}' is not configured unambiguously; the tag was not deleted."
43
+ NO_REMOTE_CAUSE = "Select the remote to delete the tag from"
44
+ REMOTE_READ_FAILED_CAUSE = "The remote configuration could not be read: {0}"
45
+
46
+
47
+ class DeleteTagDialogText(Enum):
48
+ """Texts only the Delete tag window shows, one member per text so a misspelt name is an error."""
49
+
50
+ DELETED = "deleted"
51
+
52
+
53
+ # Wording of the step statuses that have one in the result summary. Any other status is written as its own word, the
54
+ # way Git and the transport name it: that choice is explicit, and a test lists the statuses it covers.
55
+ STEP_SUMMARY_BY_STATUS: dict[OperationStatus, DeleteTagDialogText | SharedText] = {
56
+ OperationStatus.SUCCESS: DeleteTagDialogText.DELETED,
57
+ OperationStatus.UNKNOWN: SharedText.OUTCOME_UNKNOWN,
58
+ }
59
+
60
+
61
+ def step_summary_word(status: OperationStatus) -> str:
62
+ """Give the word of a step status in the result summary: its wording in the table, or else the status's own word."""
63
+ text = STEP_SUMMARY_BY_STATUS.get(status)
64
+ return status.value if text is None else text.value
65
+
66
+
67
+ RETRYABLE_OUTCOME_STATUS_SET = frozenset((OperationStatus.FAILED, OperationStatus.REFUSED, OperationStatus.PARTIAL))
68
+ RETRYABLE_STEP_STATUS_SET = frozenset((OperationStatus.FAILED, OperationStatus.REFUSED, OperationStatus.NOT_STARTED))
69
+ # Client area of the Git Extensions capture, 454 x 171 inside the window borders and title bar.
70
+ DELETE_TAG_CAPTURE_CLIENT_SIZE = QSize(454, 171)
71
+ # Height of the capture's bottom row, a Help link and its explanation; GitPane has no help, so the window loses that row.
72
+ HELP_ROW_HEIGHT = 25
73
+ DELETE_TAG_CLIENT_SIZE = QSize(DELETE_TAG_CAPTURE_CLIENT_SIZE.width(), DELETE_TAG_CAPTURE_CLIENT_SIZE.height() - HELP_ROW_HEIGHT)
74
+ # Measured on the capture in client pixels at 100 %, visible borders included: "Select tag" ink from x 10, y 18; tag list
75
+ # (126, 12, 237, 23); Delete button (370, 11, 73, 23), right of the list rather than in a button band; local text ink
76
+ # from x 10, y 55; check box square (10, 83, 13, 13) with its text from x 29; remote list (10, 106, 270, 23).
77
+ DELETE_TAG_CAPTION_LEFT = 10
78
+ TAG_COMBO_RECT = QRect(126, 12, 237, 23)
79
+ DELETE_BUTTON_RECT = QRect(370, 11, 73, 23)
80
+ REMOTE_COMBO_RECT = QRect(10, 106, 270, 23)
81
+ # Width of the caption, from its left edge to the tag list; a wider caption text widens it.
82
+ SELECT_TAG_CAPTION_WIDTH = TAG_COMBO_RECT.left() - DELETE_TAG_CAPTION_LEFT
83
+ # The window's own margins: the captions, the Delete button's top and right edges and the remote list's bottom set them.
84
+ DELETE_TAG_MARGINS = QMargins(DELETE_TAG_CAPTION_LEFT, DELETE_BUTTON_RECT.top(), DELETE_TAG_CLIENT_SIZE.width() - DELETE_BUTTON_RECT.right() - 1,
85
+ DELETE_TAG_CLIENT_SIZE.height() - REMOTE_COMBO_RECT.bottom() - 1)
86
+ # The tag list sits 1 px lower than the Delete button in the capture.
87
+ TAG_COMBO_DROP = TAG_COMBO_RECT.top() - DELETE_BUTTON_RECT.top()
88
+ TAG_COMBO_TO_BUTTON_SPACE = DELETE_BUTTON_RECT.left() - TAG_COMBO_RECT.right() - 1
89
+ # Vertical spaces between the rows, fitted so that Qt's text, square and list land where the capture has them in the
90
+ # Fusion style: the local text's ink on y 55, the check box square on y 83 and the remote list on y 106.
91
+ FIRST_ROW_TO_LOCAL_TEXT_SPACE = 16
92
+ LOCAL_TEXT_TO_CHECK_BOX_SPACE = 13
93
+ CHECK_BOX_TO_REMOTE_COMBO_SPACE = 5
94
+ # Space between the remote list and the result summary shown after a failed step.
95
+ REMOTE_COMBO_TO_SUMMARY_SPACE = 8
96
+
97
+
98
+ class DeleteTagDialog(ContentGrowthMixin, ReferenceFormDialog):
99
+ """Delete one tag locally, and optionally on one remote, like the Git Extensions Delete tag window.
100
+
101
+ The window only freezes a request. The reference form controller reads the remote configuration the first time the check box is
102
+ checked, turns the chosen remote into its single push address, runs Git and answers with ``show_outcome`` or
103
+ ``show_error``. The reference form controller, which runs the deletion, reads these attributes: ``intent`` (the tag deletion), ``remote_request``
104
+ (remote configuration, remote name, tag, push address and push target, or None for a local deletion),
105
+ ``remote_snapshot`` (the remote configuration read for this window), ``outcome`` (the last result),
106
+ ``retry_operation_id`` and ``retry_remote_success`` (set while retrying), ``remote_branch`` (always False: this
107
+ window deletes a tag) and ``local_intents`` (always empty: a tag has no local tracking branch to delete with it).
108
+ """
109
+
110
+ delete_requested = Signal(object)
111
+ remote_requested = Signal()
112
+ retry_requested = Signal()
113
+
114
+ def __init__(self, session_id: str, parent: QWidget | None = None) -> None:
115
+ """Build the rows of the capture without its help row, with the remote list disabled until the check box is checked."""
116
+ super().__init__(session_id, parent)
117
+ self.intent: ReferenceDeletionIntent | None = None
118
+ self.setWindowTitle(DELETE_TAG_TITLE)
119
+ self.full_ref = ""
120
+ self.remote_snapshot: RemoteSnapshot | None = None
121
+ self.remote_request: tuple | None = None
122
+ # Set once the remote configuration was asked for, so unchecking and checking again does not read it twice.
123
+ self.remote_read_requested = False
124
+ self.outcome: OperationOutcome | None = None
125
+ self.retry_operation_id = ""
126
+ self.retry_remote_success = False
127
+ self.remote_branch = False
128
+ self.local_intents: tuple[ReferenceDeletionIntent, ...] = ()
129
+ install_plain_form_palette(self)
130
+ layout = QVBoxLayout(self)
131
+ layout.setContentsMargins(DELETE_TAG_MARGINS)
132
+ layout.setSpacing(0)
133
+ self.caption_label = QLabel(SELECT_TAG_CAPTION)
134
+ self.tag_combo = QComboBox()
135
+ # The capture shows the tag name selected in a list one can type into; typed names are matched against the list.
136
+ self.tag_combo.setEditable(True)
137
+ self.tag_combo.setInsertPolicy(QComboBox.InsertPolicy.NoInsert)
138
+ self.tag_combo.setAccessibleName(SELECT_TAG_CAPTION)
139
+ self.caption_label.setBuddy(self.tag_combo)
140
+ tag_column = QVBoxLayout()
141
+ tag_column.setContentsMargins(0, TAG_COMBO_DROP, 0, 0)
142
+ tag_column.setSpacing(0)
143
+ tag_column.addWidget(self.tag_combo)
144
+ self.submit_button.setText(DELETE_BUTTON_TEXT)
145
+ self.submit_button.setEnabled(False)
146
+ first_row = QHBoxLayout()
147
+ first_row.setContentsMargins(0, 0, 0, 0)
148
+ first_row.setSpacing(0)
149
+ first_row.addWidget(self.caption_label, 0, Qt.AlignmentFlag.AlignVCenter)
150
+ first_row.addLayout(tag_column, 1)
151
+ first_row.addSpacing(TAG_COMBO_TO_BUTTON_SPACE)
152
+ first_row.addWidget(self.submit_button, 0, Qt.AlignmentFlag.AlignTop)
153
+ layout.addLayout(first_row)
154
+ layout.addSpacing(FIRST_ROW_TO_LOCAL_TEXT_SPACE)
155
+ self.local_label = QLabel(LOCAL_DELETION_TEXT)
156
+ layout.addWidget(self.local_label)
157
+ layout.addSpacing(LOCAL_TEXT_TO_CHECK_BOX_SPACE)
158
+ self.remote_checkbox = QCheckBox(REMOTE_DELETION_CAPTION)
159
+ layout.addWidget(self.remote_checkbox)
160
+ layout.addSpacing(CHECK_BOX_TO_REMOTE_COMBO_SPACE)
161
+ self.remote_combo = QComboBox()
162
+ self.remote_combo.setAccessibleName(REMOTE_LIST_NAME)
163
+ self.remote_combo.setFixedWidth(REMOTE_COMBO_RECT.width())
164
+ self.remote_combo.setEnabled(False)
165
+ layout.addWidget(self.remote_combo, 0, Qt.AlignmentFlag.AlignLeft)
166
+ # Shown only after a result that did not delete everything: each destination's result, then Retry failed step.
167
+ self.summary_space = QWidget()
168
+ self.summary_space.setFixedHeight(REMOTE_COMBO_TO_SUMMARY_SPACE)
169
+ self.summary_space.hide()
170
+ layout.addWidget(self.summary_space)
171
+ self.status_label = ReferenceSummaryLabel()
172
+ # A server's refusal can be longer than the screen: the summary scrolls past the window's growth cap, so Retry
173
+ # failed step under it stays inside the window.
174
+ self.status_area = ScrolledText(self.status_label)
175
+ self.status_area.hide()
176
+ layout.addWidget(self.status_area)
177
+ self.retry_button = QPushButton(RETRY_BUTTON_TEXT)
178
+ self.retry_button.setAutoDefault(False)
179
+ self.retry_button.hide()
180
+ layout.addWidget(self.retry_button, 0, Qt.AlignmentFlag.AlignLeft)
181
+ layout.addStretch(1)
182
+ self.apply_theme_look()
183
+ follow_theme(self, self.apply_theme_look)
184
+ # Enter runs Delete from anywhere in the form. The role is set only once the button belongs to the dialog, so Qt
185
+ # records it as the dialog's main default. There is no Cancel button: Escape and the title bar close the window.
186
+ self.submit_button.setDefault(True)
187
+ self.submit_button.clicked.connect(self._submit)
188
+ self.retry_button.clicked.connect(self.retry_requested.emit)
189
+ self.tag_combo.currentIndexChanged.connect(self._update_intent)
190
+ self.tag_combo.editTextChanged.connect(self._update_intent)
191
+ self.remote_checkbox.toggled.connect(self._remote_toggled)
192
+ self.remote_combo.currentIndexChanged.connect(self._update_intent)
193
+ self.resize(DELETE_TAG_CLIENT_SIZE)
194
+ self.refresh_metrics()
195
+ self.install_content_growth(LayoutContentMeasure(self), OpeningFit.GROW_ONLY, self.height())
196
+ self.tag_combo.setFocus()
197
+
198
+ def apply_theme_look(self) -> None:
199
+ """Lay the light or dark look of the tag list, Delete, the remote list and Retry failed step, decided on the application palette.
200
+
201
+ Called by the constructor and again at each switch. The remote list and Retry are built with the form, hidden or
202
+ disabled until needed, so they always carry the look of the current appearance. Git Extensions draws the remote
203
+ list like a field one can type into (a light grey border while disabled); GitPane keeps it a plain choice among
204
+ the configured remotes, so no typed name can reach the deletion. Only style sheets and palettes change: the
205
+ tag, the remote choice and any result shown stay as they are.
206
+ """
207
+ apply_windows_combo_look(self.tag_combo, editable=True)
208
+ apply_windows_button_look(self.submit_button)
209
+ apply_windows_combo_look(self.remote_combo, editable=True)
210
+ apply_windows_button_look(self.retry_button)
211
+
212
+ def fit_capture_controls(self) -> None:
213
+ """Give the caption and the Delete button the capture's sizes, or more when their text needs it; a wider caption moves the list right."""
214
+ fit_caption_column((self.caption_label,), SELECT_TAG_CAPTION_WIDTH)
215
+ fit_capture_control(self.submit_button, DELETE_BUTTON_RECT.size())
216
+
217
+ def populate(self, snapshot: ReferenceSnapshot, full_ref: str) -> None:
218
+ """Offer the tags and remotes of the references the window was opened on, the clicked tag selected.
219
+
220
+ No Git process is needed: the remote names come from the same capture. A capture arriving after the click or
221
+ after a result changes nothing, so it never replaces a deletion already asked or done.
222
+ """
223
+ if self.closed or self.busy or self.completed or self.outcome is not None:
224
+ return
225
+ self.snapshot = snapshot
226
+ self.full_ref = full_ref
227
+ with QSignalBlocker(self.tag_combo), QSignalBlocker(self.remote_combo):
228
+ self.tag_combo.clear()
229
+ for reference in sorted(snapshot.references, key=lambda reference: reference.full_name):
230
+ if reference.kind == ReferenceKind.TAGS and reference.target_oid and not reference.symbolic_target:
231
+ self.tag_combo.addItem(short_reference_name(reference.full_name), reference.full_name)
232
+ self.tag_combo.setCurrentIndex(self.tag_combo.findData(full_ref))
233
+ self.remote_combo.clear()
234
+ for remote_name in sorted(snapshot.remote_names):
235
+ self.remote_combo.addItem(remote_name, remote_name)
236
+ default_index = self.remote_combo.findData(DEFAULT_PUSH_REMOTE_NAME)
237
+ first_index = 0
238
+ self.remote_combo.setCurrentIndex(default_index if default_index >= 0 else first_index if self.remote_combo.count() else -1)
239
+ self.tag_combo.lineEdit().selectAll()
240
+ self.tag_combo.setFocus()
241
+ self._update_intent()
242
+ if not self.tag_combo.count():
243
+ self.submit_button.setToolTip(NO_TAG_CAUSE)
244
+
245
+ def selected_ref(self) -> str:
246
+ """Return the full name of the tag written in the list, or an empty text when it names no offered tag.
247
+
248
+ The typed name is taken without the blanks around it; the list keeps what was typed.
249
+ """
250
+ index = self.tag_combo.findText(typed_value(self.tag_combo.currentText()), Qt.MatchFlag.MatchExactly | Qt.MatchFlag.MatchCaseSensitive)
251
+ if index < 0:
252
+ return ""
253
+ return self.tag_combo.itemData(index) or ""
254
+
255
+ def _update_intent(self) -> None:
256
+ """Freeze the selected tag into a deletion request, and offer Delete once any remote choice can be checked."""
257
+ if self.busy or self.closed or self.completed:
258
+ return
259
+ self.intent = None
260
+ self.remote_request = None
261
+ self.full_ref = self.selected_ref()
262
+ if self.snapshot is not None and self.full_ref:
263
+ try:
264
+ self.intent = ReferenceDeletionIntent.capture(self.snapshot, self.full_ref, session_id=self.session_id)
265
+ except ValueError:
266
+ self.intent = None
267
+ remote_ready = not self.remote_checkbox.isChecked() or self.remote_snapshot is not None
268
+ self.submit_button.setToolTip("")
269
+ self.submit_button.setEnabled(self.intent is not None and remote_ready)
270
+
271
+ def _remote_toggled(self, checked: bool) -> None:
272
+ """Offer the remote list only while the check box is checked, and ask for the remote configuration the first time."""
273
+ self.remote_combo.setEnabled(checked and not self.busy and not self.completed)
274
+ if checked and not self.remote_read_requested:
275
+ self.remote_read_requested = True
276
+ self.remote_requested.emit()
277
+ self._update_intent()
278
+
279
+ def populate_remote(self, remote_snapshot: RemoteSnapshot) -> None:
280
+ """Keep the remote configuration read for this window, the one a remote deletion is checked against."""
281
+ if self.closed or self.busy or self.completed or self.outcome is not None:
282
+ return
283
+ self.remote_snapshot = remote_snapshot
284
+ self._update_intent()
285
+
286
+ def show_remote_error(self, cause: str) -> None:
287
+ """Explain why the remote configuration could not be read and uncheck the remote option, so checking it reads again."""
288
+ if self.closed or self.busy or self.completed:
289
+ return
290
+ self.remote_read_requested = False
291
+ with QSignalBlocker(self.remote_checkbox):
292
+ self.remote_checkbox.setChecked(False)
293
+ self.remote_combo.setEnabled(False)
294
+ self._update_intent()
295
+ self.show_error(REMOTE_READ_FAILED_CAUSE.format(cause))
296
+
297
+ def _submit(self) -> None:
298
+ """Freeze the form and hand its request to the reference form controller once."""
299
+ if self.closed or self.busy or self.completed or self.intent is None or not self.submit_button.isEnabled():
300
+ return
301
+ self.busy = True
302
+ self._set_controls_enabled(False)
303
+ self.delete_requested.emit(self.intent)
304
+
305
+ def show_error(self, cause: str) -> None:
306
+ """Refuse or explain a failure: in a message box before any result, under the results after one.
307
+
308
+ Before any result nothing was deleted, so the form is given back for another choice. After a result, a retry
309
+ that could not be prepared keeps the results already obtained and adds its cause below them.
310
+ """
311
+ self.busy = False
312
+ if self.outcome is None:
313
+ if not self.completed:
314
+ self._set_controls_enabled(True)
315
+ super().show_error(cause)
316
+ return
317
+ self._show_summary(self.outcome, [(cause, True)])
318
+
319
+ def show_outcome(self, outcome: OperationOutcome) -> None:
320
+ """Close once every requested destination was deleted; otherwise keep the form with each destination's result.
321
+
322
+ After a failed step the results and, when the failed step can be tried again, Retry failed step appear under the
323
+ remote list. The window follows them both ways by the common growth rule: it grows to show them, and gives back
324
+ the height of the lines that leave when a shorter text replaces them, never below its opening height.
325
+ """
326
+ self.busy = False
327
+ self.outcome = outcome
328
+ if outcome.status == OperationStatus.SUCCESS and self._requested_operation_set() <= self._successful_operation_set(outcome):
329
+ self.accept()
330
+ return
331
+ self.completed = True
332
+ self._set_controls_enabled(False)
333
+ self._show_summary(outcome, [])
334
+
335
+ def _requested_operation_set(self) -> set[str]:
336
+ """Name the deletion steps this request asked for: always the local one, the remote one when a remote was chosen."""
337
+ operation_set = {DELETE_LOCAL_TAG_OPERATION}
338
+ if self.remote_request is not None:
339
+ operation_set.add(DELETE_REMOTE_TAG_OPERATION)
340
+ return operation_set
341
+
342
+ def _successful_operation_set(self, outcome: OperationOutcome) -> set[str]:
343
+ """Name the deletion steps the result reports as done."""
344
+ operation_set: set[str] = set()
345
+ for step in outcome.steps:
346
+ if step.status == OperationStatus.SUCCESS:
347
+ operation_set.add(step.operation)
348
+ return operation_set
349
+
350
+ def _remote_name(self) -> str:
351
+ """Name the chosen remote as the summary writes it, or a generic word for a local deletion."""
352
+ if self.remote_request is None:
353
+ return REMOTE_LIST_NAME
354
+ _remote_snapshot, remote_name, _destination, _endpoint, _push_target = self.remote_request
355
+ return remote_name
356
+
357
+ def _show_summary(self, outcome: OperationOutcome, extra_lines: list[tuple[str, bool]]) -> None:
358
+ """Write one line per destination, its diagnostic and any extra cause, and offer Retry only for a failed step that can be retried."""
359
+ remote_name = self._remote_name()
360
+ destination_by_operation = {DELETE_LOCAL_TAG_OPERATION: LOCAL_DESTINATION, DELETE_REMOTE_TAG_OPERATION: remote_name}
361
+ summary_lines: list[tuple[str, bool]] = []
362
+ observed_operation_set: set[str] = set()
363
+ diagnostic_detail_set: set[str] = set()
364
+ for step in outcome.steps:
365
+ observed_operation_set.add(step.operation)
366
+ destination = destination_by_operation.get(step.operation, step.operation)
367
+ failed = step.status != OperationStatus.SUCCESS
368
+ summary_lines.append((destination + ": " + step_summary_word(step.status) + " — " + step.effect, failed))
369
+ if step.diagnostic is not None:
370
+ summary_lines.append((destination + ": " + step.diagnostic.detail, failed))
371
+ diagnostic_detail_set.add(step.diagnostic.detail)
372
+ if outcome.diagnostic is not None and outcome.diagnostic.detail not in diagnostic_detail_set:
373
+ summary_lines.append(("Operation: " + outcome.diagnostic.detail, outcome.status != OperationStatus.SUCCESS))
374
+ for operation in (DELETE_LOCAL_TAG_OPERATION, DELETE_REMOTE_TAG_OPERATION):
375
+ if operation in self._requested_operation_set() and operation not in observed_operation_set:
376
+ summary_lines.append((destination_by_operation[operation] + ": Awaiting result", False))
377
+ summary_lines.extend(extra_lines)
378
+ self.status_label.show_summary(summary_lines)
379
+ # A remote step whose outcome is unknown may have deleted the tag on the server: it is never retried blindly.
380
+ remote_unknown = any(step.operation == DELETE_REMOTE_TAG_OPERATION and step.status == OperationStatus.UNKNOWN for step in outcome.steps)
381
+ retryable = (outcome.status in RETRYABLE_OUTCOME_STATUS_SET and not remote_unknown
382
+ and any(step.status in RETRYABLE_STEP_STATUS_SET for step in outcome.steps))
383
+ self.summary_space.show()
384
+ self.status_area.show()
385
+ self.retry_button.setVisible(retryable)
386
+ self.retry_button.setEnabled(retryable)
387
+
388
+ def _set_controls_enabled(self, enabled: bool) -> None:
389
+ """Enable or disable the choices of the form together; the remote list only ever while the check box is checked."""
390
+ self.tag_combo.setEnabled(enabled)
391
+ self.remote_checkbox.setEnabled(enabled)
392
+ self.remote_combo.setEnabled(enabled and self.remote_checkbox.isChecked())
393
+ if not enabled:
394
+ self.submit_button.setEnabled(False)
@@ -0,0 +1,53 @@
1
+ """End the opening of a reference form one of three ways: shown once its check accepts it, refused with its warning alone, or closed unseen.
2
+
3
+ A reference form opens on the references its repository window last read, and GitPane checks them again before the
4
+ form can do anything. When that check refuses (the branches changed since, the read failed, the window was refreshed
5
+ meanwhile), a form shown at once would only sit greyed behind its warning, and the user would have to close both. The
6
+ form therefore stays hidden until its check answers: accepted, it is shown filled; refused, it is closed unseen and the
7
+ refusal opens alone, under the form's title, over the window the form belongs to. A form whose check ends otherwise
8
+ (its filling failed, it stopped being current) is closed unseen without a warning. A form closed unseen is deleted,
9
+ since nobody can reopen it. A refusal that comes later, once the form is in use, stays in the form.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import TYPE_CHECKING
15
+
16
+ from PySide6.QtCore import Qt
17
+ from PySide6.QtWidgets import QDialog
18
+
19
+ from gitpane.ui.reference.reference_form import reference_warning_box
20
+ from gitpane.ui.window_control.dialog_modality import open_window_modal
21
+
22
+ if TYPE_CHECKING:
23
+ from PySide6.QtWidgets import QWidget
24
+
25
+
26
+ def reveal_form(dialog: QDialog) -> None:
27
+ """Show a form its opening check accepted, blocking its repository window like any reference form; a form already shown is left as it is."""
28
+ if dialog.isVisible():
29
+ return
30
+ open_window_modal(dialog)
31
+
32
+
33
+ def close_unseen_form(dialog: QDialog) -> None:
34
+ """Close a form that was never shown, so its registry and its late answers let it go and the next click opens a new one, then delete it.
35
+
36
+ The form is deleted at the next turn of the event loop rather than left hidden in its window until that window
37
+ closes: nobody can show it again, and each new click makes a new form.
38
+ """
39
+ dialog.done(QDialog.DialogCode.Rejected)
40
+ dialog.deleteLater()
41
+
42
+
43
+ def refuse_form_opening(dialog: QDialog, cause: str) -> None:
44
+ """Close a form its opening check refused without ever showing it, and show the refusal alone over its repository window.
45
+
46
+ The warning keeps the form's title, so the user knows which action was refused, and the text the form would have
47
+ shown, with anything sensitive removed. Closing the warning leaves nothing else open.
48
+ """
49
+ window: QWidget | None = dialog.parentWidget()
50
+ box = reference_warning_box(dialog.windowTitle(), cause, window)
51
+ box.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
52
+ close_unseen_form(dialog)
53
+ open_window_modal(box)