diff --git a/src/plugins/overview/kcm/overvieweffectkcm.ui b/src/plugins/overview/kcm/overvieweffectkcm.ui index 7d2e3aad93..5c8eec6ee7 100644 --- a/src/plugins/overview/kcm/overvieweffectkcm.ui +++ b/src/plugins/overview/kcm/overvieweffectkcm.ui @@ -17,69 +17,48 @@ - - - Layout mode: - - - - - - - - Closest - - - - - Natural - - - - - Ignore minimized windows: - + - + Organize windows in the Grid View: - + - + Search results include filtered windows: - + - + diff --git a/src/plugins/overview/overviewconfig.kcfg b/src/plugins/overview/overviewconfig.kcfg index 7e38e9d603..b7e7025273 100644 --- a/src/plugins/overview/overviewconfig.kcfg +++ b/src/plugins/overview/overviewconfig.kcfg @@ -10,9 +10,6 @@ http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" > - - 1 - false diff --git a/src/plugins/overview/overvieweffect.cpp b/src/plugins/overview/overvieweffect.cpp index aed191e8d7..9c1a0585d6 100644 --- a/src/plugins/overview/overvieweffect.cpp +++ b/src/plugins/overview/overvieweffect.cpp @@ -172,7 +172,6 @@ OverviewEffect::~OverviewEffect() void OverviewEffect::reconfigure(ReconfigureFlags) { OverviewConfig::self()->read(); - setLayout(OverviewConfig::layoutMode()); setAnimationDuration(animationTime(300)); setFilterWindows(OverviewConfig::filterWindows()); @@ -252,11 +251,6 @@ QPointF OverviewEffect::desktopOffset() const return m_desktopOffset; } -int OverviewEffect::layout() const -{ - return m_layout; -} - bool OverviewEffect::ignoreMinimized() const { return OverviewConfig::ignoreMinimized(); @@ -267,14 +261,6 @@ bool OverviewEffect::organizedGrid() const return OverviewConfig::organizedGrid(); } -void OverviewEffect::setLayout(int layout) -{ - if (m_layout != layout) { - m_layout = layout; - Q_EMIT layoutChanged(); - } -} - int OverviewEffect::requestedEffectChainPosition() const { return 70; diff --git a/src/plugins/overview/overvieweffect.h b/src/plugins/overview/overvieweffect.h index 522a551e1a..1f09405b88 100644 --- a/src/plugins/overview/overvieweffect.h +++ b/src/plugins/overview/overvieweffect.h @@ -18,7 +18,6 @@ class OverviewEffect : public QuickSceneEffect { Q_OBJECT Q_PROPERTY(int animationDuration READ animationDuration NOTIFY animationDurationChanged) - Q_PROPERTY(int layout READ layout NOTIFY layoutChanged) Q_PROPERTY(bool ignoreMinimized READ ignoreMinimized NOTIFY ignoreMinimizedChanged) Q_PROPERTY(bool filterWindows READ filterWindows NOTIFY filterWindowsChanged) Q_PROPERTY(bool organizedGrid READ organizedGrid NOTIFY organizedGridChanged) @@ -36,9 +35,6 @@ public: OverviewEffect(); ~OverviewEffect() override; - int layout() const; - void setLayout(int layout); - bool ignoreMinimized() const; bool organizedGrid() const; @@ -65,7 +61,6 @@ public: Q_SIGNALS: void animationDurationChanged(); - void layoutChanged(); void overviewPartialActivationFactorChanged(); void overviewGestureInProgressChanged(); void transitionPartialActivationFactorChanged(); @@ -102,7 +97,6 @@ private: QPointF m_desktopOffset; bool m_filterWindows = true; int m_animationDuration = 400; - int m_layout = 1; }; } // namespace KWin diff --git a/src/plugins/overview/qml/main.qml b/src/plugins/overview/qml/main.qml index bd46abdf59..cb364ec46a 100644 --- a/src/plugins/overview/qml/main.qml +++ b/src/plugins/overview/qml/main.qml @@ -602,7 +602,6 @@ FocusScope { Drag.hotSpot: Qt.point(width * 0.5, height * 0.5) Drag.keys: ["kwin-desktop"] - layout.mode: effect.layout focus: current padding: Kirigami.Units.largeSpacing animationDuration: effect.animationDuration diff --git a/src/plugins/private/expolayout.cpp b/src/plugins/private/expolayout.cpp index 550a067319..f5ba77f07a 100644 --- a/src/plugins/private/expolayout.cpp +++ b/src/plugins/private/expolayout.cpp @@ -1,9 +1,6 @@ /* SPDX-FileCopyrightText: 2021 Vlad Zahorodnii - - // The layouting code is taken from the present windows effect. - SPDX-FileCopyrightText: 2007 Rivo Laks - SPDX-FileCopyrightText: 2008 Lucas Murray + SPDX-FileCopyrightText: 2024 Yifan Zhu SPDX-License-Identifier: GPL-2.0-or-later */ @@ -11,6 +8,8 @@ #include "expolayout.h" #include +#include +#include ExpoCell::ExpoCell(QObject *parent) : QObject(parent) @@ -222,45 +221,17 @@ ExpoLayout::ExpoLayout(QQuickItem *parent) { } -ExpoLayout::LayoutMode ExpoLayout::mode() const +ExpoLayout::PlacementMode ExpoLayout::placementMode() const { - return m_mode; + return m_placementMode; } -void ExpoLayout::setMode(LayoutMode mode) +void ExpoLayout::setPlacementMode(PlacementMode mode) { - if (m_mode != mode) { - m_mode = mode; + if (m_placementMode != mode) { + m_placementMode = mode; polish(); - Q_EMIT modeChanged(); - } -} - -bool ExpoLayout::fillGaps() const -{ - return m_fillGaps; -} - -void ExpoLayout::setFillGaps(bool fill) -{ - if (m_fillGaps != fill) { - m_fillGaps = fill; - polish(); - Q_EMIT fillGapsChanged(); - } -} - -int ExpoLayout::spacing() const -{ - return m_spacing; -} - -void ExpoLayout::setSpacing(int spacing) -{ - if (m_spacing != spacing) { - m_spacing = spacing; - polish(); - Q_EMIT spacingChanged(); + Q_EMIT placementModeChanged(); } } @@ -282,25 +253,6 @@ void ExpoLayout::forceLayout() updatePolish(); } -void ExpoLayout::updatePolish() -{ - if (!m_cells.isEmpty()) { - switch (m_mode) { - case LayoutClosest: - calculateWindowTransformationsClosest(); - break; - case LayoutNatural: - calculateWindowTransformationsNatural(); - break; - case LayoutNone: - resetTransformations(); - break; - } - } - - setReady(); -} - void ExpoLayout::addCell(ExpoCell *cell) { Q_ASSERT(!m_cells.contains(cell)); @@ -322,397 +274,482 @@ void ExpoLayout::geometryChange(const QRectF &newGeometry, const QRectF &oldGeom QQuickItem::geometryChange(newGeometry, oldGeometry); } -static int distance(const QPoint &a, const QPoint &b) +// Move and scale rect to fit inside area +static void moveToFit(QRectF &rect, const QRectF &area) { - const int xdiff = a.x() - b.x(); - const int ydiff = a.y() - b.y(); - return int(std::sqrt(qreal(xdiff * xdiff + ydiff * ydiff))); + qreal scale = std::min(area.width() / rect.width(), area.height() / rect.height()); + rect.setWidth(rect.width() * scale); + rect.setHeight(rect.height() * scale); + rect.moveCenter(area.center()); } -static QRect centered(ExpoCell *cell, const QRect &bounds) +void ExpoLayout::updatePolish() { - const QSize scaled = QSize(cell->naturalWidth(), cell->naturalHeight()) - .scaled(bounds.size(), Qt::KeepAspectRatio); - - return QRect(bounds.center().x() - scaled.width() / 2, - bounds.center().y() - scaled.height() / 2, - scaled.width(), - scaled.height()); -} - -void ExpoLayout::calculateWindowTransformationsClosest() -{ - QRect area = QRect(0, 0, width(), height()); - const int columns = int(std::ceil(std::sqrt(qreal(m_cells.count())))); - const int rows = int(std::ceil(m_cells.count() / qreal(columns))); - - // Assign slots - const int slotWidth = area.width() / columns; - const int slotHeight = area.height() / rows; - QList takenSlots; - takenSlots.resize(rows * columns); - takenSlots.fill(nullptr); - - // precalculate all slot centers - QList slotCenters; - slotCenters.resize(rows * columns); - for (int x = 0; x < columns; ++x) { - for (int y = 0; y < rows; ++y) { - slotCenters[x + y * columns] = QPoint(area.x() + slotWidth * x + slotWidth / 2, - area.y() + slotHeight * y + slotHeight / 2); - } + if (m_cells.isEmpty()) { + setReady(); + return; } - // Assign each window to the closest available slot - QList tmpList = m_cells; // use a QLinkedList copy instead? - while (!tmpList.isEmpty()) { - ExpoCell *cell = tmpList.first(); - int slotCandidate = -1, slotCandidateDistance = INT_MAX; - const QPoint pos = cell->naturalRect().center(); + QRectF area = QRectF(0, 0, width(), height()); - for (int i = 0; i < columns * rows; ++i) { // all slots - const int dist = distance(pos, slotCenters[i]); - if (dist < slotCandidateDistance) { // window is interested in this slot - ExpoCell *occupier = takenSlots[i]; - Q_ASSERT(occupier != cell); - if (!occupier || dist < distance(occupier->naturalRect().center(), slotCenters[i])) { - // either nobody lives here, or we're better - takeover the slot if it's our best - slotCandidate = i; - slotCandidateDistance = dist; - } - } - } - Q_ASSERT(slotCandidate != -1); - if (takenSlots[slotCandidate]) { - tmpList << takenSlots[slotCandidate]; // occupier needs a new home now :p - } - tmpList.removeAll(cell); - takenSlots[slotCandidate] = cell; // ...and we rumble in =) + std::sort(m_cells.begin(), m_cells.end(), + [](const ExpoCell *a, const ExpoCell *b) { + return a->persistentKey() < b->persistentKey(); + }); + + // Estimate the scale factor we need to apply by simple heuristics + qreal totalArea = 0; + qreal availableArea = area.width() * area.height(); + for (ExpoCell *cell : std::as_const(m_cells)) { + totalArea += cell->naturalWidth() * cell->naturalHeight(); } + qreal scale = std::sqrt(availableArea / totalArea) * 0.7; // conservative estimate + scale = std::clamp(scale, 0.1, 10.0); // don't go crazy - for (int slot = 0; slot < columns * rows; ++slot) { - ExpoCell *cell = takenSlots[slot]; - if (!cell) { // some slots might be empty - continue; - } + QList windowSizes; + for (ExpoCell *cell : std::as_const(m_cells)) { + const QMargins &margins = cell->margins(); + const QMarginsF scaledMargins(margins.left() / scale, margins.top() / scale, margins.right() / scale, margins.bottom() / scale); + windowSizes.emplace_back(cell->naturalRect().toRectF().marginsAdded(scaledMargins)); + } + auto windowLayouts = ExpoLayout::layout(area, windowSizes); + for (int i = 0; i < windowLayouts.size(); ++i) { + ExpoCell *cell = m_cells[i]; + QRectF target = windowLayouts[i]; - // Work out where the slot is - QRect target(area.x() + (slot % columns) * slotWidth, - area.y() + (slot / columns) * slotHeight, - slotWidth, slotHeight); - QRect adjustedTarget = target.adjusted(m_spacing, m_spacing, -m_spacing, -m_spacing); + QRectF adjustedTarget = target.marginsRemoved(cell->margins()); if (adjustedTarget.isValid()) { target = adjustedTarget; // Borders } - target = target.marginsRemoved(cell->margins()); - - qreal scale; - if (target.width() / qreal(cell->naturalWidth()) < target.height() / qreal(cell->naturalHeight())) { - // Center vertically - scale = target.width() / qreal(cell->naturalWidth()); - target.moveTop(target.top() + (target.height() - int(cell->naturalHeight() * scale)) / 2); - target.setHeight(int(cell->naturalHeight() * scale)); - } else { - // Center horizontally - scale = target.height() / qreal(cell->naturalHeight()); - target.moveLeft(target.left() + (target.width() - int(cell->naturalWidth() * scale)) / 2); - target.setWidth(int(cell->naturalWidth() * scale)); - } - // Don't scale the windows too much - if (scale > 2.0 || (scale > 1.0 && (cell->naturalWidth() > 300 || cell->naturalHeight() > 300))) { - scale = (cell->naturalWidth() > 300 || cell->naturalHeight() > 300) ? 1.0 : 2.0; - target = QRect( - target.center().x() - int(cell->naturalWidth() * scale) / 2, - target.center().y() - int(cell->naturalHeight() * scale) / 2, - scale * cell->naturalWidth(), scale * cell->naturalHeight()); - } - - cell->setX(target.x()); - cell->setY(target.y()); - cell->setWidth(target.width()); - cell->setHeight(target.height()); - } -} - -static inline int heightForWidth(ExpoCell *cell, int width) -{ - return int((width / qreal(cell->naturalWidth())) * cell->naturalHeight()); -} - -static bool isOverlappingAny(ExpoCell *w, const QHash &targets, const QRegion &border, int spacing) -{ - QHash::const_iterator winTarget = targets.find(w); - if (winTarget == targets.constEnd()) { - return false; - } - if (border.intersects(*winTarget)) { - return true; - } - const QMargins halfSpacing(spacing / 2, spacing / 2, spacing / 2, spacing / 2); - - // Is there a better way to do this? - QHash::const_iterator target; - for (target = targets.constBegin(); target != targets.constEnd(); ++target) { - if (target == winTarget) { - continue; - } - if (winTarget->marginsAdded(halfSpacing).intersects(target->marginsAdded(halfSpacing))) { - return true; - } - } - return false; -} - -void ExpoLayout::calculateWindowTransformationsNatural() -{ - const QRect area = QRect(0, 0, width(), height()); - - // As we are using pseudo-random movement (See "slot") we need to make sure the list - // is always sorted the same way no matter which window is currently active. - std::sort(m_cells.begin(), m_cells.end(), [](const ExpoCell *a, const ExpoCell *b) { - return a->persistentKey() < b->persistentKey(); - }); - - QRect bounds; - int direction = 0; - QHash targets; - QHash directions; - - for (ExpoCell *cell : std::as_const(m_cells)) { - const QRect cellRect(cell->naturalX(), cell->naturalY(), cell->naturalWidth(), cell->naturalHeight()); - targets[cell] = cellRect; - // Reuse the unused "slot" as a preferred direction attribute. This is used when the window - // is on the edge of the screen to try to use as much screen real estate as possible. - directions[cell] = direction; - bounds = bounds.united(cellRect); - direction++; - if (direction == 4) { - direction = 0; - } - } - - // Iterate over all windows, if two overlap push them apart _slightly_ as we try to - // brute-force the most optimal positions over many iterations. - const int halfSpacing = m_spacing / 2; - bool overlap; - do { - overlap = false; - for (ExpoCell *cell : std::as_const(m_cells)) { - QRect *target_w = &targets[cell]; - for (ExpoCell *e : std::as_const(m_cells)) { - if (cell == e) { - continue; - } - - QRect *target_e = &targets[e]; - if (target_w->adjusted(-halfSpacing, -halfSpacing, halfSpacing, halfSpacing) - .intersects(target_e->adjusted(-halfSpacing, -halfSpacing, halfSpacing, halfSpacing))) { - overlap = true; - - // Determine pushing direction - QPoint diff(target_e->center() - target_w->center()); - // Prevent dividing by zero and non-movement - if (diff.x() == 0 && diff.y() == 0) { - diff.setX(1); - } - // Try to keep screen aspect ratio - // if (bounds.height() / bounds.width() > area.height() / area.width()) - // diff.setY(diff.y() / 2); - // else - // diff.setX(diff.x() / 2); - // Approximate a vector of between 10px and 20px in magnitude in the same direction - diff *= m_accuracy / qreal(diff.manhattanLength()); - // Move both windows apart - target_w->translate(-diff); - target_e->translate(diff); - - // Try to keep the bounding rect the same aspect as the screen so that more - // screen real estate is utilised. We do this by splitting the screen into nine - // equal sections, if the window center is in any of the corner sections pull the - // window towards the outer corner. If it is in any of the other edge sections - // alternate between each corner on that edge. We don't want to determine it - // randomly as it will not produce consistant locations when using the filter. - // Only move one window so we don't cause large amounts of unnecessary zooming - // in some situations. We need to do this even when expanding later just in case - // all windows are the same size. - // (We are using an old bounding rect for this, hopefully it doesn't matter) - int xSection = (target_w->x() - bounds.x()) / (bounds.width() / 3); - int ySection = (target_w->y() - bounds.y()) / (bounds.height() / 3); - diff = QPoint(0, 0); - if (xSection != 1 || ySection != 1) { // Remove this if you want the center to pull as well - if (xSection == 1) { - xSection = (directions[cell] / 2 ? 2 : 0); - } - if (ySection == 1) { - ySection = (directions[cell] % 2 ? 2 : 0); - } - } - if (xSection == 0 && ySection == 0) { - diff = QPoint(bounds.topLeft() - target_w->center()); - } - if (xSection == 2 && ySection == 0) { - diff = QPoint(bounds.topRight() - target_w->center()); - } - if (xSection == 2 && ySection == 2) { - diff = QPoint(bounds.bottomRight() - target_w->center()); - } - if (xSection == 0 && ySection == 2) { - diff = QPoint(bounds.bottomLeft() - target_w->center()); - } - if (diff.x() != 0 || diff.y() != 0) { - diff *= m_accuracy / qreal(diff.manhattanLength()); - target_w->translate(diff); - } - - // Update bounding rect - bounds = bounds.united(*target_w); - bounds = bounds.united(*target_e); - } - } - } - } while (overlap); - - // Compute the scale factor so the bounding rect fits the target area. - qreal scale; - if (bounds.width() <= area.width() && bounds.height() <= area.height()) { - scale = 1.0; - } else if (area.width() / qreal(bounds.width()) < area.height() / qreal(bounds.height())) { - scale = area.width() / qreal(bounds.width()); - } else { - scale = area.height() / qreal(bounds.height()); - } - // Make bounding rect fill the screen size for later steps - bounds = QRect(bounds.x() - (area.width() / scale - bounds.width()) / 2, - bounds.y() - (area.height() / scale - bounds.height()) / 2, - area.width() / scale, - area.height() / scale); - - // Move all windows back onto the screen and set their scale - QHash::iterator target = targets.begin(); - while (target != targets.end()) { - target->setRect((target->x() - bounds.x()) * scale + area.x(), - (target->y() - bounds.y()) * scale + area.y(), - target->width() * scale, - target->height() * scale); - ++target; - } - - // Try to fill the gaps by enlarging windows if they have the space - if (m_fillGaps) { - // Don't expand onto or over the border - QRegion borderRegion(area.adjusted(-200, -200, 200, 200)); - borderRegion ^= area; - - bool moved; - do { - moved = false; - for (ExpoCell *cell : std::as_const(m_cells)) { - QRect oldRect; - QRect *target = &targets[cell]; - // This may cause some slight distortion if the windows are enlarged a large amount - int widthDiff = m_accuracy; - int heightDiff = heightForWidth(cell, target->width() + widthDiff) - target->height(); - int xDiff = widthDiff / 2; // Also move a bit in the direction of the enlarge, allows the - int yDiff = heightDiff / 2; // center windows to be enlarged if there is gaps on the side. - - // heightDiff (and yDiff) will be re-computed after each successful enlargement attempt - // so that the error introduced in the window's aspect ratio is minimized - - // Attempt enlarging to the top-right - oldRect = *target; - target->setRect(target->x() + xDiff, - target->y() - yDiff - heightDiff, - target->width() + widthDiff, - target->height() + heightDiff); - if (isOverlappingAny(cell, targets, borderRegion, m_spacing)) { - *target = oldRect; - } else { - moved = true; - heightDiff = heightForWidth(cell, target->width() + widthDiff) - target->height(); - yDiff = heightDiff / 2; - } - - // Attempt enlarging to the bottom-right - oldRect = *target; - target->setRect(target->x() + xDiff, - target->y() + yDiff, - target->width() + widthDiff, - target->height() + heightDiff); - if (isOverlappingAny(cell, targets, borderRegion, m_spacing)) { - *target = oldRect; - } else { - moved = true; - heightDiff = heightForWidth(cell, target->width() + widthDiff) - target->height(); - yDiff = heightDiff / 2; - } - - // Attempt enlarging to the bottom-left - oldRect = *target; - target->setRect(target->x() - xDiff - widthDiff, - target->y() + yDiff, - target->width() + widthDiff, - target->height() + heightDiff); - if (isOverlappingAny(cell, targets, borderRegion, m_spacing)) { - *target = oldRect; - } else { - moved = true; - heightDiff = heightForWidth(cell, target->width() + widthDiff) - target->height(); - yDiff = heightDiff / 2; - } - - // Attempt enlarging to the top-left - oldRect = *target; - target->setRect(target->x() - xDiff - widthDiff, - target->y() - yDiff - heightDiff, - target->width() + widthDiff, - target->height() + heightDiff); - if (isOverlappingAny(cell, targets, borderRegion, m_spacing)) { - *target = oldRect; - } else { - moved = true; - } - } - } while (moved); - - // The expanding code above can actually enlarge windows over 1.0/2.0 scale, we don't like this - // We can't add this to the loop above as it would cause a never-ending loop so we have to make - // do with the less-than-optimal space usage with using this method. - for (ExpoCell *cell : std::as_const(m_cells)) { - QRect *target = &targets[cell]; - qreal scale = target->width() / qreal(cell->naturalWidth()); - if (scale > 2.0 || (scale > 1.0 && (cell->naturalWidth() > 300 || cell->naturalHeight() > 300))) { - scale = (cell->naturalWidth() > 300 || cell->naturalHeight() > 300) ? 1.0 : 2.0; - target->setRect(target->center().x() - int(cell->naturalWidth() * scale) / 2, - target->center().y() - int(cell->naturalHeight() * scale) / 2, - cell->naturalWidth() * scale, - cell->naturalHeight() * scale); - } - } - } - - for (ExpoCell *cell : std::as_const(m_cells)) { - const QRect &cellRect = targets.value(cell); - QRect cellRectWithoutMargins = cellRect.marginsRemoved(cell->margins()); - if (!cellRectWithoutMargins.isValid()) { - cellRectWithoutMargins = cellRect; - } - const QRect rect = centered(cell, cellRectWithoutMargins); + QRectF rect = cell->naturalRect(); + moveToFit(rect, target); cell->setX(rect.x()); cell->setY(rect.y()); cell->setWidth(rect.width()); cell->setHeight(rect.height()); } + setReady(); } -void ExpoLayout::resetTransformations() +Layer::Layer(qreal maxWidth, const QList &windowSizes, const QList &windowIds, size_t startPos, size_t endPos) + : maxWidth(maxWidth) + , maxHeight(windowSizes[windowIds[endPos - 1]].height()) + , ids(windowIds.begin() + startPos, windowIds.begin() + endPos) { - for (ExpoCell *cell : std::as_const(m_cells)) { - cell->setX(cell->naturalX()); - cell->setY(cell->naturalY()); - cell->setWidth(cell->naturalWidth()); - cell->setHeight(cell->naturalHeight()); + remainingWidth = maxWidth; + for (auto id = ids.begin(); id != ids.end(); ++id) { + remainingWidth -= windowSizes[*id].width(); } } +qreal Layer::width() const +{ + return maxWidth - remainingWidth; +} + +LayeredPacking::LayeredPacking(qreal maxWidth, const QList &windowSizes, const QList &ids, const QList &layerStartPos) + : maxWidth(maxWidth) + , width(0) + , height(0) +{ + for (int i = 1; i < layerStartPos.size(); ++i) { + layers.emplace_back(maxWidth, windowSizes, ids, layerStartPos[i - 1], layerStartPos[i]); + width = std::max(width, layers.back().width()); + height += layers.back().maxHeight; + } +} + +/** + * @brief Check if @param candidate can be ignored in the future because either @param alternativeSmall or @param alternativeBig is at least as good as @param candidate for layerStart. + * + * More formally, returns false if and only if there exists a k with @param alternativeBig < k <= @param length + * such that leastWeightCandidate( @param candidate, k ) < leastWeightCandidate( @param alternativeSmall, k ) and leastWeightCandidate( @param candidate, k ) < leastWeightCandidate( + * @param alternativeBig, k ). + * + * The input must satisfy @param alternativeSmall < @param candidate < @param alternativeBig + * + * The run time of the algorithm is O(log length). + * + * The Bridge algorithm from Hirschberg, Daniel S., and Lawrence L. + * Larmore. "The least weight subsequence problem." SIAM Journal on + * Computing 16.4 (1987): 628-638 + * + * @param length The length of the sequence. + * @param leastWeightCandidate leastWeightCandidate(i, j) is the weight of arranging the first j windows, + * if we use the optimal arrangement of the first i windows, and the last layest consists of windows [i, j) + */ +static bool isDominated(size_t candidate, size_t alternativeSmall, size_t alternativeBig, size_t length, std::function leastWeightCandidate) +{ + Q_ASSERT(alternativeSmall < candidate && candidate < alternativeBig); + if (alternativeBig == length) { + return true; + } + + // We assumed that the weigth function is concave, i.e., for all i <= j < k <= l, + // weight(i,k) + weight(j,l) <= weight(i,l) + weight(j,k) + // This implies the following about leastWeightCandidate: + // For all i <= j < k <= l + // - If leastWeightCandidate(i, l) <= leastWeightCandidate(j, l), then leastWeightCandidate(i, k) <= leastWeightCandidate(j, k) + // - If leastWeightCandidate(j, k) <= leastWeightCandidate(i, k), then leastWeightCandidate(j, l) <= leastWeightCandidate(i, l) + // + // In particular, this implies that the set of ks such that + // leastWeightCandidate(candidate, k) < leastWeightCandidate(alternativeSmall, k) + // is a (possibly empty) interval [k1, length] for some k1. + // This is because if for some k, + // leastWeightCandidate(alternativeSmall, k) <= leastWeightCandidate(candidate, k), + // then for all candidate < k' <= k, + // leastWeightCandidate(alternativeSmall, k') <= leastWeightCandidate(candidate, k') + // + // Similarly, the set of ks such that + // leastWeightCandidate(candidate, k) < leastWeightCandidate(alternativeBig, k) + // is a (possibly empty) interval [alternativeBig + 1, k2] for some k2. + // This is because if for some k, + // leastWeightCandidate(alternativeBig, k) <= leastWeightCandidate(candidate, k), + // then for all k' >= k + // leastWeightCandidate(alternativeBig, k') <= leastWeightCandidate(candidate, k') + // + // Hence, to check if a k exists in both intervals, we can use binary search to find the smallest k1 such that + // leastWeightCandidate(candidate, k1) < leastWeightCandidate(alternativeSmall, k1). + // If such a k1 exists, it suffices to check if + // leastWeightCandidate(alternativeBig, k1) <= leastWeightCandidate(candidate, k1). + if (leastWeightCandidate(alternativeSmall, length) <= leastWeightCandidate(candidate, length)) { + return true; + } + // Now we know that leastWeightCandidate(candidate, length) < leastWeightCandidate(alternativeSmall, length) + // i.e, the first interval is non-empty + // Our candidate k1 is in the interval (low, high] (inclusive on high) + size_t low = alternativeBig; + size_t high = length; + while (high - low >= 2) { + size_t mid = (low + high) / 2; + if (leastWeightCandidate(alternativeSmall, mid) <= leastWeightCandidate(candidate, mid)) { + low = mid; + } else { + high = mid; + } + } + return (leastWeightCandidate(alternativeBig, high) <= leastWeightCandidate(candidate, high)); +} + +/** + * @brief Returns the layerStartPos for a good packing of the windows using the Basic algorithm + * from Hirschberg, Daniel S., and Lawrence L. Larmore. "The least weight subsequence problem." + * SIAM Journal on Computing 16.4 (1987): 628-638. + * + * The Basic algorithm solves the Least Weight Subsequence Problem (LWS) for + * concave weight functions. + * + * The LWS problem on the interval [a,b] is defined as follows: + * Given a weight function weight(i,j) for all i,j in [a,b], find a subsequence + * of [a,b], i.e. a sequence of strictly monotonically increasing indices + * i_0 < i_2 < ... < i_t, such that the total weight, + * sum_{k=1}^t weight(i_{k-1}, i_k), is minimized. + * + * A weight function is concave if for all i <= j < k <= l, the following holds: + * weight(i,k) + weight(j,l) <= weight(i,l) + weight(j,k) + * + * The run time of the algorithm is O(n log n). + * + * Modified from the version in the paper to fix some bugs. + * + * @param idealWidth The target width of each layer. All widths of windows *MUST* be smaller than idealWidth. + * @param length The length of the sequence. Solves the LWS problem on the interval [0, length]. (n in paper) + * @param cumWidths cumWidths[i] is the sum of widths of windows 0, 1, ..., i - 1 + * + * @return QList The subsequence (starting at 0 and ending at length) + * that minimizes the total weight. The ith element is the index of the first window in layer i. + * Always starts with 0 and ends with ids.size(). + */ +static QList getLayerStartPos(qreal maxWidth, qreal idealWidth, const size_t length, const QList &cumWidths) +{ + // weight(start, end) is the penalty of placing all windows in the range [start, end) in the same layer. + // The following form only works when the maximum width of a window is less than or equal to idealWidth. + // + // The weight function is designed such that + // 1. The weight function is concave (see definition in Basic algorithm) + // 2. It scales like (width - idealWidth) ^ 2 for width < idealWidth + // 3. Exceeding maxWidth is guaranteed to be worse than any other solution + // + // 1. holds as long as weight(i, j) = f(cumWidths[j] - cumWidths[i]) for some convex function f + // 3. is guaranteed by making the penalty of exceeding maxWidth at least + // cumWidths.size(), which strictly upper bounds the total weight of placing + // each window in its own layer + // + auto weight = [maxWidth, idealWidth, &cumWidths](size_t start, size_t end) { + qreal width = cumWidths[end] - cumWidths[start]; + if (width < idealWidth) { + return (width - idealWidth) * (width - idealWidth) / idealWidth / idealWidth; + } else { + qreal penaltyFactor = cumWidths.size(); + return penaltyFactor * (width - idealWidth) * (width - idealWidth) / (maxWidth - idealWidth) / (maxWidth - idealWidth); + } + }; + + // layerStart[j] is where the last layer should start, if there were only the first j windows. + // I.e., layerStart[5]=3 means that the last layer should start at window 3 if there were only the first 10 windows + // (bestLeft in paper) + QList layerStart(length + 1); + // leastWeight[i] is the least weight of any subsequence starting at 0 and ending at i (f in paper) + QList leastWeight(length + 1); + // layerStartcandidates contains all current candidates for layerStart[currentIndex] (d in paper) + std::deque layerStartCandidates; + + leastWeight[0] = 0; + + // leastWeightCandidate(lastRowStartPos, num) is a candidate value for leastWeight[num]. + // It is the weight for arranging the first num windows, assuming optimal arrangement of + // the first lastRowStartPos windows, and a last layer consisting of windows [lastRowStartPos, num) + // (g in paper) + auto leastWeightCandidate = [&leastWeight, &weight](size_t lastRowStartPos, size_t num) { + return leastWeight[lastRowStartPos] + weight(lastRowStartPos, num); + }; + layerStartCandidates.push_back(0); + for (size_t currentIndex = 1; currentIndex < length; ++currentIndex) { // currentIndex is m in paper + leastWeight[currentIndex] = leastWeightCandidate(layerStartCandidates.front(), currentIndex); + layerStart[currentIndex] = layerStartCandidates.front(); + + // Modification of algorithm in paper; + // needed so that layerStartCandidates.front can be correctly removed when layerStartCandidates.size() == 1 + layerStartCandidates.push_back(currentIndex); + + // Remove candidates from the front if they are dominated by the second candidate + // Dominate means that the second candidate is at least as good as the first candidate for layerStart + while (layerStartCandidates.size() >= 2 && leastWeightCandidate(layerStartCandidates[1], currentIndex + 1) <= leastWeightCandidate(layerStartCandidates[0], currentIndex + 1)) { + layerStartCandidates.pop_front(); + } + layerStartCandidates.pop_back(); // Modification of algorithm in paper + + // Remove candidates from the back if they are dominated by either the second to last candidate, or currentIndex + while (layerStartCandidates.size() >= 2 && isDominated(layerStartCandidates.back(), layerStartCandidates[layerStartCandidates.size() - 2], currentIndex, length, leastWeightCandidate)) { + layerStartCandidates.pop_back(); + } + + // Modification of algorithm in paper; we need at least one candidate in layerStartCandidates + if (layerStartCandidates.empty()) { + layerStartCandidates.push_back(currentIndex); + continue; + } + + // Add currentIndex to layerStartCandidates if it is not dominated by the last candidate + if (leastWeightCandidate(currentIndex, length) < leastWeightCandidate(layerStartCandidates.back(), length)) { + layerStartCandidates.push_back(currentIndex); + } + } + + // recover the solution using layerStart + leastWeight[length] = leastWeightCandidate(layerStartCandidates.front(), length); + layerStart[length] = layerStartCandidates.front(); + + QList layerStartPosReversed; + layerStartPosReversed.push_back(length); + size_t currentIndex = length; + + while (currentIndex > 0) { + currentIndex = layerStart[currentIndex]; + layerStartPosReversed.push_back(currentIndex); + } + + return QList(layerStartPosReversed.rbegin(), layerStartPosReversed.rend()); +} + +// Reflection about the line y = x +static QMarginsF reflect(const QMarginsF &margins) +{ + return QMarginsF(margins.top(), margins.right(), margins.bottom(), margins.left()); +} +static QRectF reflect(const QRectF &rect) +{ + return QRectF(rect.y(), rect.x(), rect.height(), rect.width()); +} +static QPointF reflect(const QPointF &point) +{ + return point.transposed(); +} +template +static QList reflect(const QList &v) +{ + QList result; + result.reserve(v.size()); + for (const auto &x : v) { + result.emplace_back(reflect(x)); + } + return result; +} + +QList ExpoLayout::layout(const QRectF &area, const QList &windowSizes) +{ + const qreal shortSide = std::min(area.width(), area.height()); + const QMarginsF margins(shortSide * m_relativeMarginLeft, + shortSide * m_relativeMarginTop, + shortSide * m_relativeMarginRight, + shortSide * m_relativeMarginBottom); + const qreal minLength = m_relativeMinLength * shortSide; + const QRectF minSize = QRectF(0, 0, minLength, minLength); + + QList centers; + for (const QRectF &windowSize : windowSizes) { + centers.push_back(windowSize.center()); + } + + // windows bigger than 4x the area are considered ill-behaved and their sizes are clipped + const auto adjustedSizes = adjustSizes(minSize, QRectF(0, 0, 4 * area.width(), 4 * area.height()), margins, windowSizes); + + if (placementMode() == PlacementMode::Rows) { + LayeredPacking bestPacking = findGoodPacking(area, adjustedSizes, centers, m_idealWidthRatio, m_searchTolerance); + return refineAndApplyPacking(area, margins, bestPacking, adjustedSizes, centers); + } else { + QList adjustedSizesReflected(reflect(adjustedSizes)); + QList centersReflected(reflect(centers)); + + LayeredPacking bestPacking = findGoodPacking(area.transposed(), adjustedSizesReflected, centersReflected, m_idealWidthRatio, m_searchTolerance); + return reflect(refineAndApplyPacking(area.transposed(), reflect(margins), bestPacking, adjustedSizesReflected, centersReflected)); + } +} + +QList ExpoLayout::adjustSizes(const QRectF &minSize, const QRectF &maxSize, const QMarginsF &margins, const QList &windowSizes) +{ + QList adjustedSizes; + for (QRectF windowSize : windowSizes) { + windowSize.setWidth(std::clamp(windowSize.width(), minSize.width(), maxSize.width())); + windowSize.setHeight(std::clamp(windowSize.height(), minSize.height(), maxSize.height())); + windowSize += margins; + adjustedSizes.emplace_back(windowSize); + } + return adjustedSizes; +} + +LayeredPacking +ExpoLayout::findGoodPacking(const QRectF &area, const QList &windowSizes, const QList ¢ers, qreal idealWidthRatio, qreal tol) +{ + QList> windowSizesWithIds; + + for (int i = 0; i < windowSizes.size(); ++i) { + windowSizesWithIds.emplace_back(i, windowSizes[i], centers[i]); + } + + // Sorting by height ensures that windows in same layer (row) have similar heights + std::stable_sort(windowSizesWithIds.begin(), windowSizesWithIds.end(), [](const auto &a, const auto &b) { + // in case of same height, sort by y to minimize vertical movement + return std::tuple(std::get<1>(a).height(), std::get<2>(a).y()) + < std::tuple(std::get<1>(b).height(), std::get<2>(b).y()); + }); + + QList ids; // ids of windows in sorted order + QList cumWidths; // cumWidths[i] is the sum of widths of windows 0, 1, ..., i - 1 + + // Minimum and maximum strip widths to use in the binary search. + // Strips should be at least as wide as the widest window, and at most as + // wide as the sum of all window widths. + qreal stripWidthMin = 0; + qreal stripWidthMax = 0; + + cumWidths.push_back(0); + for (const auto &windowSizeWithId : windowSizesWithIds) { + ids.push_back(std::get<0>(windowSizeWithId)); + qreal width = std::get<1>(windowSizeWithId).width(); + cumWidths.push_back(cumWidths.back() + width); + + stripWidthMin = std::max(stripWidthMin, width); + stripWidthMax += width; + } + stripWidthMin /= idealWidthRatio; + stripWidthMax /= idealWidthRatio; + + qreal targetRatio = area.height() / area.width(); + + auto findPacking = [&windowSizes, &ids, &cumWidths, idealWidthRatio](qreal stripWidth) { + QList layerStartPos = getLayerStartPos(stripWidth, stripWidth * idealWidthRatio, ids.size(), cumWidths); + LayeredPacking result(stripWidth, windowSizes, ids, layerStartPos); + Q_ASSERT(result.width <= stripWidth); + return result; + }; + + // the placement with the minimum strip width corresponds with a big aspect + // ratio (ratioHigh), and the placement with the maximum strip width + // corresponds with a small aspect ratio (ratioLow) + + LayeredPacking placementWidthMin = findPacking(stripWidthMin); + qreal ratioHigh = placementWidthMin.height / placementWidthMin.width; + + if (ratioHigh <= targetRatio) { + return placementWidthMin; + } + + LayeredPacking placementWidthMax = findPacking(stripWidthMax); + qreal ratioLow = placementWidthMax.height / placementWidthMax.width; + + if (ratioLow >= targetRatio) { + return placementWidthMax; + } + + while (stripWidthMax / stripWidthMin > 1 + tol) { + qreal stripWidthMid = std::sqrt(stripWidthMin * stripWidthMax); + LayeredPacking placementMid = findPacking(stripWidthMid); + qreal ratioMid = placementMid.height / placementMid.width; + + if (ratioMid > targetRatio) { + stripWidthMin = stripWidthMid; + placementWidthMin = placementMid; + ratioHigh = ratioMid; + } else { + // small optimization: use the actual strip width + stripWidthMax = placementMid.width; + placementWidthMax = placementMid; + ratioLow = ratioMid; + } + } + + // how much we need to scale the placement to fit + qreal scaleWidthMin = std::min(area.width() / placementWidthMin.width, area.height() / placementWidthMin.height); + qreal scaleWidthMax = std::min(area.width() / placementWidthMax.width, area.height() / placementWidthMax.height); + + if (scaleWidthMin > scaleWidthMax) { + return placementWidthMin; + } else { + return placementWidthMax; + } +} + +QList ExpoLayout::refineAndApplyPacking(const QRectF &area, const QMarginsF &margins, const LayeredPacking &packing, const QList &windowSizes, const QList ¢ers) +{ + // Scale packing to fit area + qreal scale = std::min(area.width() / packing.width, area.height() / packing.height); + scale = std::min(scale, m_maxScale); + + const QMarginsF scaledMargins = QMarginsF(margins.left() * scale, margins.top() * scale, + margins.right() * scale, margins.bottom() * scale); + + // The maximum gap in additional to margins to leave between windows + qreal maxGapY = m_maxGapRatio * (scaledMargins.top() + scaledMargins.bottom()); + qreal maxGapX = m_maxGapRatio * (scaledMargins.left() + scaledMargins.right()); + + // center align y + qreal extraY = area.height() - packing.height * scale; + qreal gapY = std::min(maxGapY, extraY / (packing.layers.size() + 1)); + qreal y = area.y() + (extraY - gapY * (packing.layers.size() - 1)) / 2; + + QList finalWindowLayouts(windowSizes); + // smaller windows "float" to the top + for (const auto &layer : packing.layers) { + qreal extraX = area.width() - layer.width() * scale; + qreal gapX = std::min(maxGapX, extraX / (layer.ids.size() + 1)); + qreal x = area.x() + (extraX - gapX * (layer.ids.size() - 1)) / 2; + + QList ids(layer.ids); + std::stable_sort(ids.begin(), ids.end(), [¢ers](size_t a, size_t b) { + return centers[a].x() < centers[b].x(); // minimize horizontal movement + }); + for (auto id : std::as_const(ids)) { + QRectF &windowLayout = finalWindowLayouts[id]; + qreal newY = y + (layer.maxHeight - windowLayout.height()) * scale / 2; // center align y + windowLayout = QRectF(x, newY, windowLayout.width() * scale, windowLayout.height() * scale); + x += windowLayout.width() + gapX; + windowLayout -= scaledMargins; + } + y += layer.maxHeight * scale + gapY; + } + return finalWindowLayouts; +} + #include "moc_expolayout.cpp" diff --git a/src/plugins/private/expolayout.h b/src/plugins/private/expolayout.h index 895aff7099..af2c924cc4 100644 --- a/src/plugins/private/expolayout.h +++ b/src/plugins/private/expolayout.h @@ -1,11 +1,13 @@ /* SPDX-FileCopyrightText: 2021 Vlad Zahorodnii + SPDX-FileCopyrightText: 2024 Yifan Zhu SPDX-License-Identifier: GPL-2.0-or-later */ #pragma once +#include #include #include #include @@ -13,33 +15,88 @@ #include class ExpoCell; +struct Layer; +struct LayeredPacking; +/** + * @brief Adapts the algorithm from [0] to layout the windows intelligently. + * + * Design goals: + * - use screen space efficiently, given diverse geometries of windows + * - be aesthetically pleasing + * - and minimize movement of windows from initial positions + * + * More concretely, the algorithm produces a layered layout, where each layer, + * or strip, is a row or column. The algorithm tries to ensure that different + * strips have similar widths, and uses binary search to find a packing with + * similar aspect ratio to the layout area. Within each strip, the algorithm + * tries to minimize horizontal movement (for rows) or vertical movement (for + * columns) of the windows. + * + * [0] Hirschberg, Daniel S., and Lawrence L. Larmore. "The least weight + * subsequence problem." SIAM Journal on Computing 16.4 (1987): 628-638. + */ class ExpoLayout : public QQuickItem { Q_OBJECT - Q_PROPERTY(LayoutMode mode READ mode WRITE setMode NOTIFY modeChanged) - Q_PROPERTY(bool fillGaps READ fillGaps WRITE setFillGaps NOTIFY fillGapsChanged) - Q_PROPERTY(int spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + // Place windows in rows or columns. + Q_PROPERTY(PlacementMode placementMode READ placementMode WRITE setPlacementMode NOTIFY placementModeChanged) Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged) + /** + * Stop binary search when the two candidate strip widths are within tol (as a fraction of the larger strip width). + * Default is 0.2. + */ + Q_PROPERTY(qreal searchTolerance MEMBER m_searchTolerance NOTIFY searchToleranceChanged) + /** + * The ideal sum of window widths in a strip (including added margins), as a fraction of the strip width. *MUST* be strictly less than 1. + * Default is 0.8. + */ + Q_PROPERTY(qreal idealWidthRatio MEMBER m_idealWidthRatio NOTIFY idealWidthRatioChanged) + /** + * Left margin size, as a ratio of the short side of layout area. Default is 0.07. + * The margins are added to each window before layout. + */ + Q_PROPERTY(qreal relativeMarginLeft MEMBER m_relativeMarginLeft NOTIFY relativeMarginLeftChanged) + /** + * Right margin size, as a ratio of the short side of layout area. Default is 0.07. + * The margins are added to each window before layout. + */ + Q_PROPERTY(qreal relativeMarginRight MEMBER m_relativeMarginRight NOTIFY relativeMarginRightChanged) + /** + * Top margin size, as a ratio of the short side of layout area. Default is 0.07. + * The margins are added to each window before layout. + */ + Q_PROPERTY(qreal relativeMarginTop MEMBER m_relativeMarginTop NOTIFY relativeMarginTopChanged) + /** + * Bottom margin size, as a ratio of the short side of layout area. Default is 0.07. + * The margins are added to each window before layout. + */ + Q_PROPERTY(qreal relativeMarginBottom MEMBER m_relativeMarginBottom NOTIFY relativeMarginBottomChanged) + /** + * Minimal length of windows, as a ratio of the short side of layout area. + * Smaller windows will be resized to this. Default is 0.15. + */ + Q_PROPERTY(qreal relativeMinLength MEMBER m_relativeMinLength NOTIFY relativeMinLengthChanged) + /** + * Maximum additional gap between windows, as a ratio of normal spacing (2*margin). Default is 1.5. + */ + Q_PROPERTY(qreal maxGapRatio MEMBER m_maxGapRatio NOTIFY maxGapRatioChanged) + /** + * Maximum scale applied to windows, *after* the minimum length is enforced. Default is 1.0. + */ + Q_PROPERTY(qreal maxScale MEMBER m_maxScale NOTIFY maxScaleChanged) public: - enum LayoutMode : uint { - LayoutClosest = 0, - LayoutNatural = 1, - LayoutNone = 2 + enum PlacementMode : uint { + Rows, + Columns, }; - Q_ENUM(LayoutMode) + Q_ENUM(PlacementMode) explicit ExpoLayout(QQuickItem *parent = nullptr); - LayoutMode mode() const; - void setMode(LayoutMode mode); - - bool fillGaps() const; - void setFillGaps(bool fill); - - int spacing() const; - void setSpacing(int spacing); + PlacementMode placementMode() const; + void setPlacementMode(PlacementMode mode); void addCell(ExpoCell *cell); void removeCell(ExpoCell *cell); @@ -53,23 +110,79 @@ protected: void geometryChange(const QRectF &newGeometry, const QRectF &oldGeometry) override; void updatePolish() override; + /** + * @brief Layout the windows with @param windowSizes into @param area. + * + * This is the main entry point for the layout algorithm. + */ + QList layout(const QRectF &area, const QList &windowSizes); + + /** + * @brief First clip @param windowSizes to be between @param minSize and + * @param maxSize. Then add @param margins to each window size, and @return + * the adjusted window sizes. + */ + QList adjustSizes(const QRectF &minSize, const QRectF &maxSize, const QMarginsF &margins, const QList &windowSizes); + + /** + * @brief Use binary search to find a good packing of the @param windowSizes + * into @param area such that the resulting packing has similar aspect ratio + * (height/width) to @param area. + * + * The binary search is performed on the logarithm of the width of the + * possible packings, and the search is terminated when the width of the + * packing is within @param tol of the ideal width. + * + * We try to find a packing such that the total widths of windows in each + * layer are close to @param idealWidthRatio times the maximum width of the + * packing. + * + * In the case of identical window heights, we also try to minimize vertical + * movement based on the @param centers of the windows. + * + * Run time is O(n log n log log (totalWidth / maxWidth)) + * Since we clip the window size, this is just O(n log n log log n) + */ + LayeredPacking + findGoodPacking(const QRectF &area, const QList &windowSizes, const QList ¢ers, qreal idealWidthRatio, qreal tol); + + /** + * @brief Output the final window layouts from the packing. + * + * Geven @param windowSizes, scale @param packing to fit @param area, + * remove previously added @param margins, add padding and align, + * and @return the final layout. + * In each layer, sort the windows by x coordinates of the @param centers. + */ + QList refineAndApplyPacking(const QRectF &area, const QMarginsF &margins, const LayeredPacking &packing, const QList &windowSizes, const QList ¢ers); + Q_SIGNALS: - void modeChanged(); - void fillGapsChanged(); - void spacingChanged(); + void placementModeChanged(); void readyChanged(); + void searchToleranceChanged(); + void idealWidthRatioChanged(); + void relativeMarginLeftChanged(); + void relativeMarginRightChanged(); + void relativeMarginTopChanged(); + void relativeMarginBottomChanged(); + void relativeMinLengthChanged(); + void maxGapRatioChanged(); + void maxScaleChanged(); private: - void calculateWindowTransformationsClosest(); - void calculateWindowTransformationsNatural(); - void resetTransformations(); - QList m_cells; - LayoutMode m_mode = LayoutNatural; - int m_accuracy = 20; - int m_spacing = 10; + PlacementMode m_placementMode = Rows; bool m_ready = false; - bool m_fillGaps = false; + + qreal m_searchTolerance = 0.2; + qreal m_idealWidthRatio = 0.8; + qreal m_relativeMarginLeft = 0.07; + qreal m_relativeMarginRight = 0.07; + qreal m_relativeMarginTop = 0.07; + qreal m_relativeMarginBottom = 0.07; + qreal m_relativeMinLength = 0.15; + qreal m_maxGapRatio = 1.5; + qreal m_maxScale = 1.0; }; class ExpoCell : public QObject @@ -162,3 +275,66 @@ private: std::optional m_height; QPointer m_layout; }; + +/** + * @brief Each Layer is a horizontal strip of windows with a maximum width and + * height. + */ +struct Layer +{ + qreal maxWidth; + qreal maxHeight; + /** + * @brief The remaining width available to new windows in this layer. + * width() + remainingWidth() == maxWidth + */ + qreal remainingWidth; + + /** + * @brief The indices of windows in this layer. + */ + QList ids; + + /** + * @brief Initializes a new layer with the given maximum width and populates + * it with the given windows. + * + * @param maxWidth The maximum width of the layer. + * @param windowSizes The sizes of all the windows. Must be sorted in + * ascending order by height. + * @param windowIds Ids of the windows. + * @param startPos windowIds[startPos] is the first window in this layer. + * @param endPos windowIds[endPos-1] is the last window in this layer. + */ + Layer(qreal maxWidth, const QList &windowSizes, const QList &windowIds, size_t startPos, size_t endPos); + + /** + * @brief The total width of all the windows in this layer. + * + */ + qreal width() const; +}; + +/** + * @brief A LayeredPacking is a packing of windows into layers, which are + * horizontal strips of windows. + */ +struct LayeredPacking +{ + qreal maxWidth; + qreal width; + qreal height; + QList layers; + + /** + * @brief Construct a new LayeredPacking object from a list of windows + * sorted by height in descending order. + * + * @param maxWidth The maximum width of the packing. + * @param windowSizes must be sorted by height in ascending order + * @param ids Ids of the windows + * @param layerStartPos Array of indices into ids that indicate the start + * of a new layer. Must start with 0 and end with ids.size(). + */ + LayeredPacking(qreal maxWidth, const QList &windowSizes, const QList &ids, const QList &layerStartPos); +}; diff --git a/src/plugins/private/qml/WindowHeap.qml b/src/plugins/private/qml/WindowHeap.qml index 14a9743130..e6462086e8 100644 --- a/src/plugins/private/qml/WindowHeap.qml +++ b/src/plugins/private/qml/WindowHeap.qml @@ -126,8 +126,8 @@ FocusScope { anchors.fill: parent anchors.margins: heap.padding - fillGaps: true - spacing: Kirigami.Units.smallSpacing * 5 + + placementMode: width >= height ? ExpoLayout.Rows : ExpoLayout.Columns Instantiator { id: windowsInstantiator diff --git a/src/plugins/windowview/kcm/windowvieweffectkcm.ui b/src/plugins/windowview/kcm/windowvieweffectkcm.ui index ce3481ef17..5f264c5a46 100644 --- a/src/plugins/windowview/kcm/windowvieweffectkcm.ui +++ b/src/plugins/windowview/kcm/windowvieweffectkcm.ui @@ -16,28 +16,14 @@ - - + + - Layout mode: + Ignore &minimized windows - - - - - Closest - - - - - Natural - - - - - + @@ -50,13 +36,6 @@ - - - - Ignore &minimized windows - - - diff --git a/src/plugins/windowview/qml/main.qml b/src/plugins/windowview/qml/main.qml index adacb47b59..b7bc1e2910 100644 --- a/src/plugins/windowview/qml/main.qml +++ b/src/plugins/windowview/qml/main.qml @@ -178,7 +178,6 @@ Item { return container.effect.selectedIds; } } - layout.mode: effect.layout model: KWinComponents.WindowFilterModel { activity: KWinComponents.Workspace.currentActivity desktop: { diff --git a/src/plugins/windowview/windowviewconfig.kcfg b/src/plugins/windowview/windowviewconfig.kcfg index 6d6991b893..c3108e4e5e 100644 --- a/src/plugins/windowview/windowviewconfig.kcfg +++ b/src/plugins/windowview/windowviewconfig.kcfg @@ -10,9 +10,6 @@ http://www.kde.org/standards/kcfg/1.0/kcfg.xsd" > - - 1 - false diff --git a/src/plugins/windowview/windowvieweffect.cpp b/src/plugins/windowview/windowvieweffect.cpp index ccf7aab830..c7e01a2230 100644 --- a/src/plugins/windowview/windowvieweffect.cpp +++ b/src/plugins/windowview/windowvieweffect.cpp @@ -133,19 +133,6 @@ void WindowViewEffect::setAnimationDuration(int duration) } } -int WindowViewEffect::layout() const -{ - return m_layout; -} - -void WindowViewEffect::setLayout(int layout) -{ - if (m_layout != layout) { - m_layout = layout; - Q_EMIT layoutChanged(); - } -} - bool WindowViewEffect::ignoreMinimized() const { return WindowViewConfig::ignoreMinimized(); @@ -160,7 +147,6 @@ void WindowViewEffect::reconfigure(ReconfigureFlags) { WindowViewConfig::self()->read(); setAnimationDuration(animationTime(300)); - setLayout(WindowViewConfig::layoutMode()); for (ElectricBorder border : std::as_const(m_borderActivate)) { effects->unreserveElectricBorder(border, this); diff --git a/src/plugins/windowview/windowvieweffect.h b/src/plugins/windowview/windowvieweffect.h index c3973407a0..6cbbf05a19 100644 --- a/src/plugins/windowview/windowvieweffect.h +++ b/src/plugins/windowview/windowvieweffect.h @@ -19,7 +19,6 @@ class WindowViewEffect : public QuickSceneEffect { Q_OBJECT Q_PROPERTY(int animationDuration READ animationDuration NOTIFY animationDurationChanged) - Q_PROPERTY(int layout READ layout NOTIFY layoutChanged) Q_PROPERTY(bool ignoreMinimized READ ignoreMinimized NOTIFY ignoreMinimizedChanged) Q_PROPERTY(PresentWindowsMode mode READ mode NOTIFY modeChanged) Q_PROPERTY(qreal partialActivationFactor READ partialActivationFactor NOTIFY partialActivationFactorChanged) @@ -50,9 +49,6 @@ public: int animationDuration() const; void setAnimationDuration(int duration); - int layout() const; - void setLayout(int layout); - bool ignoreMinimized() const; void reconfigure(ReconfigureFlags) override; @@ -85,7 +81,6 @@ Q_SIGNALS: void partialActivationFactorChanged(); void gestureInProgressChanged(); void modeChanged(); - void layoutChanged(); void ignoreMinimizedChanged(); void searchTextChanged(); void selectedIdsChanged(); @@ -121,7 +116,6 @@ private: qreal m_partialActivationFactor = 0; PresentWindowsMode m_mode; int m_animationDuration = 400; - int m_layout = 1; bool m_gestureInProgress = false; };