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,122 @@
1
+ """One rule to open a window that blocks the window it belongs to, or the whole application, chosen by Qt platform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from PySide6.QtCore import QCoreApplication, QEvent, Qt
8
+ from PySide6.QtGui import QGuiApplication, QKeySequence, QShortcut
9
+ from PySide6.QtWidgets import QDialog, QMessageBox
10
+
11
+ from gitpane.config import MACOS_PLATFORM_NAME
12
+ from gitpane.ui.look.theme_look import follow_theme
13
+
14
+ # What result() holds from the moment the dialog is shown until it is answered: QDialog.open() resets the previous answer to Rejected (0),
15
+ # and the macOS row does the same. A Cancel leaves the same value, so it never tells an unanswered dialog from a refused one.
16
+ UNANSWERED_DIALOG_RESULT = QDialog.DialogCode.Rejected.value
17
+ # Object name of the Quit shortcut a modal window carries, so that opening the same window again adds no second one.
18
+ QUIT_SHORTCUT_OBJECT_NAME = "gitpane_quit_shortcut"
19
+ # Cmd+Q: Qt names the Command key Ctrl on macOS. Written out because the standard Quit key comes from the platform theme,
20
+ # and the offscreen platform the headless tests run on has none.
21
+ MACOS_QUIT_KEY_SEQUENCE = "Ctrl+Q"
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class ModalPresentation:
26
+ """How a dialog that blocks the window it belongs to is shown on one Qt platform."""
27
+
28
+ uses_dialog_open: bool
29
+ modality: Qt.WindowModality
30
+ greys_extra_title_buttons: bool
31
+ # True where the system's Quit shortcut never reaches the application while a modal window is open, so the modal
32
+ # window carries it itself.
33
+ carries_quit_shortcut: bool = False
34
+
35
+
36
+ DEFAULT_MODAL_PRESENTATION = ModalPresentation(uses_dialog_open=True, modality=Qt.WindowModality.WindowModal, greys_extra_title_buttons=False)
37
+ # Workaround: Qt's Cocoa backend shows every window-modal window that has a parent as a sheet, with no title bar and no
38
+ # close button. Application modality is the only one that keeps a real titled window there.
39
+ MODAL_PRESENTATION_BY_PLATFORM_NAME: dict[str, ModalPresentation] = {
40
+ MACOS_PLATFORM_NAME: ModalPresentation(uses_dialog_open=False, modality=Qt.WindowModality.ApplicationModal, greys_extra_title_buttons=True,
41
+ carries_quit_shortcut=True),
42
+ }
43
+
44
+
45
+ def modal_presentation(platform_name: str) -> ModalPresentation:
46
+ """Return how a modal dialog opens on a Qt platform, named as QGuiApplication.platformName() names it."""
47
+ return MODAL_PRESENTATION_BY_PLATFORM_NAME.get(platform_name, DEFAULT_MODAL_PRESENTATION)
48
+
49
+
50
+ def open_window_modal(dialog: QDialog, *, keeps_window_buttons: bool = False, blocks_application: bool = False, platform_name: str = "") -> None:
51
+ """Show a dialog that blocks its window, titled with a working close button; where no sheet exists, exactly QDialog.open().
52
+
53
+ keeps_window_buttons leaves Qt's own title bar buttons, for the operation consoles. blocks_application makes the
54
+ dialog block every other window of the application on every system, for a dialog shared by every window and so
55
+ with no parent, such as Settings: QDialog.open() would make it window-modal, which blocks nothing without a parent.
56
+ platform_name stands in for the session's platform, so a headless test can check the row of any system. Where the
57
+ system keeps its Quit shortcut from a running modal window, the window gets its own.
58
+ """
59
+ presentation = modal_presentation(platform_name or QGuiApplication.platformName())
60
+ modality = Qt.WindowModality.ApplicationModal if blocks_application else presentation.modality
61
+ if presentation.uses_dialog_open and not blocks_application:
62
+ dialog.open()
63
+ return
64
+ # A dialog already shown the same way is left as it is: setting its flags or, for a question on a macOS build of
65
+ # Qt, its modality again would hide it and show it again.
66
+ if dialog.isVisible() and dialog.windowModality() == modality:
67
+ return
68
+ if presentation.carries_quit_shortcut:
69
+ add_quit_shortcut(dialog)
70
+ if presentation.greys_extra_title_buttons and not keeps_window_buttons and not dialog.windowFlags() & Qt.WindowType.CustomizeWindowHint:
71
+ # With this hint Qt enables each title bar button only when its own hint is set: close stays, minimize and zoom
72
+ # turn grey, and a window that asks for minimize and maximize keeps them. Set only when missing, because
73
+ # setWindowFlag hides a visible window to apply its flags.
74
+ dialog.setWindowFlag(Qt.WindowType.CustomizeWindowHint, True)
75
+ dialog.setWindowModality(modality)
76
+ # QDialog.open() clears the previous answer the same way before showing the dialog.
77
+ dialog.setResult(UNANSWERED_DIALOG_RESULT)
78
+ dialog.show()
79
+
80
+
81
+ def add_quit_shortcut(dialog: QDialog) -> None:
82
+ """Give a modal window the system's Quit shortcut, which asks the application to quit as Cmd+Q does without a modal window.
83
+
84
+ Workaround: on macOS, AppKit keeps Cmd+Q and the Dock's Quit from the application while an application-modal window
85
+ runs, so GitPane never hears that the user wants to quit. A shortcut of the window itself still receives the key.
86
+ It sends the Quit event the system would have sent, and whoever handles quitting decides what to do with the open
87
+ windows; the Dock's Quit stays without effect while a modal window is open. Added once per window.
88
+ """
89
+ if dialog.findChild(QShortcut, QUIT_SHORTCUT_OBJECT_NAME, Qt.FindChildOption.FindDirectChildrenOnly) is not None:
90
+ return
91
+ shortcut = QShortcut(QKeySequence(MACOS_QUIT_KEY_SEQUENCE), dialog)
92
+ shortcut.setObjectName(QUIT_SHORTCUT_OBJECT_NAME)
93
+ shortcut.activated.connect(send_application_quit)
94
+
95
+
96
+ def send_application_quit() -> None:
97
+ """Send the application the Quit event the system sends when the user quits it, answered before the key press ends, as Escape answers Cancel."""
98
+ application = QCoreApplication.instance()
99
+ if application is None:
100
+ return
101
+ QCoreApplication.sendEvent(application, QEvent(QEvent.Type.Quit))
102
+
103
+
104
+ def prepare_message_box(box: QMessageBox, title: str, *, platform_name: str = "") -> None:
105
+ """Keep a question a Qt window that shows its title, on every system, and give it the icon of each new appearance.
106
+
107
+ Workaround: a macOS build of Qt ignores a message box's title, even offscreen, and shows it as a native alert with no
108
+ title bar. Windows and Linux never use a native message box and set the title the same way, so nothing changes there.
109
+
110
+ The style redraws a question icon that cannot be read on a dark window, and a message box keeps the icon it got when
111
+ it was set. A question open while the appearance changes sets its own icon again, so the style draws it for the
112
+ new appearance; the palette follower has already given the box its new palette by then.
113
+
114
+ Where the system keeps its Quit shortcut from a running modal window, the question gets its own here, so it has it
115
+ whether open_window_modal shows it or its own exec() runs it. platform_name stands in for the session's platform,
116
+ as in open_window_modal.
117
+ """
118
+ box.setOption(QMessageBox.Option.DontUseNativeDialog, True)
119
+ QDialog.setWindowTitle(box, title)
120
+ follow_theme(box, lambda: box.setIcon(box.icon()))
121
+ if modal_presentation(platform_name or QGuiApplication.platformName()).carries_quit_shortcut:
122
+ add_quit_shortcut(box)
@@ -0,0 +1,42 @@
1
+ """Let Enter activate the current row of an item view on every system.
2
+
3
+ Qt for macOS does not emit ``activated`` when Enter is pressed in an item view: it tries to edit the current item,
4
+ then leaves the key to the window, and only Cmd+O activates. GitPane wants Enter to open, choose or navigate as in
5
+ Git Extensions on every system, so the views where Enter acts pass their key presses through ``activate_current_row``,
6
+ which does what Qt does on Windows and Linux. The behaviour is the same everywhere, so nothing here depends on the
7
+ system or the Qt platform.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING
13
+
14
+ from PySide6.QtCore import QEvent, Qt
15
+ from PySide6.QtWidgets import QAbstractItemView
16
+
17
+ if TYPE_CHECKING:
18
+ from PySide6.QtGui import QKeyEvent
19
+
20
+ ACTIVATION_KEY_SET = frozenset({Qt.Key.Key_Return, Qt.Key.Key_Enter})
21
+ """Keys that activate the current row: the main Enter key and the one of the numeric keypad."""
22
+
23
+
24
+ def activate_current_row(view: QAbstractItemView, event: QEvent) -> bool:
25
+ """Activate the view's current row once on Enter, and let the key go on to the window; return whether Enter was handled.
26
+
27
+ Called from an event filter on the view. On Enter, while no editor is open and a row is current, the view's
28
+ ``activated`` signal is emitted for that row and the event is marked ignored, so the key still reaches the window,
29
+ where a dialog's default button reads the row just chosen. The caller returns ``True`` so the view does not act on
30
+ the key itself: on macOS it would try to edit the row, elsewhere it would emit ``activated`` a second time.
31
+ """
32
+ if event.type() != QEvent.Type.KeyPress:
33
+ return False
34
+ key_event: QKeyEvent = event
35
+ if key_event.key() not in ACTIVATION_KEY_SET or view.state() == QAbstractItemView.State.EditingState:
36
+ return False
37
+ current_index = view.currentIndex()
38
+ if not current_index.isValid():
39
+ return False
40
+ view.activated.emit(current_index)
41
+ key_event.ignore()
42
+ return True
@@ -0,0 +1,124 @@
1
+ """Bring the modal window back in front when the window system activates a window it blocks.
2
+
3
+ Under Wayland, a compositor keeps a modal window in front of a window only when the modal window is that window's
4
+ transient. A window modal for the whole application, such as Settings, has no parent: a click on a window it blocks
5
+ makes the compositor raise and activate that window above it, while Qt still refuses that window's input. Qt's X11
6
+ plugin already relays the focus to the modal window itself, and Windows and macOS never activate a blocked window, so
7
+ the way to bring the modal window back is chosen per Qt platform, once, by the composition root.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Callable
13
+ from typing import TYPE_CHECKING
14
+
15
+ from PySide6.QtCore import QEvent, QObject, Qt
16
+ from PySide6.QtGui import QGuiApplication, QWindow
17
+ from shiboken6 import getCppPointer
18
+
19
+ from gitpane.config import WAYLAND_PLATFORM_NAME
20
+ from gitpane.ui.window_control.wayland_activation import activate_window_with_keyboard_token
21
+
22
+ if TYPE_CHECKING:
23
+ from collections.abc import Iterator
24
+
25
+ ModalWindowActivator = Callable[[QWindow], bool]
26
+ """Bring a GitPane window to the front through the window system, and tell whether it was asked."""
27
+
28
+ # Where a platform is missing (X11, Windows, macOS, the offscreen platform of the tests), the window system or Qt
29
+ # already keeps the modal window in front of the windows it blocks.
30
+ MODAL_WINDOW_ACTIVATOR_BY_PLATFORM_NAME: dict[str, ModalWindowActivator] = {
31
+ WAYLAND_PLATFORM_NAME: activate_window_with_keyboard_token,
32
+ }
33
+ # The only events the filter acts on; it runs on every event of the application, so it lets any other one through at once.
34
+ WINDOW_BLOCKING_EVENT_TYPE_SET = frozenset({QEvent.Type.WindowBlocked, QEvent.Type.WindowUnblocked})
35
+ # Place of the C++ address in what shiboken6.getCppPointer answers: index 0 is the address of the object seen as its
36
+ # first C++ base (QObject for a QWindow, which also derives from QSurface), the same at every reading of a QWindow.
37
+ CPP_ADDRESS_INDEX = 0
38
+
39
+
40
+ def modal_window_activator(platform_name: str) -> ModalWindowActivator | None:
41
+ """Return how a Qt platform, under the name Qt gives it, brings the modal window back, or None where nothing is needed.
42
+
43
+ Pure, so the table can be checked for every platform from any machine.
44
+ """
45
+ return MODAL_WINDOW_ACTIVATOR_BY_PLATFORM_NAME.get(platform_name)
46
+
47
+
48
+ def _window_address(window: QWindow) -> int:
49
+ """Return the C++ address of a window, which names it in the blocked set and still names it while it is destroyed."""
50
+ return getCppPointer(window)[CPP_ADDRESS_INDEX]
51
+
52
+
53
+ def _transient_chain(window: QWindow) -> Iterator[QWindow]:
54
+ """Yield a window, then each of its transient parents up to the window that has none."""
55
+ current: QWindow | None = window
56
+ while current is not None:
57
+ yield current
58
+ current = current.transientParent()
59
+
60
+
61
+ # Workaround: under Wayland, Qt sends a window modal for the whole application without any parent, so the compositor
62
+ # does not keep it in front of the windows it blocks, and Qt's Wayland plugin, unlike its X11 plugin, does not hand the
63
+ # focus back to the modal window when the compositor activates a blocked one. This relay follows the windows Qt
64
+ # declares blocked; when the focus reaches one of them, it asks the window system, once, to activate the modal window
65
+ # Qt hands the input to. A modal window that blocks only its own window is left to the compositor: it is that window's
66
+ # transient, which the compositor already keeps in front.
67
+ class ModalFocusRelay(QObject):
68
+ """Bring the application-modal window back in front when the focus reaches a window it blocks."""
69
+
70
+ def __init__(self, application: QGuiApplication, activate: ModalWindowActivator, parent: QObject | None = None) -> None:
71
+ """Follow the blocked windows of ``application`` and its focus window; ``activate`` brings a window back through the window system.
72
+
73
+ The relay belongs to ``parent`` rather than to the application: it runs on every event of the application, so it
74
+ must end with the wiring that created it.
75
+ """
76
+ super().__init__(parent)
77
+ self.application = application
78
+ self.activate = activate
79
+ self.blocked_window_address_set: set[int] = set()
80
+ # The windows whose end the relay already follows, so a window blocked again and again gets one connection.
81
+ self.followed_window_address_set: set[int] = set()
82
+ application.installEventFilter(self)
83
+ application.focusWindowChanged.connect(self.follow_focus_window)
84
+
85
+ def eventFilter(self, watched: QObject, event: QEvent) -> bool:
86
+ """Note each window Qt blocks or unblocks under a modal window, letting every event through."""
87
+ if event.type() not in WINDOW_BLOCKING_EVENT_TYPE_SET or not watched.isWindowType():
88
+ return False
89
+ address = _window_address(watched)
90
+ if event.type() == QEvent.Type.WindowUnblocked:
91
+ self.blocked_window_address_set.discard(address)
92
+ return False
93
+ self.blocked_window_address_set.add(address)
94
+ if address not in self.followed_window_address_set:
95
+ self.followed_window_address_set.add(address)
96
+ # The sets themselves are captured, not the relay: the window may outlive the relay, and its end must not reach a deleted relay.
97
+ watched.destroyed.connect(lambda _object=None, gone=address, blocked=self.blocked_window_address_set, followed=self.followed_window_address_set:
98
+ (blocked.discard(gone), followed.discard(gone)))
99
+ return False
100
+
101
+ def follow_focus_window(self, window: QWindow | None) -> None:
102
+ """When the focus reaches a window an application-modal window blocks, ask once to bring the modal window back.
103
+
104
+ The modal window brought back is the one Qt hands the input to, the modal window shown last; a window in its
105
+ chain of transient parents, or the modal window itself, is already kept behind it by the compositor.
106
+ """
107
+ if window is None or _window_address(window) not in self.blocked_window_address_set:
108
+ return
109
+ modal = QGuiApplication.modalWindow()
110
+ if modal is None or not blocks_whole_application(modal):
111
+ return
112
+ window_address = _window_address(window)
113
+ for chained in _transient_chain(modal):
114
+ if _window_address(chained) == window_address:
115
+ return
116
+ self.activate(modal)
117
+
118
+
119
+ def blocks_whole_application(modal: QWindow) -> bool:
120
+ """Tell whether a modal window, or one of its transient parents, is modal for the whole application."""
121
+ for chained in _transient_chain(modal):
122
+ if chained.modality() == Qt.WindowModality.ApplicationModal:
123
+ return True
124
+ return False