Fluid 1.3.0
Material Design for QtQuick and Qml
Loading...
Searching...
No Matches
ButtonGroup.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
45T.Container {
46 id: control
47
53 enum Variant { Standard, Connected }
54
60 enum SelectionMode { NoSelection, SingleSelection, MultiSelection }
61
69 enum Size { ExtraSmall, Small, Medium, Large, ExtraLarge }
70
75 enum Shape { Round, Square }
76
78 property int variant: ButtonGroup.Standard
80 property int selectionMode: ButtonGroup.NoSelection
82 property bool selectionRequired: false
84 property int size: ButtonGroup.Small
86 property int shape: ButtonGroup.Round
88 property list<int> selectedIndexes: []
89
91 signal selectionChanged(list<int> selectedIndexes)
92
93 property bool __updatingSelection: false
94 property var __effectiveSelection: []
95 property var __selectedObjects: []
96 property var __knownChildren: []
97 property bool __geometryInitialized: false
98 readonly property int __standardVariant: ButtonGroup.Standard
99 readonly property int __connectedVariant: ButtonGroup.Connected
100 readonly property int __singleSelectionMode: ButtonGroup.SingleSelection
101 readonly property int __multiSelectionMode: ButtonGroup.MultiSelection
102
103 implicitWidth: contentItem.implicitWidth + leftPadding + rightPadding
104 implicitHeight: contentItem.implicitHeight + topPadding + bottomPadding
105 padding: 0
106
107 Accessible.role: Accessible.Grouping
108
109 contentItem: ButtonGroupLayout {
110 id: layout
111 group: control
112 }
113
114 function __arraysEqual(a, b) {
115 if (a.length !== b.length)
116 return false;
117 for (let i = 0; i < a.length; ++i) {
118 if (a[i] !== b[i])
119 return false;
120 }
121 return true;
122 }
123
124 function __indexOf(item) {
125 for (let i = 0; i < count; ++i) {
126 if (itemAt(i) === item)
127 return i;
128 }
129 return -1;
130 }
131
132 function __isSupported(item) {
133 return item && item.__buttonGroup !== undefined;
134 }
135
136 function __firstRequiredIndex() {
137 for (let i = 0; i < count; ++i) {
138 const item = itemAt(i);
139 if (__isSupported(item) && item.enabled && item.visible)
140 return i;
141 }
142 for (let i = 0; i < count; ++i) {
143 if (__isSupported(itemAt(i)))
144 return i;
145 }
146 return -1;
147 }
148
149 function __canonicalIndexes(candidate) {
150 if (selectionMode === ButtonGroup.NoSelection)
151 return [];
152 const unique = {};
153 const result = [];
154 for (let i = 0; i < candidate.length; ++i) {
155 const value = Number(candidate[i]);
156 if (Number.isInteger(value) && value >= 0 && value < count
157 && __isSupported(itemAt(value)) && !unique[value]) {
158 unique[value] = true;
159 result.push(value);
160 }
161 }
162 result.sort((a, b) => a - b);
163 if (selectionMode === ButtonGroup.SingleSelection && result.length > 1)
164 result.splice(1);
165 if (selectionRequired && result.length === 0) {
166 const fallback = __firstRequiredIndex();
167 if (fallback >= 0)
168 result.push(fallback);
169 }
170 return result;
171 }
172
173 function __commitSelection(candidate) {
174 if (__updatingSelection)
175 return;
176 const canonical = __canonicalIndexes(candidate);
177 const changed = !__arraysEqual(canonical, __effectiveSelection);
178 __updatingSelection = true;
179 if (!__arraysEqual(Array.from(selectedIndexes), canonical))
180 selectedIndexes = canonical;
181 for (let i = 0; i < count; ++i) {
182 const item = itemAt(i);
183 if (__isSupported(item))
184 item.checked = canonical.indexOf(i) >= 0;
185 }
186 __effectiveSelection = canonical.slice();
187 __selectedObjects = canonical.map(index => itemAt(index));
188 __updatingSelection = false;
189 if (changed)
190 selectionChanged(canonical);
191 __updateFocusPolicies();
192 layout.schedule();
193 }
194
195 function __childCheckedChanged(item) {
196 if (__updatingSelection)
197 return;
198 const index = __indexOf(item);
199 if (index < 0)
200 return;
201 let candidate = __effectiveSelection.slice();
202 if (item.checked) {
203 if (selectionMode === ButtonGroup.SingleSelection)
204 candidate = [index];
205 else if (candidate.indexOf(index) < 0)
206 candidate.push(index);
207 } else {
208 candidate = candidate.filter(value => value !== index);
209 }
210 __commitSelection(candidate);
211 }
212
213 function __configureChild(item) {
214 if (!__isSupported(item))
215 return;
216 item.__buttonGroup = control;
217 item.size = size;
218 item.shape = shape;
219 item.checkable = selectionMode !== ButtonGroup.NoSelection;
220 }
221
222 function __releaseChild(item) {
223 if (!item || item.__buttonGroup !== control)
224 return;
225 item.__buttonGroup = null;
226 item.__groupTopLeftRadius = -1;
227 item.__groupTopRightRadius = -1;
228 item.__groupBottomLeftRadius = -1;
229 item.__groupBottomRightRadius = -1;
230 item.width = Qt.binding(() => item.implicitWidth);
231 item.x = 0;
232 item.y = 0;
233 }
234
235 function __childrenChanged() {
236 Qt.callLater(function() {
237 const preserved = [];
238 for (const item of __selectedObjects) {
239 const index = __indexOf(item);
240 if (index >= 0)
241 preserved.push(index);
242 }
243 __commitSelection(preserved);
244 __updateFocusPolicies();
245 layout.schedule();
246 });
247 }
248
249 function __syncChildren() {
250 const current = [];
251 for (let i = 0; i < count; ++i)
252 current.push(itemAt(i));
253 for (const oldItem of __knownChildren) {
254 if (current.indexOf(oldItem) < 0)
255 __releaseChild(oldItem);
256 }
257 for (const item of current)
258 __configureChild(item);
259 __knownChildren = current;
260 __childrenChanged();
261 }
262
263 function __childGeometryChanged() {
264 layout.schedule();
265 }
266
267 function __childFocused(item) {
268 const index = __indexOf(item);
269 if (index >= 0) {
270 currentIndex = index;
271 __updateFocusPolicies();
272 }
273 }
274
275 function __focusableIndexes() {
276 const result = [];
277 for (let i = 0; i < count; ++i) {
278 const item = itemAt(i);
279 if (__isSupported(item) && item.enabled && item.visible)
280 result.push(i);
281 }
282 return result;
283 }
284
285 function __updateFocusPolicies() {
286 const focusable = __focusableIndexes();
287 if (focusable.length > 0 && focusable.indexOf(currentIndex) < 0)
288 currentIndex = focusable[0];
289 for (let i = 0; i < count; ++i) {
290 const item = itemAt(i);
291 if (!__isSupported(item))
292 continue;
293 if (selectionMode === ButtonGroup.NoSelection)
294 item.focusPolicy = Qt.StrongFocus;
295 else
296 item.focusPolicy = i === currentIndex ? Qt.StrongFocus : Qt.ClickFocus;
297 }
298 }
299
300 function __handleKey(item, event) {
301 if (selectionMode === ButtonGroup.NoSelection)
302 return false;
303 const indexes = __focusableIndexes();
304 if (indexes.length === 0)
305 return false;
306 let target = -1;
307 if (event.key === Qt.Key_Home)
308 target = indexes[0];
309 else if (event.key === Qt.Key_End)
310 target = indexes[indexes.length - 1];
311 else if (event.key === Qt.Key_Left || event.key === Qt.Key_Right) {
312 const physical = indexes.slice().sort((a, b) => itemAt(a).x - itemAt(b).x);
313 let position = physical.indexOf(__indexOf(item));
314 const delta = event.key === Qt.Key_Right ? 1 : -1;
315 position = (position + delta + physical.length) % physical.length;
316 target = physical[position];
317 } else {
318 return false;
319 }
320 currentIndex = target;
321 itemAt(target).forceActiveFocus(Qt.TabFocusReason);
322 __updateFocusPolicies();
323 return true;
324 }
325
326 function __squareOuterRadius(height) {
327 const tokens = MD.Tokens.button;
328 let value = tokens.containerShapeSquareSmall.topLeft;
329 switch (size) {
330 case ButtonGroup.ExtraSmall: value = tokens.containerShapeSquareExtraSmall.topLeft; break;
331 case ButtonGroup.Medium: value = tokens.containerShapeSquareMedium.topLeft; break;
332 case ButtonGroup.Large: value = tokens.containerShapeSquareLarge.topLeft; break;
333 case ButtonGroup.ExtraLarge: value = tokens.containerShapeSquareExtraLarge.topLeft; break;
334 }
335 return Math.min(value, height / 2);
336 }
337
338 function __resolveCorner(value, width, height) {
339 return value === MD.Tokens.shape.cornerValueFull ? Math.min(width, height) / 2 : value;
340 }
341
342 function __applyConnectedCorners(items) {
343 for (let i = 0; i < count; ++i) {
344 const child = itemAt(i);
345 if (!child || child.__buttonGroup === undefined)
346 continue;
347 child.__groupTopLeftRadius = -1;
348 child.__groupTopRightRadius = -1;
349 child.__groupBottomLeftRadius = -1;
350 child.__groupBottomRightRadius = -1;
351 }
352 if (variant !== ButtonGroup.Connected || items.length <= 1)
353 return;
354 for (let i = 0; i < items.length; ++i) {
355 const child = items[i];
356 const height = child.background ? child.background.height : child.height;
357 const full = Math.min(child.width, height)
358 * MD.Tokens.buttonGroup.selectedInnerCornerPercentage / 100;
359 const outer = shape === ButtonGroup.Round
360 ? __resolveCorner(MD.Tokens.buttonGroup.connectedContainerShape.topLeft,
361 child.width, height)
362 : __squareOuterRadius(height);
363 const inner = child.pressed ? MD.Tokens.buttonGroup.pressedInnerCorner
364 : MD.Tokens.buttonGroup.connectedInnerCorner;
365 let left = i === 0 ? outer : inner;
366 let right = i === items.length - 1 ? outer : inner;
367 if (mirrored) {
368 const swap = left;
369 left = right;
370 right = swap;
371 }
372 if (child.checked && !child.pressed)
373 left = right = full;
374 child.__groupTopLeftRadius = left;
375 child.__groupBottomLeftRadius = left;
376 child.__groupTopRightRadius = right;
377 child.__groupBottomRightRadius = right;
378 }
379 }
380
381 onSelectedIndexesChanged: {
382 if (!__updatingSelection)
383 __commitSelection(Array.from(selectedIndexes));
384 }
385 onVariantChanged: layout.updateGeometry()
386 onSelectionModeChanged: {
387 for (let i = 0; i < count; ++i) {
388 const item = itemAt(i);
389 if (item && item.__buttonGroup !== undefined)
390 item.checkable = selectionMode !== ButtonGroup.NoSelection;
391 }
392 __commitSelection(__effectiveSelection);
393 }
394 onSelectionRequiredChanged: __commitSelection(__effectiveSelection)
395 onSizeChanged: {
396 for (let i = 0; i < count; ++i)
397 __configureChild(itemAt(i));
398 layout.schedule();
399 }
400 onShapeChanged: {
401 for (let i = 0; i < count; ++i)
402 __configureChild(itemAt(i));
403 layout.schedule();
404 }
405 onMirroredChanged: layout.updateGeometry()
406 onContentChildrenChanged: __syncChildren()
407 onCountChanged: __syncChildren()
408
409 Component.onCompleted: {
410 for (let i = 0; i < count; ++i)
411 __configureChild(itemAt(i));
412 __knownChildren = Array.from(contentChildren);
413 __commitSelection(Array.from(selectedIndexes));
414 layout.schedule();
415 }
416}
Material Design 3 Expressive button-group tokens.
Definition buttongroup.h:19