Coverage for graphica/gui/widgets/segmented.py: 94.4%
122 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"""セグメント(横に並んだ排他のボタン)と、今のコンボを値の持ち主にしたままの同期。"""
2from PySide6.QtCore import QPointF, QSize, Qt, Signal
3from PySide6.QtGui import QColor, QPainter, QPen
4from PySide6.QtWidgets import QButtonGroup, QFrame, QHBoxLayout, QSizePolicy, QToolButton
6from graphica.gui import theme
8# matplotlib の線の種類と、それを描く Qt のペン
9LINE_STYLE_PENS = {
10 "-": Qt.PenStyle.SolidLine,
11 "solid": Qt.PenStyle.SolidLine,
12 "--": Qt.PenStyle.DashLine,
13 "dashed": Qt.PenStyle.DashLine,
14 ":": Qt.PenStyle.DotLine,
15 "dotted": Qt.PenStyle.DotLine,
16 "-.": Qt.PenStyle.DashDotLine,
17 "dashdot": Qt.PenStyle.DashDotLine,
18}
21class _LineSegmentButton(QToolButton):
22 """文字の代わりに線の見本を描く。色は描くたびにトークンから取るので、テーマを替えても描き直すだけでよい。"""
24 def __init__(self, pen_style, parent=None):
25 super().__init__(parent)
26 self._pen_style = pen_style
28 def sizeHint(self):
29 return QSize(40, 24)
31 def paintEvent(self, event):
32 super().paintEvent(event)
33 painter = QPainter(self)
34 painter.setRenderHint(QPainter.RenderHint.Antialiasing, True)
35 pen = QPen(QColor(theme.current_tokens()["text_primary"]))
36 pen.setWidthF(2.0)
37 pen.setStyle(self._pen_style)
38 pen.setCapStyle(Qt.PenCapStyle.FlatCap)
39 painter.setPen(pen)
40 y = self.height() / 2
41 half = min(15.0, self.width() / 2 - 5)
42 center = self.width() / 2
43 painter.drawLine(QPointF(center - half, y), QPointF(center + half, y))
44 painter.end()
47class SegmentedControl(QFrame):
48 """QComboBox と同じ呼び方(currentIndex・itemData・findData・setCurrentIndex)ができる排他のボタンの列。"""
50 currentIndexChanged = Signal(int)
52 def __init__(self, parent=None):
53 super().__init__(parent)
54 self.setObjectName("segmented_control")
55 self.setFocusPolicy(Qt.FocusPolicy.StrongFocus)
56 self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
57 layout = QHBoxLayout(self)
58 layout.setContentsMargins(2, 2, 2, 2)
59 layout.setSpacing(2)
60 self._group = QButtonGroup(self)
61 self._group.setExclusive(True)
62 self._group.idClicked.connect(self.setCurrentIndex)
63 self._items = [] # (text, data)
64 self._current = -1
66 def _add_button(self, button, text, data, tooltip):
67 button.setObjectName("segment")
68 button.setCheckable(True)
69 button.setFocusPolicy(Qt.FocusPolicy.NoFocus)
70 button.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
71 button.setCursor(Qt.CursorShape.PointingHandCursor)
72 if tooltip:
73 button.setToolTip(tooltip)
74 index = len(self._items)
75 self._items.append((text, data))
76 self._group.addButton(button, index)
77 self.layout().addWidget(button)
78 if self._current < 0:
79 # QComboBox と同じく、最初の項目を足した時点でそれを選ぶ
80 self.setCurrentIndex(0)
81 return index
83 def addItem(self, text, data=None, tooltip=""):
84 button = QToolButton(self)
85 button.setText(text)
86 button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextOnly)
87 return self._add_button(button, text, data, tooltip)
89 def addIconItem(self, icon, data=None, tooltip=""):
90 button = QToolButton(self)
91 button.setIcon(icon)
92 button.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
93 return self._add_button(button, "", data, tooltip)
95 def addLineStyleItem(self, line_style, data=None, tooltip=""):
96 """線の見本のボタン。line_style は matplotlib の書き方('-'・'--'・':'・'-.' など)。"""
97 button = _LineSegmentButton(LINE_STYLE_PENS[line_style], self)
98 return self._add_button(button, "", line_style if data is None else data, tooltip)
100 def count(self):
101 return len(self._items)
103 def itemText(self, index):
104 return self._items[index][0] if 0 <= index < len(self._items) else ""
106 def itemData(self, index):
107 return self._items[index][1] if 0 <= index < len(self._items) else None
109 def findData(self, data):
110 for index, (_, item_data) in enumerate(self._items):
111 if item_data == data:
112 return index
113 return -1
115 def currentIndex(self):
116 return self._current
118 def currentData(self):
119 return self.itemData(self._current)
121 def setCurrentIndex(self, index):
122 if index < -1 or index >= len(self._items) or index == self._current:
123 return
124 self._current = index
125 if index >= 0:
126 self._group.button(index).setChecked(True)
127 else:
128 # 何も選ばない(コンボに見本に無い値が入っているとき)。排他を一時外さないとチェックを外せない
129 self._group.setExclusive(False)
130 for button in self._group.buttons():
131 button.setChecked(False)
132 self._group.setExclusive(True)
133 self.currentIndexChanged.emit(index)
135 def keyPressEvent(self, event):
136 step = {Qt.Key.Key_Left: -1, Qt.Key.Key_Up: -1, Qt.Key.Key_Right: 1, Qt.Key.Key_Down: 1}.get(event.key())
137 if step is None or not self._items: 137 ↛ 138line 137 didn't jump to line 138 because the condition on line 137 was never true
138 super().keyPressEvent(event)
139 return
140 self.setCurrentIndex(max(0, min(len(self._items) - 1, self._current + step)))
143class ComboMirror:
144 """今のコンボを値の持ち主にしたまま、見た目の代わり(セグメントなど)と同期する。
146 見た目の側で選ぶとコンボの setCurrentIndex を呼ぶ(コンボのシグナルで今までどおり値が反映される)。
147 コンボがシグナルを止めて書き換えられる(バインディングの「戻す」)と同期の知らせが来ないので、
148 戻したあとに refresh() を呼ぶこと。コンボの値は itemData、無ければ表示の文字で突き合わせる。
149 """
151 def __init__(self, view, combo):
152 self.view = view
153 self.combo = combo
154 combo.currentIndexChanged.connect(lambda _index: self.refresh())
155 view.currentIndexChanged.connect(self._on_view_changed)
156 self.refresh()
158 def _combo_key(self, index):
159 data = self.combo.itemData(index)
160 return self.combo.itemText(index) if data is None else data
162 def refresh(self):
163 index = self.view.findData(self._combo_key(self.combo.currentIndex()))
164 blocked = self.view.blockSignals(True)
165 self.view.setCurrentIndex(index)
166 self.view.blockSignals(blocked)
168 def _on_view_changed(self, index):
169 key = self.view.itemData(index)
170 combo_index = self.combo.findData(key)
171 if combo_index < 0 and isinstance(key, str):
172 combo_index = self.combo.findText(key)
173 if combo_index >= 0 and combo_index != self.combo.currentIndex(): 173 ↛ exitline 173 didn't return from function '_on_view_changed' because the condition on line 173 was always true
174 self.combo.setCurrentIndex(combo_index)