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,116 @@
|
|
|
1
|
+
"""Change GTK 3's own settings inside this process through the GTK library that Qt's GTK theme already loaded.
|
|
2
|
+
|
|
3
|
+
Workaround for Qt 6.11's GTK 3 platform theme under a desktop that switches between light and dark through the
|
|
4
|
+
freedesktop Settings portal, as GNOME does. GTK 3 does not follow the portal's ``color-scheme`` preference: with the
|
|
5
|
+
desktop's ``Adwaita`` theme it stays light. Qt reads the preference from the portal itself but takes its palette from
|
|
6
|
+
GTK, so a dark desktop gives Qt windows a dark title bar on a light content. Qt also rebuilds that palette only when
|
|
7
|
+
GTK's ``gtk-theme-name`` changes. GitPane therefore sets GTK's own dark preference (``gtk-application-prefer-dark-theme``)
|
|
8
|
+
for this process alone, then sets the theme name again to its own value, which makes Qt read GTK's colours again.
|
|
9
|
+
Nothing is written outside the process: no desktop setting, no settings file.
|
|
10
|
+
|
|
11
|
+
The library is only reached when Qt already loaded it (``RTLD_NOLOAD``): GitPane never loads GTK itself, and on a
|
|
12
|
+
system or a Qt platform where GTK is not in the process there is nothing to change.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import ctypes
|
|
18
|
+
import os
|
|
19
|
+
|
|
20
|
+
GTK_LIBRARY_NAME = "libgtk-3.so.0"
|
|
21
|
+
GOBJECT_LIBRARY_NAME = "libgobject-2.0.so.0"
|
|
22
|
+
# g_free, which releases the text g_object_get hands over, lives in GLib.
|
|
23
|
+
GLIB_LIBRARY_NAME = "libglib-2.0.so.0"
|
|
24
|
+
PREFER_DARK_PROPERTY = b"gtk-application-prefer-dark-theme"
|
|
25
|
+
THEME_NAME_PROPERTY = b"gtk-theme-name"
|
|
26
|
+
# The name of the dlopen flag that only answers a library already loaded; Windows has no such flag.
|
|
27
|
+
NO_LOAD_FLAG_NAME = "RTLD_NOLOAD"
|
|
28
|
+
# g_object_set and g_object_get read property name and value pairs until a null name.
|
|
29
|
+
PROPERTY_LIST_END = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def already_loaded_library(name: str) -> ctypes.CDLL | None:
|
|
33
|
+
"""Answer the library ``name`` when this process already loaded it, without ever loading it, or nothing."""
|
|
34
|
+
no_load_flag = getattr(os, NO_LOAD_FLAG_NAME, None)
|
|
35
|
+
if no_load_flag is None:
|
|
36
|
+
return None
|
|
37
|
+
try:
|
|
38
|
+
return ctypes.CDLL(name, mode=no_load_flag | os.RTLD_LAZY)
|
|
39
|
+
except OSError:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class GtkSettingsLibrary:
|
|
44
|
+
"""The default GTK settings object of this process, with the two changes that make Qt's GTK theme follow a dark preference.
|
|
45
|
+
|
|
46
|
+
g_object_set and g_object_get take a variable list of arguments. Each one is given a fixed signature for the one
|
|
47
|
+
shape GitPane passes: only pointers and whole numbers travel, which the x86-64 and ARM 64-bit Linux calling
|
|
48
|
+
conventions pass the same way for a variable list and a fixed one.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, gtk: ctypes.CDLL, gobject: ctypes.CDLL, glib: ctypes.CDLL, settings: int) -> None:
|
|
52
|
+
"""Keep the libraries and the settings object, and declare once the C signature of every function called."""
|
|
53
|
+
self.settings = ctypes.c_void_p(settings)
|
|
54
|
+
self.set_number_property = gobject["g_object_set"]
|
|
55
|
+
self.set_number_property.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int, ctypes.c_void_p]
|
|
56
|
+
self.set_number_property.restype = None
|
|
57
|
+
self.set_text_property = gobject["g_object_set"]
|
|
58
|
+
self.set_text_property.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_void_p]
|
|
59
|
+
self.set_text_property.restype = None
|
|
60
|
+
self.get_text_property = gobject["g_object_get"]
|
|
61
|
+
self.get_text_property.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_void_p), ctypes.c_void_p]
|
|
62
|
+
self.get_text_property.restype = None
|
|
63
|
+
self.get_number_property = gobject["g_object_get"]
|
|
64
|
+
self.get_number_property.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int), ctypes.c_void_p]
|
|
65
|
+
self.get_number_property.restype = None
|
|
66
|
+
self.free = glib["g_free"]
|
|
67
|
+
self.free.argtypes = [ctypes.c_void_p]
|
|
68
|
+
self.free.restype = None
|
|
69
|
+
# The libraries stay referenced for as long as their functions may be called.
|
|
70
|
+
self.libraries = (gtk, gobject, glib)
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def loaded(cls) -> GtkSettingsLibrary | None:
|
|
74
|
+
"""Answer the GTK settings of this process when Qt's GTK theme loaded and started GTK, otherwise nothing.
|
|
75
|
+
|
|
76
|
+
Nothing is loaded: the libraries are only looked up among those already in the process. GTK loaded but not
|
|
77
|
+
started yet has no default settings object, and nothing is answered either.
|
|
78
|
+
"""
|
|
79
|
+
gtk = already_loaded_library(GTK_LIBRARY_NAME)
|
|
80
|
+
gobject = already_loaded_library(GOBJECT_LIBRARY_NAME)
|
|
81
|
+
glib = already_loaded_library(GLIB_LIBRARY_NAME)
|
|
82
|
+
if gtk is None or gobject is None or glib is None:
|
|
83
|
+
return None
|
|
84
|
+
get_default_settings = gtk["gtk_settings_get_default"]
|
|
85
|
+
get_default_settings.argtypes = []
|
|
86
|
+
get_default_settings.restype = ctypes.c_void_p
|
|
87
|
+
settings = get_default_settings()
|
|
88
|
+
if not settings:
|
|
89
|
+
return None
|
|
90
|
+
return cls(gtk, gobject, glib, settings)
|
|
91
|
+
|
|
92
|
+
def prefers_dark(self) -> bool:
|
|
93
|
+
"""Tell whether GTK asks for the dark variant of its theme in this process.
|
|
94
|
+
|
|
95
|
+
Before GitPane changes anything, this is the value GTK read from its own configuration (``settings.ini``,
|
|
96
|
+
``gtk-application-prefer-dark-theme``); afterwards, the last value this process set.
|
|
97
|
+
"""
|
|
98
|
+
prefers_dark = ctypes.c_int(0)
|
|
99
|
+
self.get_number_property(self.settings, PREFER_DARK_PROPERTY, ctypes.byref(prefers_dark), PROPERTY_LIST_END)
|
|
100
|
+
return bool(prefers_dark.value)
|
|
101
|
+
|
|
102
|
+
def set_prefers_dark(self, prefers_dark: bool) -> None:
|
|
103
|
+
"""Ask GTK for the dark variant of its theme in this process, or for the light one; Qt does not see it until ``reload_theme``."""
|
|
104
|
+
self.set_number_property(self.settings, PREFER_DARK_PROPERTY, int(prefers_dark), PROPERTY_LIST_END)
|
|
105
|
+
|
|
106
|
+
def reload_theme(self) -> None:
|
|
107
|
+
"""Set GTK's theme name again to its own value: the one notice on which Qt's GTK theme reads GTK's colours again."""
|
|
108
|
+
theme_name_pointer = ctypes.c_void_p()
|
|
109
|
+
self.get_text_property(self.settings, THEME_NAME_PROPERTY, ctypes.byref(theme_name_pointer), PROPERTY_LIST_END)
|
|
110
|
+
if not theme_name_pointer.value:
|
|
111
|
+
return
|
|
112
|
+
try:
|
|
113
|
+
theme_name = ctypes.string_at(theme_name_pointer.value)
|
|
114
|
+
self.set_text_property(self.settings, THEME_NAME_PROPERTY, theme_name, PROPERTY_LIST_END)
|
|
115
|
+
finally:
|
|
116
|
+
self.free(theme_name_pointer)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Turn a logical icon side into the device pixels Qt asks for at a screen density."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6.QtCore import QSize
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def physical_icon_side(side: int, density: float) -> int:
|
|
9
|
+
"""Return the side in device pixels that Qt asks of an icon drawn ``side`` logical pixels wide at ``density``.
|
|
10
|
+
|
|
11
|
+
Qt rounds a half up when it scales the size it asks of an icon, whereas Python's ``round`` rounds a half to the even
|
|
12
|
+
number: at 18 logical pixels and a density of 1.25 Qt asks for 23 pixels and ``round`` gives 22. An icon one pixel
|
|
13
|
+
smaller than the size Qt asks for is stretched by Qt, which blurs it and loses its pixel ratio, so the pixmap is
|
|
14
|
+
sized by Qt's own arithmetic.
|
|
15
|
+
"""
|
|
16
|
+
return (QSize(side, side) * density).width()
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Write the checked-out branch in bold wherever a reference name is listed."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from PySide6.QtGui import QFont
|
|
8
|
+
|
|
9
|
+
from gitpane.history.repository_value import ReferenceKind
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from gitpane.history.repository_value import Reference
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def reference_font(font: QFont, reference: Reference) -> QFont:
|
|
16
|
+
"""Distinguish the checked-out branch without emphasizing other refs at the same commit."""
|
|
17
|
+
font = QFont(font)
|
|
18
|
+
font.setBold(reference.is_head and reference.kind == ReferenceKind.HEADS)
|
|
19
|
+
return font
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Derive the theme-dependent greys of GitPane's controls from the palette of the running appearance."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from PySide6.QtGui import QColor, QPalette
|
|
6
|
+
|
|
7
|
+
from gitpane.config import DARK_BACKGROUND_LIGHTNESS_LIMIT
|
|
8
|
+
|
|
9
|
+
# Shares of the way from the window color to the text color, as the text shows on the window, that give the greys of a
|
|
10
|
+
# dark theme. They were chosen on screen on the macOS dark palette, where they give #474747, #5a5a5a and #7c7c7c.
|
|
11
|
+
# The button band below a form: a little lighter than the form's body, so the band stays apart from it.
|
|
12
|
+
BUTTON_BAND_DARK_RATIO = 0.12
|
|
13
|
+
# The face of buttons, drop-down lists, tabs, column headers and scroll bars: lighter than the window they sit on.
|
|
14
|
+
CONTROL_DARK_RATIO = 0.23
|
|
15
|
+
# Every line: list borders, frames, the band's top line and separators; the grey of the lines between tool bars.
|
|
16
|
+
LINE_DARK_RATIO = 0.425
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_dark_palette(palette: QPalette) -> bool:
|
|
20
|
+
"""Tell whether text is drawn on a dark background, the signal to leave light-theme captures aside."""
|
|
21
|
+
return palette.color(QPalette.ColorRole.Base).lightnessF() < DARK_BACKGROUND_LIGHTNESS_LIMIT
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def toward_text(palette: QPalette, ratio: float) -> QColor:
|
|
25
|
+
"""Return the window color moved ``ratio`` of the way towards the text color as it shows on the window.
|
|
26
|
+
|
|
27
|
+
The text color is composed with its own transparency first: macOS gives a white text at 85 % opacity, and blending
|
|
28
|
+
from the raw white would give lighter greys than the ones chosen on screen.
|
|
29
|
+
"""
|
|
30
|
+
window, text = palette.color(QPalette.ColorRole.Window), palette.color(QPalette.ColorRole.WindowText)
|
|
31
|
+
channels: list[float] = []
|
|
32
|
+
for window_channel, text_channel in ((window.redF(), text.redF()), (window.greenF(), text.greenF()), (window.blueF(), text.blueF())):
|
|
33
|
+
shown_text_channel = window_channel + (text_channel - window_channel) * text.alphaF()
|
|
34
|
+
channels.append(window_channel + (shown_text_channel - window_channel) * ratio)
|
|
35
|
+
red, green, blue = channels
|
|
36
|
+
return QColor.fromRgbF(red, green, blue)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def theme_band_color(palette: QPalette) -> QColor:
|
|
40
|
+
"""Return the grey of a form's button band in a dark theme: a little lighter than the window, so the band stays apart."""
|
|
41
|
+
return toward_text(palette, BUTTON_BAND_DARK_RATIO)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def theme_control_color(palette: QPalette) -> QColor:
|
|
45
|
+
"""Return the face grey of buttons, drop-down lists, tabs, column headers and scroll bars in a dark theme."""
|
|
46
|
+
return toward_text(palette, CONTROL_DARK_RATIO)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def theme_line_color(palette: QPalette) -> QColor:
|
|
50
|
+
"""Return the grey of every line in a dark theme: list borders, frames, the band's top line and separators."""
|
|
51
|
+
return toward_text(palette, LINE_DARK_RATIO)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def line_number_gutter_color(palette: QPalette) -> QColor:
|
|
55
|
+
"""Return the background of the line-number gutter of the diff and file readers.
|
|
56
|
+
|
|
57
|
+
A light theme keeps the palette's alternate row color. A dark one takes the band grey: macOS gives the
|
|
58
|
+
application palette a light grey alternate row color meant for lists only, and the gutter, which is not a list,
|
|
59
|
+
would show white numbers on it that cannot be read.
|
|
60
|
+
"""
|
|
61
|
+
if is_dark_palette(palette):
|
|
62
|
+
return theme_band_color(palette)
|
|
63
|
+
return palette.color(QPalette.ColorRole.AlternateBase)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Lay a window's theme-dependent look again each time the desktop switches to the light or the dark appearance.
|
|
2
|
+
|
|
3
|
+
Some colors and style sheets are set once, when a window is built: the body and band colors of the Git Extensions
|
|
4
|
+
forms, the light-theme style sheets of their buttons and fields. They do not follow a change of appearance on their
|
|
5
|
+
own, so a window opened in light would stay light after a switch to dark. Each window gathers the functions that lay
|
|
6
|
+
that look, and one follower per window calls them again when a color they are worked out from changes.
|
|
7
|
+
|
|
8
|
+
The follower watches the top-level window rather than each widget: Qt does not pass palette notices on to children that
|
|
9
|
+
carry a style sheet, while the window always gets the application palette notice. It reacts to palette notices and not
|
|
10
|
+
to the theme notice: the theme notice comes before GitPane has laid the new application palette, so the colors read
|
|
11
|
+
then would still be the old ones.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
from PySide6.QtCore import QEvent, QObject, Qt
|
|
20
|
+
from PySide6.QtGui import QColor, QPalette
|
|
21
|
+
from PySide6.QtWidgets import QApplication
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import Callable
|
|
25
|
+
|
|
26
|
+
from PySide6.QtWidgets import QWidget
|
|
27
|
+
|
|
28
|
+
PALETTE_EVENT_TYPE_SET = frozenset({QEvent.Type.PaletteChange, QEvent.Type.ApplicationPaletteChange})
|
|
29
|
+
# Application palette colors every theme look is worked out from: the body and field surfaces, the band and line greys
|
|
30
|
+
# (from the window color towards the window text), the field text, and the selection colors a list copies into its own
|
|
31
|
+
# palette, which another accent changes on its own.
|
|
32
|
+
THEME_PALETTE_ROLES = (QPalette.ColorRole.Base, QPalette.ColorRole.Window, QPalette.ColorRole.WindowText, QPalette.ColorRole.Text, QPalette.ColorRole.Highlight,
|
|
33
|
+
QPalette.ColorRole.HighlightedText)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def theme_palette_key(palette: QPalette) -> tuple[str, ...]:
|
|
37
|
+
"""Return the colors of the palette a theme look is built from, transparency included, so any change of one of them is noticed.
|
|
38
|
+
|
|
39
|
+
Light or dark alone is not enough: switching from one dark palette to another (Windows high contrast, another
|
|
40
|
+
accent) keeps the variant but changes the colors a window copied. The transparency counts too: macOS writes its
|
|
41
|
+
text as white at 85 %, and the greys worked out from it depend on that share.
|
|
42
|
+
"""
|
|
43
|
+
color_names: list[str] = []
|
|
44
|
+
for role in THEME_PALETTE_ROLES:
|
|
45
|
+
color_names.append(palette.color(role).name(QColor.NameFormat.HexArgb))
|
|
46
|
+
return tuple(color_names)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ThemeLookFollower(QObject):
|
|
50
|
+
"""Call a window's theme look functions again, in the order they were given, when the application palette's colors change.
|
|
51
|
+
|
|
52
|
+
The window's own palette and style sheet writes raise palette notices too, so the looks run only when a color they
|
|
53
|
+
are built from really changed in the application palette, which no look ever writes. The new colors are recorded
|
|
54
|
+
before the looks run, so the notices the looks raise start nothing. Several notices for one switch end on the last
|
|
55
|
+
palette: each one compares against the colors the looks were last laid with.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, window: QWidget) -> None:
|
|
59
|
+
"""Watch the window's palette notices; the follower belongs to the window and goes with it."""
|
|
60
|
+
super().__init__(window)
|
|
61
|
+
self.looks: list[Callable[[], None]] = []
|
|
62
|
+
self.theme_key = theme_palette_key(QApplication.palette())
|
|
63
|
+
window.installEventFilter(self)
|
|
64
|
+
|
|
65
|
+
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
|
66
|
+
"""Lay every look again when a palette notice comes with new application colors; the event goes on as usual.
|
|
67
|
+
|
|
68
|
+
A look that raises is logged and the next ones still run: the colors are already recorded, so a look skipped
|
|
69
|
+
here would leave its part of the window in the old appearance until the next switch.
|
|
70
|
+
"""
|
|
71
|
+
if event.type() not in PALETTE_EVENT_TYPE_SET:
|
|
72
|
+
return False
|
|
73
|
+
theme_key = theme_palette_key(QApplication.palette())
|
|
74
|
+
if theme_key == self.theme_key:
|
|
75
|
+
return False
|
|
76
|
+
self.theme_key = theme_key
|
|
77
|
+
for look in list(self.looks):
|
|
78
|
+
try:
|
|
79
|
+
look()
|
|
80
|
+
except Exception:
|
|
81
|
+
logging.getLogger(__name__).exception("A theme look failed after an appearance change; the window's other looks still run")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def follow_theme(window: QWidget, look: Callable[[], None]) -> ThemeLookFollower:
|
|
86
|
+
"""Have ``look`` called again each time the light or dark colors of the application change while the window exists.
|
|
87
|
+
|
|
88
|
+
``look`` lays something of the window's look that is decided on the application palette; it has already been laid
|
|
89
|
+
once by the window. One follower serves the whole window: it is created on the first call and reused afterwards.
|
|
90
|
+
"""
|
|
91
|
+
follower = window.findChild(ThemeLookFollower, "", Qt.FindChildOption.FindDirectChildrenOnly)
|
|
92
|
+
if follower is None:
|
|
93
|
+
follower = ThemeLookFollower(window)
|
|
94
|
+
follower.looks.append(look)
|
|
95
|
+
return follower
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""Git Extensions colors for light desktop themes: white fields on Windows' control grey.
|
|
2
|
+
|
|
3
|
+
Light Linux themes often give the window and the fields one same color (beige in the Windows XP Luna copies, white in
|
|
4
|
+
Yaru), so nothing shows where to type. Git Extensions always draws its fields white on the grey of Windows' controls;
|
|
5
|
+
GitPane does the same in every light theme. A dark theme keeps its own palette: the light captures would glare there.
|
|
6
|
+
A desktop that asks for high contrast keeps its own palette too: its colors are chosen for readability, and Windows
|
|
7
|
+
counts its High Contrast White theme as light.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from PySide6.QtCore import QEvent, QObject, Qt, QTimer
|
|
15
|
+
from PySide6.QtGui import QColor, QPalette
|
|
16
|
+
|
|
17
|
+
from gitpane.ui.look.theme_color import is_dark_palette
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Callable
|
|
21
|
+
|
|
22
|
+
from PySide6.QtWidgets import QApplication, QWidget
|
|
23
|
+
|
|
24
|
+
# The white of the body and of the fields of the Git Extensions windows, sampled on its Cherry pick commit capture.
|
|
25
|
+
BODY_LIGHT_COLOR = "#ffffff"
|
|
26
|
+
# Windows' own control grey: the button area of a native message box, and the empty reference rows of the Git Extensions
|
|
27
|
+
# commit box. Sampled on the Merge conflicts and Cherry pick commit captures.
|
|
28
|
+
WINDOWS_CONTROL_LIGHT_COLOR = "#f0f0f0"
|
|
29
|
+
PALETTE_COLOR_GROUPS = (QPalette.ColorGroup.Active, QPalette.ColorGroup.Inactive, QPalette.ColorGroup.Disabled)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def desktop_prefers_high_contrast(application: QApplication) -> bool:
|
|
33
|
+
"""Tell whether the desktop asks applications for high contrast, as Windows does in its High Contrast themes."""
|
|
34
|
+
# PySide invalidates the accessibility hints wrapper as soon as the style hints wrapper it came from is collected,
|
|
35
|
+
# so a chained call fails with "already deleted"; the style hints are kept in a name while the hints are read.
|
|
36
|
+
style_hints = application.styleHints()
|
|
37
|
+
return style_hints.accessibility().contrastPreference() == Qt.ContrastPreference.HighContrast
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def light_field_palette(source: QPalette, *, high_contrast: bool = False) -> QPalette:
|
|
41
|
+
"""Return the desktop palette with the Git Extensions window and field colors when it is light, unchanged when dark or in high contrast.
|
|
42
|
+
|
|
43
|
+
Only the window and field backgrounds change; text, selection, tooltip and every other color stay the theme's, so
|
|
44
|
+
the theme's contrast between text and selection is kept. Alternate rows keep the theme's color unless it now
|
|
45
|
+
matches the white fields, in which case they take the control grey so the rows stay told apart. A disabled field
|
|
46
|
+
stays painted like the window when the theme paints it so, which keeps a disabled field looking disabled.
|
|
47
|
+
The source palette is never changed, and only the roles set here are pinned: every other role keeps following the
|
|
48
|
+
desktop theme.
|
|
49
|
+
"""
|
|
50
|
+
palette = QPalette(source)
|
|
51
|
+
if high_contrast or is_dark_palette(source):
|
|
52
|
+
return palette
|
|
53
|
+
window_color = QColor(WINDOWS_CONTROL_LIGHT_COLOR)
|
|
54
|
+
field_color = QColor(BODY_LIGHT_COLOR)
|
|
55
|
+
for group in PALETTE_COLOR_GROUPS:
|
|
56
|
+
disabled_field_like_window = group == QPalette.ColorGroup.Disabled and source.color(group, QPalette.ColorRole.Base) == source.color(group, QPalette.ColorRole.Window)
|
|
57
|
+
group_field_color = window_color if disabled_field_like_window else field_color
|
|
58
|
+
palette.setColor(group, QPalette.ColorRole.Window, window_color)
|
|
59
|
+
palette.setColor(group, QPalette.ColorRole.Base, group_field_color)
|
|
60
|
+
if source.color(group, QPalette.ColorRole.AlternateBase) == group_field_color:
|
|
61
|
+
palette.setColor(group, QPalette.ColorRole.AlternateBase, window_color)
|
|
62
|
+
return palette
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def desktop_theme_palette(application: QApplication) -> QPalette:
|
|
66
|
+
"""Return the palette the desktop theme gives the application, without the colors GitPane laid on top of it.
|
|
67
|
+
|
|
68
|
+
An application palette set by GitPane pins its window and field colors: Qt keeps them through a theme change and
|
|
69
|
+
only refreshes the other roles. Dropping it first is the only way to read the new theme's own window and field colors.
|
|
70
|
+
"""
|
|
71
|
+
application.setPalette(QPalette())
|
|
72
|
+
return QPalette(application.palette())
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def widget_depth(widget: QWidget) -> int:
|
|
76
|
+
"""Count a widget's ancestors, so a walk can reach every parent before its children."""
|
|
77
|
+
depth = 0
|
|
78
|
+
parent = widget.parentWidget()
|
|
79
|
+
while parent is not None:
|
|
80
|
+
depth += 1
|
|
81
|
+
parent = parent.parentWidget()
|
|
82
|
+
return depth
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def resolve_widget_palettes(application: QApplication) -> None:
|
|
86
|
+
"""Make every widget already built take its parent's palette again, parents first; colors a widget set itself stay.
|
|
87
|
+
|
|
88
|
+
Workaround for a Qt propagation gap: reading the desktop palette drops GitPane's application palette and lays it
|
|
89
|
+
again right after. Widgets built before that (the Settings window GitPane opens at startup when Git cannot be used
|
|
90
|
+
is built before the desktop's first theme notice) kept the desktop theme's own colors in their inner widgets, so a
|
|
91
|
+
light theme that paints fields like the window showed fields of the window's color. Setting each widget's palette
|
|
92
|
+
to itself resolves it again against its parent, which is already done, and keeps the roles the widget set itself.
|
|
93
|
+
Widgets drawn by a style sheet are left to it: Qt asks not to set a palette on them.
|
|
94
|
+
"""
|
|
95
|
+
widgets = sorted(application.allWidgets(), key=widget_depth)
|
|
96
|
+
for widget in widgets:
|
|
97
|
+
if widget.testAttribute(Qt.WidgetAttribute.WA_StyleSheet):
|
|
98
|
+
continue
|
|
99
|
+
widget.setPalette(widget.palette())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class LightFieldPaletteFollower(QObject):
|
|
103
|
+
"""Lay the Git Extensions colors again each time the desktop theme changes, so a light/dark switch needs no restart.
|
|
104
|
+
|
|
105
|
+
Qt announces a theme change to the style hints object, by a theme change event and, when the light or dark scheme
|
|
106
|
+
changes, by a signal; watching that one object costs nothing on the application's other events. Several notices
|
|
107
|
+
arrive for one switch, so the update is queued once and runs after them. The update itself only changes the
|
|
108
|
+
application palette, which raises no theme notice; a flag ignores any notice raised while it runs all the same.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
def __init__(self, application: QApplication, read_theme_palette: Callable[[QApplication], QPalette],
|
|
112
|
+
prefers_high_contrast: Callable[[QApplication], bool] = desktop_prefers_high_contrast) -> None:
|
|
113
|
+
"""Watch the application's style hints; ``read_theme_palette`` returns the desktop theme's own palette, ``prefers_high_contrast`` the desktop's contrast choice."""
|
|
114
|
+
super().__init__(application)
|
|
115
|
+
self.application = application
|
|
116
|
+
self.read_theme_palette = read_theme_palette
|
|
117
|
+
self.prefers_high_contrast = prefers_high_contrast
|
|
118
|
+
self.applying = False
|
|
119
|
+
self.update_timer = QTimer(self)
|
|
120
|
+
self.update_timer.setSingleShot(True)
|
|
121
|
+
self.update_timer.setInterval(0)
|
|
122
|
+
self.update_timer.timeout.connect(self.update_palette)
|
|
123
|
+
style_hints = application.styleHints()
|
|
124
|
+
style_hints.installEventFilter(self)
|
|
125
|
+
style_hints.colorSchemeChanged.connect(self.schedule_update)
|
|
126
|
+
# PySide invalidates the accessibility hints wrapper once the style hints wrapper it came from is collected;
|
|
127
|
+
# both are kept for as long as this follower listens to them.
|
|
128
|
+
self.style_hints = style_hints
|
|
129
|
+
self.accessibility_hints = style_hints.accessibility()
|
|
130
|
+
self.accessibility_hints.contrastPreferenceChanged.connect(self.schedule_update)
|
|
131
|
+
|
|
132
|
+
def eventFilter(self, watched: QObject, event: QEvent) -> bool:
|
|
133
|
+
"""Queue an update when Qt announces a desktop theme change; the event goes on as usual."""
|
|
134
|
+
if event.type() == QEvent.Type.ThemeChange:
|
|
135
|
+
self.schedule_update()
|
|
136
|
+
return False
|
|
137
|
+
|
|
138
|
+
def schedule_update(self) -> None:
|
|
139
|
+
"""Queue one palette update, unless the notice comes from the update running now."""
|
|
140
|
+
if self.applying:
|
|
141
|
+
return
|
|
142
|
+
self.update_timer.start()
|
|
143
|
+
|
|
144
|
+
def update_palette(self) -> None:
|
|
145
|
+
"""Read the desktop theme's palette again and lay the Git Extensions colors on it when it is light and not in high contrast.
|
|
146
|
+
|
|
147
|
+
Every widget already built then resolves its palette again, so no field keeps the colors seen while reading.
|
|
148
|
+
"""
|
|
149
|
+
self.applying = True
|
|
150
|
+
try:
|
|
151
|
+
theme_palette = self.read_theme_palette(self.application)
|
|
152
|
+
self.application.setPalette(light_field_palette(theme_palette, high_contrast=self.prefers_high_contrast(self.application)))
|
|
153
|
+
resolve_widget_palettes(self.application)
|
|
154
|
+
finally:
|
|
155
|
+
self.applying = False
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def install_light_field_palette(application: QApplication, read_theme_palette: Callable[[QApplication], QPalette] = desktop_theme_palette,
|
|
159
|
+
prefers_high_contrast: Callable[[QApplication], bool] = desktop_prefers_high_contrast) -> LightFieldPaletteFollower:
|
|
160
|
+
"""Give the whole application the Git Extensions field colors when the desktop theme is light, and keep it so.
|
|
161
|
+
|
|
162
|
+
Called once at startup, before the first window, so every window and every field is built on these colors. The
|
|
163
|
+
returned object, owned by the application, follows later desktop theme and contrast changes; calling this again
|
|
164
|
+
reuses it. ``read_theme_palette`` reads the desktop theme's palette and ``prefers_high_contrast`` the desktop's
|
|
165
|
+
contrast choice; tests give stand-ins for the desktop.
|
|
166
|
+
"""
|
|
167
|
+
application.setPalette(light_field_palette(application.palette(), high_contrast=prefers_high_contrast(application)))
|
|
168
|
+
for child in application.children():
|
|
169
|
+
if isinstance(child, LightFieldPaletteFollower):
|
|
170
|
+
child.read_theme_palette = read_theme_palette
|
|
171
|
+
child.prefers_high_contrast = prefers_high_contrast
|
|
172
|
+
return child
|
|
173
|
+
return LightFieldPaletteFollower(application, read_theme_palette, prefers_high_contrast)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""Paint the theme's symbolic warning icon in the warning colour when the theme has no coloured one.
|
|
2
|
+
|
|
3
|
+
Some icon themes, such as GNOME's Adwaita, only carry a symbolic warning icon: a one-colour shape meant to be painted by
|
|
4
|
+
the application in the colour of its text. Qt then finds no coloured warning icon in the theme and falls back on its
|
|
5
|
+
own bright yellow triangle, which hardly stands out from a light window. GitPane takes the theme's symbolic shape
|
|
6
|
+
instead and paints it in the orange of its warnings, the colour of its light or dark variant, so the icon reads on both
|
|
7
|
+
appearances and looks like the one the desktop's own applications show. A theme that has a coloured warning icon keeps
|
|
8
|
+
it exactly as the theme draws it, whatever its contrast with the window: its colours are the theme's choice, and KDE's
|
|
9
|
+
Breeze, whose pale orange triangle around a white mark measures under the guidelines' ratio on a light window, still
|
|
10
|
+
reads well on screen. Without any icon theme (Windows, macOS, offscreen), the icon of the style is kept.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
from PySide6.QtCore import Qt
|
|
19
|
+
from PySide6.QtGui import QColor, QIcon, QPainter, QPixmap
|
|
20
|
+
|
|
21
|
+
from gitpane.ui.look.theme_color import is_dark_palette
|
|
22
|
+
from gitpane.ui.widget.result_label import ResultTone, result_color
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from PySide6.QtCore import QSize
|
|
26
|
+
from PySide6.QtGui import QPalette
|
|
27
|
+
|
|
28
|
+
# Names icon themes give the warning icon: the coloured one, and the one-colour shape meant to be painted by the application.
|
|
29
|
+
COLOR_WARNING_ICON_NAME = "dialog-warning"
|
|
30
|
+
SYMBOLIC_WARNING_ICON_NAME = "dialog-warning-symbolic"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class WarningIconSource(Enum):
|
|
34
|
+
"""Say where the warning icon GitPane shows comes from."""
|
|
35
|
+
|
|
36
|
+
STYLE = "style"
|
|
37
|
+
"""The icon of the style, unchanged: the theme's coloured icon when it has one, otherwise the style's own drawing."""
|
|
38
|
+
TINTED_SYMBOLIC = "tinted_symbolic"
|
|
39
|
+
"""The theme's symbolic shape, painted in the warning colour: the theme has no coloured icon to show."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def warning_icon_source(has_color_icon: bool, has_symbolic_icon: bool) -> WarningIconSource:
|
|
43
|
+
"""Choose the warning icon from what the icon theme has: its coloured icon first, else its symbolic one painted, else the style's.
|
|
44
|
+
|
|
45
|
+
A coloured icon is the theme's own drawing, so it is kept as it is. Only a theme that has the symbolic shape and no
|
|
46
|
+
coloured icon gets the painted shape: without either, there is nothing better than the style's own drawing.
|
|
47
|
+
"""
|
|
48
|
+
if has_color_icon:
|
|
49
|
+
return WarningIconSource.STYLE
|
|
50
|
+
if not has_symbolic_icon:
|
|
51
|
+
return WarningIconSource.STYLE
|
|
52
|
+
return WarningIconSource.TINTED_SYMBOLIC
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def tinted_icon(source: QIcon, sizes: list[QSize], color: QColor) -> QIcon:
|
|
56
|
+
"""Return the icon drawn at each size with every visible pixel in one colour, the transparency of the source kept.
|
|
57
|
+
|
|
58
|
+
The colour replaces the source's own while its alpha shapes the result, so the edges keep their smoothing. Each image
|
|
59
|
+
keeps the device pixel ratio the source gave it, so a high-density screen gets as many pixels as it shows.
|
|
60
|
+
"""
|
|
61
|
+
result_icon = QIcon()
|
|
62
|
+
for size in sizes:
|
|
63
|
+
pixmap = source.pixmap(size)
|
|
64
|
+
tinted = QPixmap(pixmap.size())
|
|
65
|
+
tinted.setDevicePixelRatio(pixmap.devicePixelRatio())
|
|
66
|
+
tinted.fill(Qt.GlobalColor.transparent)
|
|
67
|
+
painter = QPainter(tinted)
|
|
68
|
+
painter.drawPixmap(0, 0, pixmap)
|
|
69
|
+
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_SourceIn)
|
|
70
|
+
painter.fillRect(tinted.rect(), color)
|
|
71
|
+
painter.end()
|
|
72
|
+
result_icon.addPixmap(tinted)
|
|
73
|
+
return result_icon
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def readable_warning_icon(style_icon: QIcon, palette: QPalette, sizes: list[QSize]) -> QIcon:
|
|
77
|
+
"""Return the warning icon to show on a window of that palette: the style's icon, or the theme's symbolic shape in the warning colour.
|
|
78
|
+
|
|
79
|
+
The orange is the one GitPane writes its warnings in, its light or dark variant after the palette, so the icon keeps
|
|
80
|
+
the contrast of those warnings on either appearance.
|
|
81
|
+
"""
|
|
82
|
+
source = warning_icon_source(QIcon.hasThemeIcon(COLOR_WARNING_ICON_NAME), QIcon.hasThemeIcon(SYMBOLIC_WARNING_ICON_NAME))
|
|
83
|
+
if source == WarningIconSource.STYLE:
|
|
84
|
+
return style_icon
|
|
85
|
+
color = QColor(result_color(ResultTone.WARNING, is_dark_palette(palette)))
|
|
86
|
+
return tinted_icon(QIcon.fromTheme(SYMBOLIC_WARNING_ICON_NAME), sizes, color)
|