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,504 @@
1
+ """Draw compact native action symbols at the window's screen density."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import logging
7
+ import math
8
+ from importlib.resources import files
9
+ from typing import TYPE_CHECKING
10
+
11
+ from PySide6.QtCore import QPointF, QRectF, Qt
12
+ from PySide6.QtGui import QColor, QIcon, QPainter, QPainterPath, QPen, QPixmap, QPixmapCache
13
+
14
+ from gitpane.config import ICON_SIDE, QT_ANGLE_UNITS_PER_DEGREE, UI_RESOURCE_DIR_NAME, UI_RESOURCE_PACKAGE_NAME
15
+ from gitpane.worktree.worktree_snapshot import WorktreeColorState
16
+
17
+ if TYPE_CHECKING:
18
+ from importlib.resources.abc import Traversable
19
+
20
+ # Keys of the pixmaps this module keeps in Qt's shared pixmap cache, kept apart from any other user of that cache.
21
+ ACTION_ICON_CACHE_PREFIX = "gitpane.action_icon/"
22
+ PACKAGED_PIXMAP_CACHE_KIND = "packaged/"
23
+ DRAWN_PIXMAP_CACHE_KIND = "drawn/"
24
+ PIXMAP_CACHE_KEY_SEPARATOR = "|"
25
+ # Colour of the Commit icon for each state of the worktree it summarizes.
26
+ COMMIT_COLOR_BY_STATE: dict[WorktreeColorState, str] = {
27
+ WorktreeColorState.CLEAN: "#2a9d48", WorktreeColorState.UNSTAGED: "#ef7777", WorktreeColorState.STAGED: "#428bdb", WorktreeColorState.MIXED: "#e4b229",
28
+ WorktreeColorState.UNKNOWN: "#888888",
29
+ }
30
+ STAGING_ARROW_COLOR = "#9655ba"
31
+ REFRESH_ARROW_COLOR = "#2a9d48"
32
+ REVERT_ARROW_COLOR = STAGING_ARROW_COLOR
33
+ SYMBOL_MARGIN_RATIO = 0.08
34
+ CHECK_STROKE_RATIO = 0.12
35
+ CHECK_START = (0.27, 0.51)
36
+ CHECK_CORNER = (0.44, 0.68)
37
+ CHECK_END = (0.75, 0.34)
38
+ ARROW_SHOULDER_RATIO = 0.53
39
+ ARROW_SHAFT_HALF_WIDTH_RATIO = 0.13
40
+ ARROW_HEAD_HALF_WIDTH_RATIO = 0.34
41
+ ARROW_REVERSE_DEGREES = 180
42
+ REFRESH_RADIUS_RATIO = 0.32
43
+ REFRESH_STROKE_RATIO = 0.14
44
+ REFRESH_HEAD_LENGTH_RATIO = 0.29
45
+ REFRESH_HEAD_HALF_WIDTH_RATIO = 0.19
46
+ REFRESH_START_ANGLES = (35, 215)
47
+ REFRESH_SWEEP_DEGREES = 130
48
+ CENTER_RATIO = 0.5
49
+ FULL_EXTENT_RATIO = 1.0
50
+ TOOL_OUTLINE_WIDTH = 0.055
51
+ DATABASE_OUTLINE_COLOR, DATABASE_FILL_COLOR = "#4b718d", "#a7cee7"
52
+ DATABASE_BODY = QRectF(0.17, 0.23, 0.66, 0.53)
53
+ DATABASE_CAP = QRectF(0.17, 0.11, 0.66, 0.24)
54
+ DATABASE_BASE = QRectF(0.17, 0.64, 0.66, 0.24)
55
+ DATABASE_DIVIDER_TOPS = (0.29, 0.47)
56
+ HALF_CIRCLE_DEGREES = 180
57
+ FOLDER_OUTLINE_COLOR, FOLDER_BACK_COLOR, FOLDER_FRONT_COLOR = "#a37421", "#e6b44e", "#ffda75"
58
+ FOLDER_BACK_POINTS = ((0.08, 0.21), (0.40, 0.21), (0.50, 0.32), (0.88, 0.32), (0.88, 0.84), (0.08, 0.84))
59
+ FOLDER_FRONT_POINTS = ((0.18, 0.44), (0.95, 0.44), (0.81, 0.84), (0.08, 0.84))
60
+ TERMINAL_OUTLINE_COLOR, TERMINAL_FILL_COLOR = "#152631", "#304654"
61
+ TERMINAL_BOUNDS = QRectF(0.07, 0.16, 0.86, 0.70)
62
+ TERMINAL_CORNER_RADIUS, TERMINAL_PROMPT_WIDTH = 0.06, 0.07
63
+ TERMINAL_PROMPT_POINTS = ((0.23, 0.36), (0.37, 0.50), (0.23, 0.64))
64
+ TERMINAL_CURSOR_START, TERMINAL_CURSOR_END = (0.48, 0.65), (0.73, 0.65)
65
+ GEAR_OUTLINE_COLOR, GEAR_FILL_COLOR = "#526270", "#b7c3cd"
66
+ GEAR_TOOTH_COUNT = 8
67
+ GEAR_TOOTH_POINTS = ((-0.5, 0.32), (-0.22, 0.45), (0.22, 0.45), (0.5, 0.32))
68
+ GEAR_HOLE = QRectF(0.35, 0.35, 0.30, 0.30)
69
+ TAG_RESOURCE_BY_SYMBOL = {"tag_create": "TagCreate.png", "tag_delete": "TagDelete.png", "tag": "Tag.png"}
70
+ TRANSPORT_RESOURCE_BY_SYMBOL = {"fetch": "ArrowDown.png", "push": "ArrowUp.png"}
71
+ CONFLICT_RESOURCE_BY_SYMBOL = {"stash": "Stash.png", "resolve": "Unmerged.png"}
72
+ HISTORY_RESOURCE_BY_SYMBOL = {
73
+ "copy_hash": "CopyToClipboard.png",
74
+ "checkout_commit": "checkout.png",
75
+ "branch_checkout": "BranchCheckout.png",
76
+ "branch_create": "BranchCreate.png",
77
+ "branch_rename": "Renamed.png",
78
+ "branch_delete": "BranchDelete.png",
79
+ "reset_current": "ResetCurrentBranchToHere.png",
80
+ "reset_other": "ResetAnotherBranchToHere.png",
81
+ }
82
+ CHERRY_FRUIT_COLOR, CHERRY_FRUIT_OUTLINE_COLOR, CHERRY_HIGHLIGHT_COLOR = "#d42a32", "#8f161c", "#ffb3ab"
83
+ CHERRY_STEM_COLOR, CHERRY_LEAF_COLOR = "#3c8a2c", "#5db444"
84
+ CHERRY_FRUIT_RADIUS = 0.17
85
+ CHERRY_FRUIT_CENTERS = ((0.26, 0.72), (0.74, 0.72))
86
+ CHERRY_OUTLINE_WIDTH = 0.04
87
+ CHERRY_HIGHLIGHT_OFFSET = (-0.06, -0.06)
88
+ CHERRY_HIGHLIGHT_RADIUS = 0.045
89
+ CHERRY_STEM_WIDTH = 0.08
90
+ CHERRY_STEM_JOIN = (0.55, 0.13)
91
+ CHERRY_STEM_CONTROLS = ((0.32, 0.30), (0.64, 0.34))
92
+ CHERRY_LEAF_TIP = (0.88, 0.14)
93
+ CHERRY_LEAF_CONTROLS = ((0.70, 0.00), (0.72, 0.26))
94
+ # "Choose another revision": an original GitPane drawing of a list sheet with one row picked out and a pointer on it.
95
+ REVISION_CHOICE_SHEET_COLOR, REVISION_CHOICE_OUTLINE_COLOR = "#ffffff", "#5b6b7a"
96
+ REVISION_CHOICE_ROW_COLOR, REVISION_CHOICE_HIGHLIGHT_COLOR = "#9aa7b2", "#3d8ee6"
97
+ REVISION_CHOICE_POINTER_COLOR, REVISION_CHOICE_POINTER_OUTLINE_COLOR = "#1d2429", "#ffffff"
98
+ REVISION_CHOICE_SHEET = QRectF(0.08, 0.06, 0.70, 0.80)
99
+ REVISION_CHOICE_CORNER_RADIUS = 0.08
100
+ REVISION_CHOICE_ROW_LEFT, REVISION_CHOICE_ROW_RIGHT = 0.20, 0.66
101
+ REVISION_CHOICE_ROW_TOPS = (0.24, 0.66)
102
+ REVISION_CHOICE_ROW_WIDTH = 0.08
103
+ REVISION_CHOICE_HIGHLIGHT = QRectF(0.13, 0.36, 0.60, 0.20)
104
+ REVISION_CHOICE_POINTER_POINTS = ((0.58, 0.44), (0.58, 0.98), (0.71, 0.86), (0.80, 1.00), (0.90, 0.95), (0.81, 0.81), (0.97, 0.80))
105
+ REVISION_CHOICE_POINTER_OUTLINE_WIDTH = 0.06
106
+ # "Rebase": an original GitPane drawing of a grey target branch with blue commits that a curved arrow brings down onto it.
107
+ REBASE_TARGET_COLOR, REBASE_MOVED_COLOR = "#6e6e6e", "#00a2e8"
108
+ REBASE_LANE_WIDTH = 0.08
109
+ REBASE_COMMIT_RADIUS = 0.11
110
+ REBASE_TARGET_X, REBASE_TARGET_TOP, REBASE_TARGET_BOTTOM = 0.24, 0.04, 0.96
111
+ REBASE_TARGET_COMMIT_YS = (0.15, 0.50, 0.85)
112
+ REBASE_MOVED_X, REBASE_MOVED_TOP, REBASE_MOVED_BOTTOM = 0.78, 0.12, 0.40
113
+ REBASE_MOVED_COMMIT_YS = (0.12, 0.40)
114
+ REBASE_CURVE_WIDTH = 0.12
115
+ REBASE_CURVE_CONTROL, REBASE_CURVE_END = (0.80, 0.85), (0.55, 0.85)
116
+ REBASE_ARROW_TIP = (0.38, 0.85)
117
+ REBASE_ARROW_TAIL_X = 0.58
118
+ REBASE_ARROW_HEAD_LENGTH, REBASE_ARROW_HEAD_HALF_HEIGHT = 0.19, 0.16
119
+ # Stash and Stash pop: original GitPane drawings of a sheet in a gold tray, laid out on a 16-unit grid so the one-unit outlines land on
120
+ # whole pixels at native size. tools/draw_conflict_icon.py imports this geometry to regenerate the packaged Stash picture.
121
+ STASH_CANVAS_UNITS = 16
122
+ STASH_OUTLINE_WIDTH = 1.0
123
+ STASH_PAPER_COLOR = "#fff7da"
124
+ STASH_TRAY_POINTS = ((2, 8), (5, 8), (5, 10), (11, 10), (11, 8), (14, 8), (13, 14), (3, 14))
125
+ STASH_TRAY_LIP_START, STASH_TRAY_LIP_END = (5, 12), (11, 12)
126
+ # Stash pop raises the sheet out of the tray and puts a green arrow pointing up on it.
127
+ STASH_POP_PAPER_BOUNDS = QRectF(4, 0.5, 8, 8)
128
+ STASH_POP_ARROW_OUTLINE_COLOR = "#1d6e33"
129
+ STASH_POP_ARROW_OUTLINE_WIDTH = 0.8
130
+ STASH_POP_ARROW_CENTER_X, STASH_POP_ARROW_TOP, STASH_POP_ARROW_BOTTOM = 8, 1.5, 7.5
131
+ STASH_POP_ARROW_HEAD_HALF_WIDTH, STASH_POP_ARROW_SHAFT_HALF_WIDTH, STASH_POP_ARROW_HEAD_LENGTH = 2.8, 1.1, 3.0
132
+ # Add and remove signs for list buttons: a green plus adds an entry, a red minus removes the selected one. Both colors keep
133
+ # at least a 3:1 contrast on light and dark window backgrounds.
134
+ ADD_SYMBOL_COLOR = "#2ea043"
135
+ REMOVE_SYMBOL_COLOR = "#e5383b"
136
+ SIGN_STROKE_RATIO = 0.1875
137
+ SIGN_MARGIN_RATIO = 0.1875
138
+
139
+
140
+ @functools.cache
141
+ def packaged_resource_bytes(resource: Traversable) -> bytes:
142
+ """Read one packaged picture once per session instead of at every menu, toolbar or density refresh.
143
+
144
+ The key is the resolved resource, a full path rather than a file name, so a package replaced by another one is
145
+ read again. A failed read raises and is never remembered: only successful reads are kept.
146
+ """
147
+ return resource.read_bytes()
148
+
149
+
150
+ def action_icon(symbol: str, density: float = 1.0, state: WorktreeColorState = WorktreeColorState.UNKNOWN) -> QIcon:
151
+ """Render recognizable action symbols with transparent corners and crisp screen scaling.
152
+
153
+ Decoded and drawn pixmaps are kept in Qt's own pixmap cache, which is bounded and emptied with the application,
154
+ so the same symbol at the same density and state is neither decoded nor drawn twice. A missing or unreadable
155
+ packaged picture is never cached: every use gives a null icon and the same warning.
156
+ """
157
+ resource_by_symbol = {**TAG_RESOURCE_BY_SYMBOL, **TRANSPORT_RESOURCE_BY_SYMBOL, **HISTORY_RESOURCE_BY_SYMBOL, **CONFLICT_RESOURCE_BY_SYMBOL}
158
+ if symbol in resource_by_symbol:
159
+ # Resolving the resource does not touch the disk; it gives the full path the caches are keyed on.
160
+ resource = files(UI_RESOURCE_PACKAGE_NAME).joinpath(UI_RESOURCE_DIR_NAME, resource_by_symbol[symbol])
161
+ cache_key = ACTION_ICON_CACHE_PREFIX + PACKAGED_PIXMAP_CACHE_KIND + PIXMAP_CACHE_KEY_SEPARATOR.join((str(resource), str(density)))
162
+ cached_pixmap = QPixmapCache.find(cache_key)
163
+ if cached_pixmap is not None:
164
+ return QIcon(cached_pixmap)
165
+ pixmap = QPixmap()
166
+ try:
167
+ content = packaged_resource_bytes(resource)
168
+ if not pixmap.loadFromData(content, "PNG"):
169
+ raise ValueError("Invalid packaged PNG")
170
+ except (OSError, ValueError):
171
+ logging.getLogger(__name__).warning("Cannot load packaged action icon %s; keeping the action label", resource_by_symbol[symbol])
172
+ return QIcon()
173
+ side = round(ICON_SIDE * density)
174
+ pixmap = pixmap.scaled(side, side, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
175
+ pixmap.setDevicePixelRatio(density)
176
+ QPixmapCache.insert(cache_key, pixmap)
177
+ return QIcon(pixmap)
178
+ cache_key = ACTION_ICON_CACHE_PREFIX + DRAWN_PIXMAP_CACHE_KIND + PIXMAP_CACHE_KEY_SEPARATOR.join((symbol, str(density), state.value))
179
+ cached_pixmap = QPixmapCache.find(cache_key)
180
+ if cached_pixmap is not None:
181
+ return QIcon(cached_pixmap)
182
+ pixmap = QPixmap(round(ICON_SIDE * density), round(ICON_SIDE * density))
183
+ pixmap.setDevicePixelRatio(density)
184
+ pixmap.fill(Qt.GlobalColor.transparent)
185
+ painter = QPainter(pixmap)
186
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
187
+ painter.scale(ICON_SIDE, ICON_SIDE)
188
+ painter.setPen(Qt.PenStyle.NoPen)
189
+ if symbol == "commit":
190
+ painter.setBrush(QColor(COMMIT_COLOR_BY_STATE[state]))
191
+ inset = SYMBOL_MARGIN_RATIO
192
+ diameter = FULL_EXTENT_RATIO - inset - inset
193
+ painter.drawEllipse(QRectF(inset, inset, diameter, diameter))
194
+ painter.setBrush(Qt.BrushStyle.NoBrush)
195
+ painter.setPen(QPen(Qt.GlobalColor.white, CHECK_STROKE_RATIO, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
196
+ check = QPainterPath(QPointF(*CHECK_START))
197
+ check.lineTo(QPointF(*CHECK_CORNER))
198
+ check.lineTo(QPointF(*CHECK_END))
199
+ painter.drawPath(check)
200
+ elif symbol in ("stage", "unstage"):
201
+ if symbol == "unstage":
202
+ painter.translate(FULL_EXTENT_RATIO, FULL_EXTENT_RATIO)
203
+ painter.rotate(ARROW_REVERSE_DEGREES)
204
+ painter.setBrush(QColor(STAGING_ARROW_COLOR))
205
+ arrow = QPainterPath(QPointF(CENTER_RATIO - ARROW_SHAFT_HALF_WIDTH_RATIO, SYMBOL_MARGIN_RATIO))
206
+ arrow.lineTo(CENTER_RATIO + ARROW_SHAFT_HALF_WIDTH_RATIO, SYMBOL_MARGIN_RATIO)
207
+ arrow.lineTo(CENTER_RATIO + ARROW_SHAFT_HALF_WIDTH_RATIO, ARROW_SHOULDER_RATIO)
208
+ arrow.lineTo(CENTER_RATIO + ARROW_HEAD_HALF_WIDTH_RATIO, ARROW_SHOULDER_RATIO)
209
+ arrow.lineTo(CENTER_RATIO, FULL_EXTENT_RATIO - SYMBOL_MARGIN_RATIO)
210
+ arrow.lineTo(CENTER_RATIO - ARROW_HEAD_HALF_WIDTH_RATIO, ARROW_SHOULDER_RATIO)
211
+ arrow.lineTo(CENTER_RATIO - ARROW_SHAFT_HALF_WIDTH_RATIO, ARROW_SHOULDER_RATIO)
212
+ arrow.closeSubpath()
213
+ painter.drawPath(arrow)
214
+ elif symbol == "refresh":
215
+ _paint_refresh(painter, REFRESH_ARROW_COLOR)
216
+ elif symbol == "repository":
217
+ _paint_database(painter)
218
+ elif symbol == "folder":
219
+ _paint_folder(painter)
220
+ elif symbol == "terminal":
221
+ _paint_terminal(painter)
222
+ elif symbol == "settings":
223
+ _paint_gear(painter)
224
+ elif symbol == "cherry_pick":
225
+ _paint_cherry(painter)
226
+ elif symbol == "revert":
227
+ _paint_refresh(painter, REVERT_ARROW_COLOR)
228
+ elif symbol == "choose_revision":
229
+ _paint_revision_choice(painter)
230
+ elif symbol == "rebase":
231
+ _paint_rebase(painter)
232
+ elif symbol == "stash_pop":
233
+ _paint_stash_pop(painter)
234
+ elif symbol == "add_remote":
235
+ _paint_add_sign(painter, density)
236
+ elif symbol == "remove_remote":
237
+ _paint_remove_sign(painter, density)
238
+ else:
239
+ painter.end()
240
+ raise ValueError("Unknown action icon: " + symbol)
241
+ painter.end()
242
+ QPixmapCache.insert(cache_key, pixmap)
243
+ return QIcon(pixmap)
244
+
245
+
246
+ def _sign_bars(density: float) -> tuple[QRectF, QRectF]:
247
+ """Return the horizontal and upright bars of the signs, laid on whole screen pixels so their edges stay sharp.
248
+
249
+ The bar thickness keeps the parity of the icon side, so a bar centred in the icon starts and ends on pixel borders at
250
+ every density instead of being smeared over two half-covered rows.
251
+ """
252
+ side_pixels = round(ICON_SIDE * density)
253
+ thickness_pixels = side_pixels - 2 * round((side_pixels - side_pixels * SIGN_STROKE_RATIO) / 2)
254
+ margin_pixels = round(side_pixels * SIGN_MARGIN_RATIO)
255
+ bar_start_pixels = (side_pixels - thickness_pixels) // 2
256
+ length_pixels = side_pixels - 2 * margin_pixels
257
+ pixel = FULL_EXTENT_RATIO / (ICON_SIDE * density)
258
+ horizontal = QRectF(margin_pixels * pixel, bar_start_pixels * pixel, length_pixels * pixel, thickness_pixels * pixel)
259
+ upright = QRectF(bar_start_pixels * pixel, margin_pixels * pixel, thickness_pixels * pixel, length_pixels * pixel)
260
+ return horizontal, upright
261
+
262
+
263
+ def _paint_add_sign(painter: QPainter, density: float) -> None:
264
+ """Draw a green plus centred in the icon, the usual picture for adding an entry to a list."""
265
+ horizontal, upright = _sign_bars(density)
266
+ _fill_pixel_aligned(painter, (horizontal, upright), ADD_SYMBOL_COLOR)
267
+
268
+
269
+ def _paint_remove_sign(painter: QPainter, density: float) -> None:
270
+ """Draw a red minus with the same bar as the plus, the usual picture for removing the selected entry."""
271
+ horizontal, _upright = _sign_bars(density)
272
+ _fill_pixel_aligned(painter, (horizontal,), REMOVE_SYMBOL_COLOR)
273
+
274
+
275
+ def _fill_pixel_aligned(painter: QPainter, rectangles: tuple[QRectF, ...], color: str) -> None:
276
+ """Fill rectangles that already sit on pixel borders without smoothing, so rounding never leaves a faint edge column."""
277
+ painter.save()
278
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing, False)
279
+ for rectangle in rectangles:
280
+ painter.fillRect(rectangle, QColor(color))
281
+ painter.restore()
282
+
283
+
284
+ def _paint_cherry(painter: QPainter) -> None:
285
+ """Draw a pair of red cherries hanging from joined green stems with a small leaf, the familiar picture of picking one fruit."""
286
+ painter.setBrush(Qt.BrushStyle.NoBrush)
287
+ painter.setPen(QPen(QColor(CHERRY_STEM_COLOR), CHERRY_STEM_WIDTH, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
288
+ for (center_x, center_y), control in zip(CHERRY_FRUIT_CENTERS, CHERRY_STEM_CONTROLS, strict=True):
289
+ stem = QPainterPath(QPointF(center_x, center_y - CHERRY_FRUIT_RADIUS))
290
+ stem.quadTo(QPointF(*control), QPointF(*CHERRY_STEM_JOIN))
291
+ painter.drawPath(stem)
292
+ upper_leaf_control, lower_leaf_control = CHERRY_LEAF_CONTROLS
293
+ leaf = QPainterPath(QPointF(*CHERRY_STEM_JOIN))
294
+ leaf.quadTo(QPointF(*upper_leaf_control), QPointF(*CHERRY_LEAF_TIP))
295
+ leaf.quadTo(QPointF(*lower_leaf_control), QPointF(*CHERRY_STEM_JOIN))
296
+ painter.setPen(Qt.PenStyle.NoPen)
297
+ painter.setBrush(QColor(CHERRY_LEAF_COLOR))
298
+ painter.drawPath(leaf)
299
+ highlight_offset_x, highlight_offset_y = CHERRY_HIGHLIGHT_OFFSET
300
+ for center_x, center_y in CHERRY_FRUIT_CENTERS:
301
+ painter.setPen(QPen(QColor(CHERRY_FRUIT_OUTLINE_COLOR), CHERRY_OUTLINE_WIDTH))
302
+ painter.setBrush(QColor(CHERRY_FRUIT_COLOR))
303
+ painter.drawEllipse(QPointF(center_x, center_y), CHERRY_FRUIT_RADIUS, CHERRY_FRUIT_RADIUS)
304
+ painter.setPen(Qt.PenStyle.NoPen)
305
+ painter.setBrush(QColor(CHERRY_HIGHLIGHT_COLOR))
306
+ painter.drawEllipse(QPointF(center_x + highlight_offset_x, center_y + highlight_offset_y), CHERRY_HIGHLIGHT_RADIUS, CHERRY_HIGHLIGHT_RADIUS)
307
+
308
+
309
+ def _paint_revision_choice(painter: QPainter) -> None:
310
+ """Draw a list sheet with one highlighted row and a mouse pointer on it, for the button that picks another commit.
311
+
312
+ The drawing is GitPane's own: it says "pick one entry from a list" with shapes every desktop user knows, so the
313
+ replay form does not need artwork from another program.
314
+ """
315
+ painter.setPen(QPen(QColor(REVISION_CHOICE_OUTLINE_COLOR), TOOL_OUTLINE_WIDTH))
316
+ painter.setBrush(QColor(REVISION_CHOICE_SHEET_COLOR))
317
+ painter.drawRoundedRect(REVISION_CHOICE_SHEET, REVISION_CHOICE_CORNER_RADIUS, REVISION_CHOICE_CORNER_RADIUS)
318
+ painter.setPen(QPen(QColor(REVISION_CHOICE_ROW_COLOR), REVISION_CHOICE_ROW_WIDTH, Qt.PenStyle.SolidLine, Qt.PenCapStyle.FlatCap))
319
+ for top in REVISION_CHOICE_ROW_TOPS:
320
+ painter.drawLine(QPointF(REVISION_CHOICE_ROW_LEFT, top), QPointF(REVISION_CHOICE_ROW_RIGHT, top))
321
+ painter.setPen(Qt.PenStyle.NoPen)
322
+ painter.setBrush(QColor(REVISION_CHOICE_HIGHLIGHT_COLOR))
323
+ painter.drawRect(REVISION_CHOICE_HIGHLIGHT)
324
+ first_point, *remaining_points = REVISION_CHOICE_POINTER_POINTS
325
+ pointer = QPainterPath(QPointF(*first_point))
326
+ for point in remaining_points:
327
+ pointer.lineTo(*point)
328
+ pointer.closeSubpath()
329
+ painter.setPen(QPen(QColor(REVISION_CHOICE_POINTER_OUTLINE_COLOR), REVISION_CHOICE_POINTER_OUTLINE_WIDTH, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
330
+ painter.setBrush(QColor(REVISION_CHOICE_POINTER_COLOR))
331
+ painter.drawPath(pointer)
332
+
333
+
334
+ def paint_polygon(painter: QPainter, points: tuple[tuple[float, float], ...]) -> None:
335
+ """Fill and outline one closed shape given as a sequence of vertices, with the painter's current pen and brush."""
336
+ first_point, *remaining_points = points
337
+ path = QPainterPath(QPointF(*first_point))
338
+ for point in remaining_points:
339
+ path.lineTo(QPointF(*point))
340
+ path.closeSubpath()
341
+ painter.drawPath(path)
342
+
343
+
344
+ def paint_stash_tray(painter: QPainter) -> None:
345
+ """Draw the gold tray of the Stash drawings in the 16-unit canvas, in front of whatever sheet was painted before it."""
346
+ painter.setPen(QPen(QColor(FOLDER_OUTLINE_COLOR), STASH_OUTLINE_WIDTH))
347
+ painter.setBrush(QColor(FOLDER_FRONT_COLOR))
348
+ paint_polygon(painter, STASH_TRAY_POINTS)
349
+ painter.drawLine(QPointF(*STASH_TRAY_LIP_START), QPointF(*STASH_TRAY_LIP_END))
350
+
351
+
352
+ def _paint_stash_pop(painter: QPainter) -> None:
353
+ """Draw the Stash tray with its sheet raised out of it and a green arrow pointing up on the sheet: saved work coming back out.
354
+
355
+ The drawing is GitPane's own, built from the Stash drawing so both toolbar buttons read as a pair.
356
+ """
357
+ painter.scale(1 / STASH_CANVAS_UNITS, 1 / STASH_CANVAS_UNITS)
358
+ painter.setPen(QPen(QColor(FOLDER_OUTLINE_COLOR), STASH_OUTLINE_WIDTH))
359
+ painter.setBrush(QColor(STASH_PAPER_COLOR))
360
+ painter.drawRect(STASH_POP_PAPER_BOUNDS)
361
+ center_x, top, bottom = STASH_POP_ARROW_CENTER_X, STASH_POP_ARROW_TOP, STASH_POP_ARROW_BOTTOM
362
+ head_half, shaft_half = STASH_POP_ARROW_HEAD_HALF_WIDTH, STASH_POP_ARROW_SHAFT_HALF_WIDTH
363
+ shoulder = top + STASH_POP_ARROW_HEAD_LENGTH
364
+ painter.setPen(QPen(QColor(STASH_POP_ARROW_OUTLINE_COLOR), STASH_POP_ARROW_OUTLINE_WIDTH))
365
+ painter.setBrush(QColor(REFRESH_ARROW_COLOR))
366
+ paint_polygon(painter, ((center_x, top), (center_x + head_half, shoulder), (center_x + shaft_half, shoulder), (center_x + shaft_half, bottom),
367
+ (center_x - shaft_half, bottom), (center_x - shaft_half, shoulder), (center_x - head_half, shoulder)))
368
+ paint_stash_tray(painter)
369
+
370
+
371
+ def _paint_lane(painter: QPainter, color_name: str, x: float, top: float, bottom: float, commit_ys: tuple[float, ...]) -> None:
372
+ """Draw one vertical branch line with a round commit dot at each given height, like a lane of the history graph."""
373
+ color = QColor(color_name)
374
+ painter.setPen(QPen(color, REBASE_LANE_WIDTH, Qt.PenStyle.SolidLine, Qt.PenCapStyle.FlatCap))
375
+ painter.drawLine(QPointF(x, top), QPointF(x, bottom))
376
+ painter.setPen(Qt.PenStyle.NoPen)
377
+ painter.setBrush(color)
378
+ for commit_y in commit_ys:
379
+ painter.drawEllipse(QPointF(x, commit_y), REBASE_COMMIT_RADIUS, REBASE_COMMIT_RADIUS)
380
+
381
+
382
+ def _paint_rebase(painter: QPainter) -> None:
383
+ """Draw commits being moved onto another branch: a grey target lane, blue commits beside it and a curved blue arrow landing on the target.
384
+
385
+ The drawing is GitPane's own, made from the maintainer's sketch, so the rebase entry no longer borrows the reset picture.
386
+ """
387
+ _paint_lane(painter, REBASE_TARGET_COLOR, REBASE_TARGET_X, REBASE_TARGET_TOP, REBASE_TARGET_BOTTOM, REBASE_TARGET_COMMIT_YS)
388
+ _paint_lane(painter, REBASE_MOVED_COLOR, REBASE_MOVED_X, REBASE_MOVED_TOP, REBASE_MOVED_BOTTOM, REBASE_MOVED_COMMIT_YS)
389
+ moved_color = QColor(REBASE_MOVED_COLOR)
390
+ curve = QPainterPath(QPointF(REBASE_MOVED_X, REBASE_MOVED_BOTTOM))
391
+ curve.quadTo(QPointF(*REBASE_CURVE_CONTROL), QPointF(*REBASE_CURVE_END))
392
+ painter.setPen(QPen(moved_color, REBASE_CURVE_WIDTH, Qt.PenStyle.SolidLine, Qt.PenCapStyle.FlatCap))
393
+ painter.setBrush(Qt.BrushStyle.NoBrush)
394
+ painter.drawPath(curve)
395
+ tip_x, tip_y = REBASE_ARROW_TIP
396
+ head_base_x = tip_x + REBASE_ARROW_HEAD_LENGTH
397
+ shaft_half_height = REBASE_CURVE_WIDTH * CENTER_RATIO
398
+ arrow = QPainterPath(QPointF(tip_x, tip_y))
399
+ arrow.lineTo(head_base_x, tip_y - REBASE_ARROW_HEAD_HALF_HEIGHT)
400
+ arrow.lineTo(head_base_x, tip_y - shaft_half_height)
401
+ arrow.lineTo(REBASE_ARROW_TAIL_X, tip_y - shaft_half_height)
402
+ arrow.lineTo(REBASE_ARROW_TAIL_X, tip_y + shaft_half_height)
403
+ arrow.lineTo(head_base_x, tip_y + shaft_half_height)
404
+ arrow.lineTo(head_base_x, tip_y + REBASE_ARROW_HEAD_HALF_HEIGHT)
405
+ arrow.closeSubpath()
406
+ painter.setPen(Qt.PenStyle.NoPen)
407
+ painter.setBrush(moved_color)
408
+ painter.drawPath(arrow)
409
+
410
+
411
+ def _paint_database(painter: QPainter) -> None:
412
+ """Draw a stacked cylinder that identifies a repository as a stored collection."""
413
+ painter.setPen(QPen(QColor(DATABASE_OUTLINE_COLOR), TOOL_OUTLINE_WIDTH))
414
+ painter.setBrush(QColor(DATABASE_FILL_COLOR))
415
+ painter.drawEllipse(DATABASE_BASE)
416
+ painter.setPen(Qt.PenStyle.NoPen)
417
+ painter.drawRect(DATABASE_BODY)
418
+ painter.setPen(QPen(QColor(DATABASE_OUTLINE_COLOR), TOOL_OUTLINE_WIDTH))
419
+ painter.drawLine(DATABASE_BODY.topLeft(), DATABASE_BODY.bottomLeft())
420
+ painter.drawLine(DATABASE_BODY.topRight(), DATABASE_BODY.bottomRight())
421
+ painter.setBrush(Qt.BrushStyle.NoBrush)
422
+ for top in DATABASE_DIVIDER_TOPS:
423
+ bounds = QRectF(DATABASE_CAP)
424
+ bounds.moveTop(top)
425
+ painter.drawArc(bounds, HALF_CIRCLE_DEGREES * QT_ANGLE_UNITS_PER_DEGREE, HALF_CIRCLE_DEGREES * QT_ANGLE_UNITS_PER_DEGREE)
426
+ painter.setBrush(QColor(DATABASE_FILL_COLOR).lighter())
427
+ painter.drawEllipse(DATABASE_CAP)
428
+
429
+
430
+ def _paint_folder(painter: QPainter) -> None:
431
+ """Draw a yellow folder with a raised tab and an open front."""
432
+ painter.setPen(QPen(QColor(FOLDER_OUTLINE_COLOR), TOOL_OUTLINE_WIDTH, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
433
+ for points, color in ((FOLDER_BACK_POINTS, FOLDER_BACK_COLOR), (FOLDER_FRONT_POINTS, FOLDER_FRONT_COLOR)):
434
+ path = QPainterPath()
435
+ first_point, *remaining_points = points
436
+ path.moveTo(*first_point)
437
+ for point in remaining_points:
438
+ path.lineTo(*point)
439
+ path.closeSubpath()
440
+ painter.setBrush(QColor(color))
441
+ painter.drawPath(path)
442
+
443
+
444
+ def _paint_terminal(painter: QPainter) -> None:
445
+ """Draw a dark terminal window with a high-contrast prompt and cursor."""
446
+ painter.setPen(QPen(QColor(TERMINAL_OUTLINE_COLOR), TOOL_OUTLINE_WIDTH))
447
+ painter.setBrush(QColor(TERMINAL_FILL_COLOR))
448
+ painter.drawRoundedRect(TERMINAL_BOUNDS, TERMINAL_CORNER_RADIUS, TERMINAL_CORNER_RADIUS)
449
+ painter.setPen(QPen(Qt.GlobalColor.white, TERMINAL_PROMPT_WIDTH, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
450
+ first_point, *remaining_points = TERMINAL_PROMPT_POINTS
451
+ prompt = QPainterPath(QPointF(*first_point))
452
+ for point in remaining_points:
453
+ prompt.lineTo(*point)
454
+ painter.setBrush(Qt.BrushStyle.NoBrush)
455
+ painter.drawPath(prompt)
456
+ painter.drawLine(QPointF(*TERMINAL_CURSOR_START), QPointF(*TERMINAL_CURSOR_END))
457
+
458
+
459
+ def _paint_gear(painter: QPainter) -> None:
460
+ """Draw eight teeth around an open center so settings remains legible at small sizes."""
461
+ path = QPainterPath()
462
+ started = False
463
+ for tooth in range(GEAR_TOOTH_COUNT):
464
+ for offset, radius in GEAR_TOOTH_POINTS:
465
+ angle = (tooth + offset) * math.tau / GEAR_TOOTH_COUNT
466
+ point = QPointF(CENTER_RATIO + radius * math.cos(angle), CENTER_RATIO + radius * math.sin(angle))
467
+ if not started:
468
+ path.moveTo(point)
469
+ started = True
470
+ else:
471
+ path.lineTo(point)
472
+ path.closeSubpath()
473
+ path.addEllipse(GEAR_HOLE)
474
+ path.setFillRule(Qt.FillRule.OddEvenFill)
475
+ painter.setPen(QPen(QColor(GEAR_OUTLINE_COLOR), TOOL_OUTLINE_WIDTH, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap, Qt.PenJoinStyle.RoundJoin))
476
+ painter.setBrush(QColor(GEAR_FILL_COLOR))
477
+ painter.drawPath(path)
478
+
479
+
480
+ def _paint_refresh(painter: QPainter, color_name: str = REFRESH_ARROW_COLOR) -> None:
481
+ """Draw two circulating arrows whose heads remain distinct at toolbar size.
482
+
483
+ The color is a parameter so Revert can reuse the same circular motif in purple, reading as "go back" without a second drawing.
484
+ """
485
+ color = QColor(color_name)
486
+ radius = REFRESH_RADIUS_RATIO
487
+ diameter = radius + radius
488
+ bounds = QRectF(CENTER_RATIO - radius, CENTER_RATIO - radius, diameter, diameter)
489
+ for start_angle in REFRESH_START_ANGLES:
490
+ painter.setBrush(Qt.BrushStyle.NoBrush)
491
+ painter.setPen(QPen(color, REFRESH_STROKE_RATIO, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap))
492
+ painter.drawArc(bounds, start_angle * QT_ANGLE_UNITS_PER_DEGREE, REFRESH_SWEEP_DEGREES * QT_ANGLE_UNITS_PER_DEGREE)
493
+ angle = math.radians(start_angle + REFRESH_SWEEP_DEGREES)
494
+ endpoint = QPointF(CENTER_RATIO + radius * math.cos(angle), CENTER_RATIO - radius * math.sin(angle))
495
+ tangent = QPointF(-math.sin(angle), -math.cos(angle))
496
+ normal = QPointF(math.cos(angle), -math.sin(angle))
497
+ base = endpoint - tangent * REFRESH_HEAD_LENGTH_RATIO
498
+ arrow = QPainterPath(endpoint)
499
+ arrow.lineTo(base + normal * REFRESH_HEAD_HALF_WIDTH_RATIO)
500
+ arrow.lineTo(base - normal * REFRESH_HEAD_HALF_WIDTH_RATIO)
501
+ arrow.closeSubpath()
502
+ painter.setPen(Qt.PenStyle.NoPen)
503
+ painter.setBrush(color)
504
+ painter.drawPath(arrow)
@@ -0,0 +1,78 @@
1
+ """Draw the GitPane logo, a hand-written two-colour "X", as vector geometry so it stays sharp at every icon size."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from PySide6.QtCore import QPointF, QRectF, Qt
8
+ from PySide6.QtGui import QColor, QIcon, QImage, QPainter, QPixmap, QPolygonF
9
+
10
+ if TYPE_CHECKING:
11
+ from PySide6.QtWidgets import QApplication
12
+
13
+ LOGO_RED_COLOR = "#ed1c24"
14
+ LOGO_GREEN_COLOR = "#22b14c"
15
+ # Windows picks the closest of these for the title bar, the taskbar, Alt+Tab and Explorer views.
16
+ APPLICATION_ICON_SIDES = (16, 24, 32, 48, 64, 256)
17
+ # The geometry is kept in the pixel coordinates of the maintainer's original hand drawing, so a later retouch can be measured
18
+ # against that picture directly. One uniform scale then fits the drawing into any square canvas.
19
+ LOGO_GREEN_ARROW_POINTS = ((9.5, 2.0), (24.5, 11.8), (5.5, 21.0))
20
+ LOGO_GREEN_STROKE_POINTS = ((9.5, 14.0), (20.0, 14.0), (52.25, 68.0), (43.25, 68.0))
21
+ LOGO_RED_STROKE_POINTS = ((42.5, 7.0), (51.75, 7.0), (14.75, 68.0), (5.5, 68.0))
22
+ # The box that holds every vertex above; the drawing is scaled and centered from it.
23
+ LOGO_SOURCE_BOUNDS = QRectF(5.5, 2.0, 46.75, 66.0)
24
+ LOGO_MARGIN_RATIO = 0.04
25
+ # Share of the square's side that moves the painter origin to the square's centre.
26
+ CENTRE_RATIO = 0.5
27
+
28
+
29
+ def paint_application_logo(painter: QPainter, side: float) -> None:
30
+ """Paint the logo centered in a ``side`` by ``side`` square starting at the painter origin.
31
+
32
+ The green stroke and its arrow head come first and the red stroke is painted over them, as in the hand drawing.
33
+ The drawing keeps its proportions: its height fills the square minus the margin, and it is centered horizontally.
34
+ """
35
+ scale = side * (1 - 2 * LOGO_MARGIN_RATIO) / LOGO_SOURCE_BOUNDS.height()
36
+ painter.save()
37
+ painter.setRenderHint(QPainter.RenderHint.Antialiasing)
38
+ painter.translate(side * CENTRE_RATIO, side * CENTRE_RATIO)
39
+ painter.scale(scale, scale)
40
+ painter.translate(-LOGO_SOURCE_BOUNDS.center())
41
+ painter.setPen(Qt.PenStyle.NoPen)
42
+ painter.setBrush(QColor(LOGO_GREEN_COLOR))
43
+ painter.drawPolygon(_polygon(LOGO_GREEN_STROKE_POINTS))
44
+ painter.drawPolygon(_polygon(LOGO_GREEN_ARROW_POINTS))
45
+ painter.setBrush(QColor(LOGO_RED_COLOR))
46
+ painter.drawPolygon(_polygon(LOGO_RED_STROKE_POINTS))
47
+ painter.restore()
48
+
49
+
50
+ def _polygon(points: tuple[tuple[float, float], ...]) -> QPolygonF:
51
+ """Build a closed Qt polygon from a named tuple of vertices."""
52
+ polygon = QPolygonF()
53
+ for x, y in points:
54
+ polygon.append(QPointF(x, y))
55
+ return polygon
56
+
57
+
58
+ def application_icon_image(side: int) -> QImage:
59
+ """Render the logo on a transparent square image of ``side`` pixels, ready to save or to turn into a pixmap."""
60
+ image = QImage(side, side, QImage.Format.Format_ARGB32)
61
+ image.fill(Qt.GlobalColor.transparent)
62
+ painter = QPainter(image)
63
+ paint_application_logo(painter, side)
64
+ painter.end()
65
+ return image
66
+
67
+
68
+ def application_icon() -> QIcon:
69
+ """Build the window icon with one pixmap per standard size, so Qt and Windows never have to scale a single picture."""
70
+ icon = QIcon()
71
+ for side in APPLICATION_ICON_SIDES:
72
+ icon.addPixmap(QPixmap.fromImage(application_icon_image(side)))
73
+ return icon
74
+
75
+
76
+ def install_application_icon(application: QApplication) -> None:
77
+ """Make the logo the icon of every GitPane window, so no window has to set one and the taskbar shows the same picture."""
78
+ application.setWindowIcon(application_icon())