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

1"""PlotterApp の一度きりの組み立て(シグナル、メニューバー、初めの画面の状態)と、ダークモードの切り替え。""" 

2from PySide6.QtWidgets import QApplication 

3 

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 

10 

11 

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() 

18 

19 def _create_menu_bar(self): 

20 build_menu_bar(self) 

21 

22 def _populate_dataset_menu(self): 

23 populate_dataset_actions_menu(self, self._dataset_menu) 

24 

25 def _collect_menu_actions(self): 

26 """ 

27 メニューの全項目を、階層のパス付きで集める(コマンドパレット・クイックアクセス・ショートカット一覧が使う)。 

28 区切り線・空の項目・サブメニュー自体は除く。「最近使ったファイル」はコマンドではないので除く。 

29 

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) 

37 

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)) 

52 

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 

63 

64 def _on_show_command_palette(self): 

65 dialog = CommandPaletteDialog(self._collect_menu_actions, self) 

66 dialog.exec() 

67 

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) 

76 

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() 

96 

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() 

118 

119 def _refresh_custom_svg_icons(self): 

120 """ 

121 自前の SVG アイコンを今のテーマで作り直す。_svg_icon() は作るときに色を決めるだけで、 

122 setIcon() 済みのアイコンは変わらない(ダイアログは開くたびに作るので対象外)。 

123 """ 

124 from graphica.gui.main_window import _svg_icon 

125 

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)) 

138 

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 ) 

156 

157 def _set_initial_ui_state(self): 

158 

159 self.ui.x_autoscale_checkbox.setChecked(True) 

160 self.ui.y_autoscale_checkbox.setChecked(True) 

161 self.ui.legend_visible_checkbox.setChecked(True) 

162 

163 

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) 

169 

170 self.ui.grid_visible_checkbox.setChecked(False) 

171 self.ui.minor_grid_visible_checkbox.setChecked(False) 

172 

173 self.ui.spine_width_spinbox.setValue(self._spine_width) 

174 self.ui.tick_width_spinbox.setValue(self._tick_width) 

175 

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() 

182 

183 self.property_panel.update_ui_state()