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,378 @@
|
|
|
1
|
+
"""Present one commit in a scrolling panel: its exact text, or its explicit loading state."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from html import escape
|
|
9
|
+
from typing import TYPE_CHECKING, Self
|
|
10
|
+
|
|
11
|
+
from PySide6.QtCore import QEvent, Qt, Signal
|
|
12
|
+
from PySide6.QtGui import QColor, QImage, QPalette, QPixmap, QTextBlockFormat, QTextCursor
|
|
13
|
+
from PySide6.QtWidgets import QApplication, QGridLayout, QLabel, QPushButton, QScrollArea, QSizePolicy, QVBoxLayout, QWidget
|
|
14
|
+
|
|
15
|
+
from gitpane.config import LAYOUT_SPACING, MESSAGE_LINE_HEIGHT_PERCENT, SPACING_MD, SPACING_XS, SharedText
|
|
16
|
+
from gitpane.history.avatar import AvatarService, author_color, author_initials
|
|
17
|
+
from gitpane.ui.look.avatar_label import AvatarLabel
|
|
18
|
+
from gitpane.ui.look.fixed_font import body_sized_fixed_font
|
|
19
|
+
from gitpane.ui.reader.commit_message import CommitMessage
|
|
20
|
+
from gitpane.ui.widget.commit_date_text import format_commit_time
|
|
21
|
+
from gitpane.ui.widget.plain_label import plain_label
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
|
|
26
|
+
from PySide6.QtGui import QKeyEvent
|
|
27
|
+
|
|
28
|
+
from gitpane.history.repository_value import Commit, CommitLink, CommitRelation
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class CommitPanelText(Enum):
|
|
32
|
+
"""Texts the commit detail panel shows, one member per text."""
|
|
33
|
+
|
|
34
|
+
DETAIL_EMPTY = "Select a commit to view its details."
|
|
35
|
+
DETAIL_RETRY = "Retry reading"
|
|
36
|
+
MESSAGE_EMPTY = "Empty message"
|
|
37
|
+
AUTHOR_UNKNOWN = "Unknown author"
|
|
38
|
+
AUTHOR = "Author"
|
|
39
|
+
AUTHOR_DATE = "Author date"
|
|
40
|
+
COMMITTER_DATE = "Committer date"
|
|
41
|
+
PARENT_EMPTY = "No parents"
|
|
42
|
+
CHILD_EMPTY = "No children"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CopiedHash(Enum):
|
|
46
|
+
"""Which form of a commit hash is copied, with the text of its copy control and the message shown once it is copied."""
|
|
47
|
+
|
|
48
|
+
FULL = ("full", "Copy full hash", "Full hash copied.")
|
|
49
|
+
SHORT = ("short", "Copy short hash", "Short hash copied.")
|
|
50
|
+
|
|
51
|
+
def __new__(cls, value: str, copy_label: str, copied_message: str) -> Self:
|
|
52
|
+
"""Keep the name of the hash form as the value, and its two texts beside it."""
|
|
53
|
+
member = object.__new__(cls)
|
|
54
|
+
member._value_ = value
|
|
55
|
+
member.copy_label = copy_label
|
|
56
|
+
member.copied_message = copied_message
|
|
57
|
+
return member
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class HashButton(QPushButton):
|
|
61
|
+
"""Make a displayed hash a native, keyboard-activatable copy control."""
|
|
62
|
+
|
|
63
|
+
def __init__(self) -> None:
|
|
64
|
+
"""Use the native focus indicator and underline the copied value."""
|
|
65
|
+
super().__init__()
|
|
66
|
+
self.setFlat(True)
|
|
67
|
+
self.setStyleSheet("QPushButton { text-align: left; padding: 0; }")
|
|
68
|
+
font = self.font()
|
|
69
|
+
font.setUnderline(True)
|
|
70
|
+
self.setFont(font)
|
|
71
|
+
self.setCursor(Qt.CursorShape.PointingHandCursor)
|
|
72
|
+
self.setSizePolicy(QSizePolicy.Policy.Maximum, QSizePolicy.Policy.Fixed)
|
|
73
|
+
|
|
74
|
+
def keyPressEvent(self, event: QKeyEvent) -> None:
|
|
75
|
+
"""Support Enter outside dialogs alongside the native Space activation."""
|
|
76
|
+
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
|
|
77
|
+
self.click()
|
|
78
|
+
event.accept()
|
|
79
|
+
return
|
|
80
|
+
super().keyPressEvent(event)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class RelativeLabel(QLabel):
|
|
84
|
+
"""Expose an underlined relative link with the same keyboard activation as hashes."""
|
|
85
|
+
|
|
86
|
+
def keyPressEvent(self, event: QKeyEvent) -> None:
|
|
87
|
+
"""Activate the sole local target with Enter or Space, retaining native focus."""
|
|
88
|
+
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter, Qt.Key.Key_Space):
|
|
89
|
+
self.linkActivated.emit(str(self.property("oid")))
|
|
90
|
+
event.accept()
|
|
91
|
+
return
|
|
92
|
+
super().keyPressEvent(event)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class CommitPanel(QScrollArea):
|
|
96
|
+
"""Keep metadata, copy values and navigation bound to the visible commit."""
|
|
97
|
+
|
|
98
|
+
copy_requested = Signal(str, object)
|
|
99
|
+
navigation_requested = Signal(str)
|
|
100
|
+
retry_requested = Signal(str)
|
|
101
|
+
|
|
102
|
+
def __init__(self, clock: Callable[[], datetime] | None = None) -> None:
|
|
103
|
+
"""Build plain text fields and reserve a stable square for the author image."""
|
|
104
|
+
super().__init__()
|
|
105
|
+
self.clock = clock or (lambda: datetime.now().astimezone())
|
|
106
|
+
self.commit: Commit | None = None
|
|
107
|
+
self.oid: str | None = None
|
|
108
|
+
self.avatar_service: AvatarService | None = None
|
|
109
|
+
self.avatar_identity: object | None = None
|
|
110
|
+
self.setWidgetResizable(True)
|
|
111
|
+
content = QWidget()
|
|
112
|
+
layout = QVBoxLayout(content)
|
|
113
|
+
layout.setSpacing(LAYOUT_SPACING)
|
|
114
|
+
layout.setContentsMargins(LAYOUT_SPACING, LAYOUT_SPACING, LAYOUT_SPACING, LAYOUT_SPACING)
|
|
115
|
+
self.state_label = plain_label("")
|
|
116
|
+
self.avatar_label = AvatarLabel()
|
|
117
|
+
self.avatar_label.setTextFormat(Qt.TextFormat.PlainText)
|
|
118
|
+
self.avatar_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
119
|
+
self.avatar_label.setTextInteractionFlags(Qt.TextInteractionFlag.NoTextInteraction)
|
|
120
|
+
self.avatar_label.setAccessibleName("Author avatar")
|
|
121
|
+
self.author_label = plain_label("")
|
|
122
|
+
self.author_label.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Preferred)
|
|
123
|
+
self.date_label = plain_label("")
|
|
124
|
+
self.committer_date_label = plain_label("")
|
|
125
|
+
self.full_hash_button = HashButton()
|
|
126
|
+
self.short_hash_button = HashButton()
|
|
127
|
+
self.full_hash_button.setAccessibleName(CopiedHash.FULL.copy_label)
|
|
128
|
+
self.short_hash_button.setAccessibleName(CopiedHash.SHORT.copy_label)
|
|
129
|
+
self.full_hash_button.clicked.connect(lambda: self.copy_requested.emit(self.full_hash_button.text(), CopiedHash.FULL))
|
|
130
|
+
self.short_hash_button.clicked.connect(lambda: self.copy_requested.emit(self.short_hash_button.text(), CopiedHash.SHORT))
|
|
131
|
+
self.retry_button = QPushButton(CommitPanelText.DETAIL_RETRY.value)
|
|
132
|
+
self.retry_button.clicked.connect(lambda: self.retry_requested.emit(self.oid) if self.oid else None)
|
|
133
|
+
self.message_panel = CommitMessage()
|
|
134
|
+
self.message_edit = self.message_panel.text_edit
|
|
135
|
+
self.message_state_label = plain_label("")
|
|
136
|
+
self.parent_label = plain_label("")
|
|
137
|
+
self.child_label = plain_label("")
|
|
138
|
+
self.parent_links: list[QLabel] = []
|
|
139
|
+
self.child_links: list[QLabel] = []
|
|
140
|
+
self.relation_content = QWidget()
|
|
141
|
+
self.relation_layout = QVBoxLayout(self.relation_content)
|
|
142
|
+
self.relation_layout.setContentsMargins(0, 0, 0, 0)
|
|
143
|
+
self.relation_layout.setSpacing(LAYOUT_SPACING)
|
|
144
|
+
self.relation_layout.addWidget(self.parent_label)
|
|
145
|
+
self.relation_layout.addWidget(self.child_label)
|
|
146
|
+
layout.addWidget(self.state_label)
|
|
147
|
+
header_layout = QGridLayout()
|
|
148
|
+
header_layout.setVerticalSpacing(0)
|
|
149
|
+
header_layout.setHorizontalSpacing(SPACING_MD)
|
|
150
|
+
header_layout.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
151
|
+
avatar_column, label_column, value_column = range(3)
|
|
152
|
+
first_row = 0
|
|
153
|
+
self.metadata_values = [self.author_label, self.date_label, self.committer_date_label, self.full_hash_button, self.short_hash_button]
|
|
154
|
+
self.metadata_labels: list[QLabel] = []
|
|
155
|
+
titles = (CommitPanelText.AUTHOR.value, CommitPanelText.AUTHOR_DATE.value, CommitPanelText.COMMITTER_DATE.value, "Full hash", "Short hash")
|
|
156
|
+
for row, (title, value) in enumerate(zip(titles, self.metadata_values, strict=True)):
|
|
157
|
+
label = QLabel(title)
|
|
158
|
+
self.metadata_labels.append(label)
|
|
159
|
+
header_layout.addWidget(label, row, label_column)
|
|
160
|
+
header_layout.addWidget(value, row, value_column)
|
|
161
|
+
header_layout.addWidget(self.avatar_label, first_row, avatar_column, len(self.metadata_values), 1, Qt.AlignmentFlag.AlignTop)
|
|
162
|
+
header_layout.setColumnStretch(value_column, 1)
|
|
163
|
+
layout.addLayout(header_layout)
|
|
164
|
+
layout.addWidget(self.retry_button)
|
|
165
|
+
layout.addWidget(self.relation_content)
|
|
166
|
+
layout.addWidget(self.message_state_label)
|
|
167
|
+
layout.addWidget(self.message_panel, 1)
|
|
168
|
+
self.setWidget(content)
|
|
169
|
+
self.refresh_metrics()
|
|
170
|
+
self.clear_selection()
|
|
171
|
+
|
|
172
|
+
def refresh_metrics(self) -> None:
|
|
173
|
+
"""Fit compact metadata rows to system text and size the avatar to their combined height."""
|
|
174
|
+
body_font = QApplication.font()
|
|
175
|
+
self.message_edit.setFont(body_font)
|
|
176
|
+
fixed_font = body_sized_fixed_font()
|
|
177
|
+
fixed_font.setUnderline(True)
|
|
178
|
+
for button in (self.full_hash_button, self.short_hash_button):
|
|
179
|
+
button.setFont(fixed_font)
|
|
180
|
+
self.apply_link_color()
|
|
181
|
+
self.avatar_label.setFont(body_font)
|
|
182
|
+
row_height = max(self.author_label.fontMetrics().height(), self.full_hash_button.fontMetrics().height()) + SPACING_XS
|
|
183
|
+
for control in (*self.metadata_labels, *self.metadata_values):
|
|
184
|
+
control.setMinimumHeight(row_height)
|
|
185
|
+
# Flat copy links use text-height rows; native form-button padding would spread all five lines.
|
|
186
|
+
for button in (self.full_hash_button, self.short_hash_button):
|
|
187
|
+
button.setFixedHeight(row_height)
|
|
188
|
+
row_count = len(self.metadata_values)
|
|
189
|
+
avatar_side = row_count * row_height
|
|
190
|
+
if self.avatar_label.width() != avatar_side:
|
|
191
|
+
self.avatar_label.setFixedSize(avatar_side, avatar_side)
|
|
192
|
+
self.refresh_avatar()
|
|
193
|
+
|
|
194
|
+
def apply_link_color(self) -> None:
|
|
195
|
+
"""Draw the copyable hashes in the link color of the panel's palette, the same whether the window is active or not.
|
|
196
|
+
|
|
197
|
+
The color is copied into each hash button, so it is copied again whenever the panel's palette changes: a switch
|
|
198
|
+
between light and dark changes the link color, and the light blue is hard to read on the dark panel. The active
|
|
199
|
+
color is taken by name, since the palette's current group follows whether the window is active at that moment.
|
|
200
|
+
"""
|
|
201
|
+
link_color = self.palette().color(QPalette.ColorGroup.Active, QPalette.ColorRole.Link)
|
|
202
|
+
for button in (self.full_hash_button, self.short_hash_button):
|
|
203
|
+
palette = button.palette()
|
|
204
|
+
palette.setColor(QPalette.ColorRole.ButtonText, link_color)
|
|
205
|
+
button.setPalette(palette)
|
|
206
|
+
|
|
207
|
+
def changeEvent(self, event: QEvent) -> None:
|
|
208
|
+
"""Give the hashes the new link color when the panel's palette changes."""
|
|
209
|
+
super().changeEvent(event)
|
|
210
|
+
if event.type() == QEvent.Type.PaletteChange:
|
|
211
|
+
self.apply_link_color()
|
|
212
|
+
|
|
213
|
+
def clear_selection(self) -> None:
|
|
214
|
+
"""Remove every actionable value when no commit is selected."""
|
|
215
|
+
self.cancel_avatar()
|
|
216
|
+
self.commit = None
|
|
217
|
+
self.oid = None
|
|
218
|
+
self.state_label.show()
|
|
219
|
+
self.state_label.setText(CommitPanelText.DETAIL_EMPTY.value)
|
|
220
|
+
self.avatar_label.setAutoFillBackground(False)
|
|
221
|
+
for label in (self.author_label, self.date_label, self.committer_date_label, self.avatar_label, self.message_state_label, self.parent_label, self.child_label):
|
|
222
|
+
label.clear()
|
|
223
|
+
self.parent_label.show()
|
|
224
|
+
self.child_label.show()
|
|
225
|
+
for button in (self.full_hash_button, self.short_hash_button):
|
|
226
|
+
button.setText("")
|
|
227
|
+
button.setEnabled(False)
|
|
228
|
+
self.message_edit.clear()
|
|
229
|
+
self.retry_button.hide()
|
|
230
|
+
self.clear_relation()
|
|
231
|
+
|
|
232
|
+
def clear_relation(self) -> None:
|
|
233
|
+
"""Disable former relative links immediately when the active commit changes."""
|
|
234
|
+
for link in (*self.parent_links, *self.child_links):
|
|
235
|
+
self.relation_layout.removeWidget(link)
|
|
236
|
+
link.setEnabled(False)
|
|
237
|
+
link.hide()
|
|
238
|
+
link.deleteLater()
|
|
239
|
+
self.parent_links.clear()
|
|
240
|
+
self.child_links.clear()
|
|
241
|
+
|
|
242
|
+
def show_relation(self, relation: CommitRelation) -> None:
|
|
243
|
+
"""Show known relatives without confusing a partial index with no children."""
|
|
244
|
+
if relation.oid != self.oid or self.commit is None:
|
|
245
|
+
return
|
|
246
|
+
self.clear_relation()
|
|
247
|
+
self.parent_label.setText(CommitPanelText.PARENT_EMPTY.value)
|
|
248
|
+
self.parent_label.setVisible(not relation.parents)
|
|
249
|
+
self.child_label.setText(CommitPanelText.CHILD_EMPTY.value if relation.children_complete else SharedText.CHILD_LOADING.value)
|
|
250
|
+
self.child_label.setVisible(not relation.children_complete or not relation.child_links)
|
|
251
|
+
for relative in relation.parents:
|
|
252
|
+
self.parent_links.append(self._add_link(relative, "Parent"))
|
|
253
|
+
for relative in relation.child_links:
|
|
254
|
+
self.child_links.append(self._add_link(relative, "Child"))
|
|
255
|
+
previous = self.short_hash_button
|
|
256
|
+
for link in (*self.parent_links, *self.child_links):
|
|
257
|
+
QWidget.setTabOrder(previous, link)
|
|
258
|
+
previous = link
|
|
259
|
+
QWidget.setTabOrder(previous, self.message_edit)
|
|
260
|
+
|
|
261
|
+
def _add_link(self, relative: CommitLink, kind: str) -> QLabel:
|
|
262
|
+
"""Escape Git text inside a controlled local navigation link, never an external URL."""
|
|
263
|
+
label = RelativeLabel()
|
|
264
|
+
label.setWordWrap(True)
|
|
265
|
+
label.setTextFormat(Qt.TextFormat.RichText)
|
|
266
|
+
label.setTextInteractionFlags(Qt.TextInteractionFlag.LinksAccessibleByMouse | Qt.TextInteractionFlag.LinksAccessibleByKeyboard)
|
|
267
|
+
label.setOpenExternalLinks(False)
|
|
268
|
+
label.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
|
|
269
|
+
text = f"{relative.short_oid or relative.oid} — {relative.subject or 'Empty message'}"
|
|
270
|
+
label.setText(f'{kind} : <a href="{escape(relative.oid)}">{escape(text)}</a>')
|
|
271
|
+
label.setAccessibleName(f"{kind} : {text}")
|
|
272
|
+
label.setProperty("oid", relative.oid)
|
|
273
|
+
label.linkActivated.connect(lambda _target, oid=relative.oid: self.navigation_requested.emit(oid))
|
|
274
|
+
if kind == "Parent":
|
|
275
|
+
self.relation_layout.insertWidget(self.relation_layout.indexOf(self.child_label), label)
|
|
276
|
+
else:
|
|
277
|
+
self.relation_layout.addWidget(label)
|
|
278
|
+
label.show()
|
|
279
|
+
return label
|
|
280
|
+
|
|
281
|
+
def show_loading(self, oid: str, short_oid: str = "") -> None:
|
|
282
|
+
"""Retain only the current known hashes while fresh metadata is read."""
|
|
283
|
+
self.clear_selection()
|
|
284
|
+
self.oid = oid
|
|
285
|
+
self.full_hash_button.setText(oid)
|
|
286
|
+
self.full_hash_button.setEnabled(bool(oid))
|
|
287
|
+
self.short_hash_button.setText(short_oid)
|
|
288
|
+
self.short_hash_button.setEnabled(bool(short_oid))
|
|
289
|
+
self.state_label.setText(f"Loading commit {short_oid or oid}…")
|
|
290
|
+
self.child_label.setText(SharedText.CHILD_LOADING.value)
|
|
291
|
+
|
|
292
|
+
def show_error(self, oid: str, cause: str) -> None:
|
|
293
|
+
"""Keep the current hash available and offer an explicit retry after failure."""
|
|
294
|
+
if self.oid != oid:
|
|
295
|
+
return
|
|
296
|
+
short_oid = self.short_hash_button.text()
|
|
297
|
+
self.show_loading(oid, short_oid)
|
|
298
|
+
self.state_label.setText(f"Details unavailable — {cause}")
|
|
299
|
+
self.retry_button.show()
|
|
300
|
+
|
|
301
|
+
def show_commit(self, commit: Commit) -> None:
|
|
302
|
+
"""Publish all fields together only for the selection currently being read."""
|
|
303
|
+
if self.oid != commit.oid:
|
|
304
|
+
return
|
|
305
|
+
if commit.detail_error:
|
|
306
|
+
self.show_error(commit.oid, commit.detail_error)
|
|
307
|
+
return
|
|
308
|
+
self.commit = commit
|
|
309
|
+
self.state_label.clear()
|
|
310
|
+
self.state_label.hide()
|
|
311
|
+
self.retry_button.hide()
|
|
312
|
+
self.full_hash_button.setText(commit.oid)
|
|
313
|
+
self.short_hash_button.setText(commit.short_oid)
|
|
314
|
+
self.short_hash_button.setEnabled(bool(commit.short_oid))
|
|
315
|
+
author = commit.author_name or CommitPanelText.AUTHOR_UNKNOWN.value
|
|
316
|
+
self.author_label.setText(f"{author} <{commit.author_email}>" if commit.author_email else author)
|
|
317
|
+
self.refresh_avatar()
|
|
318
|
+
self.message_edit.setPlainText(commit.message)
|
|
319
|
+
cursor = QTextCursor(self.message_edit.document())
|
|
320
|
+
cursor.select(QTextCursor.SelectionType.Document)
|
|
321
|
+
paragraph_format = QTextBlockFormat()
|
|
322
|
+
paragraph_format.setLineHeight(MESSAGE_LINE_HEIGHT_PERCENT, QTextBlockFormat.LineHeightTypes.ProportionalHeight.value)
|
|
323
|
+
cursor.mergeBlockFormat(paragraph_format)
|
|
324
|
+
self.message_state_label.setText("" if commit.message else CommitPanelText.MESSAGE_EMPTY.value)
|
|
325
|
+
self.message_state_label.setVisible(not commit.message)
|
|
326
|
+
self.refresh_date()
|
|
327
|
+
|
|
328
|
+
def set_avatar_service(self, service: AvatarService) -> None:
|
|
329
|
+
"""Connect the shared photo service while retaining a standalone offline panel."""
|
|
330
|
+
self.cancel_avatar()
|
|
331
|
+
if self.avatar_service is not None:
|
|
332
|
+
self.avatar_service.image_ready.disconnect(self._image_ready)
|
|
333
|
+
self.avatar_service = service
|
|
334
|
+
service.image_ready.connect(self._image_ready)
|
|
335
|
+
self.refresh_avatar()
|
|
336
|
+
|
|
337
|
+
def cancel_avatar(self) -> None:
|
|
338
|
+
"""Invalidate the current consumer before selection or window lifetime changes."""
|
|
339
|
+
identity = self.avatar_identity
|
|
340
|
+
self.avatar_identity = None
|
|
341
|
+
if self.avatar_service is not None and identity is not None:
|
|
342
|
+
self.avatar_service.cancel(identity)
|
|
343
|
+
|
|
344
|
+
def refresh_avatar(self, device_pixel_ratio: float | None = None) -> None:
|
|
345
|
+
"""Rebuild the photo identity after author, selection or display density changes."""
|
|
346
|
+
self.cancel_avatar()
|
|
347
|
+
if self.commit is None:
|
|
348
|
+
return
|
|
349
|
+
palette = self.avatar_label.palette()
|
|
350
|
+
palette.setColor(QPalette.ColorRole.Window, QColor(author_color(self.commit.author_name, self.commit.author_email)))
|
|
351
|
+
palette.setColor(QPalette.ColorRole.WindowText, QColor("white"))
|
|
352
|
+
self.avatar_label.setPalette(palette)
|
|
353
|
+
self.avatar_label.setAutoFillBackground(True)
|
|
354
|
+
if self.avatar_label.pixmap().isNull():
|
|
355
|
+
self.avatar_label.setText(author_initials(self.commit.author_name))
|
|
356
|
+
self.avatar_label.setAccessibleName(f"Avatar for {self.commit.author_name or 'unknown author'}")
|
|
357
|
+
if self.avatar_service is None:
|
|
358
|
+
return
|
|
359
|
+
ratio = self.devicePixelRatioF() if device_pixel_ratio is None else device_pixel_ratio
|
|
360
|
+
avatar_side = self.avatar_label.width()
|
|
361
|
+
self.avatar_identity = (uuid.uuid4().hex, self.oid, self.commit.author_email, avatar_side, ratio)
|
|
362
|
+
self.avatar_service.request(self.commit.author_email, avatar_side, ratio, self.avatar_identity)
|
|
363
|
+
|
|
364
|
+
def _image_ready(self, identity: object, image: QImage) -> None:
|
|
365
|
+
"""Ignore stale author and screen responses without altering surrounding text."""
|
|
366
|
+
if identity != self.avatar_identity or self.commit is None or image.isNull():
|
|
367
|
+
return
|
|
368
|
+
pixmap = QPixmap.fromImage(image)
|
|
369
|
+
pixmap.setDevicePixelRatio(max(image.width(), image.height()) / self.avatar_label.width())
|
|
370
|
+
self.avatar_label.setPixmap(pixmap)
|
|
371
|
+
|
|
372
|
+
def refresh_date(self) -> None:
|
|
373
|
+
"""Update relative labels without replacing the message or changing its scroll."""
|
|
374
|
+
if self.commit is None:
|
|
375
|
+
return
|
|
376
|
+
now = self.clock()
|
|
377
|
+
self.date_label.setText(format_commit_time(self.commit, self.commit.author_timestamp, now))
|
|
378
|
+
self.committer_date_label.setText(format_commit_time(self.commit, self.commit.committer_timestamp, now))
|