diff --git a/CMakeLists.txt b/CMakeLists.txt index 792f4da..8a0e073 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,113 +1,116 @@ # KDE Application Version, managed by release script set(KDE_APPLICATIONS_VERSION_MAJOR "19") set(KDE_APPLICATIONS_VERSION_MINOR "07") set(KDE_APPLICATIONS_VERSION_MICRO "70") set(KDE_APPLICATIONS_VERSION "${KDE_APPLICATIONS_VERSION_MAJOR}.${KDE_APPLICATIONS_VERSION_MINOR}.${KDE_APPLICATIONS_VERSION_MICRO}") set(SPECTACLE_VERSION ${KDE_APPLICATIONS_VERSION}) # minimum requirements cmake_minimum_required (VERSION 3.0 FATAL_ERROR) # Spectacle project project(Spectacle VERSION ${SPECTACLE_VERSION}) set(QT_MIN_VERSION "5.10.0") set(KF5_MIN_VERSION "5.53.0") set(PLASMA_MIN_VERSION "5.12.0") find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE) set( CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake/modules ${CMAKE_MODULE_PATH} ${ECM_MODULE_PATH} ) # set up kf5 include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(ECMInstallIcons) include(ECMSetupVersion) include(FeatureSummary) include(ECMQtDeclareLoggingCategory) +include(ECMAddTests) find_package( Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED Core Concurrent Widgets DBus PrintSupport Quick + Test ) find_package( KF5 ${KF5_MIN_VERSION} REQUIRED CoreAddons WidgetsAddons DBusAddons Notifications Config I18n KIO XmlGui WindowSystem DocTools Declarative NewStuff ) # optional components find_package(KF5Kipi) if (KF5Kipi_FOUND) set(KIPI_FOUND 1) endif () find_package(KDEExperimentalPurpose) if (KDEExperimentalPurpose_FOUND) set(PURPOSE_FOUND 1) endif() find_package(XCB COMPONENTS XFIXES IMAGE UTIL CURSOR) set(XCB_COMPONENTS_ERRORS FALSE) if (XCB_FOUND) find_package(Qt5X11Extras ${QT_MIN_VERSION} REQUIRED) endif() set(XCB_COMPONENTS_FOUND TRUE) if(NOT XCB_XFIXES_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-XFIXES ") set(XCB_COMPONENTS_FOUND FALSE) endif() if(NOT XCB_IMAGE_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-IMAGE ") set(XCB_COMPONENTS_FOUND FALSE) endif() if(NOT XCB_UTIL_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-UTIL ") set(XCB_COMPONENTS_FOUND FALSE) endif() if(NOT XCB_CURSOR_FOUND) set(XCB_COMPONENTS_ERRORS "${XCB_COMPONENTS_ERRORS} XCB-CURSOR ") set(XCB_COMPONENTS_FOUND FALSE) endif() # fail build if none of the platform backends can be found if (NOT XCB_FOUND OR NOT XCB_COMPONENTS_FOUND) message(FATAL_ERROR "No suitable backend platform was found. Currently supported platforms are: XCB Components Required: ${XCB_COMPONENTS_ERRORS}") endif() # hand off to subdirectories add_subdirectory(src) add_subdirectory(dbus) add_subdirectory(desktop) add_subdirectory(icons) add_subdirectory(doc) +add_subdirectory(tests) install( FILES spectacle.categories DESTINATION ${KDE_INSTALL_CONFDIR} ) # summaries feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/ExportManager.cpp b/src/ExportManager.cpp index 23fcc0b..100f327 100644 --- a/src/ExportManager.cpp +++ b/src/ExportManager.cpp @@ -1,553 +1,558 @@ /* * Copyright (C) 2015 Boudhayan Gupta * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "ExportManager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "SpectacleConfig.h" ExportManager::ExportManager(QObject *parent) : QObject(parent), mSavePixmap(QPixmap()), mTempFile(QUrl()), mTempDir(nullptr) { connect(this, &ExportManager::imageSaved, [this](const QUrl &savedAt) { SpectacleConfig::instance()->setLastSaveFile(savedAt); }); } ExportManager::~ExportManager() { delete mTempDir; } ExportManager* ExportManager::instance() { static ExportManager instance; return &instance; } // screenshot pixmap setter and getter QPixmap ExportManager::pixmap() const { return mSavePixmap; } void ExportManager::setWindowTitle(const QString &windowTitle) { mWindowTitle = windowTitle; } QString ExportManager::windowTitle() const { return mWindowTitle; } ImageGrabber::GrabMode ExportManager::grabMode() const { return mGrabMode; } void ExportManager::setGrabMode(const ImageGrabber::GrabMode &grabMode) { mGrabMode = grabMode; } void ExportManager::setPixmap(const QPixmap &pixmap) { mSavePixmap = pixmap; // reset our saved tempfile if (mTempFile.isValid()) { mUsedTempFileNames.append(mTempFile); QFile file(mTempFile.toLocalFile()); file.remove(); mTempFile = QUrl(); } } void ExportManager::updatePixmapTimestamp() { mPixmapTimestamp = QDateTime::currentDateTime(); } +void ExportManager::setTimestamp(const QDateTime ×tamp) +{ + mPixmapTimestamp = timestamp; +} + // native file save helpers QString ExportManager::defaultSaveLocation() const { QString savePath = SpectacleConfig::instance()->defaultSaveLocation(); if (savePath.isEmpty() || savePath.isNull()) { savePath = QDir::homePath(); } savePath = QDir::cleanPath(savePath); QDir savePathDir(savePath); if (!(savePathDir.exists())) { savePathDir.mkpath(QStringLiteral(".")); SpectacleConfig::instance()->setDefaultSaveLocation(savePath); } return savePath; } QUrl ExportManager::getAutosaveFilename() { const QString baseDir = defaultSaveLocation(); const QDir baseDirPath(baseDir); const QString filename = makeAutosaveFilename(); const QString fullpath = autoIncrementFilename(baseDirPath.filePath(filename), SpectacleConfig::instance()->saveImageFormat(), &ExportManager::isFileExists); const QUrl fileNameUrl = QUrl::fromUserInput(fullpath); if (fileNameUrl.isValid()) { return fileNameUrl; } else { return QUrl(); } } QString ExportManager::truncatedFilename(QString const &filename) { QString result = filename; constexpr auto maxFilenameLength = 255; constexpr auto maxExtensionLength = 5; // For example, ".jpeg" constexpr auto maxCounterLength = 20; // std::numeric_limits::max() == 18446744073709551615 constexpr auto maxLength = maxFilenameLength - maxCounterLength - maxExtensionLength; result.truncate(maxLength); return result; } QString ExportManager::makeAutosaveFilename() { return formatFilename(SpectacleConfig::instance()->autoSaveFilenameFormat()); } QString ExportManager::formatFilename(const QString &nameTemplate) { const QDateTime timestamp = mPixmapTimestamp; QString baseName = nameTemplate; const QString baseDir = defaultSaveLocation(); QString title; if (mGrabMode == ImageGrabber::GrabMode::ActiveWindow || mGrabMode == ImageGrabber::GrabMode::TransientWithParent || mGrabMode == ImageGrabber::GrabMode::WindowUnderCursor) { title = mWindowTitle.replace(QLatin1String("/"), QLatin1String("_")); // POSIX doesn't allow "/" in filenames } else { // Remove '%T' with separators around it const auto wordSymbol = QStringLiteral(R"(\p{L}\p{M}\p{N})"); const auto separator = QStringLiteral("([^%1]+)").arg(wordSymbol); const auto re = QRegularExpression(QStringLiteral("(.*?)(%1%T|%T%1)(.*?)").arg(separator)); baseName.replace(re, QStringLiteral(R"(\1\5)")); } QString result = baseName.replace(QLatin1String("%Y"), timestamp.toString(QStringLiteral("yyyy"))) .replace(QLatin1String("%y"), timestamp.toString(QStringLiteral("yy"))) .replace(QLatin1String("%M"), timestamp.toString(QStringLiteral("MM"))) .replace(QLatin1String("%D"), timestamp.toString(QStringLiteral("dd"))) .replace(QLatin1String("%H"), timestamp.toString(QStringLiteral("hh"))) .replace(QLatin1String("%m"), timestamp.toString(QStringLiteral("mm"))) .replace(QLatin1String("%S"), timestamp.toString(QStringLiteral("ss"))) .replace(QLatin1String("%T"), title); // check if basename includes %[N]d token for sequential file numbering QRegularExpression paddingRE; paddingRE.setPattern(QStringLiteral("%(\\d*)d")); QRegularExpressionMatch paddingMatch; while (result.indexOf(paddingRE, 0, &paddingMatch) > -1) { int highestFileNumber = 0; // determine padding value int paddedLength = 1; if (!paddingMatch.captured(1).isEmpty()) { paddedLength = paddingMatch.captured(1).toInt(); } // search save directory for files QDir dir(baseDir); const QStringList fileNames = dir.entryList(QDir::Files, QDir::Name); // if there are files in the directory... if (fileNames.length() > 0) { QString resultCopy = result; QRegularExpression fileNumberRE; const QString replacement = QStringLiteral("(\\d{").append(QString::number(paddedLength)).append(QLatin1String(",})")); const QString fullNameMatch = QStringLiteral("^").append(resultCopy.replace(paddingMatch.captured(),replacement)).append(QStringLiteral("\\..*$")); fileNumberRE.setPattern(fullNameMatch); // ... check the file names for string matching token with padding specified in result const QStringList filteredFiles = fileNames.filter(fileNumberRE); // if there are files in the direcory that look like the file name with sequential numbering if (filteredFiles.length() > 0) { // loop through filtered file names looking for highest number for (const QString &filteredFile: filteredFiles) { int currentFileNumber = fileNumberRE.match(filteredFile).captured(1).toInt(); if (currentFileNumber > highestFileNumber) { highestFileNumber = currentFileNumber; } } } } // replace placeholder with next number padded const QString nextFileNumberPadded = QString::number(highestFileNumber + 1).rightJustified(paddedLength, QLatin1Char('0')); result = result.replace(paddingMatch.captured(), nextFileNumberPadded); } // Remove leading and trailing '/' while (result.startsWith(QLatin1Char('/'))) { result.remove(0, 1); } while (result.endsWith(QLatin1Char('/'))) { result.chop(1); } if (result.isEmpty()) { result = SpectacleConfig::instance()->defaultFilename(); } return truncatedFilename(result); } QString ExportManager::autoIncrementFilename(const QString &baseName, const QString &extension, FileNameAlreadyUsedCheck isFileNameUsed) { QString result = truncatedFilename(baseName) + QLatin1Literal(".") + extension; if (!((this->*isFileNameUsed)(QUrl::fromUserInput(result)))) { return result; } QString fileNameFmt = truncatedFilename(baseName) + QStringLiteral("-%1."); for (quint64 i = 1; i < std::numeric_limits::max(); i++) { result = fileNameFmt.arg(i) + extension; if (!((this->*isFileNameUsed)(QUrl::fromUserInput(result)))) { return result; } } // unlikely this will ever happen, but just in case we've run // out of numbers result = fileNameFmt.arg(QStringLiteral("OVERFLOW-") + QString::number(qrand() % 10000)); return truncatedFilename(result) + extension; } QString ExportManager::makeSaveMimetype(const QUrl &url) { QMimeDatabase mimedb; QString type = mimedb.mimeTypeForUrl(url).preferredSuffix(); if (type.isEmpty()) { return SpectacleConfig::instance()->saveImageFormat(); } return type; } bool ExportManager::writeImage(QIODevice *device, const QByteArray &format) { QImageWriter imageWriter(device, format); imageWriter.setQuality(SpectacleConfig::instance()->compressionQuality()); if (!(imageWriter.canWrite())) { emit errorMessage(i18n("QImageWriter cannot write image: %1", imageWriter.errorString())); return false; } return imageWriter.write(mSavePixmap.toImage()); } bool ExportManager::localSave(const QUrl &url, const QString &mimetype) { // Create save directory if it doesn't exist const QUrl dirPath(url.adjusted(QUrl::RemoveFilename)); const QDir dir(dirPath.path()); if (!dir.mkpath(QStringLiteral("."))) { emit errorMessage(xi18nc("@info", "Cannot save screenshot because creating " "the directory failed:%1", dirPath.path())); return false; } QFile outputFile(url.toLocalFile()); outputFile.open(QFile::WriteOnly); if(!writeImage(&outputFile, mimetype.toLatin1())) { emit errorMessage(i18n("Cannot save screenshot. Error while writing file.")); return false; } return true; } bool ExportManager::remoteSave(const QUrl &url, const QString &mimetype) { // Check if remote save directory exists const QUrl dirPath(url.adjusted(QUrl::RemoveFilename)); KIO::ListJob *listJob = KIO::listDir(dirPath); listJob->exec(); if (listJob->error() != KJob::NoError) { // Create remote save directory KIO::MkpathJob *mkpathJob = KIO::mkpath(dirPath, QUrl(defaultSaveLocation())); mkpathJob->exec(); if (mkpathJob->error() != KJob::NoError) { emit errorMessage(xi18nc("@info", "Cannot save screenshot because creating the " "remote directory failed:%1", dirPath.path())); return false; } } QTemporaryFile tmpFile; if (tmpFile.open()) { if(!writeImage(&tmpFile, mimetype.toLatin1())) { emit errorMessage(i18n("Cannot save screenshot. Error while writing temporary local file.")); return false; } KIO::FileCopyJob *uploadJob = KIO::file_copy(QUrl::fromLocalFile(tmpFile.fileName()), url); uploadJob->exec(); if (uploadJob->error() != KJob::NoError) { emit errorMessage(i18n("Unable to save image. Could not upload file to remote location.")); return false; } return true; } return false; } QUrl ExportManager::tempSave(const QString &mimetype) { // if we already have a temp file saved, use that if (mTempFile.isValid()) { if (QFile(mTempFile.toLocalFile()).exists()) { return mTempFile; } } if (!mTempDir) { mTempDir = new QTemporaryDir(QDir::tempPath() + QDir::separator() + QStringLiteral("Spectacle.XXXXXX")); } if (mTempDir && mTempDir->isValid()) { // create the temporary file itself with normal file name and also unique one for this session // supports the use-case of creating multiple screenshots in a row // and exporting them to the same destination e.g. via clipboard, // where the temp file name is used as filename suggestion const QString baseFileName = mTempDir->path() + QDir::separator() + makeAutosaveFilename(); const QString fileName = autoIncrementFilename(baseFileName, mimetype, &ExportManager::isTempFileAlreadyUsed); QFile tmpFile(fileName); if (tmpFile.open(QFile::WriteOnly)) { if(writeImage(&tmpFile, mimetype.toLatin1())) { mTempFile = QUrl::fromLocalFile(tmpFile.fileName()); // try to make sure 3rd-party which gets the url of the temporary file e.g. on export // properly treats this as readonly, also hide from other users tmpFile.setPermissions(QFile::ReadUser); return mTempFile; } } } emit errorMessage(i18n("Cannot save screenshot. Error while writing temporary local file.")); return QUrl(); } bool ExportManager::save(const QUrl &url) { if (!(url.isValid())) { emit errorMessage(i18n("Cannot save screenshot. The save filename is invalid.")); return false; } QString mimetype = makeSaveMimetype(url); if (url.isLocalFile()) { return localSave(url, mimetype); } return remoteSave(url, mimetype); } bool ExportManager::isFileExists(const QUrl &url) const { if (!(url.isValid())) { return false; } KIO::StatJob * existsJob = KIO::stat(url, KIO::StatJob::DestinationSide, 0); existsJob->exec(); return (existsJob->error() == KJob::NoError); } bool ExportManager::isTempFileAlreadyUsed(const QUrl &url) const { return mUsedTempFileNames.contains(url); } // save slots void ExportManager::doSave(const QUrl &url, bool notify) { if (mSavePixmap.isNull()) { emit errorMessage(i18n("Cannot save an empty screenshot image.")); return; } QUrl savePath = url.isValid() ? url : getAutosaveFilename(); if (save(savePath)) { QDir dir(savePath.path()); dir.cdUp(); SpectacleConfig::instance()->setLastSaveFile(savePath); emit imageSaved(savePath); if (notify) { emit forceNotify(savePath); } } } bool ExportManager::doSaveAs(QWidget *parentWindow, bool notify) { QStringList supportedFilters; SpectacleConfig *config = SpectacleConfig::instance(); // construct the supported mimetype list Q_FOREACH (auto mimeType, QImageWriter::supportedMimeTypes()) { supportedFilters.append(QString::fromUtf8(mimeType).trimmed()); } // construct the file name const QString filenameExtension = SpectacleConfig::instance()->saveImageFormat(); const QString mimetype = QMimeDatabase().mimeTypeForFile(QStringLiteral("~/fakefile.") + filenameExtension, QMimeDatabase::MatchExtension).name(); QFileDialog dialog(parentWindow); dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setFileMode(QFileDialog::AnyFile); dialog.setDirectoryUrl(config->lastSaveAsLocation()); dialog.selectFile(makeAutosaveFilename() + QStringLiteral(".") + filenameExtension); dialog.setDefaultSuffix(QStringLiteral(".") + filenameExtension); dialog.setMimeTypeFilters(supportedFilters); dialog.selectMimeTypeFilter(mimetype); // launch the dialog if (dialog.exec() == QFileDialog::Accepted) { const QUrl saveUrl = dialog.selectedUrls().first(); if (saveUrl.isValid()) { if (save(saveUrl)) { emit imageSaved(saveUrl); config->setLastSaveAsFile(saveUrl); if (notify) { emit forceNotify(saveUrl); } return true; } } } return false; } // misc helpers void ExportManager::doCopyToClipboard() { QApplication::clipboard()->setPixmap(mSavePixmap, QClipboard::Clipboard); } void ExportManager::doPrint(QPrinter *printer) { QPainter painter; if (!(painter.begin(printer))) { emit errorMessage(i18n("Printing failed. The printer failed to initialize.")); delete printer; return; } QRect devRect(0, 0, printer->width(), printer->height()); QPixmap pixmap = mSavePixmap.scaled(devRect.size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); QRect srcRect = pixmap.rect(); srcRect.moveCenter(devRect.center()); painter.drawPixmap(srcRect.topLeft(), pixmap); painter.end(); delete printer; return; } const QMap ExportManager::filenamePlaceholders { {QStringLiteral("%Y"), I18N_NOOP2( "A placeholder in the user configurable filename will replaced by the specified value", "Year (4 digit)")}, {QStringLiteral("%y"), I18N_NOOP2( "A placeholder in the user configurable filename will replaced by the specified value", "Year (2 digit)")}, {QStringLiteral("%M"), I18N_NOOP2( "A placeholder in the user configurable filename will replaced by the specified value", "Month")}, {QStringLiteral("%D"), I18N_NOOP2( "A placeholder in the user configurable filename will replaced by the specified value", "Day")}, {QStringLiteral("%H"), I18N_NOOP2( "A placeholder in the user configurable filename will replaced by the specified value", "Hour")}, {QStringLiteral("%m"), I18N_NOOP2( "A placeholder in the user configurable filename will replaced by the specified value", "Minute")}, {QStringLiteral("%S"), I18N_NOOP2( "A placeholder in the user configurable filename will replaced by the specified value", "Second")}, {QStringLiteral("%T"), I18N_NOOP2( "A placeholder in the user configurable filename will replaced by the specified value", "Window Title")}, {QStringLiteral("%d"), I18N_NOOP2( "A placeholder in the user configurable filename will replaced by the specified value", "Sequential numbering")}, {QStringLiteral("%Nd"), I18N_NOOP2( "A placeholder in the user configurable filename will replaced by the specified value", "Sequential numbering, padded out to N digits")}, }; diff --git a/src/ExportManager.h b/src/ExportManager.h index 543ca7d..97b4c96 100644 --- a/src/ExportManager.h +++ b/src/ExportManager.h @@ -1,114 +1,115 @@ /* * Copyright (C) 2015 Boudhayan Gupta * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #ifndef EXPORTMANAGER_H #define EXPORTMANAGER_H #include #include #include #include #include #include #include #include "PlatformBackends/ImageGrabber.h" class QTemporaryDir; class ExportManager : public QObject { Q_OBJECT // singleton-ize the class public: static ExportManager* instance(); private: explicit ExportManager(QObject *parent = nullptr); virtual ~ExportManager(); ExportManager(ExportManager const&) = delete; void operator= (ExportManager const&) = delete; // now the usual stuff public: Q_PROPERTY(QPixmap pixmap READ pixmap WRITE setPixmap NOTIFY pixmapChanged) Q_PROPERTY(QString windowTitle READ windowTitle WRITE setWindowTitle) Q_PROPERTY(ImageGrabber::GrabMode grabMode READ grabMode WRITE setGrabMode) QString defaultSaveLocation() const; bool isFileExists(const QUrl &url) const; void setPixmap(const QPixmap &pixmap); QPixmap pixmap() const; void updatePixmapTimestamp(); + void setTimestamp(const QDateTime ×tamp); void setWindowTitle(const QString &windowTitle); QString windowTitle() const; ImageGrabber::GrabMode grabMode() const; void setGrabMode(const ImageGrabber::GrabMode &grabMode); QString formatFilename(const QString &nameTemplate); static const QMap filenamePlaceholders; Q_SIGNALS: void errorMessage(const QString &str); void pixmapChanged(const QPixmap &pixmap); void imageSaved(const QUrl &savedAt); void forceNotify(const QUrl &savedAt); public Q_SLOTS: QUrl getAutosaveFilename(); QUrl tempSave(const QString &mimetype = QStringLiteral("png")); void doSave(const QUrl &url = QUrl(), bool notify = false); bool doSaveAs(QWidget *parentWindow = nullptr, bool notify = false); void doCopyToClipboard(); void doPrint(QPrinter *printer); private: QString truncatedFilename(const QString &filename); QString makeAutosaveFilename(); using FileNameAlreadyUsedCheck = bool (ExportManager::*)(const QUrl&) const; QString autoIncrementFilename(const QString &baseName, const QString &extension, FileNameAlreadyUsedCheck isFileNameUsed); QString makeSaveMimetype(const QUrl &url); bool writeImage(QIODevice *device, const QByteArray &format); bool save(const QUrl &url); bool localSave(const QUrl &url, const QString &mimetype); bool remoteSave(const QUrl &url, const QString &mimetype); bool isTempFileAlreadyUsed(const QUrl &url) const; QPixmap mSavePixmap; QDateTime mPixmapTimestamp; QUrl mTempFile; QTemporaryDir *mTempDir; QList mUsedTempFileNames; QString mWindowTitle; ImageGrabber::GrabMode mGrabMode; }; #endif // EXPORTMANAGER_H diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..88c9b65 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,9 @@ +include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../src) +include_directories(${CMAKE_CURRENT_BINARY_DIR}) + +ecm_add_test(FilenameTest.cpp + ../src/ExportManager.cpp ../src/SpectacleConfig.cpp ../src/PlatformBackends/ImageGrabber.cpp + TEST_NAME "filename_test" + LINK_LIBRARIES Qt5::Test + Qt5::PrintSupport KF5::I18n KF5::ConfigCore KF5::KIOCore KF5::WindowSystem +) diff --git a/tests/FilenameTest.cpp b/tests/FilenameTest.cpp new file mode 100644 index 0000000..edee2b8 --- /dev/null +++ b/tests/FilenameTest.cpp @@ -0,0 +1,91 @@ +#include +#include +#include +#include + +#include "ExportManager.h" + +class FilenameTest : public QObject +{ + Q_OBJECT +private: + ExportManager* em; +private Q_SLOTS: + void initTestCase(); + void testStrings(); + void testDateTokens(); + void testWindowTitle(); + void testNumbering(); + void testCombined(); +}; + +void FilenameTest::initTestCase() +{ + em = ExportManager::instance(); + em->setTimestamp(QDateTime::fromString(QStringLiteral("2019-03-22T10:43:25"), Qt::ISODate)); + em->setWindowTitle(QStringLiteral("Spectacle")); +} + +void FilenameTest::testStrings() +{ + QCOMPARE(em->formatFilename(QStringLiteral("Screenshot")), QStringLiteral("Screenshot")); + // empty string produces Screenshot per default + QCOMPARE(em->formatFilename(QStringLiteral("")), QStringLiteral("Screenshot")); + // not a placeholder + QCOMPARE(em->formatFilename(QStringLiteral("%")), QStringLiteral("%")); + QCOMPARE(em->formatFilename(QStringLiteral("%K")), QStringLiteral("%K")); +} + +void FilenameTest::testDateTokens() +{ + QCOMPARE(em->formatFilename(QStringLiteral("%Y")), QStringLiteral("2019")); + QCOMPARE(em->formatFilename(QStringLiteral("%y")), QStringLiteral("19")); + QCOMPARE(em->formatFilename(QStringLiteral("%M")), QStringLiteral("03")); + QCOMPARE(em->formatFilename(QStringLiteral("%D")), QStringLiteral("22")); + QCOMPARE(em->formatFilename(QStringLiteral("%H")), QStringLiteral("10")); + QCOMPARE(em->formatFilename(QStringLiteral("%m")), QStringLiteral("43")); + QCOMPARE(em->formatFilename(QStringLiteral("%S")), QStringLiteral("25")); +} + +void FilenameTest::testWindowTitle() +{ + em->setGrabMode(ImageGrabber::ActiveWindow); + QCOMPARE(em->formatFilename(QStringLiteral("%T")), QStringLiteral("Spectacle")); + QCOMPARE(em->formatFilename(QStringLiteral("Before%TAfter")), + QStringLiteral("BeforeSpectacleAfter")); + em->setGrabMode(ImageGrabber::FullScreen); + //Empty String produces Screenshot + QCOMPARE(em->formatFilename(QStringLiteral("%T")), QStringLiteral("Screenshot")); + QCOMPARE(em->formatFilename(QStringLiteral("Before%TAfter")), QStringLiteral("BeforeAfter")); + QCOMPARE(em->formatFilename(QStringLiteral("Before_%T_After")), QStringLiteral("Before_After")); +} + +void FilenameTest::testNumbering() +{ + QString baseName = QStringLiteral("spectacle_test_")+ QUuid::createUuid().toString(); + QCOMPARE(em->formatFilename(baseName+QStringLiteral("_%d")), baseName+QStringLiteral("_1")); + QCOMPARE(em->formatFilename(baseName+QStringLiteral("_%1d")), baseName+QStringLiteral("_1")); + QCOMPARE(em->formatFilename(baseName+QStringLiteral("_%2d")), baseName+QStringLiteral("_01")); + QCOMPARE(em->formatFilename(baseName+QStringLiteral("_%3d")), baseName+QStringLiteral("_001")); + QCOMPARE(em->formatFilename(baseName+QStringLiteral("_%4d")), baseName+QStringLiteral("_0001")); + QCOMPARE(em->formatFilename(baseName+QStringLiteral("_%d_%2d_%3d")), + baseName+QStringLiteral("_1_01_001")); + QFile file(QDir(em->defaultSaveLocation()).filePath(baseName + QStringLiteral("_1.png"))); + file.open(QIODevice::WriteOnly); + QCOMPARE(em->formatFilename(baseName+QStringLiteral("_%d")), baseName+QStringLiteral("_2")); + file.remove(); +} + +void FilenameTest::testCombined() +{ + em->setGrabMode(ImageGrabber::ActiveWindow); + QCOMPARE(em->formatFilename(QStringLiteral("App_%T_Date_%Y%M%D_Time_%H:%m:%S%F")), + QStringLiteral("App_Spectacle_Date_20190322_Time_10:43:25%F")); + em->setGrabMode(ImageGrabber::FullScreen); + QCOMPARE(em->formatFilename(QStringLiteral("App_%T_Date_%Y%M%D_Time_%H:%m:%S%F")), + QStringLiteral("App_Date_20190322_Time_10:43:25%F")); +} + +QTEST_MAIN(FilenameTest) + +#include "FilenameTest.moc"