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,122 @@
|
|
|
1
|
+
"""One rule to open a window that blocks the window it belongs to, or the whole application, chosen by Qt platform."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from PySide6.QtCore import QCoreApplication, QEvent, Qt
|
|
8
|
+
from PySide6.QtGui import QGuiApplication, QKeySequence, QShortcut
|
|
9
|
+
from PySide6.QtWidgets import QDialog, QMessageBox
|
|
10
|
+
|
|
11
|
+
from gitpane.config import MACOS_PLATFORM_NAME
|
|
12
|
+
from gitpane.ui.look.theme_look import follow_theme
|
|
13
|
+
|
|
14
|
+
# What result() holds from the moment the dialog is shown until it is answered: QDialog.open() resets the previous answer to Rejected (0),
|
|
15
|
+
# and the macOS row does the same. A Cancel leaves the same value, so it never tells an unanswered dialog from a refused one.
|
|
16
|
+
UNANSWERED_DIALOG_RESULT = QDialog.DialogCode.Rejected.value
|
|
17
|
+
# Object name of the Quit shortcut a modal window carries, so that opening the same window again adds no second one.
|
|
18
|
+
QUIT_SHORTCUT_OBJECT_NAME = "gitpane_quit_shortcut"
|
|
19
|
+
# Cmd+Q: Qt names the Command key Ctrl on macOS. Written out because the standard Quit key comes from the platform theme,
|
|
20
|
+
# and the offscreen platform the headless tests run on has none.
|
|
21
|
+
MACOS_QUIT_KEY_SEQUENCE = "Ctrl+Q"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ModalPresentation:
|
|
26
|
+
"""How a dialog that blocks the window it belongs to is shown on one Qt platform."""
|
|
27
|
+
|
|
28
|
+
uses_dialog_open: bool
|
|
29
|
+
modality: Qt.WindowModality
|
|
30
|
+
greys_extra_title_buttons: bool
|
|
31
|
+
# True where the system's Quit shortcut never reaches the application while a modal window is open, so the modal
|
|
32
|
+
# window carries it itself.
|
|
33
|
+
carries_quit_shortcut: bool = False
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
DEFAULT_MODAL_PRESENTATION = ModalPresentation(uses_dialog_open=True, modality=Qt.WindowModality.WindowModal, greys_extra_title_buttons=False)
|
|
37
|
+
# Workaround: Qt's Cocoa backend shows every window-modal window that has a parent as a sheet, with no title bar and no
|
|
38
|
+
# close button. Application modality is the only one that keeps a real titled window there.
|
|
39
|
+
MODAL_PRESENTATION_BY_PLATFORM_NAME: dict[str, ModalPresentation] = {
|
|
40
|
+
MACOS_PLATFORM_NAME: ModalPresentation(uses_dialog_open=False, modality=Qt.WindowModality.ApplicationModal, greys_extra_title_buttons=True,
|
|
41
|
+
carries_quit_shortcut=True),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def modal_presentation(platform_name: str) -> ModalPresentation:
|
|
46
|
+
"""Return how a modal dialog opens on a Qt platform, named as QGuiApplication.platformName() names it."""
|
|
47
|
+
return MODAL_PRESENTATION_BY_PLATFORM_NAME.get(platform_name, DEFAULT_MODAL_PRESENTATION)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def open_window_modal(dialog: QDialog, *, keeps_window_buttons: bool = False, blocks_application: bool = False, platform_name: str = "") -> None:
|
|
51
|
+
"""Show a dialog that blocks its window, titled with a working close button; where no sheet exists, exactly QDialog.open().
|
|
52
|
+
|
|
53
|
+
keeps_window_buttons leaves Qt's own title bar buttons, for the operation consoles. blocks_application makes the
|
|
54
|
+
dialog block every other window of the application on every system, for a dialog shared by every window and so
|
|
55
|
+
with no parent, such as Settings: QDialog.open() would make it window-modal, which blocks nothing without a parent.
|
|
56
|
+
platform_name stands in for the session's platform, so a headless test can check the row of any system. Where the
|
|
57
|
+
system keeps its Quit shortcut from a running modal window, the window gets its own.
|
|
58
|
+
"""
|
|
59
|
+
presentation = modal_presentation(platform_name or QGuiApplication.platformName())
|
|
60
|
+
modality = Qt.WindowModality.ApplicationModal if blocks_application else presentation.modality
|
|
61
|
+
if presentation.uses_dialog_open and not blocks_application:
|
|
62
|
+
dialog.open()
|
|
63
|
+
return
|
|
64
|
+
# A dialog already shown the same way is left as it is: setting its flags or, for a question on a macOS build of
|
|
65
|
+
# Qt, its modality again would hide it and show it again.
|
|
66
|
+
if dialog.isVisible() and dialog.windowModality() == modality:
|
|
67
|
+
return
|
|
68
|
+
if presentation.carries_quit_shortcut:
|
|
69
|
+
add_quit_shortcut(dialog)
|
|
70
|
+
if presentation.greys_extra_title_buttons and not keeps_window_buttons and not dialog.windowFlags() & Qt.WindowType.CustomizeWindowHint:
|
|
71
|
+
# With this hint Qt enables each title bar button only when its own hint is set: close stays, minimize and zoom
|
|
72
|
+
# turn grey, and a window that asks for minimize and maximize keeps them. Set only when missing, because
|
|
73
|
+
# setWindowFlag hides a visible window to apply its flags.
|
|
74
|
+
dialog.setWindowFlag(Qt.WindowType.CustomizeWindowHint, True)
|
|
75
|
+
dialog.setWindowModality(modality)
|
|
76
|
+
# QDialog.open() clears the previous answer the same way before showing the dialog.
|
|
77
|
+
dialog.setResult(UNANSWERED_DIALOG_RESULT)
|
|
78
|
+
dialog.show()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def add_quit_shortcut(dialog: QDialog) -> None:
|
|
82
|
+
"""Give a modal window the system's Quit shortcut, which asks the application to quit as Cmd+Q does without a modal window.
|
|
83
|
+
|
|
84
|
+
Workaround: on macOS, AppKit keeps Cmd+Q and the Dock's Quit from the application while an application-modal window
|
|
85
|
+
runs, so GitPane never hears that the user wants to quit. A shortcut of the window itself still receives the key.
|
|
86
|
+
It sends the Quit event the system would have sent, and whoever handles quitting decides what to do with the open
|
|
87
|
+
windows; the Dock's Quit stays without effect while a modal window is open. Added once per window.
|
|
88
|
+
"""
|
|
89
|
+
if dialog.findChild(QShortcut, QUIT_SHORTCUT_OBJECT_NAME, Qt.FindChildOption.FindDirectChildrenOnly) is not None:
|
|
90
|
+
return
|
|
91
|
+
shortcut = QShortcut(QKeySequence(MACOS_QUIT_KEY_SEQUENCE), dialog)
|
|
92
|
+
shortcut.setObjectName(QUIT_SHORTCUT_OBJECT_NAME)
|
|
93
|
+
shortcut.activated.connect(send_application_quit)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def send_application_quit() -> None:
|
|
97
|
+
"""Send the application the Quit event the system sends when the user quits it, answered before the key press ends, as Escape answers Cancel."""
|
|
98
|
+
application = QCoreApplication.instance()
|
|
99
|
+
if application is None:
|
|
100
|
+
return
|
|
101
|
+
QCoreApplication.sendEvent(application, QEvent(QEvent.Type.Quit))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def prepare_message_box(box: QMessageBox, title: str, *, platform_name: str = "") -> None:
|
|
105
|
+
"""Keep a question a Qt window that shows its title, on every system, and give it the icon of each new appearance.
|
|
106
|
+
|
|
107
|
+
Workaround: a macOS build of Qt ignores a message box's title, even offscreen, and shows it as a native alert with no
|
|
108
|
+
title bar. Windows and Linux never use a native message box and set the title the same way, so nothing changes there.
|
|
109
|
+
|
|
110
|
+
The style redraws a question icon that cannot be read on a dark window, and a message box keeps the icon it got when
|
|
111
|
+
it was set. A question open while the appearance changes sets its own icon again, so the style draws it for the
|
|
112
|
+
new appearance; the palette follower has already given the box its new palette by then.
|
|
113
|
+
|
|
114
|
+
Where the system keeps its Quit shortcut from a running modal window, the question gets its own here, so it has it
|
|
115
|
+
whether open_window_modal shows it or its own exec() runs it. platform_name stands in for the session's platform,
|
|
116
|
+
as in open_window_modal.
|
|
117
|
+
"""
|
|
118
|
+
box.setOption(QMessageBox.Option.DontUseNativeDialog, True)
|
|
119
|
+
QDialog.setWindowTitle(box, title)
|
|
120
|
+
follow_theme(box, lambda: box.setIcon(box.icon()))
|
|
121
|
+
if modal_presentation(platform_name or QGuiApplication.platformName()).carries_quit_shortcut:
|
|
122
|
+
add_quit_shortcut(box)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Let Enter activate the current row of an item view on every system.
|
|
2
|
+
|
|
3
|
+
Qt for macOS does not emit ``activated`` when Enter is pressed in an item view: it tries to edit the current item,
|
|
4
|
+
then leaves the key to the window, and only Cmd+O activates. GitPane wants Enter to open, choose or navigate as in
|
|
5
|
+
Git Extensions on every system, so the views where Enter acts pass their key presses through ``activate_current_row``,
|
|
6
|
+
which does what Qt does on Windows and Linux. The behaviour is the same everywhere, so nothing here depends on the
|
|
7
|
+
system or the Qt platform.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from PySide6.QtCore import QEvent, Qt
|
|
15
|
+
from PySide6.QtWidgets import QAbstractItemView
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from PySide6.QtGui import QKeyEvent
|
|
19
|
+
|
|
20
|
+
ACTIVATION_KEY_SET = frozenset({Qt.Key.Key_Return, Qt.Key.Key_Enter})
|
|
21
|
+
"""Keys that activate the current row: the main Enter key and the one of the numeric keypad."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def activate_current_row(view: QAbstractItemView, event: QEvent) -> bool:
|
|
25
|
+
"""Activate the view's current row once on Enter, and let the key go on to the window; return whether Enter was handled.
|
|
26
|
+
|
|
27
|
+
Called from an event filter on the view. On Enter, while no editor is open and a row is current, the view's
|
|
28
|
+
``activated`` signal is emitted for that row and the event is marked ignored, so the key still reaches the window,
|
|
29
|
+
where a dialog's default button reads the row just chosen. The caller returns ``True`` so the view does not act on
|
|
30
|
+
the key itself: on macOS it would try to edit the row, elsewhere it would emit ``activated`` a second time.
|
|
31
|
+
"""
|
|
32
|
+
if event.type() != QEvent.Type.KeyPress:
|
|
33
|
+
return False
|
|
34
|
+
key_event: QKeyEvent = event
|
|
35
|
+
if key_event.key() not in ACTIVATION_KEY_SET or view.state() == QAbstractItemView.State.EditingState:
|
|
36
|
+
return False
|
|
37
|
+
current_index = view.currentIndex()
|
|
38
|
+
if not current_index.isValid():
|
|
39
|
+
return False
|
|
40
|
+
view.activated.emit(current_index)
|
|
41
|
+
key_event.ignore()
|
|
42
|
+
return True
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Bring the modal window back in front when the window system activates a window it blocks.
|
|
2
|
+
|
|
3
|
+
Under Wayland, a compositor keeps a modal window in front of a window only when the modal window is that window's
|
|
4
|
+
transient. A window modal for the whole application, such as Settings, has no parent: a click on a window it blocks
|
|
5
|
+
makes the compositor raise and activate that window above it, while Qt still refuses that window's input. Qt's X11
|
|
6
|
+
plugin already relays the focus to the modal window itself, and Windows and macOS never activate a blocked window, so
|
|
7
|
+
the way to bring the modal window back is chosen per Qt platform, once, by the composition root.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from PySide6.QtCore import QEvent, QObject, Qt
|
|
16
|
+
from PySide6.QtGui import QGuiApplication, QWindow
|
|
17
|
+
from shiboken6 import getCppPointer
|
|
18
|
+
|
|
19
|
+
from gitpane.config import WAYLAND_PLATFORM_NAME
|
|
20
|
+
from gitpane.ui.window_control.wayland_activation import activate_window_with_keyboard_token
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from collections.abc import Iterator
|
|
24
|
+
|
|
25
|
+
ModalWindowActivator = Callable[[QWindow], bool]
|
|
26
|
+
"""Bring a GitPane window to the front through the window system, and tell whether it was asked."""
|
|
27
|
+
|
|
28
|
+
# Where a platform is missing (X11, Windows, macOS, the offscreen platform of the tests), the window system or Qt
|
|
29
|
+
# already keeps the modal window in front of the windows it blocks.
|
|
30
|
+
MODAL_WINDOW_ACTIVATOR_BY_PLATFORM_NAME: dict[str, ModalWindowActivator] = {
|
|
31
|
+
WAYLAND_PLATFORM_NAME: activate_window_with_keyboard_token,
|
|
32
|
+
}
|
|
33
|
+
# The only events the filter acts on; it runs on every event of the application, so it lets any other one through at once.
|
|
34
|
+
WINDOW_BLOCKING_EVENT_TYPE_SET = frozenset({QEvent.Type.WindowBlocked, QEvent.Type.WindowUnblocked})
|
|
35
|
+
# Place of the C++ address in what shiboken6.getCppPointer answers: index 0 is the address of the object seen as its
|
|
36
|
+
# first C++ base (QObject for a QWindow, which also derives from QSurface), the same at every reading of a QWindow.
|
|
37
|
+
CPP_ADDRESS_INDEX = 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def modal_window_activator(platform_name: str) -> ModalWindowActivator | None:
|
|
41
|
+
"""Return how a Qt platform, under the name Qt gives it, brings the modal window back, or None where nothing is needed.
|
|
42
|
+
|
|
43
|
+
Pure, so the table can be checked for every platform from any machine.
|
|
44
|
+
"""
|
|
45
|
+
return MODAL_WINDOW_ACTIVATOR_BY_PLATFORM_NAME.get(platform_name)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _window_address(window: QWindow) -> int:
|
|
49
|
+
"""Return the C++ address of a window, which names it in the blocked set and still names it while it is destroyed."""
|
|
50
|
+
return getCppPointer(window)[CPP_ADDRESS_INDEX]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _transient_chain(window: QWindow) -> Iterator[QWindow]:
|
|
54
|
+
"""Yield a window, then each of its transient parents up to the window that has none."""
|
|
55
|
+
current: QWindow | None = window
|
|
56
|
+
while current is not None:
|
|
57
|
+
yield current
|
|
58
|
+
current = current.transientParent()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# Workaround: under Wayland, Qt sends a window modal for the whole application without any parent, so the compositor
|
|
62
|
+
# does not keep it in front of the windows it blocks, and Qt's Wayland plugin, unlike its X11 plugin, does not hand the
|
|
63
|
+
# focus back to the modal window when the compositor activates a blocked one. This relay follows the windows Qt
|
|
64
|
+
# declares blocked; when the focus reaches one of them, it asks the window system, once, to activate the modal window
|
|
65
|
+
# Qt hands the input to. A modal window that blocks only its own window is left to the compositor: it is that window's
|
|
66
|
+
# transient, which the compositor already keeps in front.
|
|
67
|
+
class ModalFocusRelay(QObject):
|
|
68
|
+
"""Bring the application-modal window back in front when the focus reaches a window it blocks."""
|
|
69
|
+
|
|
70
|
+
def __init__(self, application: QGuiApplication, activate: ModalWindowActivator, parent: QObject | None = None) -> None:
|
|
71
|
+
"""Follow the blocked windows of ``application`` and its focus window; ``activate`` brings a window back through the window system.
|
|
72
|
+
|
|
73
|
+
The relay belongs to ``parent`` rather than to the application: it runs on every event of the application, so it
|
|
74
|
+
must end with the wiring that created it.
|
|
75
|
+
"""
|
|
76
|
+
super().__init__(parent)
|
|
77
|
+
self.application = application
|
|
78
|
+
self.activate = activate
|
|
79
|
+
self.blocked_window_address_set: set[int] = set()
|
|
80
|
+
# The windows whose end the relay already follows, so a window blocked again and again gets one connection.
|
|
81
|
+
self.followed_window_address_set: set[int] = set()
|
|
82
|
+
application.installEventFilter(self)
|
|
83
|
+
application.focusWindowChanged.connect(self.follow_focus_window)
|
|
84
|
+
|
|
85
|
+
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
|
86
|
+
"""Note each window Qt blocks or unblocks under a modal window, letting every event through."""
|
|
87
|
+
if event.type() not in WINDOW_BLOCKING_EVENT_TYPE_SET or not watched.isWindowType():
|
|
88
|
+
return False
|
|
89
|
+
address = _window_address(watched)
|
|
90
|
+
if event.type() == QEvent.Type.WindowUnblocked:
|
|
91
|
+
self.blocked_window_address_set.discard(address)
|
|
92
|
+
return False
|
|
93
|
+
self.blocked_window_address_set.add(address)
|
|
94
|
+
if address not in self.followed_window_address_set:
|
|
95
|
+
self.followed_window_address_set.add(address)
|
|
96
|
+
# The sets themselves are captured, not the relay: the window may outlive the relay, and its end must not reach a deleted relay.
|
|
97
|
+
watched.destroyed.connect(lambda _object=None, gone=address, blocked=self.blocked_window_address_set, followed=self.followed_window_address_set:
|
|
98
|
+
(blocked.discard(gone), followed.discard(gone)))
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def follow_focus_window(self, window: QWindow | None) -> None:
|
|
102
|
+
"""When the focus reaches a window an application-modal window blocks, ask once to bring the modal window back.
|
|
103
|
+
|
|
104
|
+
The modal window brought back is the one Qt hands the input to, the modal window shown last; a window in its
|
|
105
|
+
chain of transient parents, or the modal window itself, is already kept behind it by the compositor.
|
|
106
|
+
"""
|
|
107
|
+
if window is None or _window_address(window) not in self.blocked_window_address_set:
|
|
108
|
+
return
|
|
109
|
+
modal = QGuiApplication.modalWindow()
|
|
110
|
+
if modal is None or not blocks_whole_application(modal):
|
|
111
|
+
return
|
|
112
|
+
window_address = _window_address(window)
|
|
113
|
+
for chained in _transient_chain(modal):
|
|
114
|
+
if _window_address(chained) == window_address:
|
|
115
|
+
return
|
|
116
|
+
self.activate(modal)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def blocks_whole_application(modal: QWindow) -> bool:
|
|
120
|
+
"""Tell whether a modal window, or one of its transient parents, is modal for the whole application."""
|
|
121
|
+
for chained in _transient_chain(modal):
|
|
122
|
+
if chained.modality() == Qt.WindowModality.ApplicationModal:
|
|
123
|
+
return True
|
|
124
|
+
return False
|