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,501 @@
|
|
|
1
|
+
"""Present the Cherry-pick or Revert form of a captured commit, from its metadata to its explicit replay choices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from PySide6.QtCore import QEvent, QMargins, QSize, Qt, Signal
|
|
10
|
+
from PySide6.QtGui import QColor, QPainter, QPalette
|
|
11
|
+
from PySide6.QtWidgets import (
|
|
12
|
+
QAbstractItemView,
|
|
13
|
+
QApplication,
|
|
14
|
+
QCheckBox,
|
|
15
|
+
QDialog,
|
|
16
|
+
QHBoxLayout,
|
|
17
|
+
QHeaderView,
|
|
18
|
+
QLabel,
|
|
19
|
+
QPushButton,
|
|
20
|
+
QTableWidget,
|
|
21
|
+
QTableWidgetItem,
|
|
22
|
+
QVBoxLayout,
|
|
23
|
+
QWidget,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from gitpane.config import ICON_SIDE, REPLAY_DIALOG_HEIGHT, REPLAY_DIALOG_WIDTH, keep_selection_visible
|
|
27
|
+
from gitpane.sequencer.replay import MERGE_PARENT_MINIMUM
|
|
28
|
+
from gitpane.sequencer.replay_state import ReplayKind
|
|
29
|
+
from gitpane.ui.look.action_icon import action_icon
|
|
30
|
+
from gitpane.ui.look.theme_color import is_dark_palette, theme_line_color
|
|
31
|
+
from gitpane.ui.look.theme_look import follow_theme
|
|
32
|
+
from gitpane.ui.look.theme_palette import PALETTE_COLOR_GROUPS
|
|
33
|
+
from gitpane.ui.look.windows_control_look import WINDOWS_LIST_BORDER_LIGHT_COLOR, apply_windows_button_look
|
|
34
|
+
from gitpane.ui.reference.commit_summary import CommitSummary
|
|
35
|
+
from gitpane.ui.reference.revision_picker import REVISION_BUTTON_SIZE, REVISION_CHOICE_NAME
|
|
36
|
+
from gitpane.ui.widget.content_growth import ContentGrowthMixin, OpeningFit
|
|
37
|
+
from gitpane.ui.widget.content_measure import LayoutContentMeasure
|
|
38
|
+
from gitpane.ui.widget.dialog_layout import BandButtonSizing, fit_banded_form, fit_capture_control, install_banded_layout
|
|
39
|
+
from gitpane.ui.widget.long_text import WrappingLabel
|
|
40
|
+
from gitpane.ui.widget.scrolled_text import ScrolledText
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from collections.abc import Callable
|
|
44
|
+
|
|
45
|
+
from PySide6.QtGui import QCloseEvent, QKeyEvent, QPaintEvent
|
|
46
|
+
from PySide6.QtWidgets import QLayout
|
|
47
|
+
|
|
48
|
+
from gitpane.sequencer.replay import ReplayIntent, ReplayParent
|
|
49
|
+
from gitpane.ui.reference.revision_dialog import RevisionChoiceDialog
|
|
50
|
+
|
|
51
|
+
PARENT_NUMBER_COLUMN, PARENT_MESSAGE_COLUMN, PARENT_AUTHOR_COLUMN, PARENT_DATE_COLUMN = range(4)
|
|
52
|
+
PARENT_COLUMN_COUNT = 4
|
|
53
|
+
# Selected row color of the merge parent list in the Git Extensions capture (light theme).
|
|
54
|
+
PARENT_SELECTION_LIGHT_COLOR = "#cde8ff"
|
|
55
|
+
# The merge parent list of the captures: a fixed 78 px list (two parents and some empty space, more parents scroll),
|
|
56
|
+
# a 25 px header, 19 px rows, fixed number, author and date columns, and text starting a few pixels into each column.
|
|
57
|
+
PARENT_TABLE_HEIGHT = 78
|
|
58
|
+
PARENT_HEADER_HEIGHT = 25
|
|
59
|
+
PARENT_ROW_HEIGHT = 19
|
|
60
|
+
PARENT_FIXED_COLUMN_WIDTH_BY_COLUMN = {PARENT_NUMBER_COLUMN: 44, PARENT_AUTHOR_COLUMN: 120, PARENT_DATE_COLUMN: 81}
|
|
61
|
+
PARENT_HEADER_TEXT_PADDING = 7
|
|
62
|
+
PARENT_CELL_TEXT_PADDING = 3
|
|
63
|
+
# Qt centres header text one pixel lower than the Windows list header does.
|
|
64
|
+
PARENT_HEADER_TEXT_LIFT = 1
|
|
65
|
+
PARENT_TABLE_FRAME_WIDTH = 1
|
|
66
|
+
# Windows list colors sampled on the captures: list border, header column separators, and the faint accent-blue column
|
|
67
|
+
# lines of the rows (8 % opaque, so they read #ebf4fe on white and a little darker on the selected row).
|
|
68
|
+
PARENT_TABLE_FRAME_LIGHT_COLOR = WINDOWS_LIST_BORDER_LIGHT_COLOR
|
|
69
|
+
PARENT_HEADER_LINE_LIGHT_COLOR = "#e5e5e5"
|
|
70
|
+
PARENT_COLUMN_LINE_LIGHT_COLOR = "#140576f2"
|
|
71
|
+
# Body of the forms, measured on the captures in client-area pixels: check boxes 14 px from the left, heading 11 px from
|
|
72
|
+
# the top, commit frame ending 15 px from the right, 12 px under the last check box row.
|
|
73
|
+
FORM_BODY_MARGINS = QMargins(14, 11, 15, 12)
|
|
74
|
+
HEADING_INDENT = 3
|
|
75
|
+
HEADING_TO_FRAME_SPACE = 2
|
|
76
|
+
# The commit frame is indented about 10 px from the heading text.
|
|
77
|
+
FRAME_INDENT = 13
|
|
78
|
+
FRAME_TO_REVISION_SPACE = 11
|
|
79
|
+
REVISION_ROW_RIGHT_INDENT = 4
|
|
80
|
+
REVISION_CAPTION_TO_BUTTON_SPACE = 11
|
|
81
|
+
# Space before the next control (merge parent caption or check boxes), after the frame or after the revision row.
|
|
82
|
+
SPACE_AFTER_FRAME = 3
|
|
83
|
+
SPACE_AFTER_REVISION_ROW = 6
|
|
84
|
+
PARENT_CAPTION_INDENT = 3
|
|
85
|
+
PARENT_CAPTION_TO_TABLE_SPACE = 6
|
|
86
|
+
PARENT_TABLE_INDENT = 3
|
|
87
|
+
PARENT_TABLE_TO_CHECK_SPACE = 5
|
|
88
|
+
CHECK_BLOCK_TOP_SPACE = 1
|
|
89
|
+
CHECK_ROW_HEIGHT = 25
|
|
90
|
+
# Windows Forms button sizes of the captures: the submit buttons are wider than the standard Abort button.
|
|
91
|
+
SUBMIT_BUTTON_WIDTH_BY_KIND: dict[ReplayKind, int] = {ReplayKind.CHERRY_PICK: 107, ReplayKind.REVERT: 115}
|
|
92
|
+
CANCEL_BUTTON_WIDTH = 73
|
|
93
|
+
FORM_BUTTON_HEIGHT = 23
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def indented(widget: QWidget, left: int, right: int = 0) -> QHBoxLayout:
|
|
97
|
+
"""Wrap one control in a row that moves it in from the body edges, as the captured window positions it."""
|
|
98
|
+
row = QHBoxLayout()
|
|
99
|
+
row.setContentsMargins(left, 0, right, 0)
|
|
100
|
+
row.setSpacing(0)
|
|
101
|
+
row.addWidget(widget)
|
|
102
|
+
return row
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ParentTable(QTableWidget):
|
|
106
|
+
"""Merge parent list drawn like the Windows list view of Git Extensions, with faint column lines down to its bottom."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, rows: int, columns: int) -> None:
|
|
109
|
+
"""Start without column lines; the form sets their color once it knows the theme."""
|
|
110
|
+
super().__init__(rows, columns)
|
|
111
|
+
self.column_line_color: QColor | None = None
|
|
112
|
+
|
|
113
|
+
def paintEvent(self, event: QPaintEvent) -> None:
|
|
114
|
+
"""Paint the rows, then the column lines over the whole list height, empty area included."""
|
|
115
|
+
super().paintEvent(event)
|
|
116
|
+
if self.column_line_color is None:
|
|
117
|
+
return
|
|
118
|
+
painter = QPainter(self.viewport())
|
|
119
|
+
painter.setPen(self.column_line_color)
|
|
120
|
+
header = self.horizontalHeader()
|
|
121
|
+
last_column = self.columnCount() - 1
|
|
122
|
+
for column in range(last_column):
|
|
123
|
+
x = header.sectionViewportPosition(column) + header.sectionSize(column) - 1
|
|
124
|
+
painter.drawLine(x, 0, x, self.viewport().height())
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ReplayDialog(ContentGrowthMixin, QDialog):
|
|
128
|
+
"""Collect one source and its explicit choices, then hand the launched operation over to the console.
|
|
129
|
+
|
|
130
|
+
The window opens at exactly the height of its content, and its height then follows that content both ways by the
|
|
131
|
+
common growth rule: an error or a merge's parent list grows it, and their leaving brings it back to its opening
|
|
132
|
+
height, or to the height the user set by hand.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
replay_requested = Signal(object)
|
|
136
|
+
revision_requested = Signal()
|
|
137
|
+
|
|
138
|
+
def __init__(self, session_id: str, source_oid: str, kind: ReplayKind, parent: QWidget | None = None, *, clock: Callable[[], datetime] | None = None) -> None:
|
|
139
|
+
"""Build the compact commit form with fresh defaults on each opening.
|
|
140
|
+
|
|
141
|
+
``clock`` goes to every commit box the form builds, so the relative commit date can be reproduced, as the window
|
|
142
|
+
gallery needs; the commit boxes read the system time when it is absent.
|
|
143
|
+
"""
|
|
144
|
+
super().__init__(parent)
|
|
145
|
+
self.clock = clock
|
|
146
|
+
self.session_id = session_id
|
|
147
|
+
self.source_oid = source_oid
|
|
148
|
+
self.kind = kind
|
|
149
|
+
self.intent: ReplayIntent | None = None
|
|
150
|
+
self.closed = False
|
|
151
|
+
self.busy = False
|
|
152
|
+
self.completed = False
|
|
153
|
+
self.metadata_ready = False
|
|
154
|
+
self.parents: tuple[ReplayParent, ...] = ()
|
|
155
|
+
self.source_generation = 0
|
|
156
|
+
self.revision_dialog: RevisionChoiceDialog | None = None
|
|
157
|
+
cherry_pick = kind == ReplayKind.CHERRY_PICK
|
|
158
|
+
self.setWindowTitle("Cherry pick commit" if cherry_pick else "Revert commit")
|
|
159
|
+
# The title bar keeps the GitPane icon every window shares, and no help button since there is no help to open.
|
|
160
|
+
self.setWindowFlag(Qt.WindowType.WindowContextHelpButtonHint, False)
|
|
161
|
+
# The width follows the captures; the height is fitted to the content once the form is built, then follows it.
|
|
162
|
+
self.resize(REPLAY_DIALOG_WIDTH, REPLAY_DIALOG_HEIGHT)
|
|
163
|
+
# Every distance below is measured on the Git Extensions window of the same name, so the layout adds explicit
|
|
164
|
+
# spaces instead of a uniform spacing.
|
|
165
|
+
layout = QVBoxLayout()
|
|
166
|
+
layout.setSpacing(0)
|
|
167
|
+
self.body_layout = layout
|
|
168
|
+
self.heading_label = QLabel("Cherry pick this commit:" if cherry_pick else "Revert this commit:")
|
|
169
|
+
layout.addLayout(indented(self.heading_label, HEADING_INDENT))
|
|
170
|
+
layout.addSpacing(HEADING_TO_FRAME_SPACE)
|
|
171
|
+
# The commit box reproduces the Git Extensions one: short hash in the frame line, colored reference bands.
|
|
172
|
+
self.summary = CommitSummary(source_oid, git_extensions_look=True, clock=clock)
|
|
173
|
+
self.summary_row = indented(self.summary, FRAME_INDENT)
|
|
174
|
+
layout.addLayout(self.summary_row)
|
|
175
|
+
self.revision_label = None
|
|
176
|
+
self.revision_button = None
|
|
177
|
+
if cherry_pick:
|
|
178
|
+
layout.addSpacing(FRAME_TO_REVISION_SPACE)
|
|
179
|
+
layout.addLayout(self._revision_row())
|
|
180
|
+
layout.addSpacing(SPACE_AFTER_REVISION_ROW)
|
|
181
|
+
else:
|
|
182
|
+
layout.addSpacing(SPACE_AFTER_FRAME)
|
|
183
|
+
self.parent_label = QLabel("This commit is a merge, select parent:")
|
|
184
|
+
self.parent_table = ParentTable(0, PARENT_COLUMN_COUNT)
|
|
185
|
+
self.parent_table.setHorizontalHeaderLabels(["No.", "Message", "Author", "Date"])
|
|
186
|
+
self.parent_table.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows)
|
|
187
|
+
self.parent_table.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection)
|
|
188
|
+
self.parent_table.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers)
|
|
189
|
+
self.parent_table.setSortingEnabled(False)
|
|
190
|
+
self.parent_table.setAccessibleName("Merge parent")
|
|
191
|
+
self._use_capture_list_look()
|
|
192
|
+
self.apply_theme_look()
|
|
193
|
+
self.parent_section = QWidget()
|
|
194
|
+
parent_section_layout = QVBoxLayout(self.parent_section)
|
|
195
|
+
parent_section_layout.setContentsMargins(0, 0, 0, PARENT_TABLE_TO_CHECK_SPACE)
|
|
196
|
+
parent_section_layout.setSpacing(0)
|
|
197
|
+
parent_section_layout.addLayout(indented(self.parent_label, PARENT_CAPTION_INDENT))
|
|
198
|
+
parent_section_layout.addSpacing(PARENT_CAPTION_TO_TABLE_SPACE)
|
|
199
|
+
parent_section_layout.addLayout(indented(self.parent_table, PARENT_TABLE_INDENT, PARENT_TABLE_INDENT))
|
|
200
|
+
layout.addWidget(self.parent_section)
|
|
201
|
+
self.parent_label.hide()
|
|
202
|
+
self.parent_table.hide()
|
|
203
|
+
self.parent_section.hide()
|
|
204
|
+
self.parent_table.itemSelectionChanged.connect(self._parent_selected)
|
|
205
|
+
layout.addSpacing(CHECK_BLOCK_TOP_SPACE)
|
|
206
|
+
self.create_commit_check = QCheckBox("Automatically create a commit")
|
|
207
|
+
self.create_commit_check.setChecked(True)
|
|
208
|
+
self.add_reference_check = None
|
|
209
|
+
checks = [self.create_commit_check]
|
|
210
|
+
if cherry_pick:
|
|
211
|
+
self.add_reference_check = QCheckBox("Add commit reference to commit message")
|
|
212
|
+
checks.append(self.add_reference_check)
|
|
213
|
+
for check in checks:
|
|
214
|
+
# Windows Forms places these check boxes 25 px apart; a fixed row height keeps that pitch.
|
|
215
|
+
check.setFixedHeight(CHECK_ROW_HEIGHT)
|
|
216
|
+
layout.addWidget(check)
|
|
217
|
+
self.status_label = WrappingLabel("")
|
|
218
|
+
self.status_label.setWordWrap(True)
|
|
219
|
+
# A cause read from Git can be longer than the screen: it scrolls past the window's growth cap, so the buttons
|
|
220
|
+
# under it stay inside the window.
|
|
221
|
+
self.status_area = ScrolledText(self.status_label)
|
|
222
|
+
layout.addWidget(self.status_area)
|
|
223
|
+
self.status_area.hide()
|
|
224
|
+
self.submit_button = QPushButton("Cherry pick" if cherry_pick else "Revert this commit")
|
|
225
|
+
self.submit_button.setEnabled(False)
|
|
226
|
+
self.cancel_button = QPushButton("Abort")
|
|
227
|
+
# Only the submit button is the default one. A focused auto-default button would take that role over, and
|
|
228
|
+
# then Enter pressed from a check box would abort instead of running the replay; keyPressEvent below presses a
|
|
229
|
+
# focused button on Enter instead, as Windows does.
|
|
230
|
+
for button in (self.revision_button, self.cancel_button):
|
|
231
|
+
if button is not None:
|
|
232
|
+
button.setAutoDefault(False)
|
|
233
|
+
# Git Extensions shows these forms as a white body over a grey band holding the buttons; the form reproduces it.
|
|
234
|
+
self.button_band = install_banded_layout(self, layout, [self.submit_button, self.cancel_button], body_margins=FORM_BODY_MARGINS, button_sizing=BandButtonSizing.WINDOW)
|
|
235
|
+
# Sized once the band gave the buttons their look, which changes the room their text needs.
|
|
236
|
+
self._fit_band_buttons()
|
|
237
|
+
# Like Git Extensions, Enter runs the submit button; Qt ignores Enter while that default button is disabled,
|
|
238
|
+
# so a merge without a chosen parent cannot be submitted from the keyboard either. The role is set only now,
|
|
239
|
+
# once the button belongs to the dialog: Qt then records it as the dialog's main default and gives it back
|
|
240
|
+
# whenever the focus moves, instead of dropping it the first time the button loses the focus.
|
|
241
|
+
self.submit_button.setDefault(True)
|
|
242
|
+
self.submit_button.clicked.connect(self._submit)
|
|
243
|
+
self.cancel_button.clicked.connect(self.reject)
|
|
244
|
+
self.finished.connect(self._closed)
|
|
245
|
+
follow_theme(self, self.apply_theme_look)
|
|
246
|
+
fit_banded_form(self)
|
|
247
|
+
self.install_content_growth(LayoutContentMeasure(self), OpeningFit.EXACT, self.height())
|
|
248
|
+
|
|
249
|
+
def apply_theme_look(self) -> None:
|
|
250
|
+
"""Lay everything of the form's own look that depends on the light or dark appearance; the one place that lays it again after a switch.
|
|
251
|
+
|
|
252
|
+
The revision button takes or loses its light sheet, and the merge parent list takes the frame, header, column
|
|
253
|
+
line and selection colors of the new appearance. The chosen parent, the check boxes and the commit stay as they
|
|
254
|
+
are; the commit frame already paints itself from the palette.
|
|
255
|
+
"""
|
|
256
|
+
if self.revision_button is not None:
|
|
257
|
+
apply_windows_button_look(self.revision_button)
|
|
258
|
+
# Workaround: a list with a style sheet gets back, when its sheet changes, the palette it had before that sheet
|
|
259
|
+
# was laid, and the sheet copies the list's palette to its viewport. Changing the sheet alone would bring the
|
|
260
|
+
# old selection colors back, and laying them after the sheet would leave the old ones on the viewport. The old
|
|
261
|
+
# sheet is taken off first, then the selection colors are laid, then the new sheet copies them to the viewport.
|
|
262
|
+
self.parent_table.setStyleSheet("")
|
|
263
|
+
self._use_capture_selection_color()
|
|
264
|
+
self._apply_parent_table_colors()
|
|
265
|
+
|
|
266
|
+
def _fit_band_buttons(self) -> None:
|
|
267
|
+
"""Give the submit and Abort buttons the capture's sizes, or more when their text needs it in the current font.
|
|
268
|
+
|
|
269
|
+
The band's height follows the buttons, so a taller button keeps the same space above and below it.
|
|
270
|
+
"""
|
|
271
|
+
fit_capture_control(self.submit_button, QSize(SUBMIT_BUTTON_WIDTH_BY_KIND[self.kind], FORM_BUTTON_HEIGHT))
|
|
272
|
+
fit_capture_control(self.cancel_button, QSize(CANCEL_BUTTON_WIDTH, FORM_BUTTON_HEIGHT))
|
|
273
|
+
|
|
274
|
+
def changeEvent(self, event: QEvent) -> None:
|
|
275
|
+
"""Size the band's buttons again when the text size or the style changes."""
|
|
276
|
+
super().changeEvent(event)
|
|
277
|
+
# A style or font change can arrive while the form is still being built, before its band holds the buttons.
|
|
278
|
+
if event.type() in (QEvent.Type.FontChange, QEvent.Type.StyleChange) and hasattr(self, "button_band"):
|
|
279
|
+
self._fit_band_buttons()
|
|
280
|
+
|
|
281
|
+
def _revision_row(self) -> QLayout:
|
|
282
|
+
"""Build the right-aligned "Choose another revision:" caption followed by its small icon button."""
|
|
283
|
+
self.revision_label = QLabel(REVISION_CHOICE_NAME + ":")
|
|
284
|
+
self.revision_label.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
|
285
|
+
self.revision_button = QPushButton()
|
|
286
|
+
self.revision_button.setIcon(action_icon("choose_revision", self.devicePixelRatioF()))
|
|
287
|
+
self.revision_button.setIconSize(QSize(ICON_SIDE, ICON_SIDE))
|
|
288
|
+
self.revision_button.setFixedSize(REVISION_BUTTON_SIZE)
|
|
289
|
+
self.revision_button.setAccessibleName(REVISION_CHOICE_NAME)
|
|
290
|
+
self.revision_button.setToolTip(REVISION_CHOICE_NAME)
|
|
291
|
+
self.revision_label.setBuddy(self.revision_button)
|
|
292
|
+
self.revision_button.clicked.connect(self.revision_requested)
|
|
293
|
+
self.revision_button.setEnabled(False)
|
|
294
|
+
row = QHBoxLayout()
|
|
295
|
+
row.setContentsMargins(0, 0, REVISION_ROW_RIGHT_INDENT, 0)
|
|
296
|
+
row.setSpacing(REVISION_CAPTION_TO_BUTTON_SPACE)
|
|
297
|
+
row.addWidget(self.revision_label, 1)
|
|
298
|
+
row.addWidget(self.revision_button)
|
|
299
|
+
return row
|
|
300
|
+
|
|
301
|
+
def _use_capture_list_look(self) -> None:
|
|
302
|
+
"""Give the merge parent list the layout of the Windows list view of the captures: no grid, fixed header and row heights, fixed columns."""
|
|
303
|
+
table = self.parent_table
|
|
304
|
+
table.setShowGrid(False)
|
|
305
|
+
# Long authors and messages are cut character by character ("Alexander Mordvi...") like the Windows list, not at a word.
|
|
306
|
+
table.setWordWrap(False)
|
|
307
|
+
table.setVerticalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
|
|
308
|
+
table.setFixedHeight(PARENT_TABLE_HEIGHT)
|
|
309
|
+
rows = table.verticalHeader()
|
|
310
|
+
rows.hide()
|
|
311
|
+
rows.setSectionResizeMode(QHeaderView.ResizeMode.Fixed)
|
|
312
|
+
rows.setMinimumSectionSize(PARENT_ROW_HEIGHT)
|
|
313
|
+
rows.setDefaultSectionSize(PARENT_ROW_HEIGHT)
|
|
314
|
+
header = table.horizontalHeader()
|
|
315
|
+
header.setFixedHeight(PARENT_HEADER_HEIGHT)
|
|
316
|
+
header.setDefaultAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
|
|
317
|
+
header.setHighlightSections(False)
|
|
318
|
+
header.setStretchLastSection(False)
|
|
319
|
+
header.setSectionResizeMode(PARENT_MESSAGE_COLUMN, QHeaderView.ResizeMode.Stretch)
|
|
320
|
+
for column, width in PARENT_FIXED_COLUMN_WIDTH_BY_COLUMN.items():
|
|
321
|
+
header.setSectionResizeMode(column, QHeaderView.ResizeMode.Fixed)
|
|
322
|
+
header.resizeSection(column, width)
|
|
323
|
+
|
|
324
|
+
def _apply_parent_table_colors(self) -> None:
|
|
325
|
+
"""Give the merge parent list the colors of the captures in a light theme, lines in the theme's line grey in a dark one.
|
|
326
|
+
|
|
327
|
+
In a dark theme the frame, the header lines and the column lines all take the theme's line grey, worked out from
|
|
328
|
+
the application palette like every other line of the window, so the list's columns stay visible. Every color
|
|
329
|
+
comes from the application palette, which already holds the new colors when the appearance has just changed.
|
|
330
|
+
"""
|
|
331
|
+
table = self.parent_table
|
|
332
|
+
palette = QApplication.palette()
|
|
333
|
+
base, text = palette.color(QPalette.ColorRole.Base), palette.color(QPalette.ColorRole.Text)
|
|
334
|
+
if is_dark_palette(palette):
|
|
335
|
+
frame = header_line = theme_line_color(palette)
|
|
336
|
+
table.column_line_color = frame
|
|
337
|
+
else:
|
|
338
|
+
frame, header_line = QColor(PARENT_TABLE_FRAME_LIGHT_COLOR), QColor(PARENT_HEADER_LINE_LIGHT_COLOR)
|
|
339
|
+
table.column_line_color = QColor(PARENT_COLUMN_LINE_LIGHT_COLOR)
|
|
340
|
+
# The sheet only styles the list frame and the header sections; rows keep the palette, including the selection
|
|
341
|
+
# colors set just after.
|
|
342
|
+
# No focus outline either: the Windows list shows none on an unselected row, and the selection already marks
|
|
343
|
+
# the chosen parent.
|
|
344
|
+
table.setStyleSheet(f"QTableWidget {{ border: {PARENT_TABLE_FRAME_WIDTH}px solid {frame.name()}; background-color: {base.name()}; outline: 0; }}"
|
|
345
|
+
f" QTableWidget::item {{ padding-left: {PARENT_CELL_TEXT_PADDING}px; }}"
|
|
346
|
+
f" QHeaderView::section {{ background-color: {base.name()}; color: {text.name()}; border: none;"
|
|
347
|
+
f" border-right: {PARENT_TABLE_FRAME_WIDTH}px solid {header_line.name()}; padding-left: {PARENT_HEADER_TEXT_PADDING}px;"
|
|
348
|
+
f" padding-bottom: {PARENT_HEADER_TEXT_LIFT}px; }}")
|
|
349
|
+
|
|
350
|
+
def _use_capture_selection_color(self) -> None:
|
|
351
|
+
"""Select merge parents with the Git Extensions blue in a light theme, the system highlight in a dark one.
|
|
352
|
+
|
|
353
|
+
Fusion's light highlight is a saturated blue under white text; the captures use a pale blue under the normal
|
|
354
|
+
text color, in both active and inactive windows. A dark palette keeps its own readable highlight, kept visible
|
|
355
|
+
when the list does not have the focus. The selection colors of the application palette are laid first, so a
|
|
356
|
+
switch from one appearance to the other leaves none of the old ones behind.
|
|
357
|
+
"""
|
|
358
|
+
application_palette = QApplication.palette()
|
|
359
|
+
palette = self.parent_table.palette()
|
|
360
|
+
for group in PALETTE_COLOR_GROUPS:
|
|
361
|
+
for role in (QPalette.ColorRole.Highlight, QPalette.ColorRole.HighlightedText):
|
|
362
|
+
palette.setColor(group, role, application_palette.color(group, role))
|
|
363
|
+
if is_dark_palette(application_palette):
|
|
364
|
+
self.parent_table.setPalette(palette)
|
|
365
|
+
keep_selection_visible(self.parent_table)
|
|
366
|
+
return
|
|
367
|
+
for group in (QPalette.ColorGroup.Active, QPalette.ColorGroup.Inactive):
|
|
368
|
+
palette.setColor(group, QPalette.ColorRole.Highlight, QColor(PARENT_SELECTION_LIGHT_COLOR))
|
|
369
|
+
palette.setColor(group, QPalette.ColorRole.HighlightedText, application_palette.color(group, QPalette.ColorRole.Text))
|
|
370
|
+
self.parent_table.setPalette(palette)
|
|
371
|
+
|
|
372
|
+
def _show_status(self, text: str) -> None:
|
|
373
|
+
"""Show a cause under the choices, and give its space back when there is nothing to say."""
|
|
374
|
+
self.status_label.setText(text)
|
|
375
|
+
self.status_area.setVisible(bool(text))
|
|
376
|
+
|
|
377
|
+
def populate(self, intent: ReplayIntent, fields: tuple[str, ...], parents: tuple[ReplayParent, ...] = ()) -> None:
|
|
378
|
+
"""Publish metadata only for the prepared source before submission."""
|
|
379
|
+
if self.closed or self.busy or self.completed:
|
|
380
|
+
return
|
|
381
|
+
self.intent = replace(intent, mainline_parent_number=None, mainline_parent_oid=None)
|
|
382
|
+
self.source_oid = intent.source_oid
|
|
383
|
+
if self.summary.target_oid != self.source_oid:
|
|
384
|
+
previous = self.summary
|
|
385
|
+
self.summary = CommitSummary(self.source_oid, git_extensions_look=True, clock=self.clock)
|
|
386
|
+
# replaceWidget would hand the layout item of the old commit box back to the caller: dropped, it stays
|
|
387
|
+
# allocated for as long as the application runs, one more at each change of commit, and deleting it from
|
|
388
|
+
# Python crashes once the form is deleted, since PySide still counts it as a child of the body layout.
|
|
389
|
+
# removeWidget lets Qt delete that item itself; the row holds no other widget, so the new box takes its place.
|
|
390
|
+
self.summary_row.removeWidget(previous)
|
|
391
|
+
self.summary_row.addWidget(self.summary)
|
|
392
|
+
previous.hide()
|
|
393
|
+
previous.deleteLater()
|
|
394
|
+
self.parents = parents
|
|
395
|
+
self.summary.show_commit(fields)
|
|
396
|
+
self.parent_table.setRowCount(0)
|
|
397
|
+
merge = len(parents) >= MERGE_PARENT_MINIMUM
|
|
398
|
+
if merge:
|
|
399
|
+
self.parent_table.setRowCount(len(parents))
|
|
400
|
+
for row, parent in enumerate(parents):
|
|
401
|
+
try:
|
|
402
|
+
date_text = datetime.fromisoformat(parent.commit_date).date().isoformat()
|
|
403
|
+
except ValueError:
|
|
404
|
+
date_text = parent.commit_date or "n/a"
|
|
405
|
+
for column, value in enumerate((str(parent.number), parent.subject or "n/a", parent.author or "n/a", date_text)):
|
|
406
|
+
item = QTableWidgetItem(value)
|
|
407
|
+
item.setToolTip(parent.commit_date if column == PARENT_DATE_COLUMN and parent.commit_date else value)
|
|
408
|
+
item.setData(Qt.ItemDataRole.UserRole, parent.oid)
|
|
409
|
+
self.parent_table.setItem(row, column, item)
|
|
410
|
+
self.parent_table.clearSelection()
|
|
411
|
+
self.parent_section.setVisible(merge)
|
|
412
|
+
self.parent_table.setVisible(merge)
|
|
413
|
+
self.parent_label.setVisible(merge)
|
|
414
|
+
self.metadata_ready = True
|
|
415
|
+
self.submit_button.setEnabled(not merge)
|
|
416
|
+
if self.revision_button is not None:
|
|
417
|
+
self.revision_button.setEnabled(True)
|
|
418
|
+
self._show_status("")
|
|
419
|
+
# Enter then runs Cherry pick or Revert this commit as in Git Extensions; a merge waits for a parent choice.
|
|
420
|
+
(self.parent_table if merge else self.submit_button).setFocus()
|
|
421
|
+
|
|
422
|
+
def _parent_selected(self) -> None:
|
|
423
|
+
"""Tie the explicit row selection to its captured native parent identity."""
|
|
424
|
+
if self.intent is None or self.busy or self.completed:
|
|
425
|
+
return
|
|
426
|
+
rows = self.parent_table.selectionModel().selectedRows()
|
|
427
|
+
parent = self.parents[next(iter(rows)).row()] if rows else None
|
|
428
|
+
self.intent = replace(self.intent, mainline_parent_number=parent.number if parent else None, mainline_parent_oid=parent.oid if parent else None)
|
|
429
|
+
self.submit_button.setEnabled(self.metadata_ready and (len(self.parents) < MERGE_PARENT_MINIMUM or parent is not None))
|
|
430
|
+
|
|
431
|
+
def show_error(self, cause: str) -> None:
|
|
432
|
+
"""Explain preparation failures without inventing a writable source."""
|
|
433
|
+
self._show_status(cause)
|
|
434
|
+
self.submit_button.setEnabled(False)
|
|
435
|
+
|
|
436
|
+
def _submit(self) -> None:
|
|
437
|
+
"""Freeze choices before emitting one immutable write request."""
|
|
438
|
+
if self.intent is None or not self.metadata_ready or self.busy or self.completed or self.closed:
|
|
439
|
+
return
|
|
440
|
+
if len(self.parents) >= MERGE_PARENT_MINIMUM and self.intent.mainline_parent_oid is None:
|
|
441
|
+
return
|
|
442
|
+
self.intent = replace(self.intent, create_commit=self.create_commit_check.isChecked(),
|
|
443
|
+
add_reference=self.add_reference_check is not None and self.add_reference_check.isChecked())
|
|
444
|
+
self.busy = True
|
|
445
|
+
for widget in (self.create_commit_check, self.add_reference_check, self.revision_button, self.parent_table, self.submit_button, self.cancel_button):
|
|
446
|
+
if widget is not None:
|
|
447
|
+
widget.setEnabled(False)
|
|
448
|
+
self.replay_requested.emit(self.intent)
|
|
449
|
+
|
|
450
|
+
def close_after_submission(self) -> None:
|
|
451
|
+
"""Close the form once its operation is accepted; the console and the repository window take over from here.
|
|
452
|
+
|
|
453
|
+
A form left open after launch would stay window-modal over the repository window and block every later
|
|
454
|
+
choice (conflict prompt, banner, progress), so the form never waits for Git's result.
|
|
455
|
+
"""
|
|
456
|
+
self.busy = False
|
|
457
|
+
self.completed = True
|
|
458
|
+
self.accept()
|
|
459
|
+
|
|
460
|
+
def show_submission_refused(self, cause: str) -> None:
|
|
461
|
+
"""Give the choices back when the request was refused before any console or Git command started."""
|
|
462
|
+
self.busy = False
|
|
463
|
+
for widget in (self.create_commit_check, self.add_reference_check, self.revision_button, self.parent_table, self.cancel_button):
|
|
464
|
+
if widget is not None:
|
|
465
|
+
widget.setEnabled(True)
|
|
466
|
+
self.submit_button.setEnabled(self.metadata_ready and (len(self.parents) < MERGE_PARENT_MINIMUM or self.intent is not None and self.intent.mainline_parent_oid is not None))
|
|
467
|
+
self._show_status(cause)
|
|
468
|
+
|
|
469
|
+
def reject(self) -> None:
|
|
470
|
+
"""Treat closing the idle form as cancellation without abandoning native work."""
|
|
471
|
+
if not self.busy:
|
|
472
|
+
super().reject()
|
|
473
|
+
|
|
474
|
+
def accept(self) -> None:
|
|
475
|
+
"""Ignore direct acceptance or keyboard submission while native work is active."""
|
|
476
|
+
if not self.busy:
|
|
477
|
+
super().accept()
|
|
478
|
+
|
|
479
|
+
def keyPressEvent(self, event: QKeyEvent) -> None:
|
|
480
|
+
"""Press the focused button on Enter, as Windows does; anywhere else Enter runs the default submit button."""
|
|
481
|
+
focused = self.focusWidget()
|
|
482
|
+
other_buttons = (self.revision_button, self.cancel_button)
|
|
483
|
+
if event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter) and focused is not None and focused in other_buttons:
|
|
484
|
+
if focused.isEnabled():
|
|
485
|
+
focused.click()
|
|
486
|
+
return
|
|
487
|
+
super().keyPressEvent(event)
|
|
488
|
+
|
|
489
|
+
def closeEvent(self, event: QCloseEvent) -> None:
|
|
490
|
+
"""Keep the captured request visible while its native process is owned."""
|
|
491
|
+
if self.busy:
|
|
492
|
+
event.ignore()
|
|
493
|
+
return
|
|
494
|
+
super().closeEvent(event)
|
|
495
|
+
|
|
496
|
+
def _closed(self) -> None:
|
|
497
|
+
"""Invalidate every delayed metadata callback for this form."""
|
|
498
|
+
self.closed = True
|
|
499
|
+
self.source_generation += 1
|
|
500
|
+
if self.revision_dialog is not None:
|
|
501
|
+
self.revision_dialog.reject()
|
|
File without changes
|