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,220 @@
1
+ """Show one rebase edit Git waits for until the user continues, saves or aborts it."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from enum import Enum
6
+ from typing import TYPE_CHECKING
7
+
8
+ from PySide6.QtCore import Qt, Signal
9
+ from PySide6.QtGui import QFontDatabase, QKeySequence, QPalette, QShortcut
10
+ from PySide6.QtWidgets import QApplication, QDialog, QHBoxLayout, QMessageBox, QPlainTextEdit, QPushButton, QVBoxLayout, QWidget
11
+ from shiboken6 import isValid
12
+
13
+ from gitpane.config import DARK_BACKGROUND_LIGHTNESS_LIMIT, ERROR_DARK_COLOR, ERROR_LIGHT_COLOR, LINE_SEPARATOR, OPERATION_HEIGHT, OPERATION_WIDTH
14
+ from gitpane.recent.recent_name import repository_display_name
15
+ from gitpane.tool.editor import EditorRequestKind
16
+ from gitpane.ui.look.theme_look import follow_theme
17
+ from gitpane.ui.widget.long_text import WrappingLabel
18
+ from gitpane.ui.widget.numbered_text_edit import NumberedTextEdit
19
+ from gitpane.ui.widget.system_shortcut import EDITOR_SAVE_SHORTCUTS, EDITOR_VALIDATE_SHORTCUTS
20
+ from gitpane.ui.window_control.dialog_modality import open_window_modal, prepare_message_box
21
+
22
+ if TYPE_CHECKING:
23
+ from PySide6.QtCore import QRect
24
+
25
+
26
+ class RebaseEditorText(Enum):
27
+ """Texts the rebase editor window shows, one member per text so a misspelt name is an error."""
28
+
29
+ CONTINUE = "Continue"
30
+ SAVE = "Save"
31
+ ABORT = "Abort"
32
+ ABORT_QUESTION_TITLE = "Abort the rebase?"
33
+ ABORT_QUESTION = "Abort the whole rebase and go back to where it started?"
34
+ ABORT_REBASE = "Abort rebase"
35
+ KEEP_EDITING = "Keep editing"
36
+ EMPTY_MESSAGE = "Commit message cannot be empty"
37
+ SAVE_FAILED = "Could not save the file: {cause}"
38
+ READ_FAILED = "GitPane cannot read the file Git is waiting for. {button} hands it back unchanged. {cause}"
39
+ ENCODING_FAILED = "the text cannot be written in the file's encoding"
40
+ # The cause shown if a write ends with neither a written file nor an error, which the write function never does.
41
+ WRITE_GAVE_NO_RESULT = "the write gave no result"
42
+ # The windows are not modal, so the folder of the repository tells apart the editors of two repositories.
43
+ WINDOW_TITLE = "{kind} — {repository}"
44
+
45
+
46
+ class EditorValidation(Enum):
47
+ """What the validating button of the window does: hand the text to Git so the rebase goes on, or save it while the rebase stays paused."""
48
+
49
+ CONTINUE = "continue"
50
+ SAVE = "save"
51
+
52
+ @property
53
+ def label(self) -> str:
54
+ """Answer the text of the validating button."""
55
+ return (RebaseEditorText.CONTINUE if self == EditorValidation.CONTINUE else RebaseEditorText.SAVE).value
56
+
57
+
58
+ class RebaseEditorWindow(QDialog):
59
+ """Edit the text of one file Git waits for, as Git wrote it, until the user answers with the validating button or Abort.
60
+
61
+ The window is never modal: the user can read the graph of the repository while editing. Its close button and
62
+ Escape ask before aborting the whole rebase, so neither can leave Git waiting without a window nor abort by mistake.
63
+ """
64
+
65
+ validate_requested = Signal()
66
+ save_requested = Signal()
67
+ abort_requested = Signal()
68
+
69
+ def __init__(self, kind: EditorRequestKind, validation: EditorValidation, text: str, repository_dir: str, parent: QWidget | None = None) -> None:
70
+ """Show Git's text in a numbered fixed-width editor, under a hidden context header, above a hidden error line and the two answer buttons.
71
+
72
+ The title names the kind of edit and the repository folder, so the editors of two repositories are told apart.
73
+ """
74
+ super().__init__(parent)
75
+ self.kind = kind
76
+ self.validation = validation
77
+ self.answered = False
78
+ self.busy = False
79
+ self.abort_question: QMessageBox | None = None
80
+ self.setWindowTitle(RebaseEditorText.WINDOW_TITLE.value.format(kind=kind.window_title, repository=repository_display_name(repository_dir)))
81
+ self.resize(OPERATION_WIDTH, OPERATION_HEIGHT)
82
+ layout = QVBoxLayout(self)
83
+ self.context_label = WrappingLabel()
84
+ self.context_label.setWordWrap(True)
85
+ self.context_label.hide()
86
+ layout.addWidget(self.context_label)
87
+ self.text_edit = NumberedTextEdit()
88
+ self.text_edit.setAccessibleName(kind.window_title)
89
+ # Monochrome on purpose: the user chose plain fixed-width text over coloured commands and dimmed comments.
90
+ self.text_edit.setFont(QFontDatabase.systemFont(QFontDatabase.SystemFont.FixedFont))
91
+ # An instruction stays on one visual line, as Git reads it; a message wraps in the window without adding a line to the text.
92
+ wrap_mode = QPlainTextEdit.LineWrapMode.NoWrap if kind == EditorRequestKind.SEQUENCE else QPlainTextEdit.LineWrapMode.WidgetWidth
93
+ self.text_edit.setLineWrapMode(wrap_mode)
94
+ self.text_edit.setPlainText(text)
95
+ layout.addWidget(self.text_edit, 1)
96
+ self.error_label = WrappingLabel()
97
+ self.error_label.setWordWrap(True)
98
+ self.error_label.hide()
99
+ layout.addWidget(self.error_label)
100
+ button_row = QHBoxLayout()
101
+ button_row.addStretch(1)
102
+ self.validate_button = QPushButton(validation.label)
103
+ self.validate_button.clicked.connect(self.validate_requested)
104
+ self.abort_button = QPushButton(RebaseEditorText.ABORT.value)
105
+ self.abort_button.clicked.connect(self.abort_requested)
106
+ for button in (self.validate_button, self.abort_button):
107
+ # Enter must never trigger Abort or Continue from the text; each command stays an explicit click or Space on its focused button.
108
+ button.setAutoDefault(False)
109
+ button_row.addWidget(button)
110
+ layout.addLayout(button_row)
111
+ for sequence in EDITOR_VALIDATE_SHORTCUTS:
112
+ validate_shortcut = QShortcut(QKeySequence(sequence), self)
113
+ validate_shortcut.setContext(Qt.ShortcutContext.WindowShortcut)
114
+ validate_shortcut.activated.connect(self.press_validate_button)
115
+ for sequence in EDITOR_SAVE_SHORTCUTS:
116
+ save_shortcut = QShortcut(QKeySequence(sequence), self)
117
+ save_shortcut.setContext(Qt.ShortcutContext.WindowShortcut)
118
+ save_shortcut.activated.connect(self.ask_save)
119
+ self.refresh_metrics()
120
+ follow_theme(self, self.refresh_metrics)
121
+
122
+ def edited_text(self) -> str:
123
+ """Answer the text as Git must read it back, with line feeds between lines and nothing else changed."""
124
+ return self.text_edit.raw_text()
125
+
126
+ def press_validate_button(self) -> None:
127
+ """Press the validating button from the keyboard, as a click would, unless a write already runs."""
128
+ if self.validate_button.isEnabled():
129
+ self.validate_button.click()
130
+
131
+ def ask_save(self) -> None:
132
+ """Ask for the text to be saved without answering Git, unless a write already runs."""
133
+ if not self.busy:
134
+ self.save_requested.emit()
135
+
136
+ def show_context(self, lines: tuple[str, ...]) -> None:
137
+ """Show the repository and the commits an edit is about above the text, or hide the header when there is nothing to show."""
138
+ self.context_label.setText(LINE_SEPARATOR.join(lines))
139
+ self.context_label.setVisible(bool(lines))
140
+
141
+ def show_error(self, text: str) -> None:
142
+ """Show a problem above the buttons, or hide the line when there is none."""
143
+ self.error_label.setText(text)
144
+ self.error_label.setVisible(bool(text))
145
+
146
+ def set_busy(self, busy: bool) -> None:
147
+ """Disable both answers and the shortcuts while the text is being written, so a second request cannot answer twice."""
148
+ self.busy = busy
149
+ self.validate_button.setEnabled(not busy)
150
+ self.abort_button.setEnabled(not busy)
151
+
152
+ def set_read_only(self, reason: str) -> None:
153
+ """Keep the text visible but not editable, saying why; the answers stay available."""
154
+ self.text_edit.setReadOnly(True)
155
+ self.show_error(reason)
156
+
157
+ def present(self) -> None:
158
+ """Show the window in front of the repository window with the keyboard in its text, without blocking the other windows."""
159
+ self.show()
160
+ self.raise_()
161
+ self.activateWindow()
162
+ self.text_edit.setFocus()
163
+
164
+ def close_after_answer(self) -> None:
165
+ """Close the window once Git has its answer, or once the request is gone, without any question; an open question closes with it.
166
+
167
+ Called again on a closed window, it does nothing, so the window reports its end once.
168
+ """
169
+ self.answered = True
170
+ question = self.abort_question
171
+ if question is not None and isValid(question):
172
+ question.close()
173
+ if self.isVisible():
174
+ QDialog.reject(self)
175
+
176
+ def reject(self) -> None:
177
+ """Ask before aborting the whole rebase when the user closes the window or presses Escape.
178
+
179
+ Qt closes a dialog through this method, from its close button as from Escape. Keep editing is the default and
180
+ the answer of Escape, so a second Escape never aborts; a question already open is brought back instead of a second one.
181
+ """
182
+ if self.answered:
183
+ QDialog.reject(self)
184
+ return
185
+ question = self.abort_question
186
+ if question is not None and isValid(question):
187
+ question.raise_()
188
+ question.activateWindow()
189
+ return
190
+ box = QMessageBox(QMessageBox.Icon.Warning, RebaseEditorText.ABORT_QUESTION_TITLE.value, RebaseEditorText.ABORT_QUESTION.value, parent=self)
191
+ prepare_message_box(box, RebaseEditorText.ABORT_QUESTION_TITLE.value)
192
+ box.setTextFormat(Qt.TextFormat.PlainText)
193
+ abort_rebase_button = box.addButton(RebaseEditorText.ABORT_REBASE.value, QMessageBox.ButtonRole.DestructiveRole)
194
+ keep_editing_button = box.addButton(RebaseEditorText.KEEP_EDITING.value, QMessageBox.ButtonRole.RejectRole)
195
+ box.setDefaultButton(keep_editing_button)
196
+ box.setEscapeButton(keep_editing_button)
197
+ self.abort_question = box
198
+
199
+ def finished() -> None:
200
+ """Abort only on an explicit Abort rebase while Git still waits; any other ending keeps the window and its text."""
201
+ self.abort_question = None
202
+ aborted = box.clickedButton() is abort_rebase_button and not self.answered
203
+ box.deleteLater()
204
+ if aborted:
205
+ self.abort_requested.emit()
206
+
207
+ box.finished.connect(finished)
208
+ open_window_modal(box)
209
+
210
+ def refresh_metrics(self, available: QRect | None = None) -> None:
211
+ """Keep the error line readable on light and dark backgrounds, again after each switch of appearance.
212
+
213
+ The choice is read on the application palette: at a palette notice, the window's own palette can still be the
214
+ previous one.
215
+ """
216
+ dark = QApplication.palette().color(QPalette.ColorRole.Window).lightnessF() < DARK_BACKGROUND_LIGHTNESS_LIMIT
217
+ color = ERROR_DARK_COLOR if dark else ERROR_LIGHT_COLOR
218
+ error_style = f"color: {color}; font-weight: bold;"
219
+ if self.error_label.styleSheet() != error_style:
220
+ self.error_label.setStyleSheet(error_style)
@@ -0,0 +1,105 @@
1
+ """Ask whether to resolve the conflicts of a paused Cherry-pick or Revert right away."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtCore import Qt, Signal
8
+ from PySide6.QtWidgets import QDialog, QHBoxLayout, QLabel, QPushButton, QStyle, QVBoxLayout
9
+
10
+ from gitpane.ui.look.theme_look import follow_theme
11
+ from gitpane.ui.widget.dialog_layout import (
12
+ MESSAGE_BOX_BODY_MARGINS,
13
+ MESSAGE_BOX_BUTTON_BAND,
14
+ MESSAGE_BOX_BUTTON_SIZE,
15
+ MESSAGE_BOX_ICON_SIDE,
16
+ MESSAGE_BOX_ICON_TO_TEXT_SPACE,
17
+ BandButtonSizing,
18
+ install_banded_layout,
19
+ )
20
+
21
+ if TYPE_CHECKING:
22
+ from PySide6.QtGui import QKeyEvent
23
+ from PySide6.QtWidgets import QWidget
24
+
25
+ CONFLICT_PROMPT_TITLE = "Merge conflicts"
26
+ CONFLICT_PROMPT_TEXT = "There are unresolved merge conflicts, solve conflicts now?"
27
+ # A Windows message box answers Y and N directly (with or without Alt) and shows no underlined letters, so the dialog
28
+ # handles these keys itself instead of using mnemonics.
29
+ YES_KEY, NO_KEY = Qt.Key.Key_Y, Qt.Key.Key_N
30
+ ANSWER_MODIFIER_SET = {Qt.KeyboardModifier.NoModifier, Qt.KeyboardModifier.AltModifier}
31
+
32
+
33
+ class ReplayConflictPrompt(QDialog):
34
+ """Offer an explicit Yes/No choice that never blocks the repository window.
35
+
36
+ A dedicated dialog rather than a QMessageBox keeps the Yes contract explicit (one signal, emitted once)
37
+ instead of comparing the finished() code with a button enumeration, and leaves room for a custom layout.
38
+ No, Escape and the title-bar close all leave the operation paused.
39
+ """
40
+
41
+ resolve_requested = Signal()
42
+
43
+ def __init__(self, parent: QWidget | None = None) -> None:
44
+ """Build the non-modal prompt with Yes as the default answer."""
45
+ super().__init__(parent)
46
+ self.resolve_emitted = False
47
+ self.setWindowTitle(CONFLICT_PROMPT_TITLE)
48
+ self.setWindowModality(Qt.WindowModality.NonModal)
49
+ # Like a Windows message box, the title bar has only the close button: without the system menu hint Windows
50
+ # shows no window icon, and there is no help button. The customize hint stops Qt from adding its default hints.
51
+ self.setWindowFlags(Qt.WindowType.Dialog | Qt.WindowType.CustomizeWindowHint | Qt.WindowType.WindowTitleHint | Qt.WindowType.WindowCloseButtonHint)
52
+ layout = QVBoxLayout()
53
+ layout.setSpacing(0)
54
+ message_row = QHBoxLayout()
55
+ message_row.setSpacing(MESSAGE_BOX_ICON_TO_TEXT_SPACE)
56
+ self.icon_label = QLabel()
57
+ self.apply_theme_look()
58
+ self.icon_label.setFixedSize(MESSAGE_BOX_ICON_SIDE, MESSAGE_BOX_ICON_SIDE)
59
+ message_row.addWidget(self.icon_label)
60
+ self.text_label = QLabel(CONFLICT_PROMPT_TEXT)
61
+ # The question is one short fixed sentence; like a message box it reads on a single line instead of wrapping
62
+ # into the narrow width a word-wrapped label proposes.
63
+ self.text_label.setTextFormat(Qt.TextFormat.PlainText)
64
+ self.text_label.setAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
65
+ message_row.addWidget(self.text_label, 1)
66
+ layout.addLayout(message_row)
67
+ self.yes_button = QPushButton("Yes")
68
+ self.yes_button.setFixedSize(MESSAGE_BOX_BUTTON_SIZE)
69
+ self.no_button = QPushButton("No")
70
+ self.no_button.setFixedSize(MESSAGE_BOX_BUTTON_SIZE)
71
+ self.no_button.setAutoDefault(False)
72
+ # Like the Git Extensions message box: the question on a white body, Yes and No on the grey area below it.
73
+ self.button_band = install_banded_layout(self, layout, [self.yes_button, self.no_button], MESSAGE_BOX_BUTTON_BAND, MESSAGE_BOX_BODY_MARGINS, button_sizing=BandButtonSizing.WINDOW)
74
+ follow_theme(self, self.apply_theme_look)
75
+ # Set once Yes belongs to the dialog, so Qt keeps it as the main default when the focus moves.
76
+ self.yes_button.setDefault(True)
77
+ self.yes_button.clicked.connect(self._answer_yes)
78
+ self.no_button.clicked.connect(self.reject)
79
+ # No explicit resize or move here: Qt sizes the dialog to its content when it is first shown, and the shared
80
+ # new-window placement then centres that final frame on the repository window. Fitting the geometry before the
81
+ # first show froze a stale size instead of the message-box shape.
82
+
83
+ def apply_theme_look(self) -> None:
84
+ """Show the question icon the style gives for the current appearance; laid again after a light/dark switch.
85
+
86
+ The style redraws a question icon that cannot be read on a dark window, so the icon of one appearance is not the
87
+ icon of the other. It is asked for the application palette, which already holds the new colors at a switch.
88
+ """
89
+ self.icon_label.setPixmap(self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxQuestion).pixmap(MESSAGE_BOX_ICON_SIDE, MESSAGE_BOX_ICON_SIDE))
90
+
91
+ def keyPressEvent(self, event: QKeyEvent) -> None:
92
+ """Answer Yes on Y and No on N, as a Windows message box does; other keys keep the dialog behavior."""
93
+ button_by_key = {YES_KEY: self.yes_button, NO_KEY: self.no_button}
94
+ button = button_by_key.get(event.key())
95
+ if button is None or event.modifiers() not in ANSWER_MODIFIER_SET:
96
+ super().keyPressEvent(event)
97
+ return
98
+ button.click()
99
+
100
+ def _answer_yes(self) -> None:
101
+ """Request resolution at most once, then close the prompt."""
102
+ if not self.resolve_emitted:
103
+ self.resolve_emitted = True
104
+ self.resolve_requested.emit()
105
+ self.accept()