Project tabs drag to reorder; tabs that don't fit gather in a menu (13.14)
This commit is contained in:
1 parent
0616f9b006
commit
3f2b99f045
15 files changed
+957
-138
No files matched your search
@@ -0,0 +1,278 @@
|
||||
#include "UI/ProjectTabs.h"
|
||||
#include <QMenu>
|
||||
#include <QPropertyAnimation>
|
||||
#include <QtTest>
|
||||
|
||||
// Swift 1.3's strip: tabs drag to reorder; others gather.
|
||||
namespace {
|
||||
QToolButton &select(ProjectTabButton &button)
|
||||
{
|
||||
return *button.findChild<QToolButton *>(QStringLiteral("selectTab"));
|
||||
}
|
||||
|
||||
QStringList titles(const ProjectWorkspace &workspace)
|
||||
{
|
||||
QStringList result;
|
||||
for (const std::shared_ptr<ProjectTab> &tab : workspace.tabs())
|
||||
result << tab->title();
|
||||
return result;
|
||||
}
|
||||
|
||||
ProjectTabButton &button(ProjectTabStrip &strip, const QString &title)
|
||||
{
|
||||
for (ProjectTabButton *button : strip.buttons()) {
|
||||
if (button->tab->title() == title)
|
||||
return *button;
|
||||
}
|
||||
throw std::runtime_error("no such tab");
|
||||
}
|
||||
|
||||
// Three tabs, Untitled to Untitled 3, the last in front.
|
||||
struct Strip {
|
||||
ProjectWorkspace workspace;
|
||||
ProjectTabStrip strip{workspace};
|
||||
Strip(int tabs = 3, int width = 1000)
|
||||
{
|
||||
for (int index = 1; index < tabs; ++index)
|
||||
workspace.newCanvas();
|
||||
strip.resize(width, 34);
|
||||
strip.show();
|
||||
if (!QTest::qWaitForWindowExposed(&strip))
|
||||
throw std::runtime_error("the strip never showed");
|
||||
}
|
||||
// QTest sends to a widget, not to the press's grab.
|
||||
QToolButton *pressed = nullptr;
|
||||
// Presses a tab; returns the point, in the strip.
|
||||
QPoint press(const QString &title)
|
||||
{
|
||||
ProjectTabButton &tab = button(strip, title);
|
||||
const QPoint point = tab.mapTo(&strip, QPoint(20, 14));
|
||||
pressed = &select(tab);
|
||||
QTest::mousePress(pressed, Qt::LeftButton, {}, QPoint(20 - pressed->x(), 14));
|
||||
return point;
|
||||
}
|
||||
void move(QPoint point) { QTest::mouseMove(pressed, pressed->mapFrom(&strip, point)); }
|
||||
// The release goes to the pressed title wherever it is.
|
||||
void release(const QString &title, QPoint point)
|
||||
{
|
||||
QToolButton &title_ = select(button(strip, title));
|
||||
QTest::mouseRelease(&title_, Qt::LeftButton, {}, title_.mapFrom(&strip, point));
|
||||
}
|
||||
};
|
||||
|
||||
// Every slide finished: tabs rest at their slots.
|
||||
bool settled(const ProjectTabStrip &strip)
|
||||
{
|
||||
return strip.findChildren<QPropertyAnimation *>().isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectTabDragTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void aDraggedTabFollowsThePointerAndLandsInTheGap();
|
||||
void aSmallOrUpwardMoveIsAClick();
|
||||
void aBusyProjectHoldsTheOrder();
|
||||
void aTabClosedMidDragEndsIt();
|
||||
void tabsThatDoNotFitGatherInAMenu();
|
||||
void theMenuSwitchesAndPinsTheChosenTab();
|
||||
void aBusyProjectOpensNoMenu();
|
||||
};
|
||||
|
||||
void ProjectTabDragTests::aDraggedTabFollowsThePointerAndLandsInTheGap()
|
||||
{
|
||||
Strip fixture;
|
||||
ProjectTabButton &first = button(fixture.strip, "Untitled"), &second = button(fixture.strip, "Untitled 2"),
|
||||
&third = button(fixture.strip, "Untitled 3");
|
||||
QCOMPARE(second.x(), first.width() + 6);
|
||||
const QPoint start = fixture.press("Untitled");
|
||||
// Two points stay a press; three drag, and select.
|
||||
fixture.move(start + QPoint(2, 0));
|
||||
QCOMPARE(fixture.workspace.selectedID(), fixture.workspace.tabs()[2]->id);
|
||||
fixture.move(start + QPoint(40, 0));
|
||||
QCOMPARE(fixture.workspace.selectedID(), fixture.workspace.tabs()[0]->id);
|
||||
QCOMPARE(first.x(), 40);
|
||||
// Chosen, it is set semibold: widths read now.
|
||||
const int w1 = first.width(), w2 = second.width(), w3 = third.width();
|
||||
// Nearest the second's compacted place still: nothing slides.
|
||||
QTRY_VERIFY(settled(fixture.strip));
|
||||
QCOMPARE(second.x(), w1 + 6);
|
||||
// Past the middle of the gap: the second slides left.
|
||||
fixture.move(start + QPoint(w2 / 2 + 10, 0));
|
||||
const auto *slide = second.findChild<QPropertyAnimation *>();
|
||||
QVERIFY(slide && slide->duration() == 150 && slide->easingCurve() == QEasingCurve::OutQuad);
|
||||
QCOMPARE(slide->endValue().toPoint(), QPoint(0, 3));
|
||||
QVERIFY(!first.findChild<QPropertyAnimation *>() && !third.findChild<QPropertyAnimation *>());
|
||||
// The dragged tab lies over the one it crosses.
|
||||
QWidget *hit = fixture.strip.childAt(first.x() + 2, 14);
|
||||
QVERIFY(hit == &first || first.isAncestorOf(hit));
|
||||
QTRY_VERIFY(settled(fixture.strip));
|
||||
QCOMPARE(second.x(), 0);
|
||||
QCOMPARE(third.x(), w1 + 6 + w2 + 6);
|
||||
// Far right: held at the row's end, the others compacted.
|
||||
fixture.move(start + QPoint(5000, 0));
|
||||
QTRY_VERIFY(settled(fixture.strip));
|
||||
const int end = w1 + w2 + w3 + 12;
|
||||
QCOMPARE(first.x(), end - w1);
|
||||
QCOMPARE(third.x(), w2 + 6);
|
||||
// Far left: held at the row's start.
|
||||
fixture.move(start + QPoint(-5000, 0));
|
||||
QCOMPARE(first.x(), 0);
|
||||
fixture.move(start + QPoint(5000, 0));
|
||||
QVERIFY(first.isVisible() && first.y() == 3);
|
||||
fixture.release("Untitled", start + QPoint(5000, 0));
|
||||
QCOMPARE(titles(fixture.workspace), (QStringList{"Untitled 2", "Untitled 3", "Untitled"}));
|
||||
QCOMPARE(fixture.workspace.selectedID(), first.tab->id);
|
||||
QTRY_VERIFY(settled(fixture.strip));
|
||||
QCOMPARE(second.x(), 0);
|
||||
QCOMPARE(third.x(), second.width() + 6);
|
||||
QCOMPARE(first.x(), third.x() + third.width() + 6);
|
||||
// The front tab, dragged one place left.
|
||||
const QPoint back = fixture.press("Untitled");
|
||||
fixture.move(back - QPoint(third.width() / 2 + 10, 0));
|
||||
fixture.release("Untitled", back - QPoint(third.width() / 2 + 10, 0));
|
||||
QCOMPARE(titles(fixture.workspace), (QStringList{"Untitled 2", "Untitled", "Untitled 3"}));
|
||||
// Let go where it began: the order stays.
|
||||
const QPoint still = fixture.press("Untitled 3");
|
||||
fixture.move(still + QPoint(5, 0));
|
||||
fixture.release("Untitled 3", still + QPoint(5, 0));
|
||||
QCOMPARE(titles(fixture.workspace), (QStringList{"Untitled 2", "Untitled", "Untitled 3"}));
|
||||
QTRY_VERIFY(settled(fixture.strip));
|
||||
QCOMPARE(third.x(), second.width() + first.width() + 12);
|
||||
}
|
||||
|
||||
void ProjectTabDragTests::aSmallOrUpwardMoveIsAClick()
|
||||
{
|
||||
Strip fixture;
|
||||
const QPoint start = fixture.press("Untitled");
|
||||
// Three points up, two across: no drag, no selection yet.
|
||||
fixture.move(start + QPoint(2, -3));
|
||||
QCOMPARE(fixture.workspace.selectedID(), fixture.workspace.tabs()[2]->id);
|
||||
QCOMPARE(button(fixture.strip, "Untitled").x(), 0);
|
||||
fixture.release("Untitled", start + QPoint(2, -3));
|
||||
// The click selects; the order stays.
|
||||
QCOMPARE(fixture.workspace.selectedID(), fixture.workspace.tabs()[0]->id);
|
||||
QCOMPARE(titles(fixture.workspace), (QStringList{"Untitled", "Untitled 2", "Untitled 3"}));
|
||||
// A busy project starts no drag and selects nothing.
|
||||
fixture.workspace.current().session.setIsProjectBusy(true);
|
||||
const QPoint busy = fixture.press("Untitled");
|
||||
fixture.move(busy + QPoint(200, 0));
|
||||
QCOMPARE(button(fixture.strip, "Untitled").x(), 0);
|
||||
fixture.release("Untitled", busy + QPoint(200, 0));
|
||||
QCOMPARE(titles(fixture.workspace), (QStringList{"Untitled", "Untitled 2", "Untitled 3"}));
|
||||
}
|
||||
|
||||
void ProjectTabDragTests::aBusyProjectHoldsTheOrder()
|
||||
{
|
||||
Strip fixture;
|
||||
const QPoint start = fixture.press("Untitled");
|
||||
fixture.move(start + QPoint(5000, 0));
|
||||
QVERIFY(button(fixture.strip, "Untitled").x() > 100);
|
||||
// Busy by the drop: the tabs go back.
|
||||
fixture.workspace.current().session.setIsProjectBusy(true);
|
||||
fixture.release("Untitled", start + QPoint(5000, 0));
|
||||
QCOMPARE(titles(fixture.workspace), (QStringList{"Untitled", "Untitled 2", "Untitled 3"}));
|
||||
QTRY_VERIFY(settled(fixture.strip));
|
||||
QCOMPARE(button(fixture.strip, "Untitled").x(), 0);
|
||||
QCOMPARE(button(fixture.strip, "Untitled 2").x(), button(fixture.strip, "Untitled").width() + 6);
|
||||
}
|
||||
|
||||
void ProjectTabDragTests::aTabClosedMidDragEndsIt()
|
||||
{
|
||||
Strip fixture;
|
||||
const QPoint start = fixture.press("Untitled 2");
|
||||
fixture.move(start + QPoint(5000, 0));
|
||||
QVERIFY(button(fixture.strip, "Untitled 2").x() > 100);
|
||||
fixture.workspace.removeTab(fixture.workspace.tabs()[1]->id);
|
||||
QCOMPARE(fixture.strip.buttons().size(), 2);
|
||||
QTRY_VERIFY(settled(fixture.strip));
|
||||
QCOMPARE(button(fixture.strip, "Untitled 3").x(), button(fixture.strip, "Untitled").width() + 6);
|
||||
// Another drag works at once.
|
||||
const QPoint next = fixture.press("Untitled 3");
|
||||
fixture.move(next - QPoint(5000, 0));
|
||||
fixture.release("Untitled 3", next - QPoint(5000, 0));
|
||||
QCOMPARE(titles(fixture.workspace), (QStringList{"Untitled 3", "Untitled"}));
|
||||
}
|
||||
|
||||
void ProjectTabDragTests::tabsThatDoNotFitGatherInAMenu()
|
||||
{
|
||||
Strip fixture(12, 400);
|
||||
OverflowTabsPill &pill = *fixture.strip.pill();
|
||||
QVERIFY(pill.isVisible());
|
||||
QCOMPARE(pill.objectName(), QString("projectTabsOverflow"));
|
||||
QList<ProjectTabButton *> shown;
|
||||
for (ProjectTabButton *tab : fixture.strip.buttons()) {
|
||||
if (tab->isVisible())
|
||||
shown << tab;
|
||||
}
|
||||
const int hidden = 12 - int(shown.size());
|
||||
QVERIFY(hidden > 0 && shown.size() >= 2);
|
||||
QCOMPARE(pill.text(), QStringLiteral("%1 more tabs").arg(hidden));
|
||||
QCOMPARE(pill.toolTip(), pill.text());
|
||||
QCOMPARE(pill.accessibleName(), pill.text());
|
||||
QCOMPARE(pill.geometry(), QRect(0, 3, int(OverflowTabsPill::pillWidth(hidden)), 28));
|
||||
// The right-most tabs stay, the front one last.
|
||||
QCOMPARE(shown.last()->tab->id, fixture.workspace.selectedID());
|
||||
QCOMPARE(shown.first()->tab->id, fixture.workspace.tabs()[size_t(hidden)]->id);
|
||||
QCOMPARE(shown.first()->x(), pill.width() + 6);
|
||||
QVERIFY(shown.last()->geometry().right() < 400);
|
||||
// Swift's measure: the label, 11, 4, 10, 11.
|
||||
QFont font;
|
||||
font.setPixelSize(12);
|
||||
font.setWeight(QFont::Medium);
|
||||
QCOMPARE(OverflowTabsPill::pillWidth(3), std::ceil(QFontMetricsF(font).horizontalAdvance("3 more tabs")) + 36);
|
||||
// Wide enough, every tab shows and the pill goes.
|
||||
fixture.strip.resize(3000, 34);
|
||||
QVERIFY(!pill.isVisible());
|
||||
for (ProjectTabButton *tab : fixture.strip.buttons())
|
||||
QVERIFY(tab->isVisible());
|
||||
QCOMPARE(fixture.strip.buttons().first()->x(), 0);
|
||||
}
|
||||
|
||||
void ProjectTabDragTests::theMenuSwitchesAndPinsTheChosenTab()
|
||||
{
|
||||
Strip fixture(12, 400);
|
||||
OverflowTabsPill &pill = *fixture.strip.pill();
|
||||
fixture.workspace.tabs()[1]->session.createDocument(8, 8);
|
||||
QVERIFY(fixture.workspace.tabs()[1]->session.isModified());
|
||||
QTest::mousePress(&pill, Qt::LeftButton);
|
||||
auto *menu = pill.findChild<QMenu *>("projectTabsOverflowMenu");
|
||||
QVERIFY(menu && menu->isVisible());
|
||||
QStringList entries;
|
||||
for (const QAction *action : menu->actions())
|
||||
entries << action->text();
|
||||
const int hidden = int(entries.size());
|
||||
QCOMPARE(pill.text(), QStringLiteral("%1 more tabs").arg(hidden));
|
||||
QCOMPARE(entries.mid(0, 3), (QStringList{"Untitled", "• Untitled 2", "Untitled 3"}));
|
||||
QCOMPARE(menu->mapToGlobal(QPoint(0, 0)), pill.mapToGlobal(QPoint(0, pill.height() + 4)));
|
||||
menu->actions()[1]->trigger();
|
||||
menu->close();
|
||||
QTest::mouseRelease(&pill, Qt::LeftButton);
|
||||
const QUuid chosen = fixture.workspace.tabs()[1]->id;
|
||||
QCOMPARE(fixture.workspace.selectedID(), chosen);
|
||||
// Pinned after the pill; the row stays inside.
|
||||
ProjectTabButton &pinned = button(fixture.strip, "Untitled 2");
|
||||
QVERIFY(pinned.isVisible());
|
||||
QCOMPARE(pinned.x(), pill.width() + 6);
|
||||
for (ProjectTabButton *tab : fixture.strip.buttons())
|
||||
QVERIFY(!tab->isVisible() || tab->geometry().right() < 400);
|
||||
QTRY_VERIFY(!pill.findChild<QMenu *>("projectTabsOverflowMenu"));
|
||||
}
|
||||
|
||||
void ProjectTabDragTests::aBusyProjectOpensNoMenu()
|
||||
{
|
||||
Strip fixture(12, 400);
|
||||
OverflowTabsPill &pill = *fixture.strip.pill();
|
||||
const QImage free = pill.grab().toImage();
|
||||
fixture.workspace.current().session.setIsProjectBusy(true);
|
||||
pill.update();
|
||||
// The label dims; the capsule stays.
|
||||
const QImage busy = pill.grab().toImage();
|
||||
QVERIFY(busy != free);
|
||||
QCOMPARE(busy.pixelColor(pill.width() / 2, 1), free.pixelColor(pill.width() / 2, 1));
|
||||
QTest::mouseClick(&pill, Qt::LeftButton);
|
||||
QVERIFY(!pill.findChild<QMenu *>("projectTabsOverflowMenu"));
|
||||
}
|
||||
|
||||
QTEST_MAIN(ProjectTabDragTests)
|
||||
#include "ProjectTabDragTests.moc"
|
||||
@@ -0,0 +1,152 @@
|
||||
#include "UI/ProjectTabLayout.h"
|
||||
#include <QtTest>
|
||||
|
||||
// projectTabOverflow is geometry: made-up ids and widths.
|
||||
namespace {
|
||||
const QUuid a = QUuid::createUuid(), b = QUuid::createUuid(), c = QUuid::createUuid(), d = QUuid::createUuid(), e = QUuid::createUuid();
|
||||
|
||||
// Every tab 100 wide.
|
||||
std::map<QUuid, double> widths(const std::vector<QUuid> &ids)
|
||||
{
|
||||
std::map<QUuid, double> result;
|
||||
for (const QUuid &id : ids)
|
||||
result.insert({id, 100});
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::function<double(int)> pill = [](int) { return 80.0; };
|
||||
|
||||
std::vector<QUuid> ids(const ProjectTabOverflow &layout)
|
||||
{
|
||||
std::vector<QUuid> result;
|
||||
for (const ProjectTabSlot &slot : layout.visible)
|
||||
result.push_back(slot.id);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<double> xs(const ProjectTabOverflow &layout)
|
||||
{
|
||||
std::vector<double> result;
|
||||
for (const ProjectTabSlot &slot : layout.visible)
|
||||
result.push_back(slot.x);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class ProjectTabLayoutTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void everythingFitsShowsAllTabsWithNoPill();
|
||||
void overflowHidesTheLeftmostTabsAndPillsThem();
|
||||
void selectedTabInTheHiddenSetIsPinnedRightOfThePill();
|
||||
void overflowLabelIsSingularForOneTab();
|
||||
void unmeasuredWidthShowsEverythingRatherThanGuessing();
|
||||
void aWiderSelectedTabStillFits();
|
||||
void thePillsWidthFollowsTheHiddenCount();
|
||||
};
|
||||
|
||||
void ProjectTabLayoutTests::everythingFitsShowsAllTabsWithNoPill()
|
||||
{
|
||||
const std::vector<QUuid> order{a, b, c};
|
||||
const ProjectTabOverflow result = projectTabOverflow(order, widths(order), c, 400, pill);
|
||||
QVERIFY(result.hiddenIDs.empty());
|
||||
QVERIFY(!result.pill);
|
||||
QVERIFY(ids(result) == order);
|
||||
QVERIFY((xs(result) == std::vector<double>{0, 106, 212}));
|
||||
QCOMPARE(result.contentWidth(), 312.0);
|
||||
// Exactly the room it needs still fits.
|
||||
QVERIFY(!projectTabOverflow(order, widths(order), c, 312, pill).pill);
|
||||
QVERIFY(projectTabOverflow(order, widths(order), c, 311.5, pill).pill);
|
||||
QVERIFY(projectTabOverflow({}, {}, c, 400, pill) == ProjectTabOverflow());
|
||||
QCOMPARE(ProjectTabOverflow().contentWidth(), 0.0);
|
||||
}
|
||||
|
||||
void ProjectTabLayoutTests::overflowHidesTheLeftmostTabsAndPillsThem()
|
||||
{
|
||||
const std::vector<QUuid> order{a, b, c, d, e};
|
||||
// The pill and two tabs: 292; a third needs 398.
|
||||
const ProjectTabOverflow result = projectTabOverflow(order, widths(order), e, 300, pill);
|
||||
QVERIFY((result.hiddenIDs == std::vector<QUuid>{a, b, c}));
|
||||
QVERIFY((ids(result) == std::vector<QUuid>{d, e}));
|
||||
QVERIFY((result.pill.value() == ProjectTabPillSlot{0, 80}));
|
||||
QCOMPARE(result.visible.front().x, 86.0);
|
||||
QCOMPARE(result.visible.front().width, 100.0);
|
||||
QCOMPARE(result.contentWidth(), 292.0);
|
||||
// 292 exactly still holds two; a third at 398.
|
||||
QCOMPARE(projectTabOverflow(order, widths(order), e, 292, pill).visible.size(), size_t(2));
|
||||
QCOMPARE(projectTabOverflow(order, widths(order), e, 398, pill).visible.size(), size_t(3));
|
||||
// Too narrow for even one: one tab stays.
|
||||
QVERIFY((ids(projectTabOverflow(order, widths(order), e, 50, pill)) == std::vector<QUuid>{e}));
|
||||
}
|
||||
|
||||
void ProjectTabLayoutTests::selectedTabInTheHiddenSetIsPinnedRightOfThePill()
|
||||
{
|
||||
const std::vector<QUuid> order{a, b, c, d, e};
|
||||
// a takes the first slot; d, bumped, hides in order.
|
||||
const ProjectTabOverflow result = projectTabOverflow(order, widths(order), a, 300, pill);
|
||||
QVERIFY((ids(result) == std::vector<QUuid>{a, e}));
|
||||
QVERIFY((result.hiddenIDs == std::vector<QUuid>{b, c, d}));
|
||||
QCOMPARE(result.pill.value().width, 80.0);
|
||||
QVERIFY((xs(result) == std::vector<double>{86, 192}));
|
||||
}
|
||||
|
||||
void ProjectTabLayoutTests::overflowLabelIsSingularForOneTab()
|
||||
{
|
||||
QCOMPARE(projectTabOverflowLabel(1), QString("1 more tab"));
|
||||
QCOMPARE(projectTabOverflowLabel(2), QString("2 more tabs"));
|
||||
QCOMPARE(projectTabOverflowLabel(11), QString("11 more tabs"));
|
||||
}
|
||||
|
||||
void ProjectTabLayoutTests::unmeasuredWidthShowsEverythingRatherThanGuessing()
|
||||
{
|
||||
const std::vector<QUuid> order{a, b, c, d, e};
|
||||
const ProjectTabOverflow result = projectTabOverflow(order, widths(order), a, 0, pill);
|
||||
QVERIFY(result.hiddenIDs.empty());
|
||||
QVERIFY(!result.pill);
|
||||
QCOMPARE(result.visible.size(), size_t(5));
|
||||
}
|
||||
|
||||
// A long-titled hidden tab still leaves a row that fits.
|
||||
void ProjectTabLayoutTests::aWiderSelectedTabStillFits()
|
||||
{
|
||||
std::vector<QUuid> order;
|
||||
std::map<QUuid, double> sizes;
|
||||
for (int index = 0; index < 8; ++index) {
|
||||
order.push_back(QUuid::createUuid());
|
||||
sizes.insert({order.back(), 80});
|
||||
}
|
||||
sizes[order[0]] = 190;
|
||||
const ProjectTabOverflow layout = projectTabOverflow(order, sizes, order[0], 500, [](int) { return 90.0; });
|
||||
QVERIFY2(layout.contentWidth() <= 500, qPrintable(QString::number(layout.contentWidth())));
|
||||
QCOMPARE(layout.visible.front().id, order[0]);
|
||||
// Pill 90, 190, two of 80; three would make 522.
|
||||
QVERIFY((ids(layout) == std::vector<QUuid>{order[0], order[6], order[7]}));
|
||||
QVERIFY((layout.hiddenIDs == std::vector<QUuid>{order[1], order[2], order[3], order[4], order[5]}));
|
||||
QCOMPARE(layout.contentWidth(), 90 + 6 + 190 + 6 + 80 + 6 + 80.0);
|
||||
// Wide enough, it stands alone after the pill.
|
||||
const std::vector<QUuid> five{a, b, c, d, e};
|
||||
std::map<QUuid, double> wide = widths(five);
|
||||
wide[a] = 290;
|
||||
const ProjectTabOverflow alone = projectTabOverflow(five, wide, a, 400, pill);
|
||||
QVERIFY((ids(alone) == std::vector<QUuid>{a}));
|
||||
QVERIFY((alone.hiddenIDs == std::vector<QUuid>{b, c, d, e}));
|
||||
}
|
||||
|
||||
// The pill's width is asked by its hidden count.
|
||||
void ProjectTabLayoutTests::thePillsWidthFollowsTheHiddenCount()
|
||||
{
|
||||
const std::vector<QUuid> order{a, b, c, d, e};
|
||||
std::vector<int> asked;
|
||||
const ProjectTabOverflow result = projectTabOverflow(order, widths(order), e, 300, [&](int hidden) {
|
||||
asked.push_back(hidden);
|
||||
return 50.0 + hidden;
|
||||
});
|
||||
// Three hidden fits (265); the last ask places it.
|
||||
QVERIFY((asked == std::vector<int>{0, 1, 2, 3, 3}));
|
||||
QVERIFY((ids(result) == std::vector<QUuid>{d, e}));
|
||||
QCOMPARE(result.pill.value().width, 53.0);
|
||||
QCOMPARE(result.visible.front().x, 59.0);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(ProjectTabLayoutTests)
|
||||
#include "ProjectTabLayoutTests.moc"
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "UI/ProjectTabs.h"
|
||||
#include <QFontMetricsF>
|
||||
#include <QScrollBar>
|
||||
#include <QToolButton>
|
||||
#include <QtTest>
|
||||
|
||||
@@ -26,7 +25,6 @@ class ProjectTabStripTests : public QObject {
|
||||
private slots:
|
||||
void eachTabIsAsWideAsItsTitle();
|
||||
void aTitleMovesOnlyTheTabsAfterIt();
|
||||
void onlyANewFrontTabScrollsIntoView();
|
||||
};
|
||||
|
||||
void ProjectTabStripTests::eachTabIsAsWideAsItsTitle()
|
||||
@@ -103,44 +101,5 @@ void ProjectTabStripTests::aTitleMovesOnlyTheTabsAfterIt()
|
||||
QCOMPARE(buttons[1]->x() - (buttons[0]->x() + buttons[0]->width()), 6);
|
||||
}
|
||||
|
||||
void ProjectTabStripTests::onlyANewFrontTabScrollsIntoView()
|
||||
{
|
||||
ProjectWorkspace workspace;
|
||||
for (int tab = 0; tab < 11; ++tab)
|
||||
workspace.newCanvas();
|
||||
ProjectTabStrip strip(workspace);
|
||||
strip.resize(300, 34);
|
||||
strip.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&strip));
|
||||
QScrollBar &bar = *strip.horizontalScrollBar();
|
||||
// Scrolled back, another tab's change keeps the offset.
|
||||
QTRY_VERIFY(bar.value() == bar.maximum() && bar.maximum() > 0);
|
||||
// Scrolled back by hand, another tab's change keeps the offset.
|
||||
bar.setValue(40);
|
||||
workspace.tabs()[3]->session.setProjectPath(QStringLiteral("/p/Renamed.comp"));
|
||||
QCoreApplication::processEvents();
|
||||
QCOMPARE(bar.value(), 40);
|
||||
// The front tab turns unsaved: a dot, the offset kept.
|
||||
workspace.current().session.createDocument(8, 8);
|
||||
workspace.current().session.history.markSaved();
|
||||
QVERIFY(!workspace.current().session.isModified());
|
||||
workspace.current().session.addBlankLayer();
|
||||
QVERIFY(workspace.current().session.isModified());
|
||||
QCoreApplication::processEvents();
|
||||
QVERIFY(select(*strip.buttons().last()).text().startsWith(QStringLiteral("●")));
|
||||
QCOMPARE(bar.value(), 40);
|
||||
// A tab closed behind is no new front tab either.
|
||||
const QUuid front = workspace.selectedID();
|
||||
workspace.removeTab(workspace.tabs()[5]->id);
|
||||
QCOMPARE(workspace.selectedID(), front);
|
||||
QCoreApplication::processEvents();
|
||||
QCOMPARE(bar.value(), 40);
|
||||
// Choosing another tab brings it in, as little as needed.
|
||||
workspace.select(workspace.tabs()[0]->id);
|
||||
QTRY_COMPARE(bar.value(), 0);
|
||||
workspace.select(workspace.tabs()[1]->id);
|
||||
QCOMPARE(bar.value(), 0);
|
||||
}
|
||||
|
||||
QTEST_MAIN(ProjectTabStripTests)
|
||||
#include "ProjectTabStripTests.moc"
|
||||
@@ -42,6 +42,7 @@ private slots:
|
||||
void everyChangeIsAnnouncedAndRefusalsAreSilent();
|
||||
void aDropThatIsNoLayerIdIsIgnored();
|
||||
void anOpenFilterHoldsTheSwitch();
|
||||
void moveTabReordersWithoutTouchingSelectionOrDocuments();
|
||||
};
|
||||
|
||||
void ProjectWorkspaceTests::newCanvasOpensAnEmptyTabWithoutAModal()
|
||||
@@ -358,5 +359,36 @@ void ProjectWorkspaceTests::anOpenFilterHoldsTheSwitch()
|
||||
QVERIFY(workspace.canSwitch());
|
||||
}
|
||||
|
||||
void ProjectWorkspaceTests::moveTabReordersWithoutTouchingSelectionOrDocuments()
|
||||
{
|
||||
ProjectWorkspace workspace;
|
||||
const QUuid a = workspace.current().id, b = workspace.addTab(false).id, c = workspace.addTab(false).id;
|
||||
const auto order = [&] {
|
||||
std::vector<QUuid> ids;
|
||||
for (const std::shared_ptr<ProjectTab> &tab : workspace.tabs())
|
||||
ids.push_back(tab->id);
|
||||
return ids;
|
||||
};
|
||||
QSignalSpy changed(&workspace, &ProjectWorkspace::changed);
|
||||
workspace.moveTab(c, 0);
|
||||
QVERIFY((order() == std::vector<QUuid>{c, a, b}));
|
||||
workspace.moveTab(a, 2);
|
||||
QVERIFY((order() == std::vector<QUuid>{c, b, a}));
|
||||
QCOMPARE(changed.size(), 2);
|
||||
// Out of range clamps to the ends.
|
||||
workspace.moveTab(c, 99);
|
||||
QVERIFY((order() == std::vector<QUuid>{b, a, c}));
|
||||
workspace.moveTab(c, -4);
|
||||
QVERIFY((order() == std::vector<QUuid>{c, b, a}));
|
||||
QCOMPARE(changed.size(), 4);
|
||||
// Where it is, or no tab: nothing, silently.
|
||||
workspace.moveTab(a, 2);
|
||||
workspace.moveTab(QUuid::createUuid(), 0);
|
||||
QVERIFY((order() == std::vector<QUuid>{c, b, a}));
|
||||
QCOMPARE(changed.size(), 4);
|
||||
// Chrome: the selection stays.
|
||||
QCOMPARE(workspace.selectedID(), c);
|
||||
}
|
||||
|
||||
QTEST_MAIN(ProjectWorkspaceTests)
|
||||
#include "ProjectWorkspaceTests.moc"
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QPushButton>
|
||||
#include <QTemporaryDir>
|
||||
#include <QToolButton>
|
||||
#include <QToolBar>
|
||||
#include <QtTest>
|
||||
|
||||
// The window: tabs, the toolbar, closing and quitting.
|
||||
@@ -203,29 +204,27 @@ void ProjectWorkspaceViewTests::tabsShowTheirTitlesDotsAndState()
|
||||
late.session.setProjectPath(QString("/x/Late.comp"));
|
||||
workspace.select(workspace.tabs()[0]->id);
|
||||
QCOMPARE(titles(strip), (QStringList{"● Untitled", "Untitled 2", "Late"}));
|
||||
// More tabs than fit: the front one scrolls into view.
|
||||
// More tabs than fit: the rest gather under the pill.
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&window));
|
||||
for (int index = 0; index < 8; ++index)
|
||||
for (int index = 0; index < 12; ++index)
|
||||
workspace.addTab(false).session.setProjectPath(QString("/x/Wide project number %1.comp").arg(index));
|
||||
const ProjectTabButton *last = strip.buttons().last();
|
||||
const auto wholeInView = [&](const ProjectTabButton *button) {
|
||||
return strip.viewport()->rect().contains(QRect(button->mapTo(strip.viewport(), QPoint(0, 0)), button->size()));
|
||||
return button->isVisible() && strip.rect().contains(button->geometry());
|
||||
};
|
||||
// In view once laid out, as Swift posts its reveal.
|
||||
QVERIFY(last->tab->id == workspace.selectedID());
|
||||
QTRY_VERIFY(wholeInView(last));
|
||||
QVERIFY(strip.widget()->width() > strip.viewport()->width());
|
||||
QVERIFY(last->findChild<QToolButton *>("selectTab")->width() >= 35 && strip.horizontalScrollBar()->maximum() > 0);
|
||||
// Since 1.2.4 a growing title keeps the offset.
|
||||
const int offset = strip.horizontalScrollBar()->value();
|
||||
QVERIFY(wholeInView(last));
|
||||
QVERIFY(strip.pill()->isVisible() && !strip.buttons().first()->isVisible());
|
||||
// The strip ends before the zoom buttons.
|
||||
const QWidget &fitButton = *window.findChild<QToolBar *>("toolbar")->widgetForAction(&action(window, "fitCanvas"));
|
||||
QVERIFY(strip.mapTo(&window, QPoint(strip.width(), 0)).x() <= fitButton.mapTo(&window, QPoint()).x());
|
||||
// A growing title keeps the front tab whole.
|
||||
workspace.tabs().back()->session.setProjectPath(QString("/x/") + QString(60, QLatin1Char('W')) + ".comp");
|
||||
QCoreApplication::processEvents();
|
||||
QCoreApplication::processEvents();
|
||||
QVERIFY(last->width() > 180);
|
||||
QCOMPARE(strip.horizontalScrollBar()->value(), offset);
|
||||
QVERIFY(wholeInView(last));
|
||||
workspace.select(workspace.tabs()[0]->id);
|
||||
const ProjectTabButton *head = strip.buttons().first();
|
||||
QTRY_VERIFY(wholeInView(head));
|
||||
QVERIFY(wholeInView(head));
|
||||
}
|
||||
|
||||
void ProjectWorkspaceViewTests::tabButtonsSelectAndClose()
|
||||
|
||||
@@ -198,6 +198,8 @@ compositor_test(ProjectWorkspaceSignalTests)
|
||||
compositor_test(ProjectWorkspaceTests)
|
||||
compositor_test(ProjectWorkspaceViewTests)
|
||||
compositor_test(ProjectTabStripTests)
|
||||
compositor_test(ProjectTabDragTests)
|
||||
compositor_test(ProjectTabLayoutTests)
|
||||
compositor_test(KeyboardZoomTests)
|
||||
# It starts the real app beside itself.
|
||||
add_dependencies(ProjectWorkspaceViewTests omaphoto)
|
||||
|
||||
Reference in new issue
Block a user