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,356 @@
|
|
|
1
|
+
"""Translate native text selection into captured byte-line intentions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from PySide6.QtCore import QEvent, QObject, Qt, Signal
|
|
9
|
+
from PySide6.QtWidgets import QMenu, QMessageBox
|
|
10
|
+
|
|
11
|
+
from gitpane.config import ERROR_WIDTH, SPACING_MD, STAGE_SHORTCUT
|
|
12
|
+
from gitpane.history.repository_value import BlobContentKind
|
|
13
|
+
from gitpane.history.revision_file import DiffLineKind, decode_blob_display
|
|
14
|
+
from gitpane.ui.emoji.file_action_icon import file_action_icon
|
|
15
|
+
from gitpane.ui.widget.shortcut_text import with_shortcut
|
|
16
|
+
from gitpane.ui.widget.system_shortcut import trash_key_pressed
|
|
17
|
+
from gitpane.ui.window_control.dialog_modality import prepare_message_box
|
|
18
|
+
from gitpane.worktree.file_menu_action import READ_ONLY_FILE_ACTION_SET, FileAction
|
|
19
|
+
from gitpane.worktree.ignore_rule import IgnoreDestination
|
|
20
|
+
from gitpane.worktree.partial_patch import RawLineSelection
|
|
21
|
+
from gitpane.worktree.worktree_snapshot import StagingSide, local_history_path
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
|
|
26
|
+
from PySide6.QtCore import QPoint
|
|
27
|
+
from PySide6.QtWidgets import QListWidget
|
|
28
|
+
|
|
29
|
+
from gitpane.ui.reader.diff_panel import DiffView
|
|
30
|
+
from gitpane.ui.staging.staging_window import StagingWindow
|
|
31
|
+
from gitpane.worktree.partial_patch import RawPatch
|
|
32
|
+
from gitpane.worktree.staged_reset import ResetCandidate
|
|
33
|
+
from gitpane.worktree.trash import TrashRequest
|
|
34
|
+
from gitpane.worktree.worktree_snapshot import LocalEntry, WorktreeSnapshot
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
SELECT_ONE_FILE_REASON = "Select one file"
|
|
38
|
+
MOVE_TO_TRASH_TITLE = "Move selected file(s) to Trash"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def configure_confirmation(dialog: QMessageBox, action_label: str, cancel_label: str) -> None:
|
|
42
|
+
"""Name the exact effect and make both default activation and Escape cancel safely."""
|
|
43
|
+
dialog.setTextFormat(Qt.TextFormat.PlainText)
|
|
44
|
+
dialog.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextSelectableByKeyboard)
|
|
45
|
+
dialog.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.Cancel)
|
|
46
|
+
dialog.button(QMessageBox.StandardButton.Yes).setText(action_label)
|
|
47
|
+
dialog.button(QMessageBox.StandardButton.Cancel).setText(cancel_label)
|
|
48
|
+
dialog.setDefaultButton(QMessageBox.StandardButton.Cancel)
|
|
49
|
+
dialog.setEscapeButton(QMessageBox.StandardButton.Cancel)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def show_confirmation_targets(dialog: QMessageBox, summary: str, labels: list[str]) -> None:
|
|
53
|
+
"""Keep a short native summary above scrollable, complete target names and calculated details."""
|
|
54
|
+
preview = next(iter(labels)) if len(labels) == 1 else f"{len(labels)} selected files"
|
|
55
|
+
available_width = min(ERROR_WIDTH, dialog.screen().availableGeometry().width()) - SPACING_MD * 2
|
|
56
|
+
preview = dialog.fontMetrics().elidedText(preview.replace("\r", "\\r").replace("\n", "\\n"), Qt.TextElideMode.ElideMiddle, available_width)
|
|
57
|
+
dialog.setText(summary + "\n" + preview)
|
|
58
|
+
details = dialog.detailedText()
|
|
59
|
+
dialog.setDetailedText("\n".join(labels) + ("\n\n" + details if details else ""))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FileResetMenu(QObject):
|
|
63
|
+
"""Capture exact list selections for a direct side-specific Reset action."""
|
|
64
|
+
|
|
65
|
+
requested = Signal(object)
|
|
66
|
+
delete_requested = Signal(object, object)
|
|
67
|
+
ignore_requested = Signal(object, object, object)
|
|
68
|
+
action_requested = Signal(object, object, object)
|
|
69
|
+
|
|
70
|
+
def __init__(self, window: StagingWindow) -> None:
|
|
71
|
+
"""Attach menus to both lists while keeping the source side with each entry."""
|
|
72
|
+
super().__init__(window)
|
|
73
|
+
self.window = window
|
|
74
|
+
# The system whose trash keys the file lists follow; a test sets another one to check that system's keys.
|
|
75
|
+
self.platform = sys.platform
|
|
76
|
+
for file_list in (window.unstaged_list, window.staged_list):
|
|
77
|
+
file_list.installEventFilter(self)
|
|
78
|
+
file_list.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
79
|
+
file_list.customContextMenuRequested.connect(lambda position, file_list=file_list: self._open(file_list, position))
|
|
80
|
+
|
|
81
|
+
def create_menu(self, file_list: QListWidget) -> QMenu:
|
|
82
|
+
"""Recheck availability and retain the captured selection for delayed activation."""
|
|
83
|
+
menu = QMenu(self.window)
|
|
84
|
+
entries = self.window.selected_entries(file_list)
|
|
85
|
+
snapshot = self.window.snapshot
|
|
86
|
+
unstaged = file_list is self.window.unstaged_list
|
|
87
|
+
enabled = bool(entries) and snapshot is not None and snapshot.fresh and not self.window.busy
|
|
88
|
+
readable = enabled
|
|
89
|
+
file_entries = tuple(entry for entry in entries if not entry.is_gitlink)
|
|
90
|
+
if unstaged:
|
|
91
|
+
stage_action = menu.addAction(file_action_icon(FileAction.STAGE, menu), FileAction.STAGE.label)
|
|
92
|
+
stage_action.setEnabled(enabled and self.window.stage_button.isEnabled())
|
|
93
|
+
stage_action.setToolTip(with_shortcut(FileAction.STAGE.label, STAGE_SHORTCUT))
|
|
94
|
+
stage_action.triggered.connect(lambda: self._request(FileAction.STAGE, entries, snapshot, file_list))
|
|
95
|
+
action = menu.addAction(file_action_icon(FileAction.RESET, menu), FileAction.RESET.label)
|
|
96
|
+
enabled = enabled and not any(entry.conflict_stages for entry in snapshot.entries) and not any(entry.skip_worktree for entry in entries)
|
|
97
|
+
new_file = snapshot is not None and snapshot.requires_file_confirmation(tuple(entry.path for entry in file_entries))
|
|
98
|
+
action.setEnabled(enabled and bool(file_entries) and not new_file)
|
|
99
|
+
action.setToolTip("New files cannot be reset. Use Unstage or Delete file." if new_file else "Reset changes on the selected side")
|
|
100
|
+
action.triggered.connect(lambda checked=False: self._request(FileAction.RESET, entries, snapshot, file_list))
|
|
101
|
+
delete_action = menu.addAction(file_action_icon(FileAction.DELETE, menu), FileAction.DELETE.label)
|
|
102
|
+
delete_action.setEnabled(bool(file_entries) and snapshot is not None and snapshot.fresh and not self.window.busy)
|
|
103
|
+
delete_action.triggered.connect(lambda checked=False: self._request(FileAction.DELETE, entries, snapshot, file_list))
|
|
104
|
+
menu.setToolTipsVisible(True)
|
|
105
|
+
if unstaged:
|
|
106
|
+
copy_menu = menu.addMenu(FileAction.COPY_PATHS.label)
|
|
107
|
+
copy_menu.menuAction().setIcon(file_action_icon(FileAction.COPY_PATHS, menu))
|
|
108
|
+
copy_menu.setEnabled(readable)
|
|
109
|
+
for copy_file_action in (FileAction.COPY_BASENAME, FileAction.COPY_ABSOLUTE):
|
|
110
|
+
copy_action = copy_menu.addAction(file_action_icon(copy_file_action, copy_menu), copy_file_action.label)
|
|
111
|
+
copy_action.setIconVisibleInMenu(True)
|
|
112
|
+
copy_action.triggered.connect(lambda checked=False, copy_file_action=copy_file_action: self._request(copy_file_action, entries, snapshot, file_list))
|
|
113
|
+
single_file_reason = SELECT_ONE_FILE_REASON if len(entries) != 1 else ""
|
|
114
|
+
self._add_consultation(menu, FileAction.FOLDER, single_file_reason, "Open the existing file's folder", readable, entries, snapshot, file_list)
|
|
115
|
+
text_editor_reason = single_file_reason
|
|
116
|
+
if not text_editor_reason and self.window.text_editor_reason is not None:
|
|
117
|
+
text_editor_reason = self.window.text_editor_reason(next(iter(entries)))
|
|
118
|
+
self._add_consultation(menu, FileAction.TEXT_EDITOR, text_editor_reason, "Open the working copy file in the text editor", readable, entries, snapshot, file_list)
|
|
119
|
+
history_reason = single_file_reason
|
|
120
|
+
if not history_reason and (snapshot is None or next(iter(entries)).is_gitlink or local_history_path(snapshot, next(iter(entries))) is None):
|
|
121
|
+
history_reason = "No file history exists at HEAD for this file"
|
|
122
|
+
self._add_consultation(menu, FileAction.HISTORY, history_reason, "File history (H)", readable, entries, snapshot, file_list)
|
|
123
|
+
if unstaged:
|
|
124
|
+
for destination in IgnoreDestination:
|
|
125
|
+
ignore_action = menu.addAction(file_action_icon(destination, menu), destination.label)
|
|
126
|
+
ignore_action.setEnabled(enabled and bool(file_entries))
|
|
127
|
+
ignore_action.triggered.connect(lambda checked=False, destination=destination: self._request_ignore(destination, entries, snapshot, file_list))
|
|
128
|
+
stop_action = menu.addAction(file_action_icon(FileAction.STOP_TRACKING, menu), FileAction.STOP_TRACKING.label)
|
|
129
|
+
tracked = bool(entries) and all(entry.old_mode in ("100644", "100755", "120000") for entry in entries)
|
|
130
|
+
stop_action.setEnabled(enabled and tracked)
|
|
131
|
+
stop_action.setToolTip("Keep files on disk and stage removal of their tracking" if tracked else "Select tracked files")
|
|
132
|
+
stop_action.triggered.connect(lambda: self._request(FileAction.STOP_TRACKING, entries, snapshot, file_list))
|
|
133
|
+
for action in menu.actions():
|
|
134
|
+
action.setIconVisibleInMenu(True)
|
|
135
|
+
return menu
|
|
136
|
+
|
|
137
|
+
def _add_consultation(self, menu: QMenu, file_action: FileAction, reason: str, tooltip: str, readable: bool, entries: tuple[LocalEntry, ...],
|
|
138
|
+
snapshot: WorktreeSnapshot | None, file_list: QListWidget) -> None:
|
|
139
|
+
"""Add a consultation of the selected file, greyed with its reason when it cannot apply to the selection.
|
|
140
|
+
|
|
141
|
+
The Staged list only gets the actions of the read-only set, which never change the index: the session accepts
|
|
142
|
+
exactly those from it, so the menu never offers one the session would refuse.
|
|
143
|
+
"""
|
|
144
|
+
if file_list is not self.window.unstaged_list and file_action not in READ_ONLY_FILE_ACTION_SET:
|
|
145
|
+
return
|
|
146
|
+
action = menu.addAction(file_action_icon(file_action, menu), file_action.label)
|
|
147
|
+
action.setEnabled(readable and not reason)
|
|
148
|
+
action.setToolTip(reason or tooltip)
|
|
149
|
+
action.triggered.connect(lambda checked=False: self._request(file_action, entries, snapshot, file_list))
|
|
150
|
+
|
|
151
|
+
def _fresh_current_read(self, snapshot: WorktreeSnapshot | None) -> WorktreeSnapshot | None:
|
|
152
|
+
"""Answer the window's current read while the window can act and both the menu's read and the current one are fresh, else None.
|
|
153
|
+
|
|
154
|
+
Every menu action checks this first, so a busy or closing window, or a read that went stale, never starts one.
|
|
155
|
+
"""
|
|
156
|
+
current = self.window.snapshot
|
|
157
|
+
if self.window.busy or self.window.close_requested or snapshot is None or not snapshot.fresh or current is None or not current.fresh:
|
|
158
|
+
return None
|
|
159
|
+
return current
|
|
160
|
+
|
|
161
|
+
def _selection_is_current(self, entries: tuple[LocalEntry, ...], snapshot: WorktreeSnapshot | None, file_list: QListWidget) -> bool:
|
|
162
|
+
"""Tell whether the menu's read is still the window's fresh current read and the list still selects the same entries.
|
|
163
|
+
|
|
164
|
+
Every action that works on the selection as it is now, rather than on captured content, requires both.
|
|
165
|
+
"""
|
|
166
|
+
current = self._fresh_current_read(snapshot)
|
|
167
|
+
return not (current is None or current is not snapshot or entries != self.window.selected_entries(file_list))
|
|
168
|
+
|
|
169
|
+
def _request(self, file_action: FileAction, entries: tuple[LocalEntry, ...], snapshot: WorktreeSnapshot | None, file_list: QListWidget) -> None:
|
|
170
|
+
"""Keep captured Delete content for service validation and require current selections for other actions."""
|
|
171
|
+
if file_action == FileAction.DELETE:
|
|
172
|
+
current = self._fresh_current_read(snapshot)
|
|
173
|
+
if current is None or current.key != snapshot.key or current.git_filename != snapshot.git_filename:
|
|
174
|
+
return
|
|
175
|
+
# Trash validates the original bytes so a refresh cannot replace the user's captured intention.
|
|
176
|
+
entries = tuple(entry for entry in entries if not entry.is_gitlink)
|
|
177
|
+
if entries:
|
|
178
|
+
self.delete_requested.emit(entries, snapshot)
|
|
179
|
+
return
|
|
180
|
+
if not self._selection_is_current(entries, snapshot, file_list):
|
|
181
|
+
return
|
|
182
|
+
if file_action in (FileAction.RESET, FileAction.HISTORY, FileAction.STOP_TRACKING):
|
|
183
|
+
entries = tuple(entry for entry in entries if not entry.is_gitlink)
|
|
184
|
+
if not entries:
|
|
185
|
+
return
|
|
186
|
+
if file_action == FileAction.RESET:
|
|
187
|
+
if snapshot.requires_file_confirmation(tuple(entry.path for entry in entries)):
|
|
188
|
+
return
|
|
189
|
+
self.requested.emit(entries)
|
|
190
|
+
else:
|
|
191
|
+
self.action_requested.emit(file_action, entries, snapshot)
|
|
192
|
+
|
|
193
|
+
def _request_ignore(self, destination: IgnoreDestination, entries: tuple[LocalEntry, ...], snapshot: WorktreeSnapshot | None, file_list: QListWidget) -> None:
|
|
194
|
+
"""Send a rule for the captured selection to its destination, only while the window, the read and the selection are still current.
|
|
195
|
+
|
|
196
|
+
Submodules are left out, since an ignore rule cannot apply to them; nothing is sent when no file remains.
|
|
197
|
+
"""
|
|
198
|
+
if not self._selection_is_current(entries, snapshot, file_list):
|
|
199
|
+
return
|
|
200
|
+
entries = tuple(entry for entry in entries if not entry.is_gitlink)
|
|
201
|
+
if not entries:
|
|
202
|
+
return
|
|
203
|
+
self.ignore_requested.emit(entries, snapshot, destination)
|
|
204
|
+
|
|
205
|
+
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
|
206
|
+
"""Restrict file shortcuts to their source list and suppress keyboard auto-repeat.
|
|
207
|
+
|
|
208
|
+
The filter watches only the two file lists, so the trash keys of the system (Delete, plus Backspace and
|
|
209
|
+
Cmd+Backspace on macOS) never act from a text field, where Backspace erases a character.
|
|
210
|
+
"""
|
|
211
|
+
if event.type() == QEvent.Type.KeyPress and event.key() == Qt.Key.Key_H and not event.modifiers() and watched is self.window.unstaged_list:
|
|
212
|
+
if not event.isAutoRepeat():
|
|
213
|
+
self._request(FileAction.HISTORY, self.window.selected_entries(watched), self.window.snapshot, watched)
|
|
214
|
+
return True
|
|
215
|
+
if event.type() == QEvent.Type.KeyPress and trash_key_pressed(event.key(), event.modifiers(), self.platform):
|
|
216
|
+
snapshot = self.window.snapshot
|
|
217
|
+
if not event.isAutoRepeat() and not self.window.busy and snapshot is not None and snapshot.fresh:
|
|
218
|
+
entries = self.window.selected_entries(watched)
|
|
219
|
+
if entries:
|
|
220
|
+
self._request(FileAction.DELETE, entries, snapshot, watched)
|
|
221
|
+
return True
|
|
222
|
+
return super().eventFilter(watched, event)
|
|
223
|
+
|
|
224
|
+
def confirm_stop_tracking(self, entries: tuple[LocalEntry, ...]) -> bool:
|
|
225
|
+
"""Explain cached removal and preservation of local content before modifying the index."""
|
|
226
|
+
labels: list[str] = []
|
|
227
|
+
for entry in entries:
|
|
228
|
+
labels.append(entry.label)
|
|
229
|
+
dialog = QMessageBox(QMessageBox.Icon.Warning, FileAction.STOP_TRACKING.label, "Stop tracking:\n" + "\n".join(labels), parent=self.window)
|
|
230
|
+
prepare_message_box(dialog, FileAction.STOP_TRACKING.label)
|
|
231
|
+
dialog.setTextFormat(Qt.TextFormat.PlainText)
|
|
232
|
+
dialog.setInformativeText("Removal from tracking will be staged. All file content stays on disk. No ignore rule is added. Git will refuse if the index contains a unique version; review staged changes before retrying.")
|
|
233
|
+
show_confirmation_targets(dialog, "Stop tracking:", labels)
|
|
234
|
+
configure_confirmation(dialog, "Stop tracking selected files", "Cancel tracking change")
|
|
235
|
+
accepted = dialog.exec() == QMessageBox.StandardButton.Yes
|
|
236
|
+
dialog.deleteLater()
|
|
237
|
+
return accepted
|
|
238
|
+
|
|
239
|
+
def confirm_trash(self, request: TrashRequest) -> bool:
|
|
240
|
+
"""Name every captured target and explain the difference between Trash and Reset."""
|
|
241
|
+
labels: list[str] = []
|
|
242
|
+
for target in request.targets:
|
|
243
|
+
labels.append(target.path.decode("utf-8", "replace") + (" (tracked)" if target.tracked else " (untracked)"))
|
|
244
|
+
dialog = QMessageBox(QMessageBox.Icon.Warning, MOVE_TO_TRASH_TITLE, MOVE_TO_TRASH_TITLE + ":\n" + "\n".join(labels), parent=self.window)
|
|
245
|
+
prepare_message_box(dialog, MOVE_TO_TRASH_TITLE)
|
|
246
|
+
dialog.setTextFormat(Qt.TextFormat.PlainText)
|
|
247
|
+
dialog.setInformativeText("Tracked deletions remain Unstaged. Reset restores the index version, not the deleted local edits. The OS Trash owns moved files; a batch cannot be rolled back automatically.")
|
|
248
|
+
show_confirmation_targets(dialog, MOVE_TO_TRASH_TITLE + ":", labels)
|
|
249
|
+
configure_confirmation(dialog, "Move files to Trash", "Cancel deletion")
|
|
250
|
+
accepted = dialog.exec() == QMessageBox.StandardButton.Yes
|
|
251
|
+
dialog.deleteLater()
|
|
252
|
+
return accepted
|
|
253
|
+
|
|
254
|
+
def _open(self, file_list: QListWidget, position: QPoint) -> None:
|
|
255
|
+
"""Open the captured menu without retargeting another list."""
|
|
256
|
+
menu = self.create_menu(file_list)
|
|
257
|
+
menu.exec(file_list.mapToGlobal(position))
|
|
258
|
+
menu.deleteLater()
|
|
259
|
+
|
|
260
|
+
def confirm(self, prepared: ResetCandidate) -> bool:
|
|
261
|
+
"""Explain the chosen side and show the calculated result before publication."""
|
|
262
|
+
from difflib import unified_diff
|
|
263
|
+
|
|
264
|
+
from gitpane.history.revision_file import decode_blob_display
|
|
265
|
+
|
|
266
|
+
labels: list[str] = []
|
|
267
|
+
details: list[str] = []
|
|
268
|
+
for change in prepared.changes:
|
|
269
|
+
label = change.path.decode("utf-8", "replace")
|
|
270
|
+
labels.append(label)
|
|
271
|
+
before = decode_blob_display("", change.before.read()).display_text
|
|
272
|
+
after = decode_blob_display("", change.after.read()).display_text
|
|
273
|
+
result = "File removed" if not change.after.exists else f"Result: {len(change.after.read())} bytes"
|
|
274
|
+
details.append(label + "\n" + result + "\n" + "".join(unified_diff(before.splitlines(keepends=True), after.splitlines(keepends=True), fromfile="Current", tofile="After Reset")))
|
|
275
|
+
title = "Reset " + prepared.side.title
|
|
276
|
+
dialog = QMessageBox(QMessageBox.Icon.Warning, title, "Reset " + prepared.side.label + " changes in:\n" + "\n".join(labels), parent=self.window)
|
|
277
|
+
prepare_message_box(dialog, title)
|
|
278
|
+
dialog.setTextFormat(Qt.TextFormat.PlainText)
|
|
279
|
+
dialog.setInformativeText(prepared.explanation + " Files are replaced before the index; an interrupted operation may require recovery.")
|
|
280
|
+
dialog.setDetailedText("\n\n".join(details))
|
|
281
|
+
show_confirmation_targets(dialog, "Reset " + prepared.side.label + " changes in:", labels)
|
|
282
|
+
configure_confirmation(dialog, "Reset " + prepared.side.label + " changes", "Cancel reset")
|
|
283
|
+
accepted = dialog.exec() == QMessageBox.StandardButton.Yes
|
|
284
|
+
dialog.deleteLater()
|
|
285
|
+
return accepted
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class StagingActionMenu(QObject):
|
|
289
|
+
"""Offer exactly the two Unstaged actions without changing native text selection."""
|
|
290
|
+
|
|
291
|
+
stage_requested = Signal(object)
|
|
292
|
+
reset_requested = Signal(object)
|
|
293
|
+
|
|
294
|
+
def __init__(self, view: DiffView, available: Callable[[], bool]) -> None:
|
|
295
|
+
"""Bind line identity to the displayed patch and ask the controller about freshness."""
|
|
296
|
+
super().__init__(view)
|
|
297
|
+
self.view = view
|
|
298
|
+
self.available = available
|
|
299
|
+
self.model: RawPatch | None = None
|
|
300
|
+
self.patch_index_by_line_index: dict[int, int] = {}
|
|
301
|
+
view.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
|
|
302
|
+
view.customContextMenuRequested.connect(self._open)
|
|
303
|
+
|
|
304
|
+
def bind(self, model: RawPatch | None) -> None:
|
|
305
|
+
"""Associate logical display lines explicitly, refusing incomplete previews."""
|
|
306
|
+
self.model = None
|
|
307
|
+
self.patch_index_by_line_index.clear()
|
|
308
|
+
if model is None:
|
|
309
|
+
return
|
|
310
|
+
content = decode_blob_display("", model.patch)
|
|
311
|
+
if not content.complete or content.content_kind == BlobContentKind.BINARY:
|
|
312
|
+
return
|
|
313
|
+
offset = 1 if content.diagnostic else 0
|
|
314
|
+
for change in model.changes:
|
|
315
|
+
if change.patch_index is not None and change.kind in (DiffLineKind.ADDED, DiffLineKind.REMOVED):
|
|
316
|
+
self.patch_index_by_line_index[change.patch_index + offset] = change.patch_index
|
|
317
|
+
self.model = model
|
|
318
|
+
|
|
319
|
+
def selection(self) -> RawLineSelection | None:
|
|
320
|
+
"""Include intersected logical lines, excluding a following line at its start boundary."""
|
|
321
|
+
cursor = self.view.textCursor()
|
|
322
|
+
if self.model is None or not self.available() or not cursor.hasSelection():
|
|
323
|
+
return None
|
|
324
|
+
start = self.view.source_position(cursor.selectionStart())
|
|
325
|
+
end = self.view.source_position(cursor.selectionEnd())
|
|
326
|
+
first_index = self.view.logical_line_index(cursor.selectionStart())
|
|
327
|
+
last_index = self.view.logical_line_index(cursor.selectionEnd())
|
|
328
|
+
line_ids: list[int] = []
|
|
329
|
+
for line_index in range(first_index, last_index + 1):
|
|
330
|
+
line_start = self.view.source_position(self.view.line_positions[line_index])
|
|
331
|
+
if line_start >= end:
|
|
332
|
+
break
|
|
333
|
+
patch_index = self.patch_index_by_line_index.get(line_index)
|
|
334
|
+
if patch_index is not None and end > start:
|
|
335
|
+
line_ids.append(patch_index)
|
|
336
|
+
if not line_ids:
|
|
337
|
+
return None
|
|
338
|
+
return RawLineSelection(self.model.snapshot.key, self.model.snapshot.generation, StagingSide.UNSTAGED, self.model.entry.path, tuple(line_ids))
|
|
339
|
+
|
|
340
|
+
def create_menu(self) -> QMenu:
|
|
341
|
+
"""Capture an intention at menu opening so delayed actions cannot retarget a new diff."""
|
|
342
|
+
if self.model is None:
|
|
343
|
+
return self.view.createStandardContextMenu()
|
|
344
|
+
menu = QMenu(self.view)
|
|
345
|
+
selection = self.selection()
|
|
346
|
+
for label, signal in (("Stage selected lines", self.stage_requested), ("Reset selected lines", self.reset_requested)):
|
|
347
|
+
action = menu.addAction(label)
|
|
348
|
+
action.setEnabled(selection is not None)
|
|
349
|
+
action.triggered.connect(lambda checked=False, signal=signal, selection=selection: signal.emit(selection))
|
|
350
|
+
return menu
|
|
351
|
+
|
|
352
|
+
def _open(self, position: QPoint) -> None:
|
|
353
|
+
"""Open at the requested position without moving the original text cursor."""
|
|
354
|
+
menu = self.create_menu()
|
|
355
|
+
menu.exec(self.view.mapToGlobal(position))
|
|
356
|
+
menu.deleteLater()
|