Fluid 1.3.0
Material Design for QtQuick and Qml
Loading...
Searching...
No Matches
SegmentedButtonGroup.qml
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2026 Pier Luigi Fiorini <pierluigi.fiorini@gmail.com>
2// SPDX-License-Identifier: MPL-2.0
3
4pragma ComponentBehavior: Bound
5
6import QtQuick
7import QtQuick.Templates as T
8import Fluid as MD
9import "../internal"
10
55T.Container {
56 id: control
57
63 enum SelectionMode { SingleSelection, MultiSelection }
64
66 property int selectionMode: SegmentedButtonGroup.SingleSelection
67
76 property bool selectionRequired: selectionMode === SegmentedButtonGroup.SingleSelection
77
85 property list<int> selectedIndexes: []
86
93 signal selectionChanged(list<int> selectedIndexes)
94
95 property bool __ready: false
96 property bool __updatingSelection: false
97 property bool __updatingFocus: false
98 property bool __syncingChildren: false
99 property var __effectiveSelection: []
100 property var __selectedObjects: []
101 property var __knownChildren: []
102 property list<MD.SegmentedButton> __visibleSegments: []
103 property real __naturalSegmentWidth: 0
104 property real __naturalHeight: 0
105
106 implicitWidth: __visibleSegments.length * __naturalSegmentWidth + leftPadding + rightPadding
107 implicitHeight: __naturalHeight + topPadding + bottomPadding
108 padding: 0
109 focusPolicy: Qt.NoFocus
110 Accessible.role: Accessible.Grouping
111
112 contentItem: SegmentedButtonLayout {
113 segments: control.__visibleSegments
114 mirrored: control.mirrored
115 }
116
117 function __arraysEqual(a, b) {
118 if (a.length !== b.length)
119 return false;
120 for (let i = 0; i < a.length; ++i) {
121 if (a[i] !== b[i])
122 return false;
123 }
124 return true;
125 }
126
127 function __indexOf(item) {
128 for (let i = 0; i < count; ++i) {
129 if (itemAt(i) === item)
130 return i;
131 }
132 return -1;
133 }
134
135 function __segmentAt(index: int): MD.SegmentedButton {
136 return itemAt(index) as MD.SegmentedButton;
137 }
138
139 function __firstRequiredIndex(): int {
140 let fallback = -1;
141 for (let i = 0; i < count; ++i) {
142 const segment = __segmentAt(i);
143 if (!segment)
144 continue;
145 if (fallback < 0)
146 fallback = i;
147 if (segment.enabled && segment.visible)
148 return i;
149 }
150 return fallback;
151 }
152
153 function __canonicalIndexes(candidate) {
154 const result = [];
155 for (let i = 0; i < candidate.length; ++i) {
156 const index = Number(candidate[i]);
157 if (Number.isInteger(index) && index >= 0 && index < count
158 && __segmentAt(index) && result.indexOf(index) < 0)
159 result.push(index);
160 }
161 result.sort((a, b) => a - b);
162 if (selectionMode === SegmentedButtonGroup.SingleSelection && result.length > 1)
163 result.splice(1);
164 if (selectionRequired && result.length === 0) {
165 const fallback = __firstRequiredIndex();
166 if (fallback >= 0)
167 result.push(fallback);
168 }
169 return result;
170 }
171
172 function __commitSelection(candidate) {
173 if (!__ready || __updatingSelection)
174 return;
175 const canonical = __canonicalIndexes(candidate);
176 const changed = !__arraysEqual(canonical, __effectiveSelection);
177 __updatingSelection = true;
178 if (!__arraysEqual(Array.from(selectedIndexes), canonical))
179 selectedIndexes = canonical;
180 for (let i = 0; i < count; ++i) {
181 const segment = __segmentAt(i);
182 if (segment)
183 segment.checked = canonical.indexOf(i) >= 0;
184 }
185 __effectiveSelection = canonical.slice();
186 __selectedObjects = canonical.map(index => itemAt(index));
187 __updatingSelection = false;
188 if (changed)
189 selectionChanged(canonical);
190 }
191
192 function __childCheckedChanged(segment: MD.SegmentedButton) {
193 if (!__ready || __updatingSelection)
194 return;
195 const index = __indexOf(segment);
196 if (index < 0)
197 return;
198 // Resolve identities now: an insert/move can precede the deferred sync.
199 let candidate = __selectedObjects.map(item => __indexOf(item)).filter(value => value >= 0);
200 if (segment.checked) {
201 if (selectionMode === SegmentedButtonGroup.SingleSelection)
202 candidate = [index];
203 else if (candidate.indexOf(index) < 0)
204 candidate.push(index);
205 } else if (!(selectionRequired && candidate.length === 1 && candidate[0] === index)) {
206 candidate = candidate.filter(value => value !== index);
207 }
208 __commitSelection(candidate);
209 }
210
211 function __releaseChild(segment: MD.SegmentedButton) {
212 if (!segment || segment.__segmentedButtonGroup !== control)
213 return;
214 segment.__segmentedButtonGroup = null;
215 segment.__leftEnd = true;
216 segment.__rightEnd = true;
217 segment.focusPolicy = Qt.StrongFocus;
218 segment.width = Qt.binding(() => segment.implicitWidth);
219 segment.height = Qt.binding(() => segment.implicitHeight);
220 segment.x = 0;
221 segment.y = 0;
222 }
223
224 function __syncChildren() {
225 if (!__ready)
226 return;
227 if (__syncingChildren) {
228 Qt.callLater(__syncChildren);
229 return;
230 }
231 __syncingChildren = true;
232 const current = [];
233 const visible = [];
234 let naturalWidth = 0;
235 let naturalHeight = 0;
236 for (let i = 0; i < count; ++i) {
237 const segment = __segmentAt(i);
238 if (!segment)
239 continue;
240 current.push(segment);
241 segment.__segmentedButtonGroup = control;
242 segment.checkable = true;
243 segment.autoExclusive = false;
244 if (segment.visible) {
245 visible.push(segment);
246 naturalWidth = Math.max(naturalWidth, segment.implicitWidth);
247 naturalHeight = Math.max(naturalHeight, segment.implicitHeight);
248 }
249 }
250 for (const oldItem of __knownChildren) {
251 if (current.indexOf(oldItem) < 0)
252 __releaseChild(oldItem);
253 }
254 __knownChildren = current;
255 __naturalSegmentWidth = naturalWidth;
256 __naturalHeight = naturalHeight;
257 __visibleSegments = visible;
258 __commitSelection(__selectedObjects.map(item => __indexOf(item)).filter(index => index >= 0));
259 __updateFocusPolicies();
260 __syncingChildren = false;
261 }
262
263 function __scheduleSync() {
264 if (__ready)
265 Qt.callLater(__syncChildren);
266 }
267
268 function __focusableIndexes() {
269 const result = [];
270 for (let i = 0; i < count; ++i) {
271 const segment = __segmentAt(i);
272 if (segment && segment.enabled && segment.visible)
273 result.push(i);
274 }
275 return result;
276 }
277
278 function __updateFocusPolicies() {
279 if (!__ready || __updatingFocus)
280 return;
281 __updatingFocus = true;
282 const indexes = __focusableIndexes();
283 const focusIndex = indexes.indexOf(currentIndex) >= 0 ? currentIndex
284 : (indexes.length > 0 ? indexes[0] : -1);
285 if (currentIndex !== focusIndex)
286 currentIndex = focusIndex;
287 // Qt requires moving active focus before removing its tab policy.
288 // A programmatic currentIndex change also moves focus when the group
289 // already contains the active item, without stealing external focus.
290 for (let i = 0; i < count; ++i) {
291 const segment = __segmentAt(i);
292 if (segment && segment.activeFocus && i !== focusIndex) {
293 const next = __segmentAt(focusIndex);
294 if (next)
295 next.forceActiveFocus(Qt.TabFocusReason);
296 else
297 segment.focus = false;
298 break;
299 }
300 }
301 for (let i = 0; i < count; ++i) {
302 const segment = __segmentAt(i);
303 if (segment)
304 segment.focusPolicy = i === focusIndex ? Qt.StrongFocus : Qt.ClickFocus;
305 }
306 __updatingFocus = false;
307 }
308
309 function __childFocused(segment: MD.SegmentedButton) {
310 const index = __indexOf(segment);
311 if (index >= 0)
312 currentIndex = index;
313 }
314
315 function __handleKey(segment: MD.SegmentedButton, event): bool {
316 const indexes = __focusableIndexes();
317 if (indexes.length === 0)
318 return false;
319 let target = -1;
320 if (event.key === Qt.Key_Home)
321 target = indexes[0];
322 else if (event.key === Qt.Key_End)
323 target = indexes[indexes.length - 1];
324 else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) {
325 const physical = mirrored ? indexes.slice().reverse() : indexes;
326 const position = physical.indexOf(__indexOf(segment));
327 const delta = event.key === Qt.Key_Right ? 1 : -1;
328 target = physical[(position + delta + physical.length) % physical.length];
329 } else {
330 return false;
331 }
332 const next = __segmentAt(target);
333 if (next)
334 next.forceActiveFocus(Qt.TabFocusReason);
335 currentIndex = target;
336 return true;
337 }
338
339 onSelectedIndexesChanged: {
340 if (__ready && !__updatingSelection)
341 __commitSelection(Array.from(selectedIndexes));
342 }
343 onSelectionModeChanged: __commitSelection(Array.from(selectedIndexes))
344 onSelectionRequiredChanged: __commitSelection(Array.from(selectedIndexes))
345 onCurrentIndexChanged: __updateFocusPolicies()
346 onContentChildrenChanged: __syncChildren()
347 onCountChanged: __syncChildren()
348 onVisibleChanged: __scheduleSync()
349 onEnabledChanged: __scheduleSync()
350
351 Component.onCompleted: {
352 __ready = true;
353 __commitSelection(Array.from(selectedIndexes));
354 __syncChildren();
355 }
356}
An outlined Material 3 choice in a SegmentedButtonGroup.