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
« 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
14# 列番号は dataset_mixin も使うので gui/dataset_style_icon.py にある(循環 import を避けるため)
15DATASET_TREE_VISIBILITY_COLUMN_WIDTH = 26 # 目のアイコン(16px)+クリックの余白
18# ドックの既定の配置を変えたら上げる。保存時の値と違えば保存済みの配置を戻さない
19# (戻すと、新しい既定の配置が既存の利用者に届かない)。
20DOCK_LAYOUT_VERSION = 6
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)
36# プラグインの種類名は任意の長さなので、コンボの希望幅を固定して省略表示させる
37# (広がると QFormLayout の列幅を通じてドックに横スクロールバーが出る)。
38PLOT_TYPE_COMBO_MIN_CHARS = 11
41def cap_combo_width(combo):
42 """長い項目があっても、コンボの希望幅を種別の欄と同じ文字数にそろえる(項目は開いた一覧で全文が見える)。"""
43 combo.setSizeAdjustPolicy(QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon)
44 combo.setMinimumContentsLength(PLOT_TYPE_COMBO_MIN_CHARS)
47COLORMAP_CHOICES = [
48 'viridis', 'plasma', 'inferno', 'magma', 'cividis',
49 'coolwarm', 'RdBu', 'seismic', 'jet', 'turbo',
50 'gray', 'Blues', 'Greens', 'Reds', 'YlOrRd',
51]
54# Qt 既定の 24px だと、狭いウィンドウでボタンが「>>」に押し込まれて押せない
55TOOLBAR_ICON_SIZE = 18
58def _svg_icon(name, size=20):
59 """テーマの text_secondary 色で描いたアイコン。テーマが変わっても自動では変わらない
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)
69class _DatasetTreeSelectionDelegate(QStyledItemDelegate):
70 """選択の背景を、アイコンの列と文字の列にまたがる1つの角丸で描く。
72 QSS の ::item:selected は2つの列に別々の角丸を描き、間に隙間ができる。背景だけ自前で塗り、
73 State_Selected を外してから標準の描画に任せる。
74 """
76 def paint(self, painter, option, index):
77 opt = QStyleOptionViewItem(option)
78 self.initStyleOption(opt, index)
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
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()
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
116# 一覧の行の高さと、右端の点の数の余白(見本 docs/dev/mockups/gui_redesign/ の 38px から行の間の分を引いた高さ)
117DATASET_TREE_ROW_HEIGHT = 34
118DATASET_TREE_COUNT_PADDING = 12
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('点')}"
135class _IgnoreWheelFilter(QObject):
136 def eventFilter(self, watched, event):
137 return event.type() == QEvent.Type.Wheel
140def disable_wheel_tab_switching(tab_widget):
141 """ホイールでタブが切り替わらないようにする。
143 タブバーは QTabWidget が C++ 側で作るので、wheelEvent をクラスごと書き換えても効かない。
144 """
145 tab_bar = tab_widget.tabBar()
146 tab_bar.installEventFilter(_IgnoreWheelFilter(tab_bar))
149class _ClickableMathPreviewLabel(FitWidthPixmapLabel):
150 """タイトルと軸ラベルの欄。mathtext を描いたプレビューで、クリックで編集ダイアログを開く。
152 値は隠した元の QLineEdit が持つ(textChanged もそちらから出る)。
153 """
155 clicked = Signal()
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)
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)
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)