Coverage for graphica/gui/mixins/ui_setup_mixin.py: 89.4%
124 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-12 16:38 +0000
1"""PlotterApp の一度きりの組み立て(シグナル、メニューバー、初めの画面の状態)と、ダークモードの切り替え。"""
2from PySide6.QtWidgets import QApplication
4from graphica.gui import app_settings
5from graphica.gui.theme import apply_theme
6from graphica.gui.dialogs import CommandPaletteDialog
7from graphica.gui.datasets.actions_menu import populate_dataset_actions_menu
8from graphica.gui.menu_bar import build_menu_bar
9from graphica.gui.shell import bottom_panel
12class UISetupMixin:
13 def _connect_signals(self):
14 """画面の部品のシグナルをつなぐ。__init__ から1回だけ。同じシグナルに複数つなぐものは、つないだ順に呼ばれる。"""
15 self._connect_layout_signals()
16 self._connect_axis_setting_signals()
17 self._connect_dataset_signals()
19 def _create_menu_bar(self):
20 build_menu_bar(self)
22 def _populate_dataset_menu(self):
23 populate_dataset_actions_menu(self, self._dataset_menu)
25 def _collect_menu_actions(self):
26 """
27 メニューの全項目を、階層のパス付きで集める(コマンドパレット・クイックアクセス・ショートカット一覧が使う)。
28 区切り線・空の項目・サブメニュー自体は除く。「最近使ったファイル」はコマンドではないので除く。
30 最上位のメニューは menuBar().actions() から .menu() で辿らず、組み立て時に持たせた self._file_menu などを使う
31 (辿ると、参照が切れたときにメニューごと回収されることがある)。「最近使ったファイル」の判定も、QMenu の
32 同一性ではなく持たせてある self._recent_files_menu_action で行う(繰り返すうちに QMenu の同一性が崩れ、
33 除いたはずの子の項目が混ざることがあった)。
34 """
35 results = []
36 recent_files_action = getattr(self, '_recent_files_menu_action', None)
38 def walk(menu, path):
39 for action in menu.actions():
40 if action.isSeparator() or menu is self.recent_files_menu:
41 continue
42 if action is recent_files_action:
43 continue
44 submenu = action.menu()
45 if submenu is not None:
46 if submenu is not self.recent_files_menu: 46 ↛ 39line 46 didn't jump to line 39 because the condition on line 46 was always true
47 walk(submenu, path + [action.text().replace('&', '')])
48 else:
49 text = action.text().replace('&', '').strip()
50 if text: 50 ↛ 39line 50 didn't jump to line 39 because the condition on line 50 was always true
51 results.append((path + [text], action))
53 # 「データセット」メニューは含めない。開くたびに作り直すので、候補がそのときの選択で変わり、
54 # ピン留めの識別子も安定しない(作り直すサブメニューを辿るとメニューの回収も起きやすい)
55 top_menus = [self._file_menu, self._edit_menu, self._view_menu, self._help_menu]
56 # 「プラグイン」メニューは、プラグインが何も登録していなければ作られない
57 plugin_menu = getattr(self, '_plugin_menu', None)
58 if plugin_menu is not None: 58 ↛ 60line 58 didn't jump to line 60 because the condition on line 58 was always true
59 top_menus.append(plugin_menu)
60 for top_menu in top_menus:
61 walk(top_menu, [top_menu.title().replace('&', '')])
62 return results
64 def _on_show_command_palette(self):
65 dialog = CommandPaletteDialog(self._collect_menu_actions, self)
66 dialog.exec()
68 def _on_toggle_dark_mode(self, checked):
69 """アプリ全体の配色(Qt のパレットと QSS)と、このタブのグラフ・アイコンを切り替える。"""
70 apply_theme(QApplication.instance(), checked)
71 self.canvas.dark_mode = checked
72 app_settings.DARK_MODE.write(self.settings, checked)
73 self.refresh_theme_colors()
74 # QSS はアプリ全体に効くが、グラフ・アイコン・メニューのチェックはタブごとなので、ほかのタブにも当てる
75 self._sync_dark_mode_to_sibling_tabs(checked)
77 def refresh_theme_colors(self):
78 """テーマ(明暗・テーマカラー)を替えた後、このタブで色を焼き込んで作ったものを描き直す。"""
79 self._update_plot(light=True) # 軸の数は変わらないので軽い描き直しでよい
80 # 以下は作ったときにテーマの色を焼き込んでいるので、描き直す:
81 # タイトルと軸ラベルのプレビュー
82 self._refresh_all_label_previews()
83 # 色欄の見本の枠
84 self.color_picker_widget.refresh_theme()
85 self.gradient_color2_picker.refresh_theme()
86 # 自前の SVG アイコン
87 self._refresh_custom_svg_icons()
88 # 開いたままのデータエディタ(除外した行の背景色)
89 if getattr(self, 'data_editor_dialog', None) is not None: 89 ↛ 90line 89 didn't jump to line 90 because the condition on line 89 was never true
90 self.data_editor_dialog._populate_table()
91 # 開いたままのヘルプ(表の見出しの色)
92 for dialog_attr in ('help_dialog', 'calc_help_dialog'):
93 dialog = getattr(self, dialog_attr, None)
94 if dialog is not None and hasattr(dialog, 'refresh_theme'): 94 ↛ 95line 94 didn't jump to line 95 because the condition on line 94 was never true
95 dialog.refresh_theme()
97 def _sync_dark_mode_to_sibling_tabs(self, checked):
98 """ほかのタブにも同じダークモードを当てる(QSS はアプリ全体だが、グラフやアイコンはタブごと)。"""
99 main_app_window = self.window()
100 tab_widget = getattr(main_app_window, 'tab_widget', None)
101 if tab_widget is None:
102 return
103 for i in range(tab_widget.count()):
104 tab = tab_widget.widget(i)
105 if tab is None or tab is self:
106 continue
107 if not hasattr(tab, 'dark_mode_action') or not hasattr(tab, 'canvas'): 107 ↛ 108line 107 didn't jump to line 108 because the condition on line 107 was never true
108 continue
109 if tab.canvas.dark_mode == checked: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true
110 continue
111 tab.canvas.dark_mode = checked
112 app_settings.DARK_MODE.write(tab.settings, checked)
113 # toggled を出すと、そのタブがまたほかのタブへ当て直しを連鎖させるので、シグナルを止めてチェックだけ揃える
114 tab.dark_mode_action.blockSignals(True)
115 tab.dark_mode_action.setChecked(checked)
116 tab.dark_mode_action.blockSignals(False)
117 tab.refresh_theme_colors()
119 def _refresh_custom_svg_icons(self):
120 """
121 自前の SVG アイコンを今のテーマで作り直す。_svg_icon() は作るときに色を決めるだけで、
122 setIcon() 済みのアイコンは変わらない(ダイアログは開くたびに作るので対象外)。
123 """
124 from graphica.gui.main_window import _svg_icon
126 from graphica.gui.builders.canvas_area import TOOL_ACTION_ICONS
127 for attr, icon_name in TOOL_ACTION_ICONS:
128 if hasattr(self, attr): 128 ↛ 127line 128 didn't jump to line 127 because the condition on line 128 was always true
129 getattr(self, attr).setIcon(_svg_icon(icon_name))
130 for button in (getattr(self, name, None) for name in ('card_reset_button', 'card_minimap_button',
131 'card_detach_button')):
132 if button is not None: 132 ↛ 130line 132 didn't jump to line 130 because the condition on line 132 was always true
133 button.setIcon(_svg_icon(button.property("iconName")))
134 if hasattr(self, 'manage_palette_action'): 134 ↛ 136line 134 didn't jump to line 136 because the condition on line 134 was always true
135 self.manage_palette_action.setIcon(_svg_icon("palette", size=16))
136 if hasattr(self, 'colormap_assign_action'): 136 ↛ 139line 136 didn't jump to line 139 because the condition on line 136 was always true
137 self.colormap_assign_action.setIcon(_svg_icon("palette", size=16))
139 for button, (icon_name, _label) in getattr(self, '_dataset_action_button_icons', {}).items():
140 button.setIcon(_svg_icon(icon_name, size=18))
141 # 左端の帯と左パネル(色の違うアイコンがあるので、それぞれが作り直す)
142 for part in ('activity_rail', 'dataset_page'):
143 if hasattr(self, part): 143 ↛ 142line 143 didn't jump to line 142 because the condition on line 143 was always true
144 getattr(self, part).refresh_icons()
145 for button, icon_name in getattr(self, '_field_icon_buttons', {}).items():
146 button.setIcon(_svg_icon(icon_name, size=16))
147 bottom_panel.refresh_theme(self)
148 if hasattr(self, 'lite_more_icon'): 148 ↛ 151line 148 didn't jump to line 151 because the condition on line 148 was always true
149 self.lite_more_icon.setPixmap(_svg_icon("lock", size=15).pixmap(15, 15))
150 # プロパティ欄の節の見出しも同じ矢印
151 for entry in getattr(self, '_prop_sections', {}).values():
152 button = entry['toggle']
153 button.setIcon(
154 _svg_icon("chevron-down" if button.isChecked() else "chevron-right", size=13)
155 )
157 def _set_initial_ui_state(self):
159 self.ui.x_autoscale_checkbox.setChecked(True)
160 self.ui.y_autoscale_checkbox.setChecked(True)
161 self.ui.legend_visible_checkbox.setChecked(True)
164 self.y2_label_text_label.setVisible(False)
165 self.y2_label_text_edit.setVisible(False)
166 self.tick_direction_y2_label.setVisible(False)
167 self.major_tick_direction_y2_combo.setVisible(False)
168 self.minor_tick_direction_y2_combo.setVisible(False)
170 self.ui.grid_visible_checkbox.setChecked(False)
171 self.ui.minor_grid_visible_checkbox.setChecked(False)
173 self.ui.spine_width_spinbox.setValue(self._spine_width)
174 self.ui.tick_width_spinbox.setValue(self._tick_width)
176 # チェックの状態に合わせて関係する欄の有効/無効を揃える
177 self._on_x_minor_tick_visibility_changed()
178 self._on_y_minor_tick_visibility_changed()
179 self._on_grid_visibility_changed()
180 self._on_legend_visibility_changed() # 凡例関連のUIを有効化
181 self._refresh_y2_axis_state()
183 self.property_panel.update_ui_state()