1 | /* |
2 | * SPDX-FileCopyrightText: 2020 Arjen Hiemstra <ahiemstra@heimr.nl> |
3 | * |
4 | * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL |
5 | */ |
6 | |
7 | #include "toolbarlayout.h" |
8 | |
9 | #include <cmath> |
10 | #include <unordered_map> |
11 | |
12 | #include <QDeadlineTimer> |
13 | #include <QElapsedTimer> |
14 | #include <QQmlComponent> |
15 | #include <QTimer> |
16 | |
17 | #include "loggingcategory.h" |
18 | #include "toolbarlayoutdelegate.h" |
19 | |
20 | ToolBarLayoutAttached::ToolBarLayoutAttached(QObject *parent) |
21 | : QObject(parent) |
22 | { |
23 | } |
24 | |
25 | QObject *ToolBarLayoutAttached::action() const |
26 | { |
27 | return m_action; |
28 | } |
29 | |
30 | void ToolBarLayoutAttached::setAction(QObject *action) |
31 | { |
32 | m_action = action; |
33 | } |
34 | |
35 | class ToolBarLayoutPrivate |
36 | { |
37 | ToolBarLayout *const q; |
38 | |
39 | public: |
40 | ToolBarLayoutPrivate(ToolBarLayout *qq) |
41 | : q(qq) |
42 | { |
43 | } |
44 | ~ToolBarLayoutPrivate() |
45 | { |
46 | if (moreButtonIncubator) { |
47 | moreButtonIncubator->clear(); |
48 | delete moreButtonIncubator; |
49 | } |
50 | } |
51 | |
52 | void calculateImplicitSize(); |
53 | void performLayout(); |
54 | QList<ToolBarLayoutDelegate *> createDelegates(); |
55 | ToolBarLayoutDelegate *createDelegate(QObject *action); |
56 | qreal layoutStart(qreal layoutWidth); |
57 | void maybeHideDelegate(int index, qreal ¤tWidth, qreal totalWidth); |
58 | |
59 | QList<QObject *> actions; |
60 | ToolBarLayout::ActionsProperty actionsProperty; |
61 | QList<QObject *> hiddenActions; |
62 | QQmlComponent *fullDelegate = nullptr; |
63 | QQmlComponent *iconDelegate = nullptr; |
64 | QQmlComponent *separatorDelegate = nullptr; |
65 | QQmlComponent *moreButton = nullptr; |
66 | qreal spacing = 0.0; |
67 | Qt::Alignment alignment = Qt::AlignLeft; |
68 | qreal visibleActionsWidth = 0.0; |
69 | qreal visibleWidth = 0.0; |
70 | Qt::LayoutDirection layoutDirection = Qt::LeftToRight; |
71 | ToolBarLayout::HeightMode heightMode = ToolBarLayout::ConstrainIfLarger; |
72 | |
73 | bool completed = false; |
74 | bool actionsChanged = false; |
75 | bool implicitSizeValid = false; |
76 | |
77 | std::unordered_map<QObject *, std::unique_ptr<ToolBarLayoutDelegate>> delegates; |
78 | QList<ToolBarLayoutDelegate *> sortedDelegates; |
79 | QQuickItem *moreButtonInstance = nullptr; |
80 | ToolBarDelegateIncubator *moreButtonIncubator = nullptr; |
81 | bool shouldShowMoreButton = false; |
82 | int firstHiddenIndex = -1; |
83 | |
84 | QList<QObject *> removedActions; |
85 | QTimer *removalTimer = nullptr; |
86 | |
87 | QElapsedTimer performanceTimer; |
88 | |
89 | static void appendAction(ToolBarLayout::ActionsProperty *list, QObject *action); |
90 | static qsizetype actionCount(ToolBarLayout::ActionsProperty *list); |
91 | static QObject *action(ToolBarLayout::ActionsProperty *list, qsizetype index); |
92 | static void clearActions(ToolBarLayout::ActionsProperty *list); |
93 | }; |
94 | |
95 | ToolBarLayout::ToolBarLayout(QQuickItem *parent) |
96 | : QQuickItem(parent) |
97 | , d(std::make_unique<ToolBarLayoutPrivate>(args: this)) |
98 | { |
99 | d->actionsProperty = ActionsProperty(this, |
100 | this, |
101 | ToolBarLayoutPrivate::appendAction, |
102 | ToolBarLayoutPrivate::actionCount, |
103 | ToolBarLayoutPrivate::action, |
104 | ToolBarLayoutPrivate::clearActions); |
105 | |
106 | // To prevent multiple assignments to actions from constantly recreating |
107 | // delegates, we cache the delegates and only remove them once they are no |
108 | // longer being used. This timer is responsible for triggering that removal. |
109 | d->removalTimer = new QTimer{this}; |
110 | d->removalTimer->setInterval(1000); |
111 | d->removalTimer->setSingleShot(true); |
112 | connect(sender: d->removalTimer, signal: &QTimer::timeout, context: this, slot: [this]() { |
113 | for (auto action : std::as_const(t&: d->removedActions)) { |
114 | if (!d->actions.contains(t: action)) { |
115 | d->delegates.erase(x: action); |
116 | } |
117 | } |
118 | d->removedActions.clear(); |
119 | }); |
120 | } |
121 | |
122 | ToolBarLayout::~ToolBarLayout() |
123 | { |
124 | } |
125 | |
126 | ToolBarLayout::ActionsProperty ToolBarLayout::actionsProperty() const |
127 | { |
128 | return d->actionsProperty; |
129 | } |
130 | |
131 | void ToolBarLayout::addAction(QObject *action) |
132 | { |
133 | if (action == nullptr) { |
134 | return; |
135 | } |
136 | d->actions.append(t: action); |
137 | d->actionsChanged = true; |
138 | |
139 | connect(sender: action, signal: &QObject::destroyed, context: this, slot: [this](QObject *action) { |
140 | auto itr = d->delegates.find(x: action); |
141 | if (itr != d->delegates.end()) { |
142 | d->delegates.erase(position: itr); |
143 | } |
144 | |
145 | d->actions.removeOne(t: action); |
146 | d->actionsChanged = true; |
147 | |
148 | relayout(); |
149 | }); |
150 | |
151 | relayout(); |
152 | } |
153 | |
154 | void ToolBarLayout::removeAction(QObject *action) |
155 | { |
156 | auto itr = d->delegates.find(x: action); |
157 | if (itr != d->delegates.end()) { |
158 | itr->second->hide(); |
159 | } |
160 | |
161 | d->actions.removeOne(t: action); |
162 | d->removedActions.append(t: action); |
163 | d->removalTimer->start(); |
164 | d->actionsChanged = true; |
165 | |
166 | relayout(); |
167 | } |
168 | |
169 | void ToolBarLayout::clearActions() |
170 | { |
171 | for (auto action : std::as_const(t&: d->actions)) { |
172 | auto itr = d->delegates.find(x: action); |
173 | if (itr != d->delegates.end()) { |
174 | itr->second->hide(); |
175 | } |
176 | } |
177 | |
178 | d->removedActions.append(l: d->actions); |
179 | d->actions.clear(); |
180 | d->actionsChanged = true; |
181 | |
182 | relayout(); |
183 | } |
184 | |
185 | QList<QObject *> ToolBarLayout::hiddenActions() const |
186 | { |
187 | return d->hiddenActions; |
188 | } |
189 | |
190 | QQmlComponent *ToolBarLayout::fullDelegate() const |
191 | { |
192 | return d->fullDelegate; |
193 | } |
194 | |
195 | void ToolBarLayout::setFullDelegate(QQmlComponent *newFullDelegate) |
196 | { |
197 | if (newFullDelegate == d->fullDelegate) { |
198 | return; |
199 | } |
200 | |
201 | d->fullDelegate = newFullDelegate; |
202 | d->delegates.clear(); |
203 | relayout(); |
204 | Q_EMIT fullDelegateChanged(); |
205 | } |
206 | |
207 | QQmlComponent *ToolBarLayout::iconDelegate() const |
208 | { |
209 | return d->iconDelegate; |
210 | } |
211 | |
212 | void ToolBarLayout::setIconDelegate(QQmlComponent *newIconDelegate) |
213 | { |
214 | if (newIconDelegate == d->iconDelegate) { |
215 | return; |
216 | } |
217 | |
218 | d->iconDelegate = newIconDelegate; |
219 | d->delegates.clear(); |
220 | relayout(); |
221 | Q_EMIT iconDelegateChanged(); |
222 | } |
223 | |
224 | QQmlComponent *ToolBarLayout::separatorDelegate() const |
225 | { |
226 | return d->separatorDelegate; |
227 | } |
228 | |
229 | void ToolBarLayout::setSeparatorDelegate(QQmlComponent *newSeparatorDelegate) |
230 | { |
231 | if (newSeparatorDelegate == d->separatorDelegate) { |
232 | return; |
233 | } |
234 | |
235 | d->separatorDelegate = newSeparatorDelegate; |
236 | d->delegates.clear(); |
237 | relayout(); |
238 | Q_EMIT separatorDelegateChanged(); |
239 | } |
240 | |
241 | QQmlComponent *ToolBarLayout::moreButton() const |
242 | { |
243 | return d->moreButton; |
244 | } |
245 | |
246 | void ToolBarLayout::setMoreButton(QQmlComponent *newMoreButton) |
247 | { |
248 | if (newMoreButton == d->moreButton) { |
249 | return; |
250 | } |
251 | |
252 | d->moreButton = newMoreButton; |
253 | if (d->moreButtonInstance) { |
254 | d->moreButtonInstance->deleteLater(); |
255 | d->moreButtonInstance = nullptr; |
256 | } |
257 | relayout(); |
258 | Q_EMIT moreButtonChanged(); |
259 | } |
260 | |
261 | qreal ToolBarLayout::spacing() const |
262 | { |
263 | return d->spacing; |
264 | } |
265 | |
266 | void ToolBarLayout::setSpacing(qreal newSpacing) |
267 | { |
268 | if (newSpacing == d->spacing) { |
269 | return; |
270 | } |
271 | |
272 | d->spacing = newSpacing; |
273 | relayout(); |
274 | Q_EMIT spacingChanged(); |
275 | } |
276 | |
277 | Qt::Alignment ToolBarLayout::alignment() const |
278 | { |
279 | return d->alignment; |
280 | } |
281 | |
282 | void ToolBarLayout::setAlignment(Qt::Alignment newAlignment) |
283 | { |
284 | if (newAlignment == d->alignment) { |
285 | return; |
286 | } |
287 | |
288 | d->alignment = newAlignment; |
289 | relayout(); |
290 | Q_EMIT alignmentChanged(); |
291 | } |
292 | |
293 | qreal ToolBarLayout::visibleWidth() const |
294 | { |
295 | return d->visibleWidth; |
296 | } |
297 | |
298 | qreal ToolBarLayout::minimumWidth() const |
299 | { |
300 | return d->moreButtonInstance ? d->moreButtonInstance->width() : 0; |
301 | } |
302 | |
303 | Qt::LayoutDirection ToolBarLayout::layoutDirection() const |
304 | { |
305 | return d->layoutDirection; |
306 | } |
307 | |
308 | void ToolBarLayout::setLayoutDirection(Qt::LayoutDirection &newLayoutDirection) |
309 | { |
310 | if (newLayoutDirection == d->layoutDirection) { |
311 | return; |
312 | } |
313 | |
314 | d->layoutDirection = newLayoutDirection; |
315 | relayout(); |
316 | Q_EMIT layoutDirectionChanged(); |
317 | } |
318 | |
319 | ToolBarLayout::HeightMode ToolBarLayout::heightMode() const |
320 | { |
321 | return d->heightMode; |
322 | } |
323 | |
324 | void ToolBarLayout::setHeightMode(HeightMode newHeightMode) |
325 | { |
326 | if (newHeightMode == d->heightMode) { |
327 | return; |
328 | } |
329 | |
330 | d->heightMode = newHeightMode; |
331 | relayout(); |
332 | Q_EMIT heightModeChanged(); |
333 | } |
334 | |
335 | void ToolBarLayout::relayout() |
336 | { |
337 | d->implicitSizeValid = false; |
338 | polish(); |
339 | } |
340 | |
341 | void ToolBarLayout::componentComplete() |
342 | { |
343 | QQuickItem::componentComplete(); |
344 | d->completed = true; |
345 | relayout(); |
346 | } |
347 | |
348 | void ToolBarLayout::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) |
349 | { |
350 | if (newGeometry != oldGeometry) { |
351 | relayout(); |
352 | } |
353 | QQuickItem::geometryChange(newGeometry, oldGeometry); |
354 | } |
355 | |
356 | void ToolBarLayout::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data) |
357 | { |
358 | if (change == ItemVisibleHasChanged || change == ItemSceneChange) { |
359 | relayout(); |
360 | } |
361 | QQuickItem::itemChange(change, data); |
362 | } |
363 | |
364 | void ToolBarLayout::updatePolish() |
365 | { |
366 | d->performLayout(); |
367 | } |
368 | |
369 | /* |
370 | * Calculate the implicit size for this layout. |
371 | * |
372 | * This is a separate step from performing the actual layout, because of a nasty |
373 | * little issue with Control, where it will unconditionally set the height of |
374 | * its contentItem, which means QQuickItem::heightValid() becomes useless. So |
375 | * instead, we first calculate our implicit size, ignoring any explicitly set |
376 | * item size. Then we follow that by performing the actual layouting, using the |
377 | * width and height retrieved from the item, as those will return the explicitly |
378 | * set width/height if set and the implicit size otherwise. Since control |
379 | * watches for implicit size changes, we end up with correct behaviour both when |
380 | * we get an explicit size set and when we're relying on implicit size |
381 | * calculation. |
382 | */ |
383 | void ToolBarLayoutPrivate::calculateImplicitSize() |
384 | { |
385 | if (!completed) { |
386 | return; |
387 | } |
388 | |
389 | if (!fullDelegate || !iconDelegate || !separatorDelegate || !moreButton) { |
390 | qCWarning(KirigamiLayoutsLog) << "ToolBarLayout: Unable to layout, required properties are not set" ; |
391 | return; |
392 | } |
393 | |
394 | if (actions.isEmpty()) { |
395 | q->setImplicitSize(0., 0.); |
396 | return; |
397 | } |
398 | |
399 | hiddenActions.clear(); |
400 | firstHiddenIndex = -1; |
401 | |
402 | sortedDelegates = createDelegates(); |
403 | |
404 | bool ready = std::all_of(first: delegates.cbegin(), last: delegates.cend(), pred: [](const std::pair<QObject *const, std::unique_ptr<ToolBarLayoutDelegate>> &entry) { |
405 | return entry.second->isReady(); |
406 | }); |
407 | if (!ready || !moreButtonInstance) { |
408 | return; |
409 | } |
410 | |
411 | qreal maxHeight = 0.0; |
412 | qreal maxWidth = 0.0; |
413 | |
414 | // First, calculate the total width and maximum height of all delegates. |
415 | // This will be used to determine which actions to show, which ones to |
416 | // collapse to icon-only etc. |
417 | for (auto entry : std::as_const(t&: sortedDelegates)) { |
418 | if (!entry->isActionVisible()) { |
419 | entry->hide(); |
420 | continue; |
421 | } |
422 | |
423 | if (entry->isHidden()) { |
424 | entry->hide(); |
425 | hiddenActions.append(t: entry->action()); |
426 | continue; |
427 | } |
428 | |
429 | if (entry->isIconOnly()) { |
430 | entry->showIcon(); |
431 | } else { |
432 | entry->showFull(); |
433 | } |
434 | |
435 | maxWidth += entry->width() + spacing; |
436 | maxHeight = std::max(a: maxHeight, b: entry->maxHeight()); |
437 | } |
438 | |
439 | // The last entry also gets spacing but shouldn't, so remove that. |
440 | maxWidth -= spacing; |
441 | |
442 | visibleActionsWidth = 0.0; |
443 | |
444 | q->setImplicitWidth(maxWidth); |
445 | |
446 | if (maxWidth > q->width() - (hiddenActions.isEmpty() ? 0.0 : moreButtonInstance->width() + spacing)) { |
447 | // We have more items than fit into the view, so start hiding some. |
448 | |
449 | qreal layoutWidth = q->width() - (moreButtonInstance->width() + spacing); |
450 | if (alignment & Qt::AlignHCenter) { |
451 | // When centering, we need to reserve space on both sides to make sure |
452 | // things are properly centered, otherwise we will be to the right of |
453 | // the center. |
454 | layoutWidth -= (moreButtonInstance->width() + spacing); |
455 | } |
456 | |
457 | for (int i = 0; i < sortedDelegates.size(); ++i) { |
458 | auto delegate = sortedDelegates.at(i); |
459 | |
460 | maybeHideDelegate(index: i, currentWidth&: visibleActionsWidth, totalWidth: layoutWidth); |
461 | |
462 | if (delegate->isVisible()) { |
463 | visibleActionsWidth += delegate->width() + spacing; |
464 | } |
465 | } |
466 | if (!qFuzzyIsNull(d: visibleActionsWidth)) { |
467 | // Like above, remove spacing on the last element that incorrectly gets spacing added. |
468 | visibleActionsWidth -= spacing; |
469 | } |
470 | } else { |
471 | visibleActionsWidth = maxWidth; |
472 | } |
473 | |
474 | if (!hiddenActions.isEmpty()) { |
475 | maxHeight = std::max(a: maxHeight, b: moreButtonInstance->implicitHeight()); |
476 | }; |
477 | |
478 | q->setImplicitHeight(maxHeight); |
479 | |
480 | Q_EMIT q->hiddenActionsChanged(); |
481 | |
482 | implicitSizeValid = true; |
483 | |
484 | q->polish(); |
485 | } |
486 | |
487 | void ToolBarLayoutPrivate::performLayout() |
488 | { |
489 | if (!completed || actions.isEmpty()) { |
490 | return; |
491 | } |
492 | |
493 | if (!implicitSizeValid) { |
494 | calculateImplicitSize(); |
495 | } |
496 | |
497 | if (sortedDelegates.isEmpty()) { |
498 | sortedDelegates = createDelegates(); |
499 | } |
500 | |
501 | bool ready = std::all_of(first: delegates.cbegin(), last: delegates.cend(), pred: [](const std::pair<QObject *const, std::unique_ptr<ToolBarLayoutDelegate>> &entry) { |
502 | return entry.second->isReady(); |
503 | }); |
504 | if (!ready || !moreButtonInstance) { |
505 | // Hide toolbar actions if delegates are still being |
506 | // loaded (otherwise they visibly glitch in one-by-one as they load) |
507 | q->setVisible(false); |
508 | return; |
509 | } |
510 | // Set visibility to true once all delegates have loaded |
511 | q->setVisible(true); |
512 | |
513 | qreal width = q->width(); |
514 | qreal height = q->height(); |
515 | |
516 | if (!hiddenActions.isEmpty()) { |
517 | if (layoutDirection == Qt::LeftToRight) { |
518 | moreButtonInstance->setX(width - moreButtonInstance->width()); |
519 | } else { |
520 | moreButtonInstance->setX(0.0); |
521 | } |
522 | |
523 | if (heightMode == ToolBarLayout::AlwaysFill) { |
524 | moreButtonInstance->setHeight(height); |
525 | } else if (heightMode == ToolBarLayout::ConstrainIfLarger) { |
526 | if (moreButtonInstance->implicitHeight() > height) { |
527 | moreButtonInstance->setHeight(height); |
528 | } else { |
529 | moreButtonInstance->resetHeight(); |
530 | } |
531 | } else { |
532 | moreButtonInstance->resetHeight(); |
533 | } |
534 | |
535 | moreButtonInstance->setY(qRound(d: (height - moreButtonInstance->height()) / 2.0)); |
536 | shouldShowMoreButton = true; |
537 | moreButtonInstance->setVisible(true); |
538 | } else { |
539 | shouldShowMoreButton = false; |
540 | moreButtonInstance->setVisible(false); |
541 | } |
542 | |
543 | qreal currentX = layoutStart(layoutWidth: visibleActionsWidth); |
544 | for (auto entry : std::as_const(t&: sortedDelegates)) { |
545 | if (!entry->isVisible()) { |
546 | continue; |
547 | } |
548 | |
549 | if (heightMode == ToolBarLayout::AlwaysFill) { |
550 | entry->setHeight(height); |
551 | } else if (heightMode == ToolBarLayout::ConstrainIfLarger) { |
552 | if (entry->implicitHeight() > height) { |
553 | entry->setHeight(height); |
554 | } else { |
555 | entry->resetHeight(); |
556 | } |
557 | } else { |
558 | entry->resetHeight(); |
559 | } |
560 | |
561 | qreal y = qRound(d: (height - entry->height()) / 2.0); |
562 | |
563 | if (layoutDirection == Qt::LeftToRight) { |
564 | entry->setPosition(x: currentX, y); |
565 | currentX += entry->width() + spacing; |
566 | } else { |
567 | entry->setPosition(x: currentX - entry->width(), y); |
568 | currentX -= entry->width() + spacing; |
569 | } |
570 | |
571 | entry->show(); |
572 | } |
573 | |
574 | qreal newVisibleWidth = visibleActionsWidth; |
575 | if (moreButtonInstance->isVisible()) { |
576 | newVisibleWidth += moreButtonInstance->width() + (newVisibleWidth > 0.0 ? spacing : 0.0); |
577 | } |
578 | if (!qFuzzyCompare(p1: newVisibleWidth, p2: visibleWidth)) { |
579 | visibleWidth = newVisibleWidth; |
580 | Q_EMIT q->visibleWidthChanged(); |
581 | } |
582 | |
583 | if (actionsChanged) { |
584 | // Due to the way QQmlListProperty works, if we emit changed every time |
585 | // an action is added/removed, we end up emitting way too often. So |
586 | // instead only do it after everything else is done. |
587 | Q_EMIT q->actionsChanged(); |
588 | actionsChanged = false; |
589 | } |
590 | |
591 | sortedDelegates.clear(); |
592 | } |
593 | |
594 | QList<ToolBarLayoutDelegate *> ToolBarLayoutPrivate::createDelegates() |
595 | { |
596 | QList<ToolBarLayoutDelegate *> result; |
597 | for (auto action : std::as_const(t&: actions)) { |
598 | if (delegates.find(x: action) != delegates.end()) { |
599 | result.append(t: delegates.at(k: action).get()); |
600 | } else if (action) { |
601 | auto delegate = std::unique_ptr<ToolBarLayoutDelegate>(createDelegate(action)); |
602 | if (delegate) { |
603 | result.append(t: delegate.get()); |
604 | delegates.emplace(args&: action, args: std::move(delegate)); |
605 | } |
606 | } |
607 | } |
608 | |
609 | if (!moreButtonInstance && !moreButtonIncubator) { |
610 | moreButtonIncubator = new ToolBarDelegateIncubator(moreButton, qmlContext(moreButton)); |
611 | moreButtonIncubator->setStateCallback([this](QQuickItem *item) { |
612 | item->setParentItem(q); |
613 | }); |
614 | moreButtonIncubator->setCompletedCallback([this](ToolBarDelegateIncubator *incubator) { |
615 | moreButtonInstance = qobject_cast<QQuickItem *>(o: incubator->object()); |
616 | moreButtonInstance->setVisible(false); |
617 | |
618 | QObject::connect(sender: moreButtonInstance, signal: &QQuickItem::visibleChanged, context: q, slot: [this]() { |
619 | moreButtonInstance->setVisible(shouldShowMoreButton); |
620 | }); |
621 | QObject::connect(sender: moreButtonInstance, signal: &QQuickItem::widthChanged, context: q, slot: &ToolBarLayout::minimumWidthChanged); |
622 | q->relayout(); |
623 | Q_EMIT q->minimumWidthChanged(); |
624 | |
625 | QTimer::singleShot(interval: 0, receiver: q, slot: [this]() { |
626 | delete moreButtonIncubator; |
627 | moreButtonIncubator = nullptr; |
628 | }); |
629 | }); |
630 | moreButtonIncubator->create(); |
631 | } |
632 | |
633 | return result; |
634 | } |
635 | |
636 | ToolBarLayoutDelegate *ToolBarLayoutPrivate::createDelegate(QObject *action) |
637 | { |
638 | QQmlComponent *fullComponent = nullptr; |
639 | auto displayComponent = action->property(name: "displayComponent" ); |
640 | if (displayComponent.isValid()) { |
641 | fullComponent = displayComponent.value<QQmlComponent *>(); |
642 | } |
643 | |
644 | if (!fullComponent) { |
645 | fullComponent = fullDelegate; |
646 | } |
647 | |
648 | auto separator = action->property(name: "separator" ); |
649 | if (separator.isValid() && separator.toBool()) { |
650 | fullComponent = separatorDelegate; |
651 | } |
652 | |
653 | auto result = new ToolBarLayoutDelegate(q); |
654 | result->setAction(action); |
655 | result->createItems(fullComponent, iconComponent: iconDelegate, callback: [this, action](QQuickItem *newItem) { |
656 | newItem->setParentItem(q); |
657 | auto attached = static_cast<ToolBarLayoutAttached *>(qmlAttachedPropertiesObject<ToolBarLayout>(obj: newItem, create: true)); |
658 | attached->setAction(action); |
659 | |
660 | if (!q->childItems().isEmpty() && q->childItems().first() != newItem) { |
661 | // Due to asynchronous item creation, we end up creating the last item |
662 | // first. So move items before previously inserted items to ensure |
663 | // we have a more sensible tab order. |
664 | // Note that this will be incorrect if we end up completing in random |
665 | // order. |
666 | newItem->stackBefore(q->childItems().first()); |
667 | } |
668 | }); |
669 | |
670 | return result; |
671 | } |
672 | |
673 | qreal ToolBarLayoutPrivate::layoutStart(qreal layoutWidth) |
674 | { |
675 | qreal availableWidth = moreButtonInstance->isVisible() ? q->width() - (moreButtonInstance->width() + spacing) : q->width(); |
676 | |
677 | if (alignment & Qt::AlignLeft) { |
678 | return layoutDirection == Qt::LeftToRight ? 0.0 : q->width(); |
679 | } else if (alignment & Qt::AlignHCenter) { |
680 | return (q->width() / 2) + (layoutDirection == Qt::LeftToRight ? -layoutWidth / 2.0 : layoutWidth / 2.0); |
681 | } else if (alignment & Qt::AlignRight) { |
682 | qreal offset = availableWidth - layoutWidth; |
683 | return layoutDirection == Qt::LeftToRight ? offset : q->width() - offset; |
684 | } |
685 | return 0.0; |
686 | } |
687 | |
688 | void ToolBarLayoutPrivate::maybeHideDelegate(int index, qreal ¤tWidth, qreal totalWidth) |
689 | { |
690 | auto delegate = sortedDelegates.at(i: index); |
691 | |
692 | if (!delegate->isVisible()) { |
693 | // If the delegate isn't visible anyway, do nothing. |
694 | return; |
695 | } |
696 | |
697 | if (currentWidth + delegate->width() < totalWidth && (firstHiddenIndex < 0 || index < firstHiddenIndex)) { |
698 | // If the delegate is fully visible and we have not already hidden |
699 | // actions, do nothing. |
700 | return; |
701 | } |
702 | |
703 | if (delegate->isKeepVisible()) { |
704 | // If the action is marked as KeepVisible, we need to try our best to |
705 | // keep it in view. If the full size delegate does not fit, we try the |
706 | // icon-only delegate. If that also does not fit, try and find other |
707 | // actions to hide. Finally, if that also fails, we will hide the |
708 | // delegate. |
709 | if (currentWidth + delegate->iconWidth() > totalWidth) { |
710 | // First, hide any earlier actions that are not marked as KeepVisible. |
711 | for (auto currentIndex = index - 1; currentIndex >= 0; --currentIndex) { |
712 | auto previousDelegate = sortedDelegates.at(i: currentIndex); |
713 | if (!previousDelegate->isVisible() || previousDelegate->isKeepVisible()) { |
714 | continue; |
715 | } |
716 | |
717 | auto width = previousDelegate->width(); |
718 | previousDelegate->hide(); |
719 | hiddenActions.append(t: previousDelegate->action()); |
720 | currentWidth -= (width + spacing); |
721 | |
722 | if (currentWidth + delegate->fullWidth() <= totalWidth) { |
723 | delegate->showFull(); |
724 | break; |
725 | } else if (currentWidth + delegate->iconWidth() <= totalWidth) { |
726 | delegate->showIcon(); |
727 | break; |
728 | } |
729 | } |
730 | |
731 | if (currentWidth + delegate->width() <= totalWidth) { |
732 | return; |
733 | } |
734 | |
735 | // Hiding normal actions did not help enough, so go through actions |
736 | // with KeepVisible set and try and collapse them to IconOnly. |
737 | for (auto currentIndex = index - 1; currentIndex >= 0; --currentIndex) { |
738 | auto previousDelegate = sortedDelegates.at(i: currentIndex); |
739 | if (!previousDelegate->isVisible() || !previousDelegate->isKeepVisible()) { |
740 | continue; |
741 | } |
742 | |
743 | auto = previousDelegate->width() - previousDelegate->iconWidth(); |
744 | previousDelegate->showIcon(); |
745 | currentWidth -= extraSpace; |
746 | |
747 | if (currentWidth + delegate->fullWidth() <= totalWidth) { |
748 | delegate->showFull(); |
749 | break; |
750 | } else if (currentWidth + delegate->iconWidth() <= totalWidth) { |
751 | delegate->showIcon(); |
752 | break; |
753 | } |
754 | } |
755 | |
756 | // If that also did not work, then hide this action after all. |
757 | if (currentWidth + delegate->width() > totalWidth) { |
758 | delegate->hide(); |
759 | hiddenActions.append(t: delegate->action()); |
760 | } |
761 | } else { |
762 | delegate->showIcon(); |
763 | } |
764 | } else { |
765 | // The action is not marked as KeepVisible and it does not fit within |
766 | // the current layout, so hide it. |
767 | delegate->hide(); |
768 | hiddenActions.append(t: delegate->action()); |
769 | |
770 | // If this is the first item to be hidden, mark it so we know we should |
771 | // also hide the following items. |
772 | if (firstHiddenIndex < 0) { |
773 | firstHiddenIndex = index; |
774 | } |
775 | } |
776 | } |
777 | |
778 | void ToolBarLayoutPrivate::appendAction(ToolBarLayout::ActionsProperty *list, QObject *action) |
779 | { |
780 | auto layout = reinterpret_cast<ToolBarLayout *>(list->data); |
781 | layout->addAction(action); |
782 | } |
783 | |
784 | qsizetype ToolBarLayoutPrivate::actionCount(ToolBarLayout::ActionsProperty *list) |
785 | { |
786 | return reinterpret_cast<ToolBarLayout *>(list->data)->d->actions.count(); |
787 | } |
788 | |
789 | QObject *ToolBarLayoutPrivate::action(ToolBarLayout::ActionsProperty *list, qsizetype index) |
790 | { |
791 | return reinterpret_cast<ToolBarLayout *>(list->data)->d->actions.at(i: index); |
792 | } |
793 | |
794 | void ToolBarLayoutPrivate::clearActions(ToolBarLayout::ActionsProperty *list) |
795 | { |
796 | reinterpret_cast<ToolBarLayout *>(list->data)->clearActions(); |
797 | } |
798 | |
799 | #include "moc_toolbarlayout.cpp" |
800 | |