Coverage for graphica/gui/builders/common.py: 97.1%

90 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-12 16:38 +0000

1"""画面の組み立てで共有する定数と小さな部品。main_window からも同じ名前で読める。""" 

2import os 

3from PySide6.QtCore import QEvent, QObject, QRect, QRectF, Qt, Signal 

4from PySide6.QtGui import QColor, QPainter, QPainterPath 

5from PySide6.QtWidgets import QComboBox, QStyle, QStyleOptionViewItem, QStyledItemDelegate 

6from graphica.core.i18n import tr 

7from graphica.gui import theme 

8from graphica.gui.dataset_style_icon import DATASET_TREE_NAME_COLUMN 

9from graphica.gui.icon_utils import ICONS_DIR, load_svg_icon 

10from graphica.gui.mathtext_preview import FitWidthPixmapLabel 

11from graphica.gui.resources import resource_path 

12 

13 

14# 列番号は dataset_mixin も使うので gui/dataset_style_icon.py にある(循環 import を避けるため) 

15DATASET_TREE_VISIBILITY_COLUMN_WIDTH = 26 # 目のアイコン(16px)+クリックの余白 

16 

17 

18# ドックの既定の配置を変えたら上げる。保存時の値と違えば保存済みの配置を戻さない 

19# (戻すと、新しい既定の配置が既存の利用者に届かない)。 

20DOCK_LAYOUT_VERSION = 6 

21 

22 

23# データセットのプロパティ欄の節。行は self._prop_form(キー).addRow で足す(番号指定の挿入はしない)。 

24DATASET_PROPERTY_SECTIONS = ( 

25 ('data', 'データ列'), 

26 ('style', '基本スタイル'), 

27 ('marker', 'マーカー'), 

28 ('gradient', 'グラデーション'), 

29 ('waterfall', 'ウォーターフォール'), 

30 ('map', '2Dマップ・値による配色'), 

31 ('extra', '表示の追加要素'), 

32 ('place', '配置・情報'), 

33) 

34 

35 

36# プラグインの種類名は任意の長さなので、コンボの希望幅を固定して省略表示させる 

37# (広がると QFormLayout の列幅を通じてドックに横スクロールバーが出る)。 

38PLOT_TYPE_COMBO_MIN_CHARS = 11 

39 

40 

41def cap_combo_width(combo): 

42 """長い項目があっても、コンボの希望幅を種別の欄と同じ文字数にそろえる(項目は開いた一覧で全文が見える)。""" 

43 combo.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) 

44 combo.setMinimumContentsLength(PLOT_TYPE_COMBO_MIN_CHARS) 

45 

46 

47COLORMAP_CHOICES = [ 

48 'viridis', 'plasma', 'inferno', 'magma', 'cividis', 

49 'coolwarm', 'RdBu', 'seismic', 'jet', 'turbo', 

50 'gray', 'Blues', 'Greens', 'Reds', 'YlOrRd', 

51] 

52 

53 

54# Qt 既定の 24px だと、狭いウィンドウでボタンが「>>」に押し込まれて押せない 

55TOOLBAR_ICON_SIZE = 18 

56 

57 

58def _svg_icon(name, size=20): 

59 """テーマの text_secondary 色で描いたアイコン。テーマが変わっても自動では変わらない 

60 

61 (常に見えているものは _refresh_custom_svg_icons で作り直す)。 

62 """ 

63 from graphica.gui import theme 

64 color = theme.current_tokens()["text_secondary"] 

65 return load_svg_icon(resource_path(os.path.join(ICONS_DIR, f"{name}.svg")), 

66 color=color, size=size) 

67 

68 

69class _DatasetTreeSelectionDelegate(QStyledItemDelegate): 

70 """選択の背景を、アイコンの列と文字の列にまたがる1つの角丸で描く。 

71 

72 QSS の ::item:selected は2つの列に別々の角丸を描き、間に隙間ができる。背景だけ自前で塗り、 

73 State_Selected を外してから標準の描画に任せる。 

74 """ 

75 

76 def paint(self, painter, option, index): 

77 opt = QStyleOptionViewItem(option) 

78 self.initStyleOption(opt, index) 

79 

80 if opt.state & QStyle.StateFlag.State_Selected: 

81 painter.save() 

82 painter.setRenderHint(QPainter.RenderHint.Antialiasing, True) 

83 # 展開矢印の字下げの分だけ左が空くので、左端まで塗る(字下げの列には何も描かれない) 

84 rect = QRectF(opt.rect) 

85 rect.setLeft(0) 

86 path = QPainterPath() 

