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,332 @@
|
|
|
1
|
+
"""Present a prepared diagnostic as copyable text, blocking its window the way its opener decides."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
from PySide6.QtCore import QRect, Qt
|
|
10
|
+
from PySide6.QtGui import QTextDocument
|
|
11
|
+
from PySide6.QtWidgets import QApplication, QDialog, QHBoxLayout, QLabel, QLayout, QPlainTextEdit, QPushButton, QScrollArea, QStyle, QVBoxLayout, QWidget
|
|
12
|
+
from shiboken6 import isValid
|
|
13
|
+
|
|
14
|
+
from gitpane.config import (
|
|
15
|
+
ERROR_HEIGHT,
|
|
16
|
+
ERROR_MAX_SCREEN_HEIGHT_RATIO,
|
|
17
|
+
ERROR_SUMMARY_MAX_SHARE,
|
|
18
|
+
ERROR_WIDTH,
|
|
19
|
+
LAYOUT_SPACING,
|
|
20
|
+
SPACING_MD,
|
|
21
|
+
SharedText,
|
|
22
|
+
control_height,
|
|
23
|
+
fit_window,
|
|
24
|
+
known_frame_margins,
|
|
25
|
+
unknown_decoration_estimate,
|
|
26
|
+
window_placement,
|
|
27
|
+
)
|
|
28
|
+
from gitpane.ui.look.fixed_font import body_sized_fixed_font
|
|
29
|
+
from gitpane.ui.widget.long_text import FRAME_SIDE_COUNT, WrappingLabel
|
|
30
|
+
from gitpane.ui.window_control.compositor_answer import after_compositor_answer
|
|
31
|
+
|
|
32
|
+
if TYPE_CHECKING:
|
|
33
|
+
from PySide6.QtGui import QMoveEvent, QResizeEvent, QShowEvent
|
|
34
|
+
|
|
35
|
+
from gitpane.operation.diagnostic import PreparedDiagnostic
|
|
36
|
+
|
|
37
|
+
PLAIN_TEXT_LAST_LINE_SLACK = 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ErrorDialogText(Enum):
|
|
41
|
+
"""Texts the error window shows, one member per text so a misspelt name is an error."""
|
|
42
|
+
|
|
43
|
+
CLOSE = "Close diagnostic"
|
|
44
|
+
MASKED = "Some sensitive values have been hidden."
|
|
45
|
+
HELP = "Read the details, fix the cause, then try again."
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ErrorDialog(QDialog):
|
|
49
|
+
"""Keep a real, copyable cause and its details visible while other windows and tasks continue.
|
|
50
|
+
|
|
51
|
+
The details are shown from the start, in an area as tall as their text, and the window never takes more than a
|
|
52
|
+
fixed share of the usable screen height: past it, the details scroll and the actions under them stay on screen.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(self, diagnostic: PreparedDiagnostic, parent: QWidget, log_filename: str | None = None, *, recovery_command: str = "", recovery_hint: str = "") -> None:
|
|
56
|
+
"""Show the cause, an optional manual recovery command and the diagnostic details, with Copy details and Close diagnostic under them."""
|
|
57
|
+
super().__init__(parent)
|
|
58
|
+
self.diagnostic = diagnostic
|
|
59
|
+
# The screen area and the title bar height the last height fit used, so the fit is made again with the same
|
|
60
|
+
# area once the window manager has added a title bar of another height.
|
|
61
|
+
self.fitted_available: QRect | None = None
|
|
62
|
+
self.fitted_decoration_height: int | None = None
|
|
63
|
+
# True until the window manager has framed the window: only then may a move or resize from the window system
|
|
64
|
+
# fit the height again, once.
|
|
65
|
+
self.awaiting_window_manager_frame = True
|
|
66
|
+
self.return_focus = parent.focusWidget()
|
|
67
|
+
self.finished.connect(self._restore_focus)
|
|
68
|
+
self.setWindowTitle(diagnostic.operation)
|
|
69
|
+
self.resize(ERROR_WIDTH, ERROR_HEIGHT)
|
|
70
|
+
layout = QVBoxLayout(self)
|
|
71
|
+
layout.setSizeConstraint(QLayout.SizeConstraint.SetNoConstraint)
|
|
72
|
+
layout.setSpacing(LAYOUT_SPACING)
|
|
73
|
+
layout.setContentsMargins(SPACING_MD, SPACING_MD, SPACING_MD, SPACING_MD)
|
|
74
|
+
self.summary_scroll = QScrollArea()
|
|
75
|
+
self.summary_scroll.setWidgetResizable(True)
|
|
76
|
+
summary = QWidget()
|
|
77
|
+
summary_layout = QVBoxLayout(summary)
|
|
78
|
+
self.summary_scroll.setWidget(summary)
|
|
79
|
+
layout.addWidget(self.summary_scroll, 0)
|
|
80
|
+
# The whole cause is shown, wrapped at words and inside words too long for a line; the summary area scrolls
|
|
81
|
+
# when it is taller than its share of the window, so the close and copy buttons below stay on screen.
|
|
82
|
+
self.cause_label = WrappingLabel(diagnostic.cause)
|
|
83
|
+
self.cause_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextSelectableByKeyboard)
|
|
84
|
+
self.cause_label.setWordWrap(True)
|
|
85
|
+
cause_layout = QHBoxLayout()
|
|
86
|
+
self.error_icon_label = QLabel()
|
|
87
|
+
cause_layout.addWidget(self.error_icon_label, 0, Qt.AlignmentFlag.AlignTop)
|
|
88
|
+
cause_layout.addWidget(self.cause_label, 1)
|
|
89
|
+
summary_layout.addLayout(cause_layout)
|
|
90
|
+
self.recovery_command_edit: QPlainTextEdit | None = None
|
|
91
|
+
self.copy_command_button: QPushButton | None = None
|
|
92
|
+
self.recovery_hint_label: QLabel | None = None
|
|
93
|
+
if recovery_command:
|
|
94
|
+
self.recovery_hint_label = QLabel(recovery_hint or "Manual recovery command:")
|
|
95
|
+
self.recovery_hint_label.setTextFormat(Qt.TextFormat.PlainText)
|
|
96
|
+
self.recovery_hint_label.setWordWrap(True)
|
|
97
|
+
summary_layout.addWidget(self.recovery_hint_label)
|
|
98
|
+
self.recovery_command_edit = QPlainTextEdit(recovery_command)
|
|
99
|
+
self.recovery_command_edit.setReadOnly(True)
|
|
100
|
+
self.recovery_command_edit.setAccessibleName("Manual recovery command")
|
|
101
|
+
summary_layout.addWidget(self.recovery_command_edit)
|
|
102
|
+
self.copy_command_button = QPushButton("Copy command")
|
|
103
|
+
self.copy_command_button.clicked.connect(self.copy_recovery_command)
|
|
104
|
+
summary_layout.addWidget(self.copy_command_button)
|
|
105
|
+
help_label = QLabel(ErrorDialogText.HELP.value)
|
|
106
|
+
help_label.setWordWrap(True)
|
|
107
|
+
summary_layout.addWidget(help_label)
|
|
108
|
+
if diagnostic.masked:
|
|
109
|
+
masked_label = QLabel(ErrorDialogText.MASKED.value)
|
|
110
|
+
masked_label.setWordWrap(True)
|
|
111
|
+
summary_layout.addWidget(masked_label)
|
|
112
|
+
if log_filename:
|
|
113
|
+
label = QLabel(log_filename)
|
|
114
|
+
label.setTextFormat(Qt.TextFormat.PlainText)
|
|
115
|
+
label.setWordWrap(True)
|
|
116
|
+
label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse | Qt.TextInteractionFlag.TextSelectableByKeyboard)
|
|
117
|
+
summary_layout.addWidget(label)
|
|
118
|
+
self.detail_edit = QPlainTextEdit(diagnostic.detail)
|
|
119
|
+
self.detail_edit.setReadOnly(True)
|
|
120
|
+
self.detail_edit.setAccessibleName("Diagnostic details")
|
|
121
|
+
layout.addWidget(self.detail_edit, 1)
|
|
122
|
+
self.action_layout = QVBoxLayout()
|
|
123
|
+
self.copy_button = QPushButton(SharedText.COPY_DETAILS.value)
|
|
124
|
+
# Enter closes the window: Copy details, even with the focus, never takes Close's place as the button Enter clicks.
|
|
125
|
+
self.copy_button.setAutoDefault(False)
|
|
126
|
+
self.copy_button.clicked.connect(self.copy_detail)
|
|
127
|
+
self.action_layout.addWidget(self.copy_button)
|
|
128
|
+
self.close_button = QPushButton(ErrorDialogText.CLOSE.value)
|
|
129
|
+
self.close_button.setDefault(True)
|
|
130
|
+
self.close_button.clicked.connect(self.accept)
|
|
131
|
+
self.action_layout.addWidget(self.close_button)
|
|
132
|
+
layout.addLayout(self.action_layout)
|
|
133
|
+
self.refresh_metrics()
|
|
134
|
+
|
|
135
|
+
def refresh_metrics(self, available: QRect | None = None) -> None:
|
|
136
|
+
"""Use system monospace text, size the actions for the current font, then fit the window's height to its content.
|
|
137
|
+
|
|
138
|
+
``available`` is the usable area of the screen the window is on; without it, the screen of the window it was
|
|
139
|
+
opened from is used.
|
|
140
|
+
"""
|
|
141
|
+
fixed_font = body_sized_fixed_font()
|
|
142
|
+
self.detail_edit.setFont(fixed_font)
|
|
143
|
+
if self.recovery_command_edit is not None:
|
|
144
|
+
self.recovery_command_edit.setFont(fixed_font)
|
|
145
|
+
command_line_count = 4
|
|
146
|
+
self.recovery_command_edit.setFixedHeight(self.recovery_command_edit.fontMetrics().lineSpacing() * command_line_count + SPACING_MD)
|
|
147
|
+
if self.copy_command_button is not None:
|
|
148
|
+
self.copy_command_button.setMinimumHeight(control_height(self.copy_command_button.fontMetrics().height()))
|
|
149
|
+
icon_side = self.style().pixelMetric(QStyle.PixelMetric.PM_MessageBoxIconSize)
|
|
150
|
+
self.error_icon_label.setPixmap(self.style().standardIcon(QStyle.StandardPixmap.SP_MessageBoxCritical).pixmap(icon_side, icon_side))
|
|
151
|
+
for button in (self.copy_button, self.close_button):
|
|
152
|
+
button.setMinimumHeight(control_height(button.fontMetrics().height()))
|
|
153
|
+
self.fit_height_to_content(available)
|
|
154
|
+
|
|
155
|
+
def showEvent(self, event: QShowEvent) -> None:
|
|
156
|
+
"""Fit the height again once the window is about to appear, before it is fitted to its content and centred on the window it was opened from.
|
|
157
|
+
|
|
158
|
+
Under X11 the title bar is not there yet: this fit counts the one borrowed from the window it was opened from, and
|
|
159
|
+
the move or resize the window manager makes when it adds the real one fits the height again if it differs. That
|
|
160
|
+
is the only later fit: the window then leaves its height and content alone whatever the window system does.
|
|
161
|
+
Where the system already gives the frame of the native window before it appears (macOS, Windows), this fit
|
|
162
|
+
counts the real title bar, so no later fit is awaited at all. Where the toolkit draws the title bar itself once
|
|
163
|
+
the window is shown (Wayland), no move or resize from the window system follows the show: the fit is made again
|
|
164
|
+
once the compositor has answered the show, which would otherwise overwrite a size asked before its answer.
|
|
165
|
+
"""
|
|
166
|
+
super().showEvent(event)
|
|
167
|
+
self.fit_height_to_content()
|
|
168
|
+
if self.native_frame_margins_known():
|
|
169
|
+
self.awaiting_window_manager_frame = False
|
|
170
|
+
after_compositor_answer(self, self.refit_once_frame_known)
|
|
171
|
+
|
|
172
|
+
def refit_once_frame_known(self) -> None:
|
|
173
|
+
"""Fit the height again, once, with the screen area of the last fit, when the title bar now known has another height than the last fit counted.
|
|
174
|
+
|
|
175
|
+
The window then stops waiting for its title bar, as after a move or resize from the window system. A title bar
|
|
176
|
+
still unknown leaves the window waiting: under X11 the window manager's move or resize does that work later.
|
|
177
|
+
"""
|
|
178
|
+
if not self.awaiting_window_manager_frame or not self.native_frame_margins_known():
|
|
179
|
+
return
|
|
180
|
+
if self.decoration_height() != self.fitted_decoration_height:
|
|
181
|
+
self.fit_height_to_content(self.fitted_available)
|
|
182
|
+
self.awaiting_window_manager_frame = False
|
|
183
|
+
|
|
184
|
+
def moveEvent(self, event: QMoveEvent) -> None:
|
|
185
|
+
"""Fit the height again when the window manager has just framed the window with a title bar of another height than the last fit counted."""
|
|
186
|
+
super().moveEvent(event)
|
|
187
|
+
self.refit_after_window_manager_frame(event)
|
|
188
|
+
|
|
189
|
+
def resizeEvent(self, event: QResizeEvent) -> None:
|
|
190
|
+
"""Fit the height again when the window manager has just framed the window with a title bar of another height than the last fit counted."""
|
|
191
|
+
super().resizeEvent(event)
|
|
192
|
+
self.refit_after_window_manager_frame(event)
|
|
193
|
+
|
|
194
|
+
def refit_after_window_manager_frame(self, event: QMoveEvent | QResizeEvent) -> None:
|
|
195
|
+
"""Fit the height again, once, with the screen area of the last fit, when the window manager framed the window with a title bar of another height.
|
|
196
|
+
|
|
197
|
+
Workaround: under X11 the window manager adds the title bar after the window is shown, and Qt sends no event for
|
|
198
|
+
the title bar itself; the only sign of it is the move or resize the window system reports (a spontaneous event)
|
|
199
|
+
when the window manager frames the window. A fit made before that counted an estimated title bar, or none, so the
|
|
200
|
+
frame can end below the screen edge. The fit is made again only when the title bar height read now differs from
|
|
201
|
+
the one the last fit counted: the moves and resizes the fit makes itself are not spontaneous, and the window
|
|
202
|
+
system's confirmations of them find the same title bar.
|
|
203
|
+
The hook then stops at the first spontaneous event that comes with the native frame margins known, whether it
|
|
204
|
+
fitted or not: the title bar is in place, and fitting again later (borders dropped on maximize, another screen)
|
|
205
|
+
would compute the content once more and could undo a move or resize the user just made. Where the compositor
|
|
206
|
+
answers the show with a size of its own (Wayland), the hook does nothing: the fit made once the compositor has
|
|
207
|
+
answered the show does this work. The only spontaneous move there comes when the compositor puts the window on
|
|
208
|
+
another screen than Qt assumed, and it is delivered before the size of the same answer, which would overwrite a
|
|
209
|
+
fit made then and leave the frame taller than the cap.
|
|
210
|
+
"""
|
|
211
|
+
if not self.awaiting_window_manager_frame or not event.spontaneous() or window_placement().size_waits_for_compositor_answer:
|
|
212
|
+
return
|
|
213
|
+
if self.decoration_height() != self.fitted_decoration_height:
|
|
214
|
+
self.fit_height_to_content(self.fitted_available)
|
|
215
|
+
if self.native_frame_margins_known():
|
|
216
|
+
self.awaiting_window_manager_frame = False
|
|
217
|
+
|
|
218
|
+
def native_frame_margins_known(self) -> bool:
|
|
219
|
+
"""Say whether the window system has given the margins of the frame it put around the window, title bar included."""
|
|
220
|
+
native_window = self.windowHandle()
|
|
221
|
+
return native_window is not None and not native_window.frameMargins().isNull()
|
|
222
|
+
|
|
223
|
+
def fit_height_to_content(self, available: QRect | None = None) -> None:
|
|
224
|
+
"""Give the cause and the details the height of their text at the window's width, within a share of the usable screen height.
|
|
225
|
+
|
|
226
|
+
Past that cap the window stays at the cap: the cause takes at most a share of the room left to the two text areas
|
|
227
|
+
and scrolls, the details take the rest and scroll, and the buttons stay visible under them. The screen area and
|
|
228
|
+
the title bar height used are kept, so the window can be fitted again with the same area once its real title bar
|
|
229
|
+
is known.
|
|
230
|
+
"""
|
|
231
|
+
layout = self.layout()
|
|
232
|
+
margins = layout.contentsMargins()
|
|
233
|
+
decoration_height = self.decoration_height()
|
|
234
|
+
self.fitted_available = available
|
|
235
|
+
self.fitted_decoration_height = decoration_height
|
|
236
|
+
height_cap = round(ERROR_MAX_SCREEN_HEIGHT_RATIO * self.usable_screen_area(available).height()) - decoration_height
|
|
237
|
+
text_width = self.width() - margins.left() - margins.right()
|
|
238
|
+
self.action_layout.invalidate()
|
|
239
|
+
fixed_height = margins.top() + margins.bottom() + layout.spacing() * (layout.count() - 1) + self.action_layout.sizeHint().height()
|
|
240
|
+
summary_height = self.summary_content_height(text_width) + FRAME_SIDE_COUNT * self.summary_scroll.frameWidth()
|
|
241
|
+
detail_frame_height = FRAME_SIDE_COUNT * self.detail_edit.frameWidth()
|
|
242
|
+
detail_height = self.detail_text_height(text_width - detail_frame_height, height_cap) + detail_frame_height
|
|
243
|
+
window_height = fixed_height + summary_height + detail_height
|
|
244
|
+
if window_height > height_cap:
|
|
245
|
+
text_room = max(0, height_cap - fixed_height)
|
|
246
|
+
summary_height = min(summary_height, round(text_room * ERROR_SUMMARY_MAX_SHARE))
|
|
247
|
+
detail_height = text_room - summary_height
|
|
248
|
+
window_height = height_cap
|
|
249
|
+
self.summary_scroll.setFixedHeight(summary_height)
|
|
250
|
+
# The details area must not ask the layout for more than the height it was given, or the window would be too
|
|
251
|
+
# short for its layout and the buttons would be pushed out of it.
|
|
252
|
+
self.detail_edit.setMinimumHeight(min(detail_height, self.detail_edit.minimumSizeHint().height()))
|
|
253
|
+
self.resize(self.width(), window_height)
|
|
254
|
+
fit_window(self, 0, 0, available)
|
|
255
|
+
|
|
256
|
+
def usable_screen_area(self, available: QRect | None) -> QRect:
|
|
257
|
+
"""Answer the usable area of the screen the window opens on: the one given, else the screen of the window it was opened from, else its own."""
|
|
258
|
+
if available is not None:
|
|
259
|
+
return available
|
|
260
|
+
parent = self.parentWidget()
|
|
261
|
+
if parent is not None and isValid(parent):
|
|
262
|
+
return parent.window().screen().availableGeometry()
|
|
263
|
+
return self.screen().availableGeometry()
|
|
264
|
+
|
|
265
|
+
def decoration_height(self) -> int:
|
|
266
|
+
"""Answer the height of the title bar and borders, estimated from the window it was opened from while the window system has not added them yet.
|
|
267
|
+
|
|
268
|
+
The native window's frame margins come first: under X11 they are read again from the window manager once it has
|
|
269
|
+
framed the window, and under Wayland Qt draws the title bar itself after the window is shown, while the widget's
|
|
270
|
+
frame is a copy Qt only refreshes on a move or a resize. Then the margins the window system already gave, then
|
|
271
|
+
the estimate.
|
|
272
|
+
"""
|
|
273
|
+
native_window = self.windowHandle()
|
|
274
|
+
if native_window is not None:
|
|
275
|
+
native_margins = native_window.frameMargins()
|
|
276
|
+
if not native_margins.isNull():
|
|
277
|
+
return native_margins.top() + native_margins.bottom()
|
|
278
|
+
known_margins = known_frame_margins(self)
|
|
279
|
+
if not known_margins.isNull():
|
|
280
|
+
return known_margins.top() + known_margins.bottom()
|
|
281
|
+
parent = self.parentWidget()
|
|
282
|
+
decoration = unknown_decoration_estimate(self, parent.window() if parent is not None and isValid(parent) else None)
|
|
283
|
+
return decoration.top() + decoration.bottom()
|
|
284
|
+
|
|
285
|
+
def summary_content_height(self, area_width: int) -> int:
|
|
286
|
+
"""Answer the height the cause, recovery and help texts need when wrapped inside a summary area of the given width, without a scroll bar.
|
|
287
|
+
|
|
288
|
+
The width comes from the window, not from the area's viewport, because the viewport is only laid out once the
|
|
289
|
+
window is shown.
|
|
290
|
+
"""
|
|
291
|
+
content = self.summary_scroll.widget()
|
|
292
|
+
content_width = max(1, area_width - FRAME_SIDE_COUNT * self.summary_scroll.frameWidth())
|
|
293
|
+
if content.hasHeightForWidth():
|
|
294
|
+
return content.heightForWidth(content_width)
|
|
295
|
+
return content.minimumSizeHint().height()
|
|
296
|
+
|
|
297
|
+
def detail_text_height(self, text_width: int, height_cap: int) -> int:
|
|
298
|
+
"""Answer the height of the details text wrapped at the given width, as the details area lays it out.
|
|
299
|
+
|
|
300
|
+
A detail whose lines alone already pass the cap is not measured further: the area would be capped anyway, and a
|
|
301
|
+
long Git output would cost a full text layout for nothing.
|
|
302
|
+
"""
|
|
303
|
+
text = self.detail_edit.toPlainText()
|
|
304
|
+
line_count = text.count("\n") + 1
|
|
305
|
+
if line_count * self.detail_edit.fontMetrics().lineSpacing() > height_cap:
|
|
306
|
+
return height_cap
|
|
307
|
+
document = QTextDocument()
|
|
308
|
+
document.setDefaultFont(self.detail_edit.font())
|
|
309
|
+
document.setDocumentMargin(self.detail_edit.document().documentMargin())
|
|
310
|
+
option = document.defaultTextOption()
|
|
311
|
+
option.setWrapMode(self.detail_edit.wordWrapMode())
|
|
312
|
+
document.setDefaultTextOption(option)
|
|
313
|
+
document.setTextWidth(max(1, text_width))
|
|
314
|
+
document.setPlainText(text)
|
|
315
|
+
# Workaround: a plain text area counts its last line as fully visible only when it ends one pixel above the
|
|
316
|
+
# bottom margin; at the document's exact height it keeps a scroll bar for that one line, so the pixel is added.
|
|
317
|
+
return math.ceil(document.size().height()) + PLAIN_TEXT_LAST_LINE_SLACK
|
|
318
|
+
|
|
319
|
+
def _restore_focus(self) -> None:
|
|
320
|
+
"""Return focus to the trigger only when dismissing its own diagnostic."""
|
|
321
|
+
control = self.return_focus
|
|
322
|
+
if control is not None and isValid(control) and control.isVisible() and control.isEnabled():
|
|
323
|
+
control.setFocus(Qt.FocusReason.OtherFocusReason)
|
|
324
|
+
|
|
325
|
+
def copy_detail(self) -> None:
|
|
326
|
+
"""Copy exactly the already-filtered text displayed in the details area."""
|
|
327
|
+
QApplication.clipboard().setText(self.diagnostic.detail)
|
|
328
|
+
|
|
329
|
+
def copy_recovery_command(self) -> None:
|
|
330
|
+
"""Copy only the displayed command for manual use without executing it."""
|
|
331
|
+
if self.recovery_command_edit is not None:
|
|
332
|
+
QApplication.clipboard().setText(self.recovery_command_edit.toPlainText())
|