Changeset View
Changeset View
Standalone View
Standalone View
src/ExportManager.cpp
1 | /* This file is part of Spectacle, the KDE screenshot utility | 1 | /* This file is part of Spectacle, the KDE screenshot utility | ||
---|---|---|---|---|---|
2 | * Copyright 2019 David Redondo <kde@david-redondo.de> | ||||
2 | * Copyright (C) 2015 Boudhayan Gupta <bgupta@kde.org> | 3 | * Copyright (C) 2015 Boudhayan Gupta <bgupta@kde.org> | ||
3 | * | 4 | * | ||
4 | * This program is free software; you can redistribute it and/or modify | 5 | * This program is free software; you can redistribute it and/or modify | ||
5 | * it under the terms of the GNU Lesser General Public License as published by | 6 | * it under the terms of the GNU Lesser General Public License as published by | ||
6 | * the Free Software Foundation; either version 2 of the License, or | 7 | * the Free Software Foundation; either version 2 of the License, or | ||
7 | * (at your option) any later version. | 8 | * (at your option) any later version. | ||
8 | * | 9 | * | ||
9 | * This program is distributed in the hope that it will be useful, | 10 | * This program is distributed in the hope that it will be useful, | ||
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
12 | * GNU General Public License for more details. | 13 | * GNU General Public License for more details. | ||
13 | * | 14 | * | ||
14 | * You should have received a copy of the GNU Lesser General Public License | 15 | * You should have received a copy of the GNU Lesser General Public License | ||
15 | * along with this program; if not, write to the Free Software | 16 | * along with this program; if not, write to the Free Software | ||
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, | 17 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, | ||
17 | * Boston, MA 02110-1301, USA. | 18 | * Boston, MA 02110-1301, USA. | ||
18 | * | 19 | * | ||
19 | * SPDX-License-Identifier: LGPL-2.0-or-later | 20 | * SPDX-License-Identifier: LGPL-2.0-or-later | ||
20 | */ | 21 | */ | ||
21 | 22 | | |||
22 | #include "ExportManager.h" | 23 | #include "ExportManager.h" | ||
23 | 24 | | |||
25 | #include "settings.h" | ||||
26 | | ||||
24 | #include <QDir> | 27 | #include <QDir> | ||
25 | #include <QMimeData> | 28 | #include <QMimeData> | ||
26 | #include <QMimeDatabase> | 29 | #include <QMimeDatabase> | ||
27 | #include <QImageWriter> | 30 | #include <QImageWriter> | ||
28 | #include <QTemporaryDir> | 31 | #include <QTemporaryDir> | ||
29 | #include <QTemporaryFile> | 32 | #include <QTemporaryFile> | ||
30 | #include <QApplication> | 33 | #include <QApplication> | ||
31 | #include <QClipboard> | 34 | #include <QClipboard> | ||
32 | #include <QPainter> | 35 | #include <QPainter> | ||
33 | #include <QFileDialog> | 36 | #include <QFileDialog> | ||
34 | #include <QRegularExpression> | 37 | #include <QRegularExpression> | ||
35 | #include <QRegularExpressionMatch> | 38 | #include <QRegularExpressionMatch> | ||
36 | #include <QString> | 39 | #include <QString> | ||
37 | #include <QRandomGenerator> | 40 | #include <QRandomGenerator> | ||
38 | 41 | | |||
39 | #include <KSharedConfig> | 42 | #include <KSharedConfig> | ||
40 | #include <KIO/ListJob> | 43 | #include <KIO/ListJob> | ||
41 | #include <KIO/MkpathJob> | 44 | #include <KIO/MkpathJob> | ||
42 | #include <KIO/FileCopyJob> | 45 | #include <KIO/FileCopyJob> | ||
43 | #include <KIO/StatJob> | 46 | #include <KIO/StatJob> | ||
44 | 47 | | |||
45 | #include "SpectacleConfig.h" | 48 | | ||
46 | 49 | | |||
47 | ExportManager::ExportManager(QObject *parent) : | 50 | ExportManager::ExportManager(QObject *parent) : | ||
48 | QObject(parent), | 51 | QObject(parent), | ||
49 | mSavePixmap(QPixmap()), | 52 | mSavePixmap(QPixmap()), | ||
50 | mTempFile(QUrl()), | 53 | mTempFile(QUrl()), | ||
51 | mTempDir(nullptr) | 54 | mTempDir(nullptr) | ||
52 | { | 55 | { | ||
53 | connect(this, &ExportManager::imageSaved, [](const QUrl &savedAt) { | 56 | connect(this, &ExportManager::imageSaved, &Settings::setLastSaveLocation); | ||
54 | SpectacleConfig::instance()->setLastSaveFile(savedAt); | | |||
55 | }); | | |||
56 | } | 57 | } | ||
57 | 58 | | |||
58 | ExportManager::~ExportManager() | 59 | ExportManager::~ExportManager() | ||
59 | { | 60 | { | ||
60 | delete mTempDir; | 61 | delete mTempDir; | ||
61 | } | 62 | } | ||
62 | 63 | | |||
63 | ExportManager* ExportManager::instance() | 64 | ExportManager* ExportManager::instance() | ||
▲ Show 20 Lines • Show All 51 Lines • ▼ Show 20 Line(s) | |||||
115 | { | 116 | { | ||
116 | mPixmapTimestamp = timestamp; | 117 | mPixmapTimestamp = timestamp; | ||
117 | } | 118 | } | ||
118 | 119 | | |||
119 | // native file save helpers | 120 | // native file save helpers | ||
120 | 121 | | |||
121 | QString ExportManager::defaultSaveLocation() const | 122 | QString ExportManager::defaultSaveLocation() const | ||
122 | { | 123 | { | ||
123 | QString savePath = SpectacleConfig::instance()->defaultSaveLocation().toLocalFile(); | 124 | QString savePath = Settings::self()->defaultSaveLocation().toLocalFile(); | ||
124 | savePath = QDir::cleanPath(savePath); | 125 | savePath = QDir::cleanPath(savePath); | ||
125 | 126 | | |||
126 | QDir savePathDir(savePath); | 127 | QDir savePathDir(savePath); | ||
127 | if (!(savePathDir.exists())) { | 128 | if (!(savePathDir.exists())) { | ||
128 | savePathDir.mkpath(QStringLiteral(".")); | 129 | savePathDir.mkpath(QStringLiteral(".")); | ||
129 | } | 130 | } | ||
130 | 131 | | |||
131 | return savePath; | 132 | return savePath; | ||
132 | } | 133 | } | ||
133 | 134 | | |||
134 | QUrl ExportManager::getAutosaveFilename() | 135 | QUrl ExportManager::getAutosaveFilename() | ||
135 | { | 136 | { | ||
136 | const QString baseDir = defaultSaveLocation(); | 137 | const QString baseDir = defaultSaveLocation(); | ||
137 | const QDir baseDirPath(baseDir); | 138 | const QDir baseDirPath(baseDir); | ||
138 | const QString filename = makeAutosaveFilename(); | 139 | const QString filename = makeAutosaveFilename(); | ||
139 | const QString fullpath = autoIncrementFilename(baseDirPath.filePath(filename), | 140 | const QString fullpath = autoIncrementFilename(baseDirPath.filePath(filename), | ||
140 | SpectacleConfig::instance()->saveImageFormat(), | 141 | Settings::self()->defaultSaveImageFormat(), | ||
141 | &ExportManager::isFileExists); | 142 | &ExportManager::isFileExists); | ||
142 | 143 | | |||
143 | const QUrl fileNameUrl = QUrl::fromUserInput(fullpath); | 144 | const QUrl fileNameUrl = QUrl::fromUserInput(fullpath); | ||
144 | if (fileNameUrl.isValid()) { | 145 | if (fileNameUrl.isValid()) { | ||
145 | return fileNameUrl; | 146 | return fileNameUrl; | ||
146 | } else { | 147 | } else { | ||
147 | return QUrl(); | 148 | return QUrl(); | ||
148 | } | 149 | } | ||
149 | } | 150 | } | ||
150 | 151 | | |||
151 | QString ExportManager::truncatedFilename(QString const &filename) | 152 | QString ExportManager::truncatedFilename(QString const &filename) | ||
152 | { | 153 | { | ||
153 | QString result = filename; | 154 | QString result = filename; | ||
154 | constexpr auto maxFilenameLength = 255; | 155 | constexpr auto maxFilenameLength = 255; | ||
155 | constexpr auto maxExtensionLength = 5; // For example, ".jpeg" | 156 | constexpr auto maxExtensionLength = 5; // For example, ".jpeg" | ||
156 | constexpr auto maxCounterLength = 20; // std::numeric_limits<quint64>::max() == 18446744073709551615 | 157 | constexpr auto maxCounterLength = 20; // std::numeric_limits<quint64>::max() == 18446744073709551615 | ||
157 | constexpr auto maxLength = maxFilenameLength - maxCounterLength - maxExtensionLength; | 158 | constexpr auto maxLength = maxFilenameLength - maxCounterLength - maxExtensionLength; | ||
158 | result.truncate(maxLength); | 159 | result.truncate(maxLength); | ||
159 | return result; | 160 | return result; | ||
160 | } | 161 | } | ||
161 | 162 | | |||
162 | QString ExportManager::makeAutosaveFilename() | 163 | QString ExportManager::makeAutosaveFilename() | ||
163 | { | 164 | { | ||
164 | return formatFilename(SpectacleConfig::instance()->autoSaveFilenameFormat()); | 165 | return formatFilename(Settings::self()->saveFilenameFormat()); | ||
165 | } | 166 | } | ||
166 | 167 | | |||
167 | QString ExportManager::formatFilename(const QString &nameTemplate) | 168 | QString ExportManager::formatFilename(const QString &nameTemplate) | ||
168 | { | 169 | { | ||
169 | const QDateTime timestamp = mPixmapTimestamp; | 170 | const QDateTime timestamp = mPixmapTimestamp; | ||
170 | QString baseName = nameTemplate; | 171 | QString baseName = nameTemplate; | ||
171 | const QString baseDir = defaultSaveLocation(); | 172 | const QString baseDir = defaultSaveLocation(); | ||
172 | QString title; | 173 | QString title; | ||
▲ Show 20 Lines • Show All 68 Lines • ▼ Show 20 Line(s) | |||||
241 | while (result.startsWith(QLatin1Char('/'))) { | 242 | while (result.startsWith(QLatin1Char('/'))) { | ||
242 | result.remove(0, 1); | 243 | result.remove(0, 1); | ||
243 | } | 244 | } | ||
244 | while (result.endsWith(QLatin1Char('/'))) { | 245 | while (result.endsWith(QLatin1Char('/'))) { | ||
245 | result.chop(1); | 246 | result.chop(1); | ||
246 | } | 247 | } | ||
247 | 248 | | |||
248 | if (result.isEmpty()) { | 249 | if (result.isEmpty()) { | ||
249 | result = SpectacleConfig::instance()->defaultFilename(); | 250 | result = QStringLiteral("Screenshot"); | ||
250 | } | 251 | } | ||
251 | return truncatedFilename(result); | 252 | return truncatedFilename(result); | ||
252 | } | 253 | } | ||
253 | 254 | | |||
254 | QString ExportManager::autoIncrementFilename(const QString &baseName, const QString &extension, | 255 | QString ExportManager::autoIncrementFilename(const QString &baseName, const QString &extension, | ||
255 | FileNameAlreadyUsedCheck isFileNameUsed) | 256 | FileNameAlreadyUsedCheck isFileNameUsed) | ||
256 | { | 257 | { | ||
257 | QString result = truncatedFilename(baseName) + QLatin1String(".") + extension; | 258 | QString result = truncatedFilename(baseName) + QLatin1String(".") + extension; | ||
Show All 17 Lines | |||||
275 | } | 276 | } | ||
276 | 277 | | |||
277 | QString ExportManager::makeSaveMimetype(const QUrl &url) | 278 | QString ExportManager::makeSaveMimetype(const QUrl &url) | ||
278 | { | 279 | { | ||
279 | QMimeDatabase mimedb; | 280 | QMimeDatabase mimedb; | ||
280 | QString type = mimedb.mimeTypeForUrl(url).preferredSuffix(); | 281 | QString type = mimedb.mimeTypeForUrl(url).preferredSuffix(); | ||
281 | 282 | | |||
282 | if (type.isEmpty()) { | 283 | if (type.isEmpty()) { | ||
283 | return SpectacleConfig::instance()->saveImageFormat(); | 284 | return Settings::self()->defaultSaveImageFormat(); | ||
284 | } | 285 | } | ||
285 | return type; | 286 | return type; | ||
286 | } | 287 | } | ||
287 | 288 | | |||
288 | bool ExportManager::writeImage(QIODevice *device, const QByteArray &format) | 289 | bool ExportManager::writeImage(QIODevice *device, const QByteArray &format) | ||
289 | { | 290 | { | ||
290 | QImageWriter imageWriter(device, format); | 291 | QImageWriter imageWriter(device, format); | ||
291 | imageWriter.setQuality(SpectacleConfig::instance()->compressionQuality()); | 292 | imageWriter.setQuality(Settings::self()->compressionQuality()); | ||
292 | /** Set compression 50 if the format is png. Otherwise if no compression value is specified | 293 | /** Set compression 50 if the format is png. Otherwise if no compression value is specified | ||
293 | * it will fallback to using quality (QTBUG-43618) and produce huge files. | 294 | * it will fallback to using quality (QTBUG-43618) and produce huge files. | ||
294 | * See also qpnghandler.cpp#n1075. The other formats that do compression seem to have it | 295 | * See also qpnghandler.cpp#n1075. The other formats that do compression seem to have it | ||
295 | * enabled by default and only disabled if compression is set to 0, also any value except 0 | 296 | * enabled by default and only disabled if compression is set to 0, also any value except 0 | ||
296 | * has the same effect for them. | 297 | * has the same effect for them. | ||
297 | */ | 298 | */ | ||
298 | if (format == "png") { | 299 | if (format == "png") { | ||
299 | imageWriter.setCompression(50); | 300 | imageWriter.setCompression(50); | ||
▲ Show 20 Lines • Show All 150 Lines • ▼ Show 20 Line(s) | 450 | if (mSavePixmap.isNull()) { | |||
450 | emit errorMessage(i18n("Cannot save an empty screenshot image.")); | 451 | emit errorMessage(i18n("Cannot save an empty screenshot image.")); | ||
451 | return; | 452 | return; | ||
452 | } | 453 | } | ||
453 | 454 | | |||
454 | QUrl savePath = url.isValid() ? url : getAutosaveFilename(); | 455 | QUrl savePath = url.isValid() ? url : getAutosaveFilename(); | ||
455 | if (save(savePath)) { | 456 | if (save(savePath)) { | ||
456 | QDir dir(savePath.path()); | 457 | QDir dir(savePath.path()); | ||
457 | dir.cdUp(); | 458 | dir.cdUp(); | ||
458 | SpectacleConfig::instance()->setLastSaveFile(savePath); | | |||
459 | 459 | | |||
460 | emit imageSaved(savePath); | 460 | emit imageSaved(savePath); | ||
461 | if (notify) { | 461 | if (notify) { | ||
462 | emit forceNotify(savePath); | 462 | emit forceNotify(savePath); | ||
463 | } | 463 | } | ||
464 | } | 464 | } | ||
465 | } | 465 | } | ||
466 | 466 | | |||
467 | bool ExportManager::doSaveAs(QWidget *parentWindow, bool notify) | 467 | bool ExportManager::doSaveAs(QWidget *parentWindow, bool notify) | ||
468 | { | 468 | { | ||
469 | QStringList supportedFilters; | 469 | QStringList supportedFilters; | ||
470 | SpectacleConfig *config = SpectacleConfig::instance(); | | |||
471 | 470 | | |||
472 | // construct the supported mimetype list | 471 | // construct the supported mimetype list | ||
473 | const auto mimeTypes = QImageWriter::supportedMimeTypes(); | 472 | const auto mimeTypes = QImageWriter::supportedMimeTypes(); | ||
474 | for (const auto &mimeType : mimeTypes) { | 473 | for (const auto &mimeType : mimeTypes) { | ||
475 | supportedFilters.append(QString::fromUtf8(mimeType).trimmed()); | 474 | supportedFilters.append(QString::fromUtf8(mimeType).trimmed()); | ||
476 | } | 475 | } | ||
477 | 476 | | |||
478 | // construct the file name | 477 | // construct the file name | ||
479 | const QString filenameExtension = SpectacleConfig::instance()->saveImageFormat(); | 478 | const QString filenameExtension = Settings::self()->defaultSaveImageFormat(); | ||
480 | const QString mimetype = QMimeDatabase().mimeTypeForFile(QLatin1String("~/fakefile.") + filenameExtension, QMimeDatabase::MatchExtension).name(); | 479 | const QString mimetype = QMimeDatabase().mimeTypeForFile(QStringLiteral("~/fakefile.") + filenameExtension, QMimeDatabase::MatchExtension).name(); | ||
481 | QFileDialog dialog(parentWindow); | 480 | QFileDialog dialog(parentWindow); | ||
482 | dialog.setAcceptMode(QFileDialog::AcceptSave); | 481 | dialog.setAcceptMode(QFileDialog::AcceptSave); | ||
483 | dialog.setFileMode(QFileDialog::AnyFile); | 482 | dialog.setFileMode(QFileDialog::AnyFile); | ||
484 | dialog.setDirectoryUrl(config->lastSaveAsLocation()); | 483 | dialog.setDirectoryUrl(Settings::self()->lastSaveAsLocation()); | ||
485 | dialog.selectFile(makeAutosaveFilename() + QStringLiteral(".") + filenameExtension); | 484 | dialog.selectFile(makeAutosaveFilename() + QStringLiteral(".") + filenameExtension); | ||
486 | dialog.setDefaultSuffix(QStringLiteral(".") + filenameExtension); | 485 | dialog.setDefaultSuffix(QStringLiteral(".") + filenameExtension); | ||
487 | dialog.setMimeTypeFilters(supportedFilters); | 486 | dialog.setMimeTypeFilters(supportedFilters); | ||
488 | dialog.selectMimeTypeFilter(mimetype); | 487 | dialog.selectMimeTypeFilter(mimetype); | ||
489 | 488 | | |||
490 | // launch the dialog | 489 | // launch the dialog | ||
491 | if (dialog.exec() == QFileDialog::Accepted) { | 490 | if (dialog.exec() == QFileDialog::Accepted) { | ||
492 | const QUrl saveUrl = dialog.selectedUrls().constFirst(); | 491 | const QUrl saveUrl = dialog.selectedUrls().constFirst(); | ||
493 | if (saveUrl.isValid()) { | 492 | if (saveUrl.isValid()) { | ||
494 | if (save(saveUrl)) { | 493 | if (save(saveUrl)) { | ||
495 | emit imageSaved(saveUrl); | 494 | emit imageSaved(saveUrl); | ||
496 | config->setLastSaveAsFile(saveUrl); | 495 | Settings::setLastSaveAsLocation(saveUrl); | ||
497 | | ||||
498 | if (notify) { | 496 | if (notify) { | ||
499 | emit forceNotify(saveUrl); | 497 | emit forceNotify(saveUrl); | ||
500 | } | 498 | } | ||
501 | return true; | 499 | return true; | ||
502 | } | 500 | } | ||
503 | } | 501 | } | ||
504 | } | 502 | } | ||
505 | return false; | 503 | return false; | ||
506 | } | 504 | } | ||
507 | 505 | | |||
508 | void ExportManager::doSaveAndCopy(const QUrl &url) | 506 | void ExportManager::doSaveAndCopy(const QUrl &url) | ||
509 | { | 507 | { | ||
510 | if (mSavePixmap.isNull()) { | 508 | if (mSavePixmap.isNull()) { | ||
511 | emit errorMessage(i18n("Cannot save an empty screenshot image.")); | 509 | emit errorMessage(i18n("Cannot save an empty screenshot image.")); | ||
512 | return; | 510 | return; | ||
513 | } | 511 | } | ||
514 | 512 | | |||
515 | QUrl savePath = url.isValid() ? url : getAutosaveFilename(); | 513 | QUrl savePath = url.isValid() ? url : getAutosaveFilename(); | ||
516 | if (save(savePath)) { | 514 | if (save(savePath)) { | ||
517 | QDir dir(savePath.path()); | 515 | QDir dir(savePath.path()); | ||
518 | dir.cdUp(); | 516 | dir.cdUp(); | ||
519 | SpectacleConfig::instance()->setLastSaveFile(savePath); | 517 | Settings::setLastSaveLocation(savePath); | ||
520 | 518 | | |||
521 | doCopyToClipboard(false); | 519 | doCopyToClipboard(false); | ||
522 | emit imageSavedAndCopied(savePath); | 520 | emit imageSavedAndCopied(savePath); | ||
523 | } | 521 | } | ||
524 | } | 522 | } | ||
525 | 523 | | |||
526 | // misc helpers | 524 | // misc helpers | ||
527 | | ||||
528 | void ExportManager::doCopyToClipboard(bool notify) | 525 | void ExportManager::doCopyToClipboard(bool notify) | ||
529 | { | 526 | { | ||
530 | auto data = new QMimeData(); | 527 | auto data = new QMimeData(); | ||
531 | data->setImageData(mSavePixmap.toImage()); | 528 | data->setImageData(mSavePixmap.toImage()); | ||
532 | data->setData(QStringLiteral("x-kde-force-image-copy"), QByteArray()); | 529 | data->setData(QStringLiteral("x-kde-force-image-copy"), QByteArray()); | ||
533 | QApplication::clipboard()->setMimeData(data, QClipboard::Clipboard); | 530 | QApplication::clipboard()->setMimeData(data, QClipboard::Clipboard); | ||
534 | emit imageCopied(); | 531 | emit imageCopied(); | ||
535 | if (notify) { | 532 | if (notify) { | ||
▲ Show 20 Lines • Show All 58 Lines • Show Last 20 Lines |