Open Recent, print sizes through an invalid resolution, Color Grading under Color (12.14b)
This commit is contained in:
1 parent
94b884a2ef
commit
2183348aa6
21 files changed
+717
-38
No files matched your search
@@ -104,6 +104,7 @@ private slots:
|
||||
void aTypedNumberAppliesAndIsHeldInRange();
|
||||
void altOnALightSliderShowsItsClippingUntilLetGo();
|
||||
void eachGroupOpensAndItsEyeHidesItFromThePreview();
|
||||
void eachEyeHidesItsOwnGroup();
|
||||
void theScopeSwitchesAndItsTrianglesShowClipping();
|
||||
void theReadoutNamesThePixel();
|
||||
void whiteBalanceAutoCustomAndTheEyedropper();
|
||||
@@ -214,11 +215,18 @@ void CameraRawPanelTests::altOnALightSliderShowsItsClippingUntilLetGo()
|
||||
void CameraRawPanelTests::eachGroupOpensAndItsEyeHidesItFromThePreview()
|
||||
{
|
||||
Panel panel;
|
||||
const QStringList groups{"Light", "Color", "Effects", "Curve", "ColorMixer", "ColorGrading", "Detail", "Optics", "Geometry", "Calibration"};
|
||||
// Swift's 7e9afbe: Color Grading under Color, open.
|
||||
const QStringList groups{"Light", "Color", "ColorGrading", "Effects", "Curve", "ColorMixer", "Detail", "Optics", "Geometry", "Calibration"};
|
||||
int above = -1;
|
||||
for (const QString &group : groups) {
|
||||
const int top = panel.child<QToolButton>(group + QStringLiteral("Section")).mapTo(panel.controls.get(), QPoint()).y();
|
||||
QVERIFY2(top > above, qPrintable(group));
|
||||
above = top;
|
||||
}
|
||||
for (const QString &group : groups) {
|
||||
auto &disclosure = panel.child<QToolButton>(group + QStringLiteral("Section"));
|
||||
auto *body = disclosure.parentWidget()->findChildren<QWidget *>(QString(), Qt::FindDirectChildrenOnly).last();
|
||||
const bool open = group == QLatin1String("Light") || group == QLatin1String("Color");
|
||||
const bool open = group == QLatin1String("Light") || group == QLatin1String("Color") || group == QLatin1String("ColorGrading");
|
||||
QCOMPARE(body->isVisible(), open);
|
||||
disclosure.click();
|
||||
QCOMPARE(body->isVisible(), !open);
|
||||
@@ -238,6 +246,69 @@ void CameraRawPanelTests::eachGroupOpensAndItsEyeHidesItFromThePreview()
|
||||
QVERIFY(panel.panel().shows.light);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// Undoes the eye test's one adjustment to that group.
|
||||
void reset(CameraRawSettings &raw, const QString &eye)
|
||||
{
|
||||
if (eye == QLatin1String("ColorGradingEye"))
|
||||
raw.grading.shadows.saturation = 0;
|
||||
else if (eye == QLatin1String("EffectsEye"))
|
||||
raw.clarity = 0;
|
||||
else if (eye == QLatin1String("CurveEye"))
|
||||
raw.curve.shadows = 0;
|
||||
else
|
||||
raw.mixer.hue[0] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void CameraRawPanelTests::eachEyeHidesItsOwnGroup()
|
||||
{
|
||||
Panel panel;
|
||||
FilterSettings adjusted = panel.session.filterEdit().value().settings;
|
||||
adjusted.cameraRaw.grading.shadows.saturation = 20;
|
||||
adjusted.cameraRaw.clarity = 30;
|
||||
adjusted.cameraRaw.curve.shadows = 20;
|
||||
adjusted.cameraRaw.mixer.hue[0] = 20;
|
||||
panel.session.updateFilter(adjusted, true);
|
||||
using Adjusts = bool (*)(const CameraRawSettings &);
|
||||
const std::array<std::tuple<QString, bool CameraRawGroups::*, Adjusts>, 4> eyes{{
|
||||
{QStringLiteral("ColorGradingEye"), &CameraRawGroups::grading, [](const CameraRawSettings &raw) { return raw.grading.adjusts(); }},
|
||||
{QStringLiteral("EffectsEye"), &CameraRawGroups::effects, [](const CameraRawSettings &raw) { return raw.adjustsEffects(); }},
|
||||
{QStringLiteral("CurveEye"), &CameraRawGroups::curve, [](const CameraRawSettings &raw) { return raw.curve.adjusts(); }},
|
||||
{QStringLiteral("ColorMixerEye"), &CameraRawGroups::mixer, [](const CameraRawSettings &raw) { return raw.mixer.adjusts(); }},
|
||||
}};
|
||||
// Alone, each adjustment shows its own eye only.
|
||||
for (const auto &[name, group, adjusts] : eyes) {
|
||||
FilterSettings alone = adjusted;
|
||||
for (const auto &[other, otherGroup, otherAdjusts] : eyes) {
|
||||
if (other != name)
|
||||
reset(alone.cameraRaw, other);
|
||||
}
|
||||
panel.session.updateFilter(alone, true);
|
||||
for (const auto &[other, otherGroup, otherAdjusts] : eyes)
|
||||
QVERIFY2(panel.child<QToolButton>(other).isVisible() == (other == name), qPrintable(name + " " + other));
|
||||
reset(alone.cameraRaw, name);
|
||||
panel.session.updateFilter(alone, true);
|
||||
QVERIFY2(!panel.child<QToolButton>(name).isVisible(), qPrintable(name));
|
||||
}
|
||||
panel.session.updateFilter(adjusted, true);
|
||||
for (const auto &[name, group, adjusts] : eyes) {
|
||||
auto &eye = panel.child<QToolButton>(name);
|
||||
QVERIFY2(eye.isVisible(), qPrintable(name));
|
||||
eye.click();
|
||||
// That group alone leaves the preview.
|
||||
CameraRawGroups expected;
|
||||
expected.*group = false;
|
||||
QVERIFY2(panel.panel().shows == expected, qPrintable(name));
|
||||
const CameraRawSettings previewed = panel.session.filterEdit().value().previewJob().settings.cameraRaw;
|
||||
for (const auto &[other, otherGroup, otherAdjusts] : eyes)
|
||||
QVERIFY2(otherAdjusts(previewed) == (other != name), qPrintable(name + " " + other));
|
||||
eye.click();
|
||||
QVERIFY2(panel.panel().shows == CameraRawGroups(), qPrintable(name));
|
||||
QVERIFY(adjusts(panel.session.filterEdit().value().previewJob().settings.cameraRaw));
|
||||
}
|
||||
}
|
||||
|
||||
void CameraRawPanelTests::theScopeSwitchesAndItsTrianglesShowClipping()
|
||||
{
|
||||
Panel panel;
|
||||
|
||||
@@ -50,7 +50,7 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand()
|
||||
for (const QMenu *menu : menus)
|
||||
titles << menu->title();
|
||||
QCOMPARE(titles, (QStringList{"&File", "&Edit", "&View", "&Select", "&Image", "Fil&ter", "&Layer"}));
|
||||
QCOMPARE(menus[0]->actions().size(), 11);
|
||||
QCOMPARE(menus[0]->actions().size(), 12);
|
||||
QCOMPARE(menus[1]->actions().size(), 13);
|
||||
QCOMPARE(menus[2]->actions().size(), 16);
|
||||
QCOMPARE(menus[3]->actions().size(), 10);
|
||||
@@ -76,7 +76,8 @@ void CompositorMenusTests::fileEntriesFollowTheControllersGate()
|
||||
{
|
||||
QTemporaryDir folder;
|
||||
Bar bar;
|
||||
QVERIFY(bar.action("newCanvas").isEnabled() && bar.action("openProject").isEnabled() && bar.action("closeProject").isEnabled());
|
||||
QVERIFY(bar.action("newCanvas").isEnabled() && bar.action("openProject").isEnabled() && bar.action("openRecent").isEnabled()
|
||||
&& bar.action("closeProject").isEnabled());
|
||||
QVERIFY(bar.action("importImages").isEnabled());
|
||||
// Nothing to save without a canvas.
|
||||
QVERIFY(!bar.action("save").isEnabled() && !bar.action("saveAs").isEnabled());
|
||||
@@ -84,26 +85,30 @@ void CompositorMenusTests::fileEntriesFollowTheControllersGate()
|
||||
QVERIFY(bar.action("save").isEnabled() && bar.action("saveAs").isEnabled());
|
||||
// Busy holds the file entries; a sheet holds Import.
|
||||
bar.session().setIsProjectBusy(true);
|
||||
QVERIFY(!bar.action("newCanvas").isEnabled() && !bar.action("openProject").isEnabled() && !bar.action("save").isEnabled()
|
||||
QVERIFY(!bar.action("newCanvas").isEnabled() && !bar.action("openProject").isEnabled() && !bar.action("openRecent").isEnabled()
|
||||
&& !bar.action("save").isEnabled()
|
||||
&& !bar.action("saveAs").isEnabled() && !bar.action("closeProject").isEnabled());
|
||||
QVERIFY(bar.action("importImages").isEnabled());
|
||||
QTRY_VERIFY(!bar.action("importImages").isEnabled());
|
||||
bar.session().setIsProjectBusy(false);
|
||||
bar.session().setShowsNewDocument(true);
|
||||
QVERIFY(!bar.action("importImages").isEnabled() && !bar.action("newCanvas").isEnabled());
|
||||
QVERIFY(!bar.action("importImages").isEnabled() && !bar.action("newCanvas").isEnabled() && !bar.action("openRecent").isEnabled());
|
||||
bar.session().setShowsNewDocument(false);
|
||||
QVERIFY(bar.action("openRecent").isEnabled());
|
||||
// Importing alone holds Import as well.
|
||||
bar.session().setIsImporting(true);
|
||||
QVERIFY(!bar.action("importImages").isEnabled());
|
||||
QVERIFY(!bar.action("importImages").isEnabled() && !bar.action("openProject").isEnabled() && !bar.action("openRecent").isEnabled());
|
||||
bar.session().setIsImporting(false);
|
||||
QVERIFY(bar.action("importImages").isEnabled());
|
||||
QVERIFY(bar.action("importImages").isEnabled() && bar.action("openRecent").isEnabled());
|
||||
// The entries do what the toolbar and the controller do.
|
||||
bar.action("newCanvas").trigger();
|
||||
QCOMPARE(int(bar.workspace.tabs().size()), 2);
|
||||
bar.workspace.select(bar.workspace.tabs()[0]->id);
|
||||
bar.action("importImages").trigger();
|
||||
QVERIFY(bar.session().showsImporter());
|
||||
QVERIFY(!bar.action("openRecent").isEnabled());
|
||||
bar.session().setShowsImporter(false);
|
||||
QVERIFY(bar.action("openRecent").isEnabled());
|
||||
DialogDesk desk;
|
||||
desk.replies = {folder.filePath("Saved.comp")};
|
||||
bar.action("save").trigger();
|
||||
@@ -119,8 +124,14 @@ void CompositorMenusTests::fileEntriesFollowTheControllersGate()
|
||||
QTRY_COMPARE(bar.session().projectPath(), std::optional(folder.filePath("Copy.comp")));
|
||||
QCOMPARE(desk.seen.size(), 2);
|
||||
desk.replies = {"<cancel>"};
|
||||
// The workspace's panel holds Open Recent until answered.
|
||||
desk.note = [&] { return QStringLiteral("managing=%1 recent=%2").arg(bar.workspace.isManaging()).arg(bar.action("openRecent").isEnabled()); };
|
||||
bar.action("openProject").trigger();
|
||||
QTRY_COMPARE(desk.seen.size(), 3);
|
||||
QVERIFY2(desk.seen[2].endsWith("|managing=1 recent=0"), qPrintable(desk.seen[2]));
|
||||
desk.note = nullptr;
|
||||
QTRY_VERIFY(!bar.workspace.isManaging());
|
||||
QVERIFY(bar.action("openRecent").isEnabled());
|
||||
// Close Project closes this tab, not the window.
|
||||
bar.window.show();
|
||||
bar.action("closeProject").trigger();
|
||||
|
||||
@@ -16,6 +16,7 @@ private slots:
|
||||
void theSheetShowsTheCanvas();
|
||||
void pixelsFollowUnitsAndTheLock();
|
||||
void aNewResolutionKeepsThePrintSize();
|
||||
void aDocumentsOwnResolutionIsTheLastUsable();
|
||||
void withoutResamplingOnlyTheResolutionMoves();
|
||||
void anInvalidSizeRestsResize();
|
||||
void returnWithoutTypingKeepsTheNumbers();
|
||||
@@ -24,11 +25,18 @@ private slots:
|
||||
};
|
||||
|
||||
namespace {
|
||||
CanvasDocument document(int width, int height, double resolution)
|
||||
{
|
||||
CanvasDocument made{width, height};
|
||||
made.resolution = resolution;
|
||||
return made;
|
||||
}
|
||||
|
||||
struct Sheet {
|
||||
std::optional<std::optional<ImageSizeOptions>> answer;
|
||||
ImageSizeSheet sheet;
|
||||
explicit Sheet(int width = 1200, int height = 800)
|
||||
: sheet(CanvasDocument{width, height}, [this](std::optional<ImageSizeOptions> options) { answer = options; })
|
||||
explicit Sheet(int width = 1200, int height = 800, double resolution = 72)
|
||||
: sheet(document(width, height, resolution), [this](std::optional<ImageSizeOptions> options) { answer = options; })
|
||||
{
|
||||
sheet.show();
|
||||
}
|
||||
@@ -152,16 +160,99 @@ void ImageSizeSheetTests::aNewResolutionKeepsThePrintSize()
|
||||
QCOMPARE(shown.result(), QString("Result: 2,400 × 1,600 pixels"));
|
||||
shown.find<QPushButton>("imageResize").click();
|
||||
QCOMPARE(shown.answer.value().value().resolution, 300.0);
|
||||
// Zero changes nothing but the resolution, which is refused.
|
||||
// Swift's 555417e: zero and below keep 8 by 5.333 inches.
|
||||
shown.unit("Inches");
|
||||
QCOMPARE(shown.shown("imageWidth"), QString("8"));
|
||||
shown.type("imageResolution", "0");
|
||||
QCOMPARE(shown.result(), QStringLiteral("Use 1–30,000 pixels per side, up to %1 megapixels, and 1–9,600 pixels/inch.").arg(DocumentLimits::maxSurfaceMegapixels()));
|
||||
// A size typed meanwhile is ignored.
|
||||
shown.type("imageWidth", "2");
|
||||
shown.type("imageResolution", "-5");
|
||||
shown.type("imageHeight", "3");
|
||||
shown.type("imageResolution", "150");
|
||||
QCOMPARE(shown.result(), QString("Result: 2,400 × 1,600 pixels"));
|
||||
QCOMPARE(shown.result(), QString("Result: 1,200 × 800 pixels"));
|
||||
QCOMPARE(shown.shown("imageWidth"), QString("8"));
|
||||
// Centimetres keep the print size alike.
|
||||
shown.unit("Centimeters");
|
||||
shown.type("imageResolution", "0");
|
||||
shown.type("imageResolution", "300");
|
||||
QCOMPARE(shown.result(), QString("Result: 4,800 × 3,200 pixels"));
|
||||
QCOMPARE(shown.result(), QString("Result: 2,400 × 1,600 pixels"));
|
||||
}
|
||||
|
||||
void ImageSizeSheetTests::aDocumentsOwnResolutionIsTheLastUsable()
|
||||
{
|
||||
for (const QString &unit : {QStringLiteral("Inches"), QStringLiteral("Centimeters")}) {
|
||||
Sheet shown(1200, 800, 300);
|
||||
shown.unit(unit);
|
||||
shown.type("imageResolution", "0");
|
||||
shown.type("imageWidth", "1");
|
||||
shown.type("imageResolution", "-5");
|
||||
shown.type("imageResolution", "150");
|
||||
QCOMPARE(shown.result(), QString("Result: 600 × 400 pixels"));
|
||||
}
|
||||
// Pixels and Percent need no resolution: those edits apply.
|
||||
for (const auto &[unit, width] : {std::pair(QStringLiteral("Pixels"), QStringLiteral("600")), std::pair(QStringLiteral("Percent"), QStringLiteral("50"))}) {
|
||||
Sheet shown(1200, 800, 300);
|
||||
shown.unit(unit);
|
||||
shown.type("imageResolution", "0");
|
||||
shown.type("imageWidth", width);
|
||||
shown.type("imageResolution", "150");
|
||||
QCOMPARE(shown.result(), QString("Result: 600 × 400 pixels"));
|
||||
}
|
||||
// Infinity is no usable resolution either.
|
||||
for (const QString &unit : {QStringLiteral("Inches"), QStringLiteral("Centimeters")}) {
|
||||
Sheet shown(1200, 800, 300);
|
||||
shown.unit(unit);
|
||||
shown.type("imageResolution", "inf");
|
||||
QVERIFY(!shown.find<QPushButton>("imageResize").isEnabled());
|
||||
shown.type("imageWidth", "1");
|
||||
shown.type("imageResolution", "150");
|
||||
QCOMPARE(shown.result(), QString("Result: 600 × 400 pixels"));
|
||||
shown.find<QPushButton>("imageResize").click();
|
||||
const ImageSizeOptions options = shown.answer.value().value();
|
||||
QVERIFY(options.width == 600 && options.height == 400 && options.resolution == 150);
|
||||
}
|
||||
// Without resampling, print sizes wait for a usable resolution too.
|
||||
for (const QString &unit : {QStringLiteral("Inches"), QStringLiteral("Centimeters")}) {
|
||||
for (const QString &invalid : {QStringLiteral("0"), QStringLiteral("-5"), QStringLiteral("inf")}) {
|
||||
Sheet held(1200, 800, 300);
|
||||
held.unit(unit);
|
||||
held.find<QCheckBox>("imageResample").click();
|
||||
held.type("imageResolution", invalid);
|
||||
const QString shownResolution = held.shown("imageResolution");
|
||||
held.type("imageWidth", "1");
|
||||
held.type("imageHeight", "2");
|
||||
QCOMPARE(held.shown("imageResolution"), shownResolution);
|
||||
QVERIFY(!held.find<QPushButton>("imageResize").isEnabled());
|
||||
held.type("imageResolution", "150");
|
||||
QCOMPARE(held.result(), QString("Result: 1,200 × 800 pixels"));
|
||||
held.find<QCheckBox>("imageResample").click();
|
||||
held.type("imageResolution", "300");
|
||||
QCOMPARE(held.result(), QString("Result: 2,400 × 1,600 pixels"));
|
||||
}
|
||||
}
|
||||
// Any finite positive resolution counts, even past Resize's range.
|
||||
for (const QString &unit : {QStringLiteral("Inches"), QStringLiteral("Centimeters")}) {
|
||||
Sheet low(12, 8, 300);
|
||||
low.type("imageResolution", "0.5");
|
||||
low.unit(unit);
|
||||
low.type("imageResolution", "150");
|
||||
QCOMPARE(low.result(), QString("Result: 3,600 × 2,400 pixels"));
|
||||
Sheet high(300, 200, 300);
|
||||
high.type("imageResolution", "12000");
|
||||
high.unit(unit);
|
||||
high.type("imageResolution", "6000");
|
||||
QCOMPARE(high.result(), QString("Result: 150 × 100 pixels"));
|
||||
}
|
||||
// Without resampling the pixels stay, across the toggle.
|
||||
Sheet shown(1200, 800, 300);
|
||||
shown.type("imageResolution", "0");
|
||||
shown.find<QCheckBox>("imageResample").click();
|
||||
shown.type("imageResolution", "150");
|
||||
QCOMPARE(shown.result(), QString("Result: 1,200 × 800 pixels"));
|
||||
shown.find<QCheckBox>("imageResample").click();
|
||||
shown.type("imageResolution", "300");
|
||||
QCOMPARE(shown.result(), QString("Result: 2,400 × 1,600 pixels"));
|
||||
}
|
||||
|
||||
void ImageSizeSheetTests::withoutResamplingOnlyTheResolutionMoves()
|
||||
@@ -195,6 +286,13 @@ void ImageSizeSheetTests::withoutResamplingOnlyTheResolutionMoves()
|
||||
QCOMPARE(shown.units(), (QStringList{"Pixels", "Percent", "Inches", "Centimeters"}));
|
||||
QCOMPARE(shown.find<QComboBox>("imageUnits").currentText(), QString("Centimeters"));
|
||||
QVERIFY(shown.find<QCheckBox>("imageLocked").isEnabled());
|
||||
// A print-size edit updates the last usable resolution.
|
||||
shown.find<QCheckBox>("imageResample").click();
|
||||
shown.type("imageHeight", "25.4");
|
||||
QCOMPARE(shown.shown("imageResolution"), QString("80"));
|
||||
shown.find<QCheckBox>("imageResample").click();
|
||||
shown.type("imageResolution", "160");
|
||||
QCOMPARE(shown.result(), QString("Result: 2,400 × 1,600 pixels"));
|
||||
}
|
||||
|
||||
void ImageSizeSheetTests::anInvalidSizeRestsResize()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QApplication>
|
||||
#include <QDir>
|
||||
#include <QLabel>
|
||||
#include <QMessageBox>
|
||||
#include <QStyle>
|
||||
#include <QStyleFactory>
|
||||
#include <QTemporaryDir>
|
||||
@@ -202,6 +203,10 @@ void OmarchyThemeTests::withoutAThemeTheBuiltInDarkApplies()
|
||||
QCOMPARE(QApplication::palette().color(QPalette::PlaceholderText), QColor(0x8a, 0x8a, 0x8a));
|
||||
QCOMPARE(QApplication::palette().color(QPalette::BrightText), QColor(0xff, 0x9f, 0x0a));
|
||||
QCOMPARE(QApplication::style()->objectName(), QString("fusion"));
|
||||
// Swift's d62a132: alerts are dark too.
|
||||
QMessageBox alert(QMessageBox::Warning, QStringLiteral("Title"), QStringLiteral("Text"));
|
||||
QCOMPARE(alert.palette().color(QPalette::Window), QColor(0x24, 0x24, 0x24));
|
||||
QCOMPARE(alert.style()->objectName(), QString("fusion"));
|
||||
Swatch swatch;
|
||||
QCOMPARE(swatch.pixel(), qRgb(0x24, 0x24, 0x24));
|
||||
// A theme appearing later is taken up, unnamed or not.
|
||||
|
||||
@@ -0,0 +1,273 @@
|
||||
#include "ExternalChangeFixtures.h"
|
||||
#include "IO/RecentProjects.h"
|
||||
#include "MenuFixtures.h"
|
||||
#include <QApplication>
|
||||
#include <QLabel>
|
||||
#include <QMenu>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
|
||||
// Swift's 73dc0f4: File > Open Recent.
|
||||
namespace {
|
||||
QString canonical(const QString &path)
|
||||
{
|
||||
return QFileInfo(path).canonicalFilePath();
|
||||
}
|
||||
|
||||
// A saved project named `name` in `root`.
|
||||
QString project(const QTemporaryDir &root, const QString &name)
|
||||
{
|
||||
const QString path = root.filePath(name + QStringLiteral(".comp"));
|
||||
if (!QDir().rename(savedProject(root), path))
|
||||
throw std::runtime_error("could not name " + path.toStdString());
|
||||
return path;
|
||||
}
|
||||
|
||||
QStringList titles(QMenu &menu)
|
||||
{
|
||||
QStringList shown;
|
||||
for (const QAction *entry : menu.actions())
|
||||
shown << (entry->isSeparator() ? QStringLiteral("-") : entry->text() + (entry->isEnabled() ? "" : " (off)"));
|
||||
return shown;
|
||||
}
|
||||
}
|
||||
|
||||
class OpenRecentTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void init();
|
||||
void opensAndSavesAreNotedNewestFirst();
|
||||
void theListKeepsTenAndOneEntryAPlace();
|
||||
void movedProjectsLeaveTheList();
|
||||
void theMenuListsOpensAndClears();
|
||||
void aProjectDeletedWhileListedIsNamed();
|
||||
void comingBackToTheAppChecksAgain();
|
||||
void onlyWhatLandsIsNoted();
|
||||
void aReloadAfterSavingNotesItsPlaceLast();
|
||||
|
||||
private:
|
||||
QTemporaryDir m_stored;
|
||||
};
|
||||
|
||||
void OpenRecentTests::initTestCase()
|
||||
{
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
// Starts from what earlier runs stored, missing places dropped.
|
||||
QVERIFY(m_stored.isValid());
|
||||
QSettings().setValue(QStringLiteral("recentProjects"), QStringList{m_stored.filePath("Missing.comp"), m_stored.path()});
|
||||
QCOMPARE(RecentProjects::shared().paths(), QStringList{m_stored.path()});
|
||||
}
|
||||
|
||||
void OpenRecentTests::init()
|
||||
{
|
||||
RecentProjects::shared().clear();
|
||||
}
|
||||
|
||||
void OpenRecentTests::opensAndSavesAreNotedNewestFirst()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
const QString opened = project(root, QStringLiteral("Opened"));
|
||||
EditorSession session;
|
||||
ProjectController controller(session);
|
||||
QVERIFY(answered([&](auto done) { controller.open(opened + QStringLiteral("/"), done); }));
|
||||
QCOMPARE(RecentProjects::shared().paths(), QStringList{canonical(opened)});
|
||||
// Saved elsewhere: the copy comes first.
|
||||
const QString copy = root.filePath(QStringLiteral("Copy.comp"));
|
||||
DialogDesk desk;
|
||||
desk.replies = {copy};
|
||||
QVERIFY(answered([&](auto done) { controller.save(true, done); }));
|
||||
QCOMPARE(RecentProjects::shared().paths(), (QStringList{canonical(copy), canonical(opened)}));
|
||||
// Opened again: back to the front, listed once.
|
||||
QVERIFY(answered([&](auto done) { controller.open(opened, done); }));
|
||||
QCOMPARE(RecentProjects::shared().paths(), (QStringList{canonical(opened), canonical(copy)}));
|
||||
// A failed open notes nothing.
|
||||
QDir(root.filePath("Broken.comp")).mkpath(".");
|
||||
desk.replies = {"OK"};
|
||||
QVERIFY(!answered([&](auto done) { controller.open(root.filePath("Broken.comp"), done); }));
|
||||
QCOMPARE(RecentProjects::shared().paths(), (QStringList{canonical(opened), canonical(copy)}));
|
||||
QCOMPARE(QSettings().value(QStringLiteral("recentProjects")).toStringList(), RecentProjects::shared().paths());
|
||||
}
|
||||
|
||||
void OpenRecentTests::theListKeepsTenAndOneEntryAPlace()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
QStringList made;
|
||||
for (int index = 0; index < 12; ++index) {
|
||||
const QString place = root.filePath(QString::number(index));
|
||||
QVERIFY(QDir().mkpath(place));
|
||||
made.prepend(canonical(place));
|
||||
RecentProjects::shared().note(place);
|
||||
}
|
||||
QCOMPARE(RecentProjects::shared().paths(), made.mid(0, 10));
|
||||
// A link to a listed place is that place.
|
||||
QVERIFY(QFile::link(root.filePath("5"), root.filePath("link")));
|
||||
RecentProjects::shared().note(root.filePath("link"));
|
||||
QCOMPARE(RecentProjects::shared().paths().front(), made[6]);
|
||||
QCOMPARE(RecentProjects::shared().paths().size(), 10);
|
||||
}
|
||||
|
||||
void OpenRecentTests::movedProjectsLeaveTheList()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
const QString kept = project(root, QStringLiteral("Kept"));
|
||||
const QString moved = project(root, QStringLiteral("Moved"));
|
||||
RecentProjects::shared().note(kept);
|
||||
RecentProjects::shared().note(moved);
|
||||
int announced = 0;
|
||||
connect(&RecentProjects::shared(), &RecentProjects::changed, this, [&] { ++announced; });
|
||||
QVERIFY(QDir().rename(moved, root.filePath("Elsewhere.comp")));
|
||||
RecentProjects::shared().refresh();
|
||||
QCOMPARE(RecentProjects::shared().paths(), QStringList{canonical(kept)});
|
||||
QCOMPARE(announced, 1);
|
||||
// Nothing changed: no signal.
|
||||
RecentProjects::shared().refresh();
|
||||
QCOMPARE(announced, 1);
|
||||
// Back in place, it is listed again.
|
||||
QVERIFY(QDir().rename(root.filePath("Elsewhere.comp"), moved));
|
||||
RecentProjects::shared().refresh();
|
||||
QCOMPARE(RecentProjects::shared().paths().size(), 2);
|
||||
disconnect(&RecentProjects::shared(), nullptr, this, nullptr);
|
||||
}
|
||||
|
||||
void OpenRecentTests::theMenuListsOpensAndClears()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
const QString first = project(root, QStringLiteral("First"));
|
||||
const QString second = project(root, QStringLiteral("Second"));
|
||||
Bar bar;
|
||||
QMenu &recent = *bar.action("openRecent").menu();
|
||||
// Swift's place: right after Open Project…, no key.
|
||||
const QList<QAction *> file = bar.menus->parent()->findChildren<QMenu *>(QString(), Qt::FindDirectChildrenOnly).front()->actions();
|
||||
QCOMPARE(file.indexOf(&bar.action("openRecent")), file.indexOf(&bar.action("openProject")) + 1);
|
||||
QCOMPARE(file.indexOf(&bar.action("importImages")), file.indexOf(&bar.action("openRecent")) + 1);
|
||||
QCOMPARE(bar.action("openRecent").text(), QString("Open Recent"));
|
||||
QVERIFY(bar.action("openRecent").shortcut().isEmpty());
|
||||
QCOMPARE(titles(recent), (QStringList{"-", "Clear Menu (off)"}));
|
||||
RecentProjects::shared().note(first);
|
||||
RecentProjects::shared().note(second);
|
||||
QCOMPARE(titles(recent), (QStringList{"Second", "First", "-", "Clear Menu"}));
|
||||
// An entry opens its project in a tab.
|
||||
recent.actions()[1]->trigger();
|
||||
QTRY_VERIFY(bar.session().projectPath().has_value());
|
||||
QCOMPARE(bar.session().projectPath().value(), canonical(first));
|
||||
QCOMPARE(titles(recent), (QStringList{"First", "Second", "-", "Clear Menu"}));
|
||||
// Busy holds the whole menu, as Open Project.
|
||||
QVERIFY(bar.action("openRecent").isEnabled());
|
||||
bar.session().setIsProjectBusy(true);
|
||||
QVERIFY(!bar.action("openRecent").isEnabled());
|
||||
bar.session().setIsProjectBusy(false);
|
||||
QVERIFY(bar.action("openRecent").isEnabled());
|
||||
bar.action("clearRecent").trigger();
|
||||
QCOMPARE(titles(recent), (QStringList{"-", "Clear Menu (off)"}));
|
||||
QVERIFY(RecentProjects::shared().paths().isEmpty());
|
||||
// An ampersand is part of the name, no mnemonic.
|
||||
RecentProjects::shared().note(project(root, QStringLiteral("R&D")));
|
||||
QCOMPARE(recent.actions()[0]->text(), QString("R&&D"));
|
||||
QCOMPARE(recent.actions()[0]->iconText(), QString("R&D"));
|
||||
}
|
||||
|
||||
void OpenRecentTests::aProjectDeletedWhileListedIsNamed()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
const QString gone = project(root, QStringLiteral("<b>Gone"));
|
||||
RecentProjects::shared().note(gone);
|
||||
const QString listed = canonical(gone);
|
||||
QVERIFY(QDir(gone).removeRecursively());
|
||||
EditorSession session;
|
||||
ProjectController controller(session);
|
||||
DialogDesk desk;
|
||||
desk.replies = {"OK"};
|
||||
// The name shows literally, never as markup.
|
||||
desk.note = [] {
|
||||
for (QWidget *top : QApplication::topLevelWidgets()) {
|
||||
if (auto *label = top->findChild<QLabel *>(QStringLiteral("qt_msgbox_informativelabel")))
|
||||
return QStringLiteral("format=%1").arg(int(label->textFormat()));
|
||||
}
|
||||
return QStringLiteral("no label");
|
||||
};
|
||||
QVERIFY(!answered([&](auto done) { controller.open(listed, done); }));
|
||||
QCOMPARE(desk.seen.size(), 1);
|
||||
QVERIFY2(desk.seen[0].startsWith("alert|2|Couldn’t open the project|The file “<b>Gone.comp” couldn’t be opened because there is no such file.")
|
||||
&& desk.seen[0].endsWith("|format=0"),
|
||||
qPrintable(desk.seen[0]));
|
||||
QVERIFY(RecentProjects::shared().paths().isEmpty());
|
||||
QVERIFY(!session.isProjectBusy());
|
||||
}
|
||||
|
||||
void OpenRecentTests::comingBackToTheAppChecksAgain()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
const QString moved = project(root, QStringLiteral("Moved"));
|
||||
RecentProjects::shared().note(moved);
|
||||
QVERIFY(QDir().rename(moved, root.filePath("Elsewhere.comp")));
|
||||
// Only becoming active looks again.
|
||||
emit qGuiApp->applicationStateChanged(Qt::ApplicationInactive);
|
||||
QCOMPARE(RecentProjects::shared().paths().size(), 1);
|
||||
emit qGuiApp->applicationStateChanged(Qt::ApplicationActive);
|
||||
QVERIFY(RecentProjects::shared().paths().isEmpty());
|
||||
}
|
||||
|
||||
void OpenRecentTests::onlyWhatLandsIsNoted()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
const QString kept = project(root, QStringLiteral("Kept")), other = project(root, QStringLiteral("Other"));
|
||||
const QString incoming = project(root, QStringLiteral("Incoming"));
|
||||
EditorSession session;
|
||||
ProjectController controller(session);
|
||||
QVERIFY(answered([&](auto done) { controller.open(kept, done); }));
|
||||
RecentProjects::shared().note(other);
|
||||
const QStringList before{canonical(other), canonical(kept)};
|
||||
// Replacing unsaved work cancelled: the incoming project stays out.
|
||||
session.addBlankLayer();
|
||||
DialogDesk desk;
|
||||
desk.replies = {"Cancel", "<cancel>"};
|
||||
QVERIFY(!answered([&](auto done) { controller.open(incoming, done); }));
|
||||
QCOMPARE(RecentProjects::shared().paths(), before);
|
||||
// Save As cancelled.
|
||||
QVERIFY(!answered([&](auto done) { controller.save(true, done); }));
|
||||
QCOMPARE(RecentProjects::shared().paths(), before);
|
||||
// A failed save over a listed place promotes nothing.
|
||||
QVERIFY(QFile::setPermissions(root.path(), QFileDevice::ReadOwner | QFileDevice::ExeOwner));
|
||||
desk.replies = {"OK"};
|
||||
QVERIFY(!answered([&](auto done) { controller.save(false, done); }));
|
||||
QVERIFY(QFile::setPermissions(root.path(), QFileDevice::ReadOwner | QFileDevice::WriteOwner | QFileDevice::ExeOwner));
|
||||
QCOMPARE(RecentProjects::shared().paths(), before);
|
||||
// A save is noted once it lands.
|
||||
std::optional<bool> saved;
|
||||
controller.save(false, [&](bool value) { saved = value; });
|
||||
QVERIFY(controller.externalChanges.saving);
|
||||
QCOMPARE(RecentProjects::shared().paths(), before);
|
||||
QTRY_VERIFY(saved.has_value());
|
||||
QVERIFY(saved.value());
|
||||
QCOMPARE(RecentProjects::shared().paths(), (QStringList{canonical(kept), canonical(other)}));
|
||||
QCOMPARE(QSettings().value(QStringLiteral("recentProjects")).toStringList(), RecentProjects::shared().paths());
|
||||
}
|
||||
|
||||
void OpenRecentTests::aReloadAfterSavingNotesItsPlaceLast()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
const QString reopened = project(root, QStringLiteral("Reopened")), other = project(root, QStringLiteral("Other"));
|
||||
EditorSession session;
|
||||
ProjectController controller(session);
|
||||
QVERIFY(answered([&](auto done) { controller.open(reopened, done); }));
|
||||
RecentProjects::shared().note(other);
|
||||
// Another place noted as the save lands, before the reload.
|
||||
bool armed = true;
|
||||
connect(&RecentProjects::shared(), &RecentProjects::changed, this, [&] {
|
||||
if (armed && RecentProjects::shared().paths().front() == canonical(reopened)) {
|
||||
armed = false;
|
||||
RecentProjects::shared().note(other);
|
||||
}
|
||||
});
|
||||
session.addBlankLayer();
|
||||
DialogDesk desk;
|
||||
desk.replies = {"Save"};
|
||||
QVERIFY(answered([&](auto done) { controller.open(reopened, done); }));
|
||||
disconnect(&RecentProjects::shared(), nullptr, this, nullptr);
|
||||
QVERIFY(!armed);
|
||||
QCOMPARE(RecentProjects::shared().paths(), (QStringList{canonical(reopened), canonical(other)}));
|
||||
}
|
||||
|
||||
QTEST_MAIN(OpenRecentTests)
|
||||
#include "OpenRecentTests.moc"
|
||||
@@ -246,11 +246,11 @@ void ProjectControllerTests::aCompletionNeverRunsInsideTheCall()
|
||||
int calls = 0;
|
||||
controller.save(false, [&](bool) { calls += 1; });
|
||||
controller.confirmQuit([&](bool) { calls += 1; });
|
||||
controller.open(QString("/nowhere.comp"), [&](bool) { calls += 1; });
|
||||
QCOMPARE(calls, 0);
|
||||
DialogDesk desk;
|
||||
desk.replies = {"OK"};
|
||||
QTest::ignoreMessage(QtWarningMsg, QRegularExpression("^Couldn’t open the project: .+"));
|
||||
controller.open(QString("/nowhere.comp"), [&](bool) { calls += 1; });
|
||||
QCOMPARE(calls, 0);
|
||||
QTRY_COMPARE(calls, 3);
|
||||
}
|
||||
|
||||
|
||||
@@ -155,9 +155,12 @@ void ProjectOpenTests::aDamagedOrMissingProjectKeepsTheLiveDocument()
|
||||
QCOMPARE(desk.seen, (QStringList{damaged + "|busy"}));
|
||||
QCOMPARE(described(session), before);
|
||||
QVERIFY(session.isModified() && !session.isProjectBusy());
|
||||
QTest::ignoreMessage(QtWarningMsg, "Couldn’t open the project: This is not a valid OmaPhoto project, or its metadata is damaged.");
|
||||
// A missing project is named.
|
||||
QTest::ignoreMessage(QtWarningMsg, "Couldn’t open the project: The file “Nowhere.comp” couldn’t be opened because there is no such file.");
|
||||
QCOMPARE(answered([&](auto done) { controller.open(folder.filePath("Nowhere.comp"), done); }), std::optional(false));
|
||||
QCOMPARE(desk.seen.size(), 2);
|
||||
QVERIFY2(desk.seen[1].startsWith("alert|2|Couldn’t open the project|The file “Nowhere.comp” couldn’t be opened because there is no such file.|"),
|
||||
qPrintable(desk.seen[1]));
|
||||
QCOMPARE(described(session), before);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "IO/RecentProjects.h"
|
||||
#include <QDir>
|
||||
#include <QProcess>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
#include <QTemporaryDir>
|
||||
#include <QtTest>
|
||||
#include <cstdio>
|
||||
|
||||
// The list without a GUI application: no activation to follow.
|
||||
class RecentProjectsCoreTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void theListWorksWithoutAGuiApplication();
|
||||
void theListOutlivesItsProcess();
|
||||
};
|
||||
|
||||
namespace {
|
||||
// One step in a fresh process: its output.
|
||||
QString step(const QString &what)
|
||||
{
|
||||
QProcess child;
|
||||
QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
|
||||
environment.insert(QStringLiteral("RECENT_STEP"), what);
|
||||
child.setProcessEnvironment(environment);
|
||||
child.start(QCoreApplication::applicationFilePath(), {});
|
||||
if (!child.waitForFinished(30'000) || child.exitStatus() != QProcess::NormalExit || child.exitCode() != 0)
|
||||
throw std::runtime_error("the step failed: " + what.toStdString());
|
||||
return QString::fromUtf8(child.readAllStandardOutput());
|
||||
}
|
||||
}
|
||||
|
||||
void RecentProjectsCoreTests::theListWorksWithoutAGuiApplication()
|
||||
{
|
||||
QVERIFY(!qobject_cast<QGuiApplication *>(QCoreApplication::instance()));
|
||||
QTemporaryDir root;
|
||||
RecentProjects::shared().clear();
|
||||
RecentProjects::shared().note(root.path());
|
||||
QCOMPARE(RecentProjects::shared().paths(), QStringList{QDir(root.path()).canonicalPath()});
|
||||
QCOMPARE(QSettings().value(QStringLiteral("recentProjects")).toStringList(), RecentProjects::shared().paths());
|
||||
RecentProjects::shared().clear();
|
||||
QVERIFY(RecentProjects::shared().paths().isEmpty());
|
||||
}
|
||||
|
||||
void RecentProjectsCoreTests::theListOutlivesItsProcess()
|
||||
{
|
||||
QTemporaryDir first, second;
|
||||
step(QStringLiteral("clear"));
|
||||
step(QStringLiteral("note:") + first.path());
|
||||
step(QStringLiteral("note:") + second.path());
|
||||
// A later launch reads what the earlier ones stored.
|
||||
QCOMPARE(step(QStringLiteral("read")), QDir(second.path()).canonicalPath() + "\n" + QDir(first.path()).canonicalPath() + "\n");
|
||||
step(QStringLiteral("clear"));
|
||||
QCOMPARE(step(QStringLiteral("read")), QString());
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
QStandardPaths::setTestModeEnabled(true);
|
||||
// A child run does one step, then exits.
|
||||
const QString what = qEnvironmentVariable("RECENT_STEP");
|
||||
if (what == QLatin1String("clear")) {
|
||||
RecentProjects::shared().clear();
|
||||
} else if (what.startsWith(QLatin1String("note:"))) {
|
||||
RecentProjects::shared().note(what.mid(5));
|
||||
} else if (what == QLatin1String("read")) {
|
||||
for (const QString &path : RecentProjects::shared().paths())
|
||||
std::printf("%s\n", qPrintable(path));
|
||||
} else if (!what.isEmpty()) {
|
||||
return 2;
|
||||
} else {
|
||||
RecentProjectsCoreTests tests;
|
||||
return QTest::qExec(&tests, argc, argv);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
#include "RecentProjectsCoreTests.moc"
|
||||
@@ -166,6 +166,8 @@ compositor_test(ProjectReceiveTests)
|
||||
compositor_test(ProjectReplaceTests)
|
||||
compositor_test(ProjectWorkspaceFlowTests)
|
||||
compositor_test(QuitWhileEditingTextTests)
|
||||
compositor_test(OpenRecentTests)
|
||||
compositor_test(RecentProjectsCoreTests)
|
||||
compositor_test(ProjectWorkspaceSignalTests)
|
||||
compositor_test(ProjectWorkspaceTests)
|
||||
compositor_test(ProjectWorkspaceViewTests)
|
||||
|
||||
Reference in new issue
Block a user