KWin
Loading...
Searching...
No Matches
expolayout.cpp
Go to the documentation of this file.
1/*
2 SPDX-FileCopyrightText: 2021 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
3
4 // The layouting code is taken from the present windows effect.
5 SPDX-FileCopyrightText: 2007 Rivo Laks <rivolaks@hot.ee>
6 SPDX-FileCopyrightText: 2008 Lucas Murray <lmurray@undefinedfire.com>
7
8 SPDX-License-Identifier: GPL-2.0-or-later
9*/
10
11#include "expolayout.h"
12
13#include <cmath>
14
15ExpoCell::ExpoCell(QObject *parent)
16 : QObject(parent)
17{
18}
19
21{
22 setLayout(nullptr);
23}
24
26{
27 return m_layout;
28}
29
31{
32 if (m_layout == layout) {
33 return;
34 }
35 if (m_layout) {
36 m_layout->removeCell(this);
37 }
38 m_layout = layout;
39 if (m_layout && m_enabled) {
40 m_layout->addCell(this);
41 }
42 Q_EMIT layoutChanged();
43}
44
46{
47 return m_enabled;
48}
49
50void ExpoCell::setEnabled(bool enabled)
51{
52 if (m_enabled != enabled) {
53 m_enabled = enabled;
54 if (enabled) {
55 if (m_layout) {
56 m_layout->addCell(this);
57 }
58 } else {
59 if (m_layout) {
60 m_layout->removeCell(this);
61 }
62 }
63 Q_EMIT enabledChanged();
64 }
65}
66
68{
69 if (m_layout) {
70 m_layout->polish();
71 }
72}
73
75{
76 return m_naturalX;
77}
78
80{
81 if (m_naturalX != x) {
82 m_naturalX = x;
83 update();
84 Q_EMIT naturalXChanged();
85 }
86}
87
89{
90 return m_naturalY;
91}
92
94{
95 if (m_naturalY != y) {
96 m_naturalY = y;
97 update();
98 Q_EMIT naturalYChanged();
99 }
100}
101
103{
104 return m_naturalWidth;
105}
106
108{
109 if (m_naturalWidth != width) {
110 m_naturalWidth = width;
111 update();
112 Q_EMIT naturalWidthChanged();
113 }
114}
115
117{
118 return m_naturalHeight;
119}
120
122{
123 if (m_naturalHeight != height) {
124 m_naturalHeight = height;
125 update();
126 Q_EMIT naturalHeightChanged();
127 }
128}
129
131{
132 return QRect(naturalX(), naturalY(), naturalWidth(), naturalHeight());
133}
134
135QMargins ExpoCell::margins() const
136{
137 return m_margins;
138}
139
140int ExpoCell::x() const
141{
142 return m_x.value_or(0);
143}
144
145void ExpoCell::setX(int x)
146{
147 if (m_x != x) {
148 m_x = x;
149 Q_EMIT xChanged();
150 }
151}
152
153int ExpoCell::y() const
154{
155 return m_y.value_or(0);
156}
157
158void ExpoCell::setY(int y)
159{
160 if (m_y != y) {
161 m_y = y;
162 Q_EMIT yChanged();
163 }
164}
165
167{
168 return m_width.value_or(0);
169}
170
171void ExpoCell::setWidth(int width)
172{
173 if (m_width != width) {
174 m_width = width;
175 Q_EMIT widthChanged();
176 }
177}
178
180{
181 return m_height.value_or(0);
182}
183
184void ExpoCell::setHeight(int height)
185{
186 if (m_height != height) {
187 m_height = height;
188 Q_EMIT heightChanged();
189 }
190}
191
193{
194 return m_persistentKey;
195}
196
197void ExpoCell::setPersistentKey(const QString &key)
198{
199 if (m_persistentKey != key) {
200 m_persistentKey = key;
201 update();
202 Q_EMIT persistentKeyChanged();
203 }
204}
205
207{
208 return m_margins.bottom();
209}
210
212{
213 if (m_margins.bottom() != margin) {
214 m_margins.setBottom(margin);
215 update();
216 Q_EMIT bottomMarginChanged();
217 }
218}
219
220ExpoLayout::ExpoLayout(QQuickItem *parent)
221 : QQuickItem(parent)
222{
223}
224
226{
227 return m_mode;
228}
229
231{
232 if (m_mode != mode) {
233 m_mode = mode;
234 polish();
235 Q_EMIT modeChanged();
236 }
237}
238
240{
241 return m_fillGaps;
242}
243
245{
246 if (m_fillGaps != fill) {
247 m_fillGaps = fill;
248 polish();
249 Q_EMIT fillGapsChanged();
250 }
251}
252
254{
255 return m_spacing;
256}
257
258void ExpoLayout::setSpacing(int spacing)
259{
260 if (m_spacing != spacing) {
261 m_spacing = spacing;
262 polish();
263 Q_EMIT spacingChanged();
264 }
265}
266
268{
269 return m_ready;
270}
271
273{
274 if (!m_ready) {
275 m_ready = true;
276 Q_EMIT readyChanged();
277 }
278}
279
281{
282 updatePolish();
283}
284
286{
287 if (!m_cells.isEmpty()) {
288 switch (m_mode) {
289 case LayoutClosest:
290 calculateWindowTransformationsClosest();
291 break;
292 case LayoutNatural:
293 calculateWindowTransformationsNatural();
294 break;
295 case LayoutNone:
296 resetTransformations();
297 break;
298 }
299 }
300
301 setReady();
302}
303
305{
306 Q_ASSERT(!m_cells.contains(cell));
307 m_cells.append(cell);
308 polish();
309}
310
312{
313 m_cells.removeOne(cell);
314 polish();
315}
316
317void ExpoLayout::geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry)
318{
319 if (newGeometry.size() != oldGeometry.size()) {
320 polish();
321 }
322 QQuickItem::geometryChange(newGeometry, oldGeometry);
323}
324
325static int distance(const QPoint &a, const QPoint &b)
326{
327 const int xdiff = a.x() - b.x();
328 const int ydiff = a.y() - b.y();
329 return int(std::sqrt(qreal(xdiff * xdiff + ydiff * ydiff)));
330}
331
332static QRect centered(ExpoCell *cell, const QRect &bounds)
333{
334 const QSize scaled = QSize(cell->naturalWidth(), cell->naturalHeight())
335 .scaled(bounds.size(), Qt::KeepAspectRatio);
336
337 return QRect(bounds.center().x() - scaled.width() / 2,
338 bounds.center().y() - scaled.height() / 2,
339 scaled.width(),
340 scaled.height());
341}
342
343void ExpoLayout::calculateWindowTransformationsClosest()
344{
345 QRect area = QRect(0, 0, width(), height());
346 const int columns = int(std::ceil(std::sqrt(qreal(m_cells.count()))));
347 const int rows = int(std::ceil(m_cells.count() / qreal(columns)));
348
349 // Assign slots
350 const int slotWidth = area.width() / columns;
351 const int slotHeight = area.height() / rows;
352 QList<ExpoCell *> takenSlots;
353 takenSlots.resize(rows * columns);
354 takenSlots.fill(nullptr);
355
356 // precalculate all slot centers
357 QList<QPoint> slotCenters;
358 slotCenters.resize(rows * columns);
359 for (int x = 0; x < columns; ++x) {
360 for (int y = 0; y < rows; ++y) {
361 slotCenters[x + y * columns] = QPoint(area.x() + slotWidth * x + slotWidth / 2,
362 area.y() + slotHeight * y + slotHeight / 2);
363 }
364 }
365
366 // Assign each window to the closest available slot
367 QList<ExpoCell *> tmpList = m_cells; // use a QLinkedList copy instead?
368 while (!tmpList.isEmpty()) {
369 ExpoCell *cell = tmpList.first();
370 int slotCandidate = -1, slotCandidateDistance = INT_MAX;
371 const QPoint pos = cell->naturalRect().center();
372
373 for (int i = 0; i < columns * rows; ++i) { // all slots
374 const int dist = distance(pos, slotCenters[i]);
375 if (dist < slotCandidateDistance) { // window is interested in this slot
376 ExpoCell *occupier = takenSlots[i];
377 Q_ASSERT(occupier != cell);
378 if (!occupier || dist < distance(occupier->naturalRect().center(), slotCenters[i])) {
379 // either nobody lives here, or we're better - takeover the slot if it's our best
380 slotCandidate = i;
381 slotCandidateDistance = dist;
382 }
383 }
384 }
385 Q_ASSERT(slotCandidate != -1);
386 if (takenSlots[slotCandidate]) {
387 tmpList << takenSlots[slotCandidate]; // occupier needs a new home now :p
388 }
389 tmpList.removeAll(cell);
390 takenSlots[slotCandidate] = cell; // ...and we rumble in =)
391 }
392
393 for (int slot = 0; slot < columns * rows; ++slot) {
394 ExpoCell *cell = takenSlots[slot];
395 if (!cell) { // some slots might be empty
396 continue;
397 }
398
399 // Work out where the slot is
400 QRect target(area.x() + (slot % columns) * slotWidth,
401 area.y() + (slot / columns) * slotHeight,
402 slotWidth, slotHeight);
403 QRect adjustedTarget = target.adjusted(m_spacing, m_spacing, -m_spacing, -m_spacing);
404 if (adjustedTarget.isValid()) {
405 target = adjustedTarget; // Borders
406 }
407 target = target.marginsRemoved(cell->margins());
408
409 qreal scale;
410 if (target.width() / qreal(cell->naturalWidth()) < target.height() / qreal(cell->naturalHeight())) {
411 // Center vertically
412 scale = target.width() / qreal(cell->naturalWidth());
413 target.moveTop(target.top() + (target.height() - int(cell->naturalHeight() * scale)) / 2);
414 target.setHeight(int(cell->naturalHeight() * scale));
415 } else {
416 // Center horizontally
417 scale = target.height() / qreal(cell->naturalHeight());
418 target.moveLeft(target.left() + (target.width() - int(cell->naturalWidth() * scale)) / 2);
419 target.setWidth(int(cell->naturalWidth() * scale));
420 }
421 // Don't scale the windows too much
422 if (scale > 2.0 || (scale > 1.0 && (cell->naturalWidth() > 300 || cell->naturalHeight() > 300))) {
423 scale = (cell->naturalWidth() > 300 || cell->naturalHeight() > 300) ? 1.0 : 2.0;
424 target = QRect(
425 target.center().x() - int(cell->naturalWidth() * scale) / 2,
426 target.center().y() - int(cell->naturalHeight() * scale) / 2,
427 scale * cell->naturalWidth(), scale * cell->naturalHeight());
428 }
429
430 cell->setX(target.x());
431 cell->setY(target.y());
432 cell->setWidth(target.width());
433 cell->setHeight(target.height());
434 }
435}
436
437static inline int heightForWidth(ExpoCell *cell, int width)
438{
439 return int((width / qreal(cell->naturalWidth())) * cell->naturalHeight());
440}
441
442static bool isOverlappingAny(ExpoCell *w, const QHash<ExpoCell *, QRect> &targets, const QRegion &border, int spacing)
443{
444 QHash<ExpoCell *, QRect>::const_iterator winTarget = targets.find(w);
445 if (winTarget == targets.constEnd()) {
446 return false;
447 }
448 if (border.intersects(*winTarget)) {
449 return true;
450 }
451 const QMargins halfSpacing(spacing / 2, spacing / 2, spacing / 2, spacing / 2);
452
453 // Is there a better way to do this?
454 QHash<ExpoCell *, QRect>::const_iterator target;
455 for (target = targets.constBegin(); target != targets.constEnd(); ++target) {
456 if (target == winTarget) {
457 continue;
458 }
459 if (winTarget->marginsAdded(halfSpacing).intersects(target->marginsAdded(halfSpacing))) {
460 return true;
461 }
462 }
463 return false;
464}
465
466void ExpoLayout::calculateWindowTransformationsNatural()
467{
468 const QRect area = QRect(0, 0, width(), height());
469
470 // As we are using pseudo-random movement (See "slot") we need to make sure the list
471 // is always sorted the same way no matter which window is currently active.
472 std::sort(m_cells.begin(), m_cells.end(), [](const ExpoCell *a, const ExpoCell *b) {
473 return a->persistentKey() < b->persistentKey();
474 });
475
476 QRect bounds;
477 int direction = 0;
478 QHash<ExpoCell *, QRect> targets;
479 QHash<ExpoCell *, int> directions;
480
481 for (ExpoCell *cell : std::as_const(m_cells)) {
482 const QRect cellRect(cell->naturalX(), cell->naturalY(), cell->naturalWidth(), cell->naturalHeight());
483 targets[cell] = cellRect;
484 // Reuse the unused "slot" as a preferred direction attribute. This is used when the window
485 // is on the edge of the screen to try to use as much screen real estate as possible.
486 directions[cell] = direction;
487 bounds = bounds.united(cellRect);
488 direction++;
489 if (direction == 4) {
490 direction = 0;
491 }
492 }
493
494 // Iterate over all windows, if two overlap push them apart _slightly_ as we try to
495 // brute-force the most optimal positions over many iterations.
496 const int halfSpacing = m_spacing / 2;
497 bool overlap;
498 do {
499 overlap = false;
500 for (ExpoCell *cell : std::as_const(m_cells)) {
501 QRect *target_w = &targets[cell];
502 for (ExpoCell *e : std::as_const(m_cells)) {
503 if (cell == e) {
504 continue;
505 }
506
507 QRect *target_e = &targets[e];
508 if (target_w->adjusted(-halfSpacing, -halfSpacing, halfSpacing, halfSpacing)
509 .intersects(target_e->adjusted(-halfSpacing, -halfSpacing, halfSpacing, halfSpacing))) {
510 overlap = true;
511
512 // Determine pushing direction
513 QPoint diff(target_e->center() - target_w->center());
514 // Prevent dividing by zero and non-movement
515 if (diff.x() == 0 && diff.y() == 0) {
516 diff.setX(1);
517 }
518 // Try to keep screen aspect ratio
519 // if (bounds.height() / bounds.width() > area.height() / area.width())
520 // diff.setY(diff.y() / 2);
521 // else
522 // diff.setX(diff.x() / 2);
523 // Approximate a vector of between 10px and 20px in magnitude in the same direction
524 diff *= m_accuracy / qreal(diff.manhattanLength());
525 // Move both windows apart
526 target_w->translate(-diff);
527 target_e->translate(diff);
528
529 // Try to keep the bounding rect the same aspect as the screen so that more
530 // screen real estate is utilised. We do this by splitting the screen into nine
531 // equal sections, if the window center is in any of the corner sections pull the
532 // window towards the outer corner. If it is in any of the other edge sections
533 // alternate between each corner on that edge. We don't want to determine it
534 // randomly as it will not produce consistant locations when using the filter.
535 // Only move one window so we don't cause large amounts of unnecessary zooming
536 // in some situations. We need to do this even when expanding later just in case
537 // all windows are the same size.
538 // (We are using an old bounding rect for this, hopefully it doesn't matter)
539 int xSection = (target_w->x() - bounds.x()) / (bounds.width() / 3);
540 int ySection = (target_w->y() - bounds.y()) / (bounds.height() / 3);
541 diff = QPoint(0, 0);
542 if (xSection != 1 || ySection != 1) { // Remove this if you want the center to pull as well
543 if (xSection == 1) {
544 xSection = (directions[cell] / 2 ? 2 : 0);
545 }
546 if (ySection == 1) {
547 ySection = (directions[cell] % 2 ? 2 : 0);
548 }
549 }
550 if (xSection == 0 && ySection == 0) {
551 diff = QPoint(bounds.topLeft() - target_w->center());
552 }
553 if (xSection == 2 && ySection == 0) {
554 diff = QPoint(bounds.topRight() - target_w->center());
555 }
556 if (xSection == 2 && ySection == 2) {
557 diff = QPoint(bounds.bottomRight() - target_w->center());
558 }
559 if (xSection == 0 && ySection == 2) {
560 diff = QPoint(bounds.bottomLeft() - target_w->center());
561 }
562 if (diff.x() != 0 || diff.y() != 0) {
563 diff *= m_accuracy / qreal(diff.manhattanLength());
564 target_w->translate(diff);
565 }
566
567 // Update bounding rect
568 bounds = bounds.united(*target_w);
569 bounds = bounds.united(*target_e);
570 }
571 }
572 }
573 } while (overlap);
574
575 // Compute the scale factor so the bounding rect fits the target area.
576 qreal scale;
577 if (bounds.width() <= area.width() && bounds.height() <= area.height()) {
578 scale = 1.0;
579 } else if (area.width() / qreal(bounds.width()) < area.height() / qreal(bounds.height())) {
580 scale = area.width() / qreal(bounds.width());
581 } else {
582 scale = area.height() / qreal(bounds.height());
583 }
584 // Make bounding rect fill the screen size for later steps
585 bounds = QRect(bounds.x() - (area.width() / scale - bounds.width()) / 2,
586 bounds.y() - (area.height() / scale - bounds.height()) / 2,
587 area.width() / scale,
588 area.height() / scale);
589
590 // Move all windows back onto the screen and set their scale
591 QHash<ExpoCell *, QRect>::iterator target = targets.begin();
592 while (target != targets.end()) {
593 target->setRect((target->x() - bounds.x()) * scale + area.x(),
594 (target->y() - bounds.y()) * scale + area.y(),
595 target->width() * scale,
596 target->height() * scale);
597 ++target;
598 }
599
600 // Try to fill the gaps by enlarging windows if they have the space
601 if (m_fillGaps) {
602 // Don't expand onto or over the border
603 QRegion borderRegion(area.adjusted(-200, -200, 200, 200));
604 borderRegion ^= area;
605
606 bool moved;
607 do {
608 moved = false;
609 for (ExpoCell *cell : std::as_const(m_cells)) {
610 QRect oldRect;
611 QRect *target = &targets[cell];
612 // This may cause some slight distortion if the windows are enlarged a large amount
613 int widthDiff = m_accuracy;
614 int heightDiff = heightForWidth(cell, target->width() + widthDiff) - target->height();
615 int xDiff = widthDiff / 2; // Also move a bit in the direction of the enlarge, allows the
616 int yDiff = heightDiff / 2; // center windows to be enlarged if there is gaps on the side.
617
618 // heightDiff (and yDiff) will be re-computed after each successful enlargement attempt
619 // so that the error introduced in the window's aspect ratio is minimized
620
621 // Attempt enlarging to the top-right
622 oldRect = *target;
623 target->setRect(target->x() + xDiff,
624 target->y() - yDiff - heightDiff,
625 target->width() + widthDiff,
626 target->height() + heightDiff);
627 if (isOverlappingAny(cell, targets, borderRegion, m_spacing)) {
628 *target = oldRect;
629 } else {
630 moved = true;
631 heightDiff = heightForWidth(cell, target->width() + widthDiff) - target->height();
632 yDiff = heightDiff / 2;
633 }
634
635 // Attempt enlarging to the bottom-right
636 oldRect = *target;
637 target->setRect(target->x() + xDiff,
638 target->y() + yDiff,
639 target->width() + widthDiff,
640 target->height() + heightDiff);
641 if (isOverlappingAny(cell, targets, borderRegion, m_spacing)) {
642 *target = oldRect;
643 } else {
644 moved = true;
645 heightDiff = heightForWidth(cell, target->width() + widthDiff) - target->height();
646 yDiff = heightDiff / 2;
647 }
648
649 // Attempt enlarging to the bottom-left
650 oldRect = *target;
651 target->setRect(target->x() - xDiff - widthDiff,
652 target->y() + yDiff,
653 target->width() + widthDiff,
654 target->height() + heightDiff);
655 if (isOverlappingAny(cell, targets, borderRegion, m_spacing)) {
656 *target = oldRect;
657 } else {
658 moved = true;
659 heightDiff = heightForWidth(cell, target->width() + widthDiff) - target->height();
660 yDiff = heightDiff / 2;
661 }
662
663 // Attempt enlarging to the top-left
664 oldRect = *target;
665 target->setRect(target->x() - xDiff - widthDiff,
666 target->y() - yDiff - heightDiff,
667 target->width() + widthDiff,
668 target->height() + heightDiff);
669 if (isOverlappingAny(cell, targets, borderRegion, m_spacing)) {
670 *target = oldRect;
671 } else {
672 moved = true;
673 }
674 }
675 } while (moved);
676
677 // The expanding code above can actually enlarge windows over 1.0/2.0 scale, we don't like this
678 // We can't add this to the loop above as it would cause a never-ending loop so we have to make
679 // do with the less-than-optimal space usage with using this method.
680 for (ExpoCell *cell : std::as_const(m_cells)) {
681 QRect *target = &targets[cell];
682 qreal scale = target->width() / qreal(cell->naturalWidth());
683 if (scale > 2.0 || (scale > 1.0 && (cell->naturalWidth() > 300 || cell->naturalHeight() > 300))) {
684 scale = (cell->naturalWidth() > 300 || cell->naturalHeight() > 300) ? 1.0 : 2.0;
685 target->setRect(target->center().x() - int(cell->naturalWidth() * scale) / 2,
686 target->center().y() - int(cell->naturalHeight() * scale) / 2,
687 cell->naturalWidth() * scale,
688 cell->naturalHeight() * scale);
689 }
690 }
691 }
692
693 for (ExpoCell *cell : std::as_const(m_cells)) {
694 const QRect &cellRect = targets.value(cell);
695 QRect cellRectWithoutMargins = cellRect.marginsRemoved(cell->margins());
696 if (!cellRectWithoutMargins.isValid()) {
697 cellRectWithoutMargins = cellRect;
698 }
699 const QRect rect = centered(cell, cellRectWithoutMargins);
700
701 cell->setX(rect.x());
702 cell->setY(rect.y());
703 cell->setWidth(rect.width());
704 cell->setHeight(rect.height());
705 }
706}
707
708void ExpoLayout::resetTransformations()
709{
710 for (ExpoCell *cell : std::as_const(m_cells)) {
711 cell->setX(cell->naturalX());
712 cell->setY(cell->naturalY());
713 cell->setWidth(cell->naturalWidth());
714 cell->setHeight(cell->naturalHeight());
715 }
716}
717
718#include "moc_expolayout.cpp"
int bottomMargin
Definition expolayout.h:89
void bottomMarginChanged()
void naturalHeightChanged()
void persistentKeyChanged()
void naturalXChanged()
void update()
void setHeight(int height)
void enabledChanged()
void setX(int x)
ExpoCell(QObject *parent=nullptr)
int height
Definition expolayout.h:87
QString persistentKey
Definition expolayout.h:88
void yChanged()
int width
Definition expolayout.h:86
~ExpoCell() override
int naturalY
Definition expolayout.h:81
void naturalYChanged()
void naturalWidthChanged()
QMargins margins() const
void setNaturalY(int y)
void setNaturalHeight(int height)
ExpoLayout * layout
Definition expolayout.h:78
int naturalX
Definition expolayout.h:80
void setY(int y)
void setNaturalX(int x)
int naturalHeight
Definition expolayout.h:83
bool enabled
Definition expolayout.h:79
void widthChanged()
void setNaturalWidth(int width)
void setBottomMargin(int margin)
void heightChanged()
void setLayout(ExpoLayout *layout)
QRect naturalRect() const
void setEnabled(bool enabled)
void setPersistentKey(const QString &key)
void setWidth(int width)
int naturalWidth
Definition expolayout.h:82
void xChanged()
bool isEnabled() const
void layoutChanged()
Q_INVOKABLE void forceLayout()
ExpoLayout(QQuickItem *parent=nullptr)
void setFillGaps(bool fill)
bool isReady() const
void setMode(LayoutMode mode)
void removeCell(ExpoCell *cell)
void spacingChanged()
LayoutMode mode
Definition expolayout.h:20
void setReady()
void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override
void readyChanged()
void modeChanged()
void updatePolish() override
void fillGapsChanged()
bool fillGaps
Definition expolayout.h:21
void addCell(ExpoCell *cell)
void setSpacing(int spacing)
#define dist(a, b)
Definition xcursor.c:384