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.
- gitpane/__init__.py +0 -0
- gitpane/account/__init__.py +0 -0
- gitpane/account/account_choice.py +297 -0
- gitpane/account/address_owner.py +67 -0
- gitpane/account/authentication.py +1811 -0
- gitpane/account/authentication_worker.py +836 -0
- gitpane/account/credential_manager.py +480 -0
- gitpane/account/credential_manager_install.py +171 -0
- gitpane/account/keyring_reader.py +135 -0
- gitpane/account/keyring_state.py +158 -0
- gitpane/account/ssh.py +312 -0
- gitpane/account/ssh_askpass.py +216 -0
- gitpane/account/ssh_prompt.py +222 -0
- gitpane/application.py +684 -0
- gitpane/commit/__init__.py +0 -0
- gitpane/commit/commit_metadata.py +88 -0
- gitpane/commit/commit_push.py +276 -0
- gitpane/commit/commit_write.py +830 -0
- gitpane/config.py +1066 -0
- gitpane/controller/__init__.py +0 -0
- gitpane/controller/account_controller.py +677 -0
- gitpane/controller/clone_controller.py +776 -0
- gitpane/controller/close_readiness.py +212 -0
- gitpane/controller/commit_timing_controller.py +566 -0
- gitpane/controller/conflict_controller.py +885 -0
- gitpane/controller/console_registry.py +38 -0
- gitpane/controller/emoji_window_controller.py +95 -0
- gitpane/controller/file_action_controller.py +479 -0
- gitpane/controller/history_read_controller.py +671 -0
- gitpane/controller/history_rewrite_controller.py +739 -0
- gitpane/controller/navigation_controller.py +658 -0
- gitpane/controller/opening_controller.py +375 -0
- gitpane/controller/operation_policy.py +76 -0
- gitpane/controller/rebase_controller.py +797 -0
- gitpane/controller/rebase_editor_controller.py +309 -0
- gitpane/controller/reference_form_controller.py +1677 -0
- gitpane/controller/reference_read_controller.py +242 -0
- gitpane/controller/refresh_controller.py +477 -0
- gitpane/controller/replay_controller.py +805 -0
- gitpane/controller/session_registry.py +337 -0
- gitpane/controller/setting_controller.py +592 -0
- gitpane/controller/shutdown_controller.py +345 -0
- gitpane/controller/staging_controller.py +199 -0
- gitpane/controller/staging_session_registry.py +47 -0
- gitpane/controller/stash_controller.py +367 -0
- gitpane/controller/transport_controller.py +813 -0
- gitpane/controller/welcome_controller.py +102 -0
- gitpane/controller/window_activation.py +116 -0
- gitpane/controller/worktree_state_controller.py +169 -0
- gitpane/emoji/__init__.py +0 -0
- gitpane/emoji/emoji_catalogue.py +156 -0
- gitpane/git_config/__init__.py +0 -0
- gitpane/git_config/git_setting.py +441 -0
- gitpane/history/__init__.py +0 -0
- gitpane/history/avatar.py +241 -0
- gitpane/history/file_history.py +359 -0
- gitpane/history/graph.py +145 -0
- gitpane/history/history_filter.py +31 -0
- gitpane/history/history_parse.py +605 -0
- gitpane/history/repository.py +1600 -0
- gitpane/history/repository_value.py +341 -0
- gitpane/history/revision.py +180 -0
- gitpane/history/revision_file.py +182 -0
- gitpane/history_rewrite/__init__.py +0 -0
- gitpane/history_rewrite/graph.py +210 -0
- gitpane/history_rewrite/guard.py +288 -0
- gitpane/history_rewrite/index.py +125 -0
- gitpane/history_rewrite/prepare.py +378 -0
- gitpane/history_rewrite/protocol.py +320 -0
- gitpane/history_rewrite/publish.py +444 -0
- gitpane/history_rewrite/recovery.py +481 -0
- gitpane/history_rewrite/value.py +229 -0
- gitpane/operation/__init__.py +0 -0
- gitpane/operation/console_line.py +313 -0
- gitpane/operation/console_tone.py +76 -0
- gitpane/operation/diagnostic.py +954 -0
- gitpane/operation/launch_signature.py +254 -0
- gitpane/operation/operation_status.py +87 -0
- gitpane/operation/operation_trace.py +766 -0
- gitpane/operation/repository_operation.py +781 -0
- gitpane/preference/__init__.py +0 -0
- gitpane/preference/preference_location.py +40 -0
- gitpane/preference/preference_store.py +590 -0
- gitpane/process/__init__.py +0 -0
- gitpane/process/exact_arguments.py +77 -0
- gitpane/process/git_output_text.py +20 -0
- gitpane/process/process_group.py +851 -0
- gitpane/process/process_log.py +114 -0
- gitpane/process/process_runner.py +1033 -0
- gitpane/process/released_child_watch.py +138 -0
- gitpane/recent/__init__.py +0 -0
- gitpane/recent/mount_table.py +422 -0
- gitpane/recent/recent_availability.py +414 -0
- gitpane/recent/recent_name.py +11 -0
- gitpane/recent/recent_search.py +114 -0
- gitpane/reference/__init__.py +0 -0
- gitpane/reference/reference_edit.py +2030 -0
- gitpane/reference/reference_snapshot.py +196 -0
- gitpane/reference/stash.py +730 -0
- gitpane/reference/stash_list.py +164 -0
- gitpane/sequencer/__init__.py +0 -0
- gitpane/sequencer/commit_message_rule.py +154 -0
- gitpane/sequencer/conflict.py +506 -0
- gitpane/sequencer/merge_recovery.py +163 -0
- gitpane/sequencer/merge_state.py +166 -0
- gitpane/sequencer/mergetool.py +856 -0
- gitpane/sequencer/rebase.py +750 -0
- gitpane/sequencer/rebase_edit_file.py +224 -0
- gitpane/sequencer/rebase_state.py +523 -0
- gitpane/sequencer/replay.py +1435 -0
- gitpane/sequencer/replay_legacy.py +253 -0
- gitpane/sequencer/replay_state.py +747 -0
- gitpane/submodule/__init__.py +0 -0
- gitpane/submodule/checkout_refusal.py +102 -0
- gitpane/submodule/checkout_refusal_notice.py +111 -0
- gitpane/submodule/cloned_without_checkout.py +65 -0
- gitpane/submodule/submodule_discovery.py +1180 -0
- gitpane/submodule/submodule_loss.py +218 -0
- gitpane/system/__init__.py +0 -0
- gitpane/system/appearance_portal.py +125 -0
- gitpane/system/application_identity.py +48 -0
- gitpane/system/command_line.py +36 -0
- gitpane/system/desktop_icon_fact.py +128 -0
- gitpane/system/desktop_icon_support.py +270 -0
- gitpane/system/desktop_shortcut.py +536 -0
- gitpane/system/icns_image.py +35 -0
- gitpane/system/installed_launcher.py +136 -0
- gitpane/system/linux_distribution.py +151 -0
- gitpane/system/network_folder.py +46 -0
- gitpane/system/path_identity.py +95 -0
- gitpane/system/private_tree_removal.py +47 -0
- gitpane/system/process_program.py +63 -0
- gitpane/system/qt_folder.py +38 -0
- gitpane/system/ready_channel_watch.py +67 -0
- gitpane/system/startup_activation.py +65 -0
- gitpane/system/terminal_detach.py +384 -0
- gitpane/system/xdg_session.py +85 -0
- gitpane/text/__init__.py +0 -0
- gitpane/text/text_query.py +97 -0
- gitpane/text/typed_path.py +75 -0
- gitpane/text/typed_text.py +17 -0
- gitpane/thread/__init__.py +0 -0
- gitpane/thread/weak_signal.py +93 -0
- gitpane/tool/__init__.py +0 -0
- gitpane/tool/app_bundle.py +164 -0
- gitpane/tool/desktop_text_editor.py +156 -0
- gitpane/tool/difftool.py +429 -0
- gitpane/tool/editor.py +445 -0
- gitpane/tool/editor_bridge.py +193 -0
- gitpane/tool/editor_recipe.py +481 -0
- gitpane/tool/file_reveal.py +94 -0
- gitpane/tool/git_executable.py +264 -0
- gitpane/tool/git_version.py +62 -0
- gitpane/tool/kdiff3.py +41 -0
- gitpane/tool/kdiff3_install.py +162 -0
- gitpane/tool/launch_services_text_editor.py +104 -0
- gitpane/tool/mergetool_recipe.py +84 -0
- gitpane/tool/system_editor_default.py +80 -0
- gitpane/tool/system_text_editor.py +44 -0
- gitpane/tool/system_tool.py +816 -0
- gitpane/tool/terminal_recipe.py +390 -0
- gitpane/transport/__init__.py +0 -0
- gitpane/transport/branch_exchange.py +1690 -0
- gitpane/transport/clone.py +1273 -0
- gitpane/transport/credential_read_retry.py +35 -0
- gitpane/transport/git_refusal.py +207 -0
- gitpane/transport/push_tracking.py +121 -0
- gitpane/transport/refusal_notice.py +225 -0
- gitpane/transport/remote.py +747 -0
- gitpane/transport/remote_config.py +544 -0
- gitpane/ui/__init__.py +0 -0
- gitpane/ui/console/__init__.py +0 -0
- gitpane/ui/console/error_dialog.py +332 -0
- gitpane/ui/console/operation_dialog.py +408 -0
- gitpane/ui/console/operation_output.py +91 -0
- gitpane/ui/console/operation_output_view.py +373 -0
- gitpane/ui/emoji/__init__.py +0 -0
- gitpane/ui/emoji/emoji_font.py +257 -0
- gitpane/ui/emoji/emoji_grid.py +125 -0
- gitpane/ui/emoji/emoji_window.py +154 -0
- gitpane/ui/emoji/file_action_icon.py +131 -0
- gitpane/ui/emoji/opentype_family_name.py +182 -0
- gitpane/ui/history_rewrite/__init__.py +0 -0
- gitpane/ui/history_rewrite/details.py +78 -0
- gitpane/ui/history_rewrite/preview.py +565 -0
- gitpane/ui/history_rewrite/preview_model.py +291 -0
- gitpane/ui/history_rewrite/question.py +87 -0
- gitpane/ui/history_rewrite/recovery_dialog.py +262 -0
- gitpane/ui/look/__init__.py +0 -0
- gitpane/ui/look/action_icon.py +504 -0
- gitpane/ui/look/application_icon.py +78 -0
- gitpane/ui/look/application_style.py +317 -0
- gitpane/ui/look/avatar_label.py +53 -0
- gitpane/ui/look/color_contrast.py +43 -0
- gitpane/ui/look/desktop_appearance_follower.py +118 -0
- gitpane/ui/look/fixed_font.py +21 -0
- gitpane/ui/look/gtk_settings.py +116 -0
- gitpane/ui/look/icon_density.py +16 -0
- gitpane/ui/look/reference_font.py +19 -0
- gitpane/ui/look/theme_color.py +63 -0
- gitpane/ui/look/theme_look.py +95 -0
- gitpane/ui/look/theme_palette.py +173 -0
- gitpane/ui/look/warning_icon.py +86 -0
- gitpane/ui/look/windows_control_look.py +250 -0
- gitpane/ui/reader/__init__.py +0 -0
- gitpane/ui/reader/commit_message.py +129 -0
- gitpane/ui/reader/commit_panel.py +378 -0
- gitpane/ui/reader/diff_panel.py +626 -0
- gitpane/ui/reader/file_action.py +138 -0
- gitpane/ui/reader/file_tree_panel.py +235 -0
- gitpane/ui/reader/reader_toolbar.py +56 -0
- gitpane/ui/reader/revision_tree.py +80 -0
- gitpane/ui/reader/search_result.py +165 -0
- gitpane/ui/reader/source_text.py +134 -0
- gitpane/ui/reader/source_view.py +151 -0
- gitpane/ui/reader/syntax_span.py +67 -0
- gitpane/ui/reference/__init__.py +0 -0
- gitpane/ui/reference/branch_selector.py +106 -0
- gitpane/ui/reference/checkout_dialog.py +419 -0
- gitpane/ui/reference/checkout_revision_dialog.py +182 -0
- gitpane/ui/reference/commit_summary.py +339 -0
- gitpane/ui/reference/create_branch_dialog.py +346 -0
- gitpane/ui/reference/create_tag_dialog.py +283 -0
- gitpane/ui/reference/delete_local_branch_dialog.py +323 -0
- gitpane/ui/reference/delete_tag_dialog.py +394 -0
- gitpane/ui/reference/form_opening.py +53 -0
- gitpane/ui/reference/reference_dialog.py +417 -0
- gitpane/ui/reference/reference_feedback.py +62 -0
- gitpane/ui/reference/reference_form.py +139 -0
- gitpane/ui/reference/rename_branch_dialog.py +179 -0
- gitpane/ui/reference/reset_branch_dialog.py +430 -0
- gitpane/ui/reference/reset_dialog.py +263 -0
- gitpane/ui/reference/revision_dialog.py +121 -0
- gitpane/ui/reference/revision_picker.py +227 -0
- gitpane/ui/reference/select_branches_dialog.py +199 -0
- gitpane/ui/reference/stash_dialog.py +171 -0
- gitpane/ui/repository/__init__.py +0 -0
- gitpane/ui/repository/graph_delegate.py +298 -0
- gitpane/ui/repository/history_avatar.py +130 -0
- gitpane/ui/repository/history_model.py +179 -0
- gitpane/ui/repository/repository_list_popup.py +319 -0
- gitpane/ui/repository/repository_window.py +2580 -0
- gitpane/ui/resource/ArrowDown.png +0 -0
- gitpane/ui/resource/ArrowUp.png +0 -0
- gitpane/ui/resource/BranchCheckout.png +0 -0
- gitpane/ui/resource/BranchCreate.png +0 -0
- gitpane/ui/resource/BranchDelete.png +0 -0
- gitpane/ui/resource/CopyToClipboard.png +0 -0
- gitpane/ui/resource/GitPane-16.png +0 -0
- gitpane/ui/resource/GitPane-24.png +0 -0
- gitpane/ui/resource/GitPane-256.png +0 -0
- gitpane/ui/resource/GitPane-32.png +0 -0
- gitpane/ui/resource/GitPane-48.png +0 -0
- gitpane/ui/resource/GitPane-64.png +0 -0
- gitpane/ui/resource/GitPane.ico +0 -0
- gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
- gitpane/ui/resource/NotoColorEmoji_WindowsCompatible.ttf +0 -0
- gitpane/ui/resource/Renamed.png +0 -0
- gitpane/ui/resource/ResetAnotherBranchToHere.png +0 -0
- gitpane/ui/resource/ResetCurrentBranchToHere.png +0 -0
- gitpane/ui/resource/Stash.png +0 -0
- gitpane/ui/resource/Tag.png +0 -0
- gitpane/ui/resource/TagCreate.png +0 -0
- gitpane/ui/resource/TagDelete.png +0 -0
- gitpane/ui/resource/Unmerged.png +0 -0
- gitpane/ui/resource/checkout.png +0 -0
- gitpane/ui/sequencer/__init__.py +0 -0
- gitpane/ui/sequencer/conflict_dialog.py +532 -0
- gitpane/ui/sequencer/merge_recovery_dialog.py +67 -0
- gitpane/ui/sequencer/rebase_dialog.py +461 -0
- gitpane/ui/sequencer/rebase_editor_window.py +220 -0
- gitpane/ui/sequencer/replay_conflict_prompt.py +105 -0
- gitpane/ui/sequencer/replay_dialog.py +501 -0
- gitpane/ui/setting/__init__.py +0 -0
- gitpane/ui/setting/account_page.py +724 -0
- gitpane/ui/setting/add_account_dialog.py +1005 -0
- gitpane/ui/setting/credential_manager_dialog.py +151 -0
- gitpane/ui/setting/git_setting_widget.py +388 -0
- gitpane/ui/setting/setting_block.py +19 -0
- gitpane/ui/setting/setting_dialog.py +750 -0
- gitpane/ui/setting/ssh_widget.py +232 -0
- gitpane/ui/setting/utility_page.py +240 -0
- gitpane/ui/staging/__init__.py +0 -0
- gitpane/ui/staging/staging_action.py +356 -0
- gitpane/ui/staging/staging_window.py +745 -0
- gitpane/ui/transport/__init__.py +0 -0
- gitpane/ui/transport/clone_dialog.py +879 -0
- gitpane/ui/transport/push_dialog.py +580 -0
- gitpane/ui/transport/push_rejection_prompt.py +100 -0
- gitpane/ui/transport/push_tracking_question.py +88 -0
- gitpane/ui/transport/remote_dialog.py +943 -0
- gitpane/ui/welcome/__init__.py +0 -0
- gitpane/ui/welcome/invalid_repository_prompt.py +114 -0
- gitpane/ui/welcome/welcome_window.py +1137 -0
- gitpane/ui/widget/__init__.py +0 -0
- gitpane/ui/widget/busy_indicator.py +106 -0
- gitpane/ui/widget/commit_date_text.py +55 -0
- gitpane/ui/widget/content_growth.py +262 -0
- gitpane/ui/widget/content_measure.py +99 -0
- gitpane/ui/widget/dialog_layout.py +668 -0
- gitpane/ui/widget/drop_down_wheel_filter.py +32 -0
- gitpane/ui/widget/flow_layout.py +110 -0
- gitpane/ui/widget/long_text.py +542 -0
- gitpane/ui/widget/mnemonic.py +13 -0
- gitpane/ui/widget/numbered_text_edit.py +127 -0
- gitpane/ui/widget/plain_label.py +15 -0
- gitpane/ui/widget/plain_tooltip.py +19 -0
- gitpane/ui/widget/refresh_advice.py +25 -0
- gitpane/ui/widget/result_label.py +227 -0
- gitpane/ui/widget/result_status_bar.py +53 -0
- gitpane/ui/widget/scrolled_text.py +69 -0
- gitpane/ui/widget/shortcut_text.py +31 -0
- gitpane/ui/widget/standard_icon_label.py +55 -0
- gitpane/ui/widget/system_shortcut.py +75 -0
- gitpane/ui/widget/unavailable_reason.py +29 -0
- gitpane/ui/widget/wheel_safe_drop_down.py +24 -0
- gitpane/ui/window_control/__init__.py +0 -0
- gitpane/ui/window_control/button_focus.py +42 -0
- gitpane/ui/window_control/compositor_answer.py +152 -0
- gitpane/ui/window_control/compositor_growth_cap.py +73 -0
- gitpane/ui/window_control/desktop_activation.py +203 -0
- gitpane/ui/window_control/dialog_modality.py +122 -0
- gitpane/ui/window_control/item_activation.py +42 -0
- gitpane/ui/window_control/modal_focus_relay.py +124 -0
- gitpane/ui/window_control/wayland_activation.py +695 -0
- gitpane/ui/window_control/window_placement.py +305 -0
- gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
- gitpane/vendor/w32.exe +0 -0
- gitpane/vendor/w64-arm.exe +0 -0
- gitpane/vendor/w64.exe +0 -0
- gitpane/worktree/__init__.py +0 -0
- gitpane/worktree/command_sequence.py +110 -0
- gitpane/worktree/file_action_target.py +61 -0
- gitpane/worktree/file_menu_action.py +52 -0
- gitpane/worktree/historical_restore.py +361 -0
- gitpane/worktree/ignore_rule.py +266 -0
- gitpane/worktree/mutation.py +1539 -0
- gitpane/worktree/partial_patch.py +435 -0
- gitpane/worktree/staged_reset.py +447 -0
- gitpane/worktree/staging.py +1418 -0
- gitpane/worktree/trash.py +302 -0
- gitpane/worktree/worktree_file.py +406 -0
- gitpane/worktree/worktree_service.py +584 -0
- gitpane/worktree/worktree_snapshot.py +630 -0
- gitpane-1.0.0.dist-info/METADATA +141 -0
- gitpane-1.0.0.dist-info/RECORD +353 -0
- gitpane-1.0.0.dist-info/WHEEL +5 -0
- gitpane-1.0.0.dist-info/entry_points.txt +2 -0
- gitpane-1.0.0.dist-info/licenses/LICENSE +21 -0
- gitpane-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +109 -0
- gitpane-1.0.0.dist-info/licenses/src/gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
- gitpane-1.0.0.dist-info/licenses/src/gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
- 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())
|