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,332 @@
1
+ """Present a prepared diagnostic as copyable text, blocking its window the way its opener decides."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from enum import Enum
7
+ from typing import TYPE_CHECKING
8
+
9
+ from PySide6.QtCore import QRect, Qt
10
+ from PySide6.QtGui import QTextDocument
11
+ from PySide6.QtWidgets import QApplication, QDialog, QHBoxLayout, QLabel, QLayout, QPlainTextEdit, QPushButton, QScrollArea, QStyle, QVBoxLayout, QWidget
12
+ from shiboken6 import isValid
13
+
14
+ from gitpane.config import (
15
+ ERROR_HEIGHT,
16
+ ERROR_MAX_SCREEN_HEIGHT_RATIO,
17
+ ERROR_SUMMARY_MAX_SHARE,
18
+ ERROR_WIDTH,
19
+ LAYOUT_SPACING,
20
+ SPACING_MD,
21
+ SharedText,
22
+ control_height,
23
+ fit_window,
24
+ known_frame_margins,
25
+ unknown_decoration_estimate,
26
+ window_placement,
27
+ )
28
+ from gitpane.ui.look.fixed_font import body_sized_fixed_font
29
+ from gitpane.ui.widget.long_text import FRAME_SIDE_COUNT, WrappingLabel
30
+ from gitpane.ui.window_control.compositor_answer import after_compositor_answer
31
+
32
+ if TYPE_CHECKING:
33
+ from PySide6.QtGui import QMoveEvent, QResizeEvent, QShowEvent
34
+
35
+ from gitpane.operation.diagnostic import PreparedDiagnostic
36
+
37
+ PLAIN_TEXT_LAST_LINE_SLACK = 1
38
+
39
+
40
+ class ErrorDialogText(Enum):
41
+ """Texts the error window shows, one member per text so a misspelt name is an error."""
42
+
43
+ CLOSE = "Close diagnostic"
44
+ MASKED = "Some sensitive values have been hidden."
45
+ HELP = "Read the details, fix the cause, then try again."
46
+
47
+
48
+ class ErrorDialog(QDialog):
49
+ """Keep a real, copyable cause and its details visible while other windows and tasks continue.
50
+
51
+ The details are shown from the start, in an area as tall as their text, and the window never takes more than a
52
+ fixed share of the usable screen height: past it, the details scroll and the actions under them stay on screen.
53
+ """
54
+
55
+ def __init__(self, diagnostic: PreparedDiagnostic, parent: QWidget, log_filename: str | None = None, *, recovery_command: str = "", recovery_hint: str = "") -> None:
56
+ """Show the cause, an optional manual recovery command and the diagnostic details, with Copy details and Close diagnostic under them."""
57
+ super().__init__(parent)
58
+ self.diagnostic = diagnostic
59
+ # The screen area and the title bar height the last height fit used, so the fit is made again with the same
60
+ # area once the window manager has added a title bar of another height.
61
+ self.fitted_available: QRect | None = None
62
+ self.fitted_decoration_height: int | None = None
63
+ # True until the window manager has framed the window: only then may a move or resize from the window system
64
+ # fit the height again, once.
65
+ self.awaiting_window_manager_frame = True
66
+ self.return_focus = parent.focusWidget()
67
+ self.finished.connect(self._restore_focus)
68
+ self.setWindowTitle(diagnostic.operation)
69
+ self.resize(ERROR_WIDTH, ERROR_HEIGHT)
70
+ layout = QVBoxLayout(self)
71
+ layout.setSizeConstraint(QLayout.SizeConstraint.SetNoConstraint)
72
+ layout.setSpacing(LAYOUT_SPACING)
73
+ layout.setContentsMargins(SPACING_MD, SPACING_MD, SPACING_MD, SPACING_MD)
74
+ self.summary_scroll = QScrollArea()
75
+ self.summary_scroll.setWidgetResizable(True)
76
+ summary = QWidget()
77
+ summary_layout = QVBoxLayout(summary)
78
+ self.summary_scroll.setWidget(summary)
79
+ layout.addWidget(self.summary_scroll, 0)
80
+ # The whole cause is shown, wrapped at words and inside words too long for a line; the summary area scrolls
81
+ # when it is taller than its share of the window, so the close and copy buttons below stay on screen.
82
+ self.cause_label = WrappingLabel(diagnostic.cause)
83
+ self.cause_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextSelectableByKeyboard)
84
+ self.cause_label.setWordWrap(True)
85
+ cause_layout = QHBoxLayout()
86
+ self.error_icon_label = QLabel()
87
+ cause_layout.addWidget(self.error_icon_label, 0, Qt.AlignmentFlag.AlignTop)
88
+ cause_layout.addWidget(self.cause_label, 1)
89
+ summary_layout.addLayout(cause_layout)
90
+ self.recovery_command_edit: QPlainTextEdit | None = None
91
+ self.copy_command_button: QPushButton | None = None
92
+ self.recovery_hint_label: QLabel | None = None
93
+ if recovery_command:
94
+ self.recovery_hint_label = QLabel(recovery_hint or "Manual recovery command:")
95
+ self.recovery_hint_label.setTextFormat(Qt.TextFormat.PlainText)
96
+ self.recovery_hint_label.setWordWrap(True)
97
+ summary_layout.addWidget(self.recovery_hint_label)
98
+ self.recovery_command_edit = QPlainTextEdit(recovery_command)
99
+ self.recovery_command_edit.setReadOnly(True)
100
+ self.recovery_command_edit.setAccessibleName("Manual recovery command")
101
+ summary_layout.addWidget(self.recovery_command_edit)
102
+ self.copy_command_button = QPushButton("Copy command")
103
+ self.copy_command_button.clicked.connect(self.copy_recovery_command)
104
+ summary_layout.addWidget(self.copy_command_button)
105
+ help_label = QLabel(ErrorDialogText.HELP.value)
106
+ help_label.setWordWrap(True)
107
+ summary_layout.addWidget(help_label)
108
+ if diagnostic.masked:
109
+ masked_label = QLabel(ErrorDialogText.MASKED.value)
110
+ masked_label.setWordWrap(True)
111
+ summary_layout.addWidget(masked_label)
112
+ if log_filename:
113
+ label = QLabel(log_filename)
114
+ label.setTextFormat(Qt.TextFormat.PlainText)
115
+ label.setWordWrap(True)
116
+ label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextSelectableByKeyboard)
117
+ summary_layout.addWidget(label)
118
+ self.detail_edit = QPlainTextEdit(diagnostic.detail)
119
+ self.detail_edit.setReadOnly(True)
120
+ self.detail_edit.setAccessibleName("Diagnostic details")
121
+ layout.addWidget(self.detail_edit, 1)
122
+ self.action_layout = QVBoxLayout()
123
+ self.copy_button = QPushButton(SharedText.COPY_DETAILS.value)
124
+ # Enter closes the window: Copy details, even with the focus, never takes Close's place as the button Enter clicks.
125
+ self.copy_button.setAutoDefault(False)
126
+ self.copy_button.clicked.connect(self.copy_detail)
127
+ self.action_layout.addWidget(self.copy_button)
128
+ self.close_button = QPushButton(ErrorDialogText.CLOSE.value)
129
+ self.close_button.setDefault(True)
130
+ self.close_button.clicked.connect(self.accept)
131
+ self.action_layout.addWidget(self.close_button)
132
+ layout.addLayout(self.action_layout)
133
+ self.refresh_metrics()
134
+
135
+ def refresh_metrics(self, available: QRect | None = None) -> None:
136
+ """Use system monospace text, size the actions for the current font, then fit the window's height to its content.
137
+
138
+ ``available`` is the usable area of the screen the window is on; without it, the screen of the window it was
139
+ opened from is used.
140
+ """
141
+ fixed_font = body_sized_fixed_font()
142
+ self.detail_edit.setFont(fixed_font)
143
+ if self.recovery_command_edit is not None:
144
+ self.recovery_command_edit.setFont(fixed_font)
145
+ command_line_count = 4
146
+ self.recovery_command_edit.setFixedHeight(self.recovery_command_edit.fontMetrics().lineSpacing() * command_line_count + SPACING_MD)
147
+ if self.copy_command_button is not None:
148
+ self.copy_command_button.setMinimumHeight(control_height(self.copy_command_button.fontMetrics().height()))
149
+ icon_side = self.style().pixelMetric(QStyle.PixelMetric.PM_MessageBoxIconSize)
150
+ self.error_icon_label.setPixmap(self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxCritical).pixmap(icon_side, icon_side))
151
+ for button in (self.copy_button, self.close_button):
152
+ button.setMinimumHeight(control_height(button.fontMetrics().height()))
153
+ self.fit_height_to_content(available)
154
+
155
+ def showEvent(self, event: QShowEvent) -> None:
156
+ """Fit the height again once the window is about to appear, before it is fitted to its content and centred on the window it was opened from.
157
+
158
+ Under X11 the title bar is not there yet: this fit counts the one borrowed from the window it was opened from, and
159
+ the move or resize the window manager makes when it adds the real one fits the height again if it differs. That
160
+ is the only later fit: the window then leaves its height and content alone whatever the window system does.
161
+ Where the system already gives the frame of the native window before it appears (macOS, Windows), this fit
162
+ counts the real title bar, so no later fit is awaited at all. Where the toolkit draws the title bar itself once
163
+ the window is shown (Wayland), no move or resize from the window system follows the show: the fit is made again
164
+ once the compositor has answered the show, which would otherwise overwrite a size asked before its answer.
165
+ """
166
+ super().showEvent(event)
167
+ self.fit_height_to_content()
168
+ if self.native_frame_margins_known():
169
+ self.awaiting_window_manager_frame = False
170
+ after_compositor_answer(self, self.refit_once_frame_known)
171
+
172
+ def refit_once_frame_known(self) -> None:
173
+ """Fit the height again, once, with the screen area of the last fit, when the title bar now known has another height than the last fit counted.
174
+
175
+ The window then stops waiting for its title bar, as after a move or resize from the window system. A title bar
176
+ still unknown leaves the window waiting: under X11 the window manager's move or resize does that work later.
177
+ """
178
+ if not self.awaiting_window_manager_frame or not self.native_frame_margins_known():
179
+ return
180
+ if self.decoration_height() != self.fitted_decoration_height:
181
+ self.fit_height_to_content(self.fitted_available)
182
+ self.awaiting_window_manager_frame = False
183
+
184
+ def moveEvent(self, event: QMoveEvent) -> None:
185
+ """Fit the height again when the window manager has just framed the window with a title bar of another height than the last fit counted."""
186
+ super().moveEvent(event)
187
+ self.refit_after_window_manager_frame(event)
188
+
189
+ def resizeEvent(self, event: QResizeEvent) -> None:
190
+ """Fit the height again when the window manager has just framed the window with a title bar of another height than the last fit counted."""
191
+ super().resizeEvent(event)
192
+ self.refit_after_window_manager_frame(event)
193
+
194
+ def refit_after_window_manager_frame(self, event: QMoveEvent | QResizeEvent) -> None:
195
+ """Fit the height again, once, with the screen area of the last fit, when the window manager framed the window with a title bar of another height.
196
+
197
+ Workaround: under X11 the window manager adds the title bar after the window is shown, and Qt sends no event for
198
+ the title bar itself; the only sign of it is the move or resize the window system reports (a spontaneous event)
199
+ when the window manager frames the window. A fit made before that counted an estimated title bar, or none, so the
200
+ frame can end below the screen edge. The fit is made again only when the title bar height read now differs from
201
+ the one the last fit counted: the moves and resizes the fit makes itself are not spontaneous, and the window
202
+ system's confirmations of them find the same title bar.
203
+ The hook then stops at the first spontaneous event that comes with the native frame margins known, whether it
204
+ fitted or not: the title bar is in place, and fitting again later (borders dropped on maximize, another screen)
205
+ would compute the content once more and could undo a move or resize the user just made. Where the compositor
206
+ answers the show with a size of its own (Wayland), the hook does nothing: the fit made once the compositor has
207
+ answered the show does this work. The only spontaneous move there comes when the compositor puts the window on
208
+ another screen than Qt assumed, and it is delivered before the size of the same answer, which would overwrite a
209
+ fit made then and leave the frame taller than the cap.
210
+ """
211
+ if not self.awaiting_window_manager_frame or not event.spontaneous() or window_placement().size_waits_for_compositor_answer:
212
+ return
213
+ if self.decoration_height() != self.fitted_decoration_height:
214
+ self.fit_height_to_content(self.fitted_available)
215
+ if self.native_frame_margins_known():
216
+ self.awaiting_window_manager_frame = False
217
+
218
+ def native_frame_margins_known(self) -> bool:
219
+ """Say whether the window system has given the margins of the frame it put around the window, title bar included."""
220
+ native_window = self.windowHandle()
221
+ return native_window is not None and not native_window.frameMargins().isNull()
222
+
223
+ def fit_height_to_content(self, available: QRect | None = None) -> None:
224
+ """Give the cause and the details the height of their text at the window's width, within a share of the usable screen height.
225
+
226
+ Past that cap the window stays at the cap: the cause takes at most a share of the room left to the two text areas
227
+ and scrolls, the details take the rest and scroll, and the buttons stay visible under them. The screen area and
228
+ the title bar height used are kept, so the window can be fitted again with the same area once its real title bar
229
+ is known.
230
+ """
231
+ layout = self.layout()
232
+ margins = layout.contentsMargins()
233
+ decoration_height = self.decoration_height()
234
+ self.fitted_available = available
235
+ self.fitted_decoration_height = decoration_height
236
+ height_cap = round(ERROR_MAX_SCREEN_HEIGHT_RATIO * self.usable_screen_area(available).height()) - decoration_height
237
+ text_width = self.width() - margins.left() - margins.right()
238
+ self.action_layout.invalidate()
239
+ fixed_height = margins.top() + margins.bottom() + layout.spacing() * (layout.count() - 1) + self.action_layout.sizeHint().height()
240
+ summary_height = self.summary_content_height(text_width) + FRAME_SIDE_COUNT * self.summary_scroll.frameWidth()
241
+ detail_frame_height = FRAME_SIDE_COUNT * self.detail_edit.frameWidth()
242
+ detail_height = self.detail_text_height(text_width - detail_frame_height, height_cap) + detail_frame_height
243
+ window_height = fixed_height + summary_height + detail_height
244
+ if window_height > height_cap:
245
+ text_room = max(0, height_cap - fixed_height)
246
+ summary_height = min(summary_height, round(text_room * ERROR_SUMMARY_MAX_SHARE))
247
+ detail_height = text_room - summary_height
248
+ window_height = height_cap
249
+ self.summary_scroll.setFixedHeight(summary_height)
250
+ # The details area must not ask the layout for more than the height it was given, or the window would be too
251
+ # short for its layout and the buttons would be pushed out of it.
252
+ self.detail_edit.setMinimumHeight(min(detail_height, self.detail_edit.minimumSizeHint().height()))
253
+ self.resize(self.width(), window_height)
254
+ fit_window(self, 0, 0, available)
255
+
256
+ def usable_screen_area(self, available: QRect | None) -> QRect:
257
+ """Answer the usable area of the screen the window opens on: the one given, else the screen of the window it was opened from, else its own."""
258
+ if available is not None:
259
+ return available
260
+ parent = self.parentWidget()
261
+ if parent is not None and isValid(parent):
262
+ return parent.window().screen().availableGeometry()
263
+ return self.screen().availableGeometry()
264
+
265
+ def decoration_height(self) -> int:
266
+ """Answer the height of the title bar and borders, estimated from the window it was opened from while the window system has not added them yet.
267
+
268
+ The native window's frame margins come first: under X11 they are read again from the window manager once it has
269
+ framed the window, and under Wayland Qt draws the title bar itself after the window is shown, while the widget's
270
+ frame is a copy Qt only refreshes on a move or a resize. Then the margins the window system already gave, then
271
+ the estimate.
272
+ """
273
+ native_window = self.windowHandle()
274
+ if native_window is not None:
275
+ native_margins = native_window.frameMargins()
276
+ if not native_margins.isNull():
277
+ return native_margins.top() + native_margins.bottom()
278
+ known_margins = known_frame_margins(self)
279
+ if not known_margins.isNull():
280
+ return known_margins.top() + known_margins.bottom()
281
+ parent = self.parentWidget()
282
+ decoration = unknown_decoration_estimate(self, parent.window() if parent is not None and isValid(parent) else None)
283
+ return decoration.top() + decoration.bottom()
284
+
285
+ def summary_content_height(self, area_width: int) -> int:
286
+ """Answer the height the cause, recovery and help texts need when wrapped inside a summary area of the given width, without a scroll bar.
287
+
288
+ The width comes from the window, not from the area's viewport, because the viewport is only laid out once the
289
+ window is shown.
290
+ """
291
+ content = self.summary_scroll.widget()
292
+ content_width = max(1, area_width - FRAME_SIDE_COUNT * self.summary_scroll.frameWidth())
293
+ if content.hasHeightForWidth():
294
+ return content.heightForWidth(content_width)
295
+ return content.minimumSizeHint().height()
296
+
297
+ def detail_text_height(self, text_width: int, height_cap: int) -> int:
298
+ """Answer the height of the details text wrapped at the given width, as the details area lays it out.
299
+
300
+ A detail whose lines alone already pass the cap is not measured further: the area would be capped anyway, and a
301
+ long Git output would cost a full text layout for nothing.
302
+ """
303
+ text = self.detail_edit.toPlainText()
304
+ line_count = text.count("\n") + 1
305
+ if line_count * self.detail_edit.fontMetrics().lineSpacing() > height_cap:
306
+ return height_cap
307
+ document = QTextDocument()
308
+ document.setDefaultFont(self.detail_edit.font())
309
+ document.setDocumentMargin(self.detail_edit.document().documentMargin())
310
+ option = document.defaultTextOption()
311
+ option.setWrapMode(self.detail_edit.wordWrapMode())
312
+ document.setDefaultTextOption(option)
313
+ document.setTextWidth(max(1, text_width))
314
+ document.setPlainText(text)
315
+ # Workaround: a plain text area counts its last line as fully visible only when it ends one pixel above the
316
+ # bottom margin; at the document's exact height it keeps a scroll bar for that one line, so the pixel is added.
317
+ return math.ceil(document.size().height()) + PLAIN_TEXT_LAST_LINE_SLACK
318
+
319
+ def _restore_focus(self) -> None:
320
+ """Return focus to the trigger only when dismissing its own diagnostic."""
321
+ control = self.return_focus
322
+ if control is not None and isValid(control) and control.isVisible() and control.isEnabled():
323
+ control.setFocus(Qt.FocusReason.OtherFocusReason)
324
+
325
+ def copy_detail(self) -> None:
326
+ """Copy exactly the already-filtered text displayed in the details area."""
327
+ QApplication.clipboard().setText(self.diagnostic.detail)
328
+
329
+ def copy_recovery_command(self) -> None:
330
+ """Copy only the displayed command for manual use without executing it."""
331
+ if self.recovery_command_edit is not None:
332
+ QApplication.clipboard().setText(self.recovery_command_edit.toPlainText())