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,337 @@
|
|
|
1
|
+
"""Hold the open repository sessions of GitPane with what every controller reads about them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import weakref
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import TYPE_CHECKING, Self
|
|
9
|
+
|
|
10
|
+
from PySide6.QtCore import QEvent, QObject, QTimer, Signal
|
|
11
|
+
from shiboken6 import isValid
|
|
12
|
+
|
|
13
|
+
from gitpane.history.file_history import WINDOW_CAPABILITY_BY_MODE
|
|
14
|
+
from gitpane.history.repository_value import FilePanel, validate_oid
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from PySide6.QtCore import QMetaObject
|
|
18
|
+
from PySide6.QtGui import QScreen
|
|
19
|
+
from PySide6.QtWidgets import QApplication, QMessageBox
|
|
20
|
+
|
|
21
|
+
from gitpane.controller.refresh_controller import RefreshRequest
|
|
22
|
+
from gitpane.history.file_history import FileHistoryAnchor, FileHistoryEngine, FileHistoryResult
|
|
23
|
+
from gitpane.history.repository import GitRepository
|
|
24
|
+
from gitpane.history.repository_value import Commit, ComparisonGroup, FileReadIdentity, HistorySnapshot, RepositoryContext, RequestIdentity, RevisionEntry, TreeEntry
|
|
25
|
+
from gitpane.ui.console.error_dialog import ErrorDialog
|
|
26
|
+
from gitpane.ui.repository.repository_window import RepositoryWindow, ViewAnchor
|
|
27
|
+
from gitpane.ui.setting.setting_dialog import SettingDialog
|
|
28
|
+
from gitpane.ui.staging.staging_window import StagingWindow
|
|
29
|
+
from gitpane.ui.transport.clone_dialog import CloneDialog
|
|
30
|
+
from gitpane.ui.welcome.welcome_window import WelcomeWindow
|
|
31
|
+
from gitpane.worktree.worktree_snapshot import WorktreeKey
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WindowDisplayBinding(QObject):
|
|
35
|
+
"""Follow one window's screen and system appearance without altering its session.
|
|
36
|
+
|
|
37
|
+
The binding never keeps a QScreen object. PySide has a single Python object per screen and makes it a child of the
|
|
38
|
+
last widget or native window whose screen() returned it; once that window is destroyed, PySide marks the object
|
|
39
|
+
deleted for everyone who holds it, although the screen is still there. GitPane centres each repository window on the
|
|
40
|
+
Open repository window, reading that window's screen, then deletes the Open window: a kept object was dead from the
|
|
41
|
+
start and the repository window stopped following font, density and usable-area changes. Any other window that read
|
|
42
|
+
the screen last and is then closed does the same. The screen is therefore asked from the window at each refresh, and
|
|
43
|
+
the screen's notifications are held as connections, which can be cut without that object.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(self, window: WelcomeWindow | RepositoryWindow | StagingWindow | SettingDialog | ErrorDialog, application: QApplication) -> None:
|
|
47
|
+
"""Connect once per native window and coalesce appearance events into one update."""
|
|
48
|
+
super().__init__(window)
|
|
49
|
+
self.window_ref = weakref.ref(window)
|
|
50
|
+
self.application = application
|
|
51
|
+
self.screen_connections: list[QMetaObject.Connection] = []
|
|
52
|
+
self.handle = None
|
|
53
|
+
self.ratio: float | None = None
|
|
54
|
+
self.refreshing = False
|
|
55
|
+
self.timer = QTimer(self)
|
|
56
|
+
self.timer.setSingleShot(True)
|
|
57
|
+
self.timer.timeout.connect(self.refresh)
|
|
58
|
+
window.installEventFilter(self)
|
|
59
|
+
application.screenRemoved.connect(self.screen_removed)
|
|
60
|
+
self.attach_handle()
|
|
61
|
+
|
|
62
|
+
def attach_handle(self) -> None:
|
|
63
|
+
"""Attach to a newly created native handle without duplicating its screen callback."""
|
|
64
|
+
window = self.window_ref()
|
|
65
|
+
if window is None or not isValid(window):
|
|
66
|
+
return
|
|
67
|
+
handle = window.windowHandle()
|
|
68
|
+
if handle is None or handle is self.handle:
|
|
69
|
+
return
|
|
70
|
+
if self.handle is not None and isValid(self.handle):
|
|
71
|
+
self.handle.screenChanged.disconnect(self.set_screen)
|
|
72
|
+
self.handle = handle
|
|
73
|
+
handle.screenChanged.connect(self.set_screen)
|
|
74
|
+
self.set_screen(handle.screen())
|
|
75
|
+
|
|
76
|
+
def set_screen(self, screen: QScreen | None) -> None:
|
|
77
|
+
"""Listen to the usable-area and density changes of the window's new screen only, then refresh."""
|
|
78
|
+
screen = screen if screen is not None else self.application.primaryScreen()
|
|
79
|
+
for connection in self.screen_connections:
|
|
80
|
+
QObject.disconnect(connection)
|
|
81
|
+
self.screen_connections = []
|
|
82
|
+
if screen is not None:
|
|
83
|
+
self.screen_connections = [screen.availableGeometryChanged.connect(self.schedule_refresh),
|
|
84
|
+
screen.logicalDotsPerInchChanged.connect(self.schedule_refresh)]
|
|
85
|
+
self.schedule_refresh()
|
|
86
|
+
|
|
87
|
+
def screen_removed(self, screen: QScreen) -> None:
|
|
88
|
+
"""Follow the primary screen when the window's screen is unplugged, until Qt moves the window to a remaining one."""
|
|
89
|
+
if self.handle is not None and isValid(self.handle) and self.handle.screen() is screen:
|
|
90
|
+
self.set_screen(self.application.primaryScreen())
|
|
91
|
+
|
|
92
|
+
def schedule_refresh(self, *arguments: object) -> None:
|
|
93
|
+
"""Collapse bursts of native events while ignoring our own layout changes."""
|
|
94
|
+
if not self.refreshing and not self.timer.isActive():
|
|
95
|
+
self.timer.start(0)
|
|
96
|
+
|
|
97
|
+
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
|
98
|
+
"""React to window appearance changes while preserving the native event behavior."""
|
|
99
|
+
if event.type() in (QEvent.Type.Show, QEvent.Type.WinIdChange):
|
|
100
|
+
self.attach_handle()
|
|
101
|
+
if event.type() in (QEvent.Type.Show, QEvent.Type.ApplicationFontChange, QEvent.Type.FontChange, QEvent.Type.ApplicationPaletteChange,
|
|
102
|
+
QEvent.Type.PaletteChange, QEvent.Type.DevicePixelRatioChange, QEvent.Type.ScreenChangeInternal):
|
|
103
|
+
self.schedule_refresh()
|
|
104
|
+
return super().eventFilter(watched, event)
|
|
105
|
+
|
|
106
|
+
def refresh(self) -> None:
|
|
107
|
+
"""Rebuild logical metrics and density-specific photos without resetting data or focus, for the screen the window is on now."""
|
|
108
|
+
window = self.window_ref()
|
|
109
|
+
if window is None or not isValid(window) or self.refreshing or not window.isVisible():
|
|
110
|
+
return
|
|
111
|
+
screen = window.screen()
|
|
112
|
+
if screen is None:
|
|
113
|
+
return
|
|
114
|
+
self.refreshing = True
|
|
115
|
+
try:
|
|
116
|
+
window.refresh_metrics(screen.availableGeometry())
|
|
117
|
+
ratio = window.devicePixelRatioF()
|
|
118
|
+
panel = getattr(window, "commit_panel", None)
|
|
119
|
+
if panel is not None and ratio != self.ratio:
|
|
120
|
+
panel.refresh_avatar(ratio)
|
|
121
|
+
self.ratio = ratio
|
|
122
|
+
window.update()
|
|
123
|
+
finally:
|
|
124
|
+
self.refreshing = False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class NavigationKind(Enum):
|
|
128
|
+
"""The kind of one change of repository inside the same window.
|
|
129
|
+
|
|
130
|
+
It decides what the window carries over: a walk into a submodule or back to its parent keeps the family, the
|
|
131
|
+
position and the filter of the repository it returns to; a repository chosen in the switch menu keeps nothing.
|
|
132
|
+
Each kind carries its own texts: the operation named in a failure's details, the status line of a failure, and the
|
|
133
|
+
cause given when the target turns out to be another repository.
|
|
134
|
+
"""
|
|
135
|
+
|
|
136
|
+
SUBMODULE = ("submodule", "Open submodule",
|
|
137
|
+
"Could not open submodule '{directory}'. The current repository is still open. Review the details, fix the reported problem, then open the submodule again.",
|
|
138
|
+
"The target does not resolve to its declared repository.")
|
|
139
|
+
SWITCH = ("switch", "Open repository",
|
|
140
|
+
"Could not open repository '{directory}'. The current repository is still open. Review the details, fix the reported problem, then choose it again.",
|
|
141
|
+
"The folder is no longer the top folder of a Git repository: Git opens another repository from it.")
|
|
142
|
+
|
|
143
|
+
def __new__(cls, value: str, operation_label: str, failure_status: str, target_mismatch_text: str) -> Self:
|
|
144
|
+
"""Keep the kind's name as its value, and its three texts beside it."""
|
|
145
|
+
member = object.__new__(cls)
|
|
146
|
+
member._value_ = value
|
|
147
|
+
member.operation_label = operation_label
|
|
148
|
+
member.failure_status = failure_status
|
|
149
|
+
member.target_mismatch_text = target_mismatch_text
|
|
150
|
+
return member
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class StreamedHistory:
|
|
155
|
+
"""The rows the history read delivered for one history, in the order they came.
|
|
156
|
+
|
|
157
|
+
They are kept so that a search with an empty query, which shows every commit, can publish them again without
|
|
158
|
+
reading Git: a refresh, a walk into another repository, a file history without a filter, or a cleared filter.
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
snapshot: HistorySnapshot
|
|
162
|
+
commits: list[Commit] = field(default_factory=list)
|
|
163
|
+
|
|
164
|
+
def complete_commits(self, snapshot: HistorySnapshot) -> list[Commit] | None:
|
|
165
|
+
"""Answer the kept rows only when they are exactly every commit of this complete history, in its order."""
|
|
166
|
+
if self.snapshot is not snapshot or not snapshot.complete or len(self.commits) != len(snapshot.ordered_oids):
|
|
167
|
+
return None
|
|
168
|
+
for commit, oid in zip(self.commits, snapshot.ordered_oids, strict=True):
|
|
169
|
+
if commit.oid != oid:
|
|
170
|
+
return None
|
|
171
|
+
return self.commits
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def keep_streamed_commits(streamed_history: StreamedHistory | None, snapshot: HistorySnapshot, commits: list[Commit]) -> StreamedHistory:
|
|
175
|
+
"""Add delivered rows to the kept rows of their history, starting over when they belong to a new history."""
|
|
176
|
+
if streamed_history is None or streamed_history.snapshot is not snapshot:
|
|
177
|
+
streamed_history = StreamedHistory(snapshot)
|
|
178
|
+
streamed_history.commits.extend(commits)
|
|
179
|
+
return streamed_history
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
class RepositorySession(QObject):
|
|
183
|
+
"""Own navigation and request generations for one repository window."""
|
|
184
|
+
|
|
185
|
+
selection_changed = Signal(object)
|
|
186
|
+
navigation_requested = Signal(str)
|
|
187
|
+
closed = Signal()
|
|
188
|
+
|
|
189
|
+
def __init__(self, session_id: str, repository_context: RepositoryContext, repository: GitRepository, file_history_anchor: FileHistoryAnchor | None = None) -> None:
|
|
190
|
+
"""Associate independent navigation state with an injected Git service."""
|
|
191
|
+
super().__init__()
|
|
192
|
+
self.session_id = session_id
|
|
193
|
+
self.repository_context = repository_context
|
|
194
|
+
self.repository = repository
|
|
195
|
+
self.file_history_anchor = file_history_anchor
|
|
196
|
+
self.file_history_generation = 0
|
|
197
|
+
self.file_history_result: FileHistoryResult | None = None
|
|
198
|
+
self.file_history_engine: FileHistoryEngine | None = None
|
|
199
|
+
self.mode = "file_history" if file_history_anchor is not None else "repository"
|
|
200
|
+
self.capabilities = WINDOW_CAPABILITY_BY_MODE[self.mode]
|
|
201
|
+
self.visible_oid_set: set[str] = set()
|
|
202
|
+
self.generation = 0
|
|
203
|
+
self.selection_generation = 0
|
|
204
|
+
self.selected_oid: str | None = None
|
|
205
|
+
self.filter_text = ""
|
|
206
|
+
self.anchor_oid: str | None = None
|
|
207
|
+
self.task_id_set: set[str] = set()
|
|
208
|
+
self.is_closed = False
|
|
209
|
+
self.snapshot: HistorySnapshot | None = None
|
|
210
|
+
self.search_generation = 0
|
|
211
|
+
self.search_identity: RequestIdentity | None = None
|
|
212
|
+
self.search_pending = False
|
|
213
|
+
self.matching_commits: list[Commit] = []
|
|
214
|
+
self.streamed_history: StreamedHistory | None = None
|
|
215
|
+
self.refresh_active = False
|
|
216
|
+
# The refresh asked while another one runs; it keeps its scope, its capture and its generation until it starts.
|
|
217
|
+
self.refresh_queued: RefreshRequest | None = None
|
|
218
|
+
self.view_anchor: ViewAnchor | None = None
|
|
219
|
+
self.relation_generation = 0
|
|
220
|
+
self.relation_identity: RequestIdentity | None = None
|
|
221
|
+
self.navigation_oid: str | None = None
|
|
222
|
+
self.navigation_commit: Commit | None = None
|
|
223
|
+
self.navigation_filter_cleared = False
|
|
224
|
+
self.panel_generation = 0
|
|
225
|
+
self.file_identity_by_kind: dict[FilePanel, FileReadIdentity] = {}
|
|
226
|
+
self.diff_groups: list[ComparisonGroup] = []
|
|
227
|
+
self.selected_entry: RevisionEntry | None = None
|
|
228
|
+
self.tree_entries: list[TreeEntry] = []
|
|
229
|
+
# The tree read that answered while its tab was hidden; its entries are built when the File tree tab comes back.
|
|
230
|
+
self.pending_tree_identity: FileReadIdentity | None = None
|
|
231
|
+
self.selected_tree_entry: TreeEntry | None = None
|
|
232
|
+
# The request whose commit details are on screen; the file panels read for that very request.
|
|
233
|
+
self.commit_identity: RequestIdentity | None = None
|
|
234
|
+
|
|
235
|
+
def search(self, text: str) -> None:
|
|
236
|
+
"""Keep the exact query and invalidate every older message scan immediately."""
|
|
237
|
+
if self.is_closed:
|
|
238
|
+
return
|
|
239
|
+
self.filter_text = text
|
|
240
|
+
self.search_generation += 1
|
|
241
|
+
self.search_identity = None
|
|
242
|
+
self.search_pending = True
|
|
243
|
+
self.matching_commits.clear()
|
|
244
|
+
self.repository.cancel_search(self.session_id)
|
|
245
|
+
|
|
246
|
+
def select_commit(self, oid: str) -> None:
|
|
247
|
+
"""Change this window's selection without affecting another open repository."""
|
|
248
|
+
if self.is_closed or self.selected_oid == oid:
|
|
249
|
+
return
|
|
250
|
+
if self.mode == "file_history" and (self.search_pending or oid not in self.visible_oid_set):
|
|
251
|
+
return
|
|
252
|
+
self.selected_oid = oid
|
|
253
|
+
self.navigation_oid = None
|
|
254
|
+
self.navigation_commit = None
|
|
255
|
+
self.navigation_filter_cleared = False
|
|
256
|
+
self.selection_generation += 1
|
|
257
|
+
self.invalidate_file_selection()
|
|
258
|
+
self.selection_changed.emit(oid)
|
|
259
|
+
|
|
260
|
+
def invalidate_file_selection(self) -> None:
|
|
261
|
+
"""Forget every old file comparison before a new commit can be displayed."""
|
|
262
|
+
self.panel_generation += 1
|
|
263
|
+
self.file_identity_by_kind.clear()
|
|
264
|
+
self.diff_groups.clear()
|
|
265
|
+
self.selected_entry = None
|
|
266
|
+
self.tree_entries.clear()
|
|
267
|
+
self.pending_tree_identity = None
|
|
268
|
+
self.selected_tree_entry = None
|
|
269
|
+
for panel_kind in (FilePanel.DIFF, FilePanel.TREE, FilePanel.BLOB):
|
|
270
|
+
self.repository.cancel_file(self.session_id, panel_kind)
|
|
271
|
+
|
|
272
|
+
def close(self) -> None:
|
|
273
|
+
"""Invalidate outstanding work once and release this session's navigation."""
|
|
274
|
+
if self.is_closed:
|
|
275
|
+
return
|
|
276
|
+
self.is_closed = True
|
|
277
|
+
if self.file_history_engine is not None:
|
|
278
|
+
self.file_history_engine.finished.disconnect()
|
|
279
|
+
self.file_history_engine.progressed.disconnect()
|
|
280
|
+
self.file_history_engine.close()
|
|
281
|
+
self.generation += 1
|
|
282
|
+
self.selection_generation += 1
|
|
283
|
+
self.search_generation += 1
|
|
284
|
+
self.file_history_generation += 1
|
|
285
|
+
self.relation_generation += 1
|
|
286
|
+
self.search_identity = None
|
|
287
|
+
self.relation_identity = None
|
|
288
|
+
self.search_pending = False
|
|
289
|
+
self.matching_commits.clear()
|
|
290
|
+
self.streamed_history = None
|
|
291
|
+
self.navigation_oid = None
|
|
292
|
+
self.navigation_commit = None
|
|
293
|
+
self.invalidate_file_selection()
|
|
294
|
+
self.task_id_set.clear()
|
|
295
|
+
self.repository.cancel(self.session_id)
|
|
296
|
+
self.repository.context_by_session.pop(self.session_id, None)
|
|
297
|
+
self.closed.emit()
|
|
298
|
+
|
|
299
|
+
def navigate_to_commit(self, oid: str) -> None:
|
|
300
|
+
"""Validate an explicit target and invalidate any older navigation before reading it."""
|
|
301
|
+
if self.is_closed or (self.mode == "file_history" and self.search_pending):
|
|
302
|
+
return
|
|
303
|
+
validate_oid(oid)
|
|
304
|
+
if self.mode == "file_history" and oid not in self.visible_oid_set:
|
|
305
|
+
return
|
|
306
|
+
self.select_commit(oid)
|
|
307
|
+
self.selection_generation += 1
|
|
308
|
+
self.navigation_oid = oid
|
|
309
|
+
self.navigation_requested.emit(oid)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class SessionRegistry:
|
|
313
|
+
"""The sessions, windows, dialogs and flags that the controllers of GitPane share."""
|
|
314
|
+
|
|
315
|
+
def __init__(self) -> None:
|
|
316
|
+
"""Start with no session open; the wiring builds the registry before any service, so every controller can receive it."""
|
|
317
|
+
self.dialogs: list[ErrorDialog | QMessageBox] = []
|
|
318
|
+
self.clone_dialog_by_session: dict[str, CloneDialog] = {}
|
|
319
|
+
self.clone_generation_by_session: dict[str, int] = {}
|
|
320
|
+
self.export_owner_by_identity: dict[FileReadIdentity, str] = {}
|
|
321
|
+
self.setting_dialog: SettingDialog | None = None
|
|
322
|
+
self.welcome_by_session: dict[str, WelcomeWindow] = {}
|
|
323
|
+
self.window_by_session: dict[str, RepositoryWindow] = {}
|
|
324
|
+
self.session_by_id: dict[str, RepositorySession] = {}
|
|
325
|
+
self.generation_by_session: dict[str, int] = {}
|
|
326
|
+
self.opening_dir_by_session: dict[str, str] = {}
|
|
327
|
+
self.identity_by_session: dict[str, RequestIdentity] = {}
|
|
328
|
+
self.navigation_owner_by_id: dict[str, str] = {}
|
|
329
|
+
self.shutdown_requested = False
|
|
330
|
+
self.quit_pending = False
|
|
331
|
+
self.close_session_set: set[str] = set()
|
|
332
|
+
self.close_reread_count_by_key: dict[WorktreeKey, int] = {}
|
|
333
|
+
self.navigation_request_by_session: dict[str, tuple[RepositorySession, str, NavigationKind]] = {}
|
|
334
|
+
# Sessions whose conflict list opens, for a paused replay, or whose conflict resolution is offered, after a Stash
|
|
335
|
+
# pop left conflicts, once the next fresh read of their worktree arrives.
|
|
336
|
+
self.replay_pending_resolve_set: set[str] = set()
|
|
337
|
+
self.conflict_pending_session_set: set[str] = set()
|