87 path.addRoundedRect( 

88 rect, theme.DATASET_LIST_ITEM_RADIUS, theme.DATASET_LIST_ITEM_RADIUS 

89 ) 

90 painter.fillPath(path, theme.current_accent_soft_qcolor()) 

91 painter.restore() 

92 # 標準の選択の背景を重ねないよう落とす(文字とアイコンの色は選択で変わらない) 

93 opt.state &= ~QStyle.StateFlag.State_Selected 

94 

95 count_text = _point_count_text(index) 

96 if not count_text: 

97 super().paint(painter, opt, index) 

98 return 

99 # 右端に点の数を出す。名前はその手前で省略されるよう、文字の幅を縮めてから描く 

100 full_rect = QRect(opt.rect) 

101 count_width = opt.fontMetrics.horizontalAdvance(count_text) + DATASET_TREE_COUNT_PADDING 

102 opt.rect = full_rect.adjusted(0, 0, -count_width, 0) 

103 super().paint(painter, opt, index) 

104 painter.save() 

105 painter.setPen(QColor(theme.current_tokens()["text_muted"])) 

106 painter.drawText(full_rect.adjusted(0, 0, -DATASET_TREE_COUNT_PADDING // 2, 0), 

107 int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), count_text) 

108 painter.restore() 

109 

110 def sizeHint(self, option, index): 

111 hint = super().sizeHint(option, index) 

112 hint.setHeight(max(hint.height(), DATASET_TREE_ROW_HEIGHT)) 

113 return hint 

114 

115 

116# 一覧の行の高さと、右端の点の数の余白(見本 docs/dev/mockups/gui_redesign/ の 38px から行の間の分を引いた高さ) 

117DATASET_TREE_ROW_HEIGHT = 34 

118DATASET_TREE_COUNT_PADDING = 12 

119 

120 

121def _point_count_text(index): 

122 """データセットの行なら「41 点」(除外した行は数えない)。フォルダや目の列は空。""" 

123 if index.column() != DATASET_TREE_NAME_COLUMN: 

124 return "" 

125 dataset = index.data(Qt.ItemDataRole.UserRole) 

126 if dataset is None: 

127 return "" 

128 try: 

129 count = len(dataset.df) - len(getattr(dataset, "masked_row_indices", ()) or ()) 

130 except TypeError: 

131 return "" 

132 return f"{max(count, 0)} {tr('点')}" 

133 

134 

135class _IgnoreWheelFilter(QObject): 

136 def eventFilter(self, watched, event): 

137 return event.type() == QEvent.Type.Wheel 

138 

139 

140def disable_wheel_tab_switching(tab_widget): 

141 """ホイールでタブが切り替わらないようにする。 

142 

143 タブバーは QTabWidget が C++ 側で作るので、wheelEvent をクラスごと書き換えても効かない。 

144 """ 

145 tab_bar = tab_widget.tabBar() 

146 tab_bar.installEventFilter(_IgnoreWheelFilter(tab_bar)) 

147 

148 

149class _ClickableMathPreviewLabel(FitWidthPixmapLabel): 

150 """タイトルと軸ラベルの欄。mathtext を描いたプレビューで、クリックで編集ダイアログを開く。 

151 

152 値は隠した元の QLineEdit が持つ(textChanged もそちらから出る)。 

153 """ 

154 

155 clicked = Signal() 

156 

157 def __init__(self, parent=None): 

158 super().__init__(parent) 

159 self.setObjectName("mathtext_preview_label") 

160 self.setCursor(Qt.CursorShape.PointingHandCursor) 

161 self.setMinimumHeight(28) 

162 self.setToolTip(tr("クリックして編集")) 

163 # QLabel は既定でホバーを追跡せず、QSS の :hover が効かない 

164 self.setAttribute(Qt.WidgetAttribute.WA_Hover, True) 

165 

166 def mousePressEvent(self, event): 

167 if event.button() == Qt.MouseButton.LeftButton: 167 ↛ 169line 167 didn't jump to line 169 because the condition on line 167 was always true

168 self.clicked.emit() 

169 super().mousePressEvent(event) 

170 

171 

172def _insert_form_row_after(form, anchor, *row): 

173 """form の anchor(ラベルか欄)の行の次に入れる。行番号で入れると、ほかの挿入の順番が変わったときに黙って別の位置に入る。""" 

174 anchor_row, _ = form.getWidgetPosition(anchor) 

175 if anchor_row < 0: 

176 raise ValueError(f"{anchor!r} は {form.objectName()} にありません") 

177 form.insertRow(anchor_row + 1, *row)