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 | 40 | | |||
38 | #include <KSharedConfig> | 41 | #include <KSharedConfig> | ||
39 | #include <KIO/ListJob> | 42 | #include <KIO/ListJob> | ||
40 | #include <KIO/MkpathJob> | 43 | #include <KIO/MkpathJob> | ||
41 | #include <KIO/FileCopyJob> | 44 | #include <KIO/FileCopyJob> | ||
42 | #include <KIO/StatJob> | 45 | #include <KIO/StatJob> | ||
43 | 46 | | |||
44 | #include "SpectacleConfig.h" | 47 | | ||
45 | 48 | | |||
46 | ExportManager::ExportManager(QObject *parent) : | 49 | ExportManager::ExportManager(QObject *parent) : | ||
47 | QObject(parent), | 50 | QObject(parent), | ||
48 | mSavePixmap(QPixmap()), | 51 | mSavePixmap(QPixmap()), | ||
49 | mTempFile(QUrl()), | 52 | mTempFile(QUrl()), | ||
50 | mTempDir(nullptr) | 53 | mTempDir(nullptr) | ||
51 | { | 54 | { | ||
52 | connect(this, &ExportManager::imageSaved, [](const QUrl &savedAt) { | 55 | connect(this, &ExportManager::imageSaved, &Settings::setLastSaveLocation); | ||
53 | SpectacleConfig::instance()->setLastSaveFile(savedAt); | | |||
54 | }); | | |||
55 | } | 56 | } | ||
56 | 57 | | |||
57 | ExportManager::~ExportManager() | 58 | ExportManager::~ExportManager() | ||
58 | { | 59 | { | ||
59 | delete mTempDir; | 60 | delete mTempDir; | ||
60 | } | 61 | } | ||
61 | 62 | | |||
62 | ExportManager* ExportManager::instance() | 63 | ExportManager* ExportManager::instance() | ||
▲ Show 20 Lines • Show All 51 Lines • ▼ Show 20 Line(s) | |||||
114 | { | 115 | { | ||
115 | mPixmapTimestamp = timestamp; | 116 | mPixmapTimestamp = timestamp; | ||
116 | } | 117 | } | ||
117 | 118 | | |||
118 | // native file save helpers | 119 | // native file save helpers | ||
119 | 120 | | |||
120 | QString ExportManager::defaultSaveLocation() const | 121 | QString ExportManager::defaultSaveLocation() const | ||
121 | { | 122 | { | ||
122 | QString savePath = SpectacleConfig::instance()->defaultSaveLocation().toLocalFile(); | 123 | QString savePath = Settings::self()->defaultSaveLocation().toLocalFile(); | ||
123 | savePath = QDir::cleanPath(savePath); | 124 | savePath = QDir::cleanPath(savePath); | ||
124 | 125 | | |||
125 | QDir savePathDir(savePath); | 126 | QDir savePathDir(savePath); | ||
126 | if (!(savePathDir.exists())) { | 127 | if (!(savePathDir.exists())) { | ||
127 | savePathDir.mkpath(QStringLiteral(".")); | 128 | savePathDir.mkpath(QStringLiteral(".")); | ||
128 | } | 129 | } | ||
129 | 130 | | |||
130 | return savePath; | 131 | return savePath; | ||
131 | } | 132 | } | ||
132 | 133 | | |||
133 | QUrl ExportManager::getAutosaveFilename() | 134 | QUrl ExportManager::getAutosaveFilename() | ||
134 | { | 135 | { | ||
135 | const QString baseDir = defaultSaveLocation(); | 136 | const QString baseDir = defaultSaveLocation(); | ||
136 | const QDir baseDirPath(baseDir); | 137 | const QDir baseDirPath(baseDir); | ||
137 | const QString filename = makeAutosaveFilename(); | 138 | const QString filename = makeAutosaveFilename(); | ||
138 | const QString fullpath = autoIncrementFilename(baseDirPath.filePath(filename), | 139 | const QString fullpath = autoIncrementFilename(baseDirPath.filePath(filename), | ||
139 | SpectacleConfig::instance()->saveImageFormat(), | 140 | Settings::self()->defaultSaveImageFormat(), | ||
140 | &ExportManager::isFileExists); | 141 | &ExportManager::isFileExists); | ||
141 | 142 | | |||
142 | const QUrl fileNameUrl = QUrl::fromUserInput(fullpath); | 143 | const QUrl fileNameUrl = QUrl::fromUserInput(fullpath); | ||
143 | if (fileNameUrl.isValid()) { | 144 | if (fileNameUrl.isValid()) { | ||
144 | return fileNameUrl; | 145 | return fileNameUrl; | ||
145 | } else { | 146 | } else { | ||
146 | return QUrl(); | 147 | return QUrl(); | ||
147 | } | 148 | } | ||
148 | } | 149 | } | ||
149 | 150 | | |||
150 | QString ExportManager::truncatedFilename(QString const &filename) | 151 | QString ExportManager::truncatedFilename(QString const &filename) | ||
151 | { | 152 | { | ||
152 | QString result = filename; | 153 | QString result = filename; | ||
153 | constexpr auto maxFilenameLength = 255; | 154 | constexpr auto maxFilenameLength = 255; | ||
154 | constexpr auto maxExtensionLength = 5; // For example, ".jpeg" | 155 | constexpr auto maxExtensionLength = 5; // For example, ".jpeg" | ||
155 | constexpr auto maxCounterLength = 20; // std::numeric_limits<quint64>::max() == 18446744073709551615 | 156 | constexpr auto maxCounterLength = 20; // std::numeric_limits<quint64>::max() == 18446744073709551615 | ||
156 | constexpr auto maxLength = maxFilenameLength - maxCounterLength - maxExtensionLength; | 157 | constexpr auto maxLength = maxFilenameLength - maxCounterLength - maxExtensionLength; | ||
157 | result.truncate(maxLength); | 158 | result.truncate(maxLength); | ||
158 | return result; | 159 | return result; | ||
159 | } | 160 | } | ||
160 | 161 | | |||
161 | QString ExportManager::makeAutosaveFilename() | 162 | QString ExportManager::makeAutosaveFilename() | ||
162 | { | 163 | { | ||
163 | return formatFilename(SpectacleConfig::instance()->autoSaveFilenameFormat()); | 164 | return formatFilename(Settings::self()->saveFilenameFormat()); | ||
164 | } | 165 | } | ||
165 | 166 | | |||
166 | QString ExportManager::formatFilename(const QString &nameTemplate) | 167 | QString ExportManager::formatFilename(const QString &nameTemplate) | ||
167 | { | 168 | { | ||
168 | const QDateTime timestamp = mPixmapTimestamp; | 169 | const QDateTime timestamp = mPixmapTimestamp; | ||
169 | QString baseName = nameTemplate; | 170 | QString baseName = nameTemplate; | ||
170 | const QString baseDir = defaultSaveLocation(); | 171 | const QString baseDir = defaultSaveLocation(); | ||
171 | QString title; | 172 | QString title; | ||
▲ Show 20 Lines • Show All 68 Lines • ▼ Show 20 Line(s) | |||||
240 | while (result.startsWith(QLatin1Char('/'))) { | 241 | while (result.startsWith(QLatin1Char('/'))) { | ||
241 | result.remove(0, 1); | 242 | result.remove(0, 1); | ||
242 | } | 243 | } | ||
243 | while (result.endsWith(QLatin1Char('/'))) { | 244 | while (result.endsWith(QLatin1Char('/'))) { | ||
244 | result.chop(1); | 245 | result.chop(1); | ||
245 | } | 246 | } | ||
246 | 247 | | |||
247 | if (result.isEmpty()) { | 248 | if (result.isEmpty()) { | ||
248 | result = SpectacleConfig::instance()->defaultFilename(); | 249 | result = QStringLiteral("Screenshot"); | ||
249 | } | 250 | } | ||
250 | return truncatedFilename(result); | 251 | return truncatedFilename(result); | ||
251 | } | 252 | } | ||
252 | 253 | | |||
253 | QString ExportManager::autoIncrementFilename(const QString &baseName, const QString &extension, | 254 | QString ExportManager::autoIncrementFilename(const QString &baseName, const QString &extension, | ||
254 | FileNameAlreadyUsedCheck isFileNameUsed) | 255 | FileNameAlreadyUsedCheck isFileNameUsed) | ||
255 | { | 256 | { | ||
256 | QString result = truncatedFilename(baseName) + QLatin1Literal(".") + extension; | 257 | QString result = truncatedFilename(baseName) + QLatin1Literal(".") + extension; | ||
Show All 17 Lines | |||||
274 | } | 275 | } | ||
275 | 276 | | |||
276 | QString ExportManager::makeSaveMimetype(const QUrl &url) | 277 | QString ExportManager::makeSaveMimetype(const QUrl &url) | ||
277 | { | 278 | { | ||
278 | QMimeDatabase mimedb; | 279 | QMimeDatabase mimedb; | ||
279 | QString type = mimedb.mimeTypeForUrl(url).preferredSuffix(); | 280 | QString type = mimedb.mimeTypeForUrl(url).preferredSuffix(); | ||
280 | 281 | | |||
281 | if (type.isEmpty()) { | 282 | if (type.isEmpty()) { | ||
282 | return SpectacleConfig::instance()->saveImageFormat(); | 283 | return Settings::self()->defaultSaveImageFormat(); | ||
283 | } | 284 | } | ||
284 | return type; | 285 | return type; | ||
285 | } | 286 | } | ||
286 | 287 | | |||
287 | bool ExportManager::writeImage(QIODevice *device, const QByteArray &format) | 288 | bool ExportManager::writeImage(QIODevice *device, const QByteArray &format) | ||
288 | { | 289 | { | ||
289 | QImageWriter imageWriter(device, format); | 290 | QImageWriter imageWriter(device, format); | ||
290 | imageWriter.setQuality(SpectacleConfig::instance()->compressionQuality()); | 291 | imageWriter.setQuality(Settings::self()->compressionQuality()); | ||
291 | /** Set compression 50 if the format is png. Otherwise if no compression value is specified | 292 | /** Set compression 50 if the format is png. Otherwise if no compression value is specified | ||
292 | * it will fallback to using quality (QTBUG-43618) and produce huge files. | 293 | * it will fallback to using quality (QTBUG-43618) and produce huge files. | ||
293 | * See also qpnghandler.cpp#n1075. The other formats that do compression seem to have it | 294 | * See also qpnghandler.cpp#n1075. The other formats that do compression seem to have it | ||
294 | * enabled by default and only disabled if compression is set to 0, also any value except 0 | 295 | * enabled by default and only disabled if compression is set to 0, also any value except 0 | ||
295 | * has the same effect for them. | 296 | * has the same effect for them. | ||
296 | */ | 297 | */ | ||
297 | if (format == "png") { | 298 | if (format == "png") { | ||
298 | imageWriter.setCompression(50); | 299 | imageWriter.setCompression(50); | ||
▲ Show 20 Lines • Show All 150 Lines • ▼ Show 20 Line(s) | 449 | if (mSavePixmap.isNull()) { | |||
449 | emit errorMessage(i18n("Cannot save an empty screenshot image.")); | 450 | emit errorMessage(i18n("Cannot save an empty screenshot image.")); | ||
450 | return; | 451 | return; | ||
451 | } | 452 | } | ||
452 | 453 | | |||
453 | QUrl savePath = url.isValid() ? url : getAutosaveFilename(); | 454 | QUrl savePath = url.isValid() ? url : getAutosaveFilename(); | ||
454 | if (save(savePath)) { | 455 | if (save(savePath)) { | ||
455 | QDir dir(savePath.path()); | 456 | QDir dir(savePath.path()); | ||
456 | dir.cdUp(); | 457 | dir.cdUp(); | ||
457 | SpectacleConfig::instance()->setLastSaveFile(savePath); | | |||
458 | 458 | | |||
459 | emit imageSaved(savePath); | 459 | emit imageSaved(savePath); | ||
460 | if (notify) { | 460 | if (notify) { | ||
461 | emit forceNotify(savePath); | 461 | emit forceNotify(savePath); | ||
462 | } | 462 | } | ||
463 | } | 463 | } | ||
464 | } | 464 | } | ||
465 | 465 | | |||
466 | bool ExportManager::doSaveAs(QWidget *parentWindow, bool notify) | 466 | bool ExportManager::doSaveAs(QWidget *parentWindow, bool notify) | ||
467 | { | 467 | { | ||
468 | QStringList supportedFilters; | 468 | QStringList supportedFilters; | ||
469 | SpectacleConfig *config = SpectacleConfig::instance(); | | |||
470 | 469 | | |||
471 | // construct the supported mimetype list | 470 | // construct the supported mimetype list | ||
472 | const auto mimeTypes = QImageWriter::supportedMimeTypes(); | 471 | const auto mimeTypes = QImageWriter::supportedMimeTypes(); | ||
473 | for (const auto &mimeType : mimeTypes) { | 472 | for (const auto &mimeType : mimeTypes) { | ||
474 | supportedFilters.append(QString::fromUtf8(mimeType).trimmed()); | 473 | supportedFilters.append(QString::fromUtf8(mimeType).trimmed()); | ||
475 | } | 474 | } | ||
476 | 475 | | |||
477 | // construct the file name | 476 | // construct the file name | ||
478 | const QString filenameExtension = SpectacleConfig::instance()->saveImageFormat(); | 477 | const QString filenameExtension = Settings::self()->defaultSaveImageFormat(); | ||
479 | const QString mimetype = QMimeDatabase().mimeTypeForFile(QStringLiteral("~/fakefile.") + filenameExtension, QMimeDatabase::MatchExtension).name(); | 478 | const QString mimetype = QMimeDatabase().mimeTypeForFile(QStringLiteral("~/fakefile.") + filenameExtension, QMimeDatabase::MatchExtension).name(); | ||
480 | QFileDialog dialog(parentWindow); | 479 | QFileDialog dialog(parentWindow); | ||
481 | dialog.setAcceptMode(QFileDialog::AcceptSave); | 480 | dialog.setAcceptMode(QFileDialog::AcceptSave); | ||
482 | dialog.setFileMode(QFileDialog::AnyFile); | 481 | dialog.setFileMode(QFileDialog::AnyFile); | ||
483 | dialog.setDirectoryUrl(config->lastSaveAsLocation()); | 482 | dialog.setDirectoryUrl(Settings::self()->lastSaveAsLocation()); | ||
484 | dialog.selectFile(makeAutosaveFilename() + QStringLiteral(".") + filenameExtension); | 483 | dialog.selectFile(makeAutosaveFilename() + QStringLiteral(".") + filenameExtension); | ||
485 | dialog.setDefaultSuffix(QStringLiteral(".") + filenameExtension); | 484 | dialog.setDefaultSuffix(QStringLiteral(".") + filenameExtension); | ||
486 | dialog.setMimeTypeFilters(supportedFilters); | 485 | dialog.setMimeTypeFilters(supportedFilters); | ||
487 | dialog.selectMimeTypeFilter(mimetype); | 486 | dialog.selectMimeTypeFilter(mimetype); | ||
488 | 487 | | |||
489 | // launch the dialog | 488 | // launch the dialog | ||
490 | if (dialog.exec() == QFileDialog::Accepted) { | 489 | if (dialog.exec() == QFileDialog::Accepted) { | ||
491 | const QUrl saveUrl = dialog.selectedUrls().constFirst(); | 490 | const QUrl saveUrl = dialog.selectedUrls().constFirst(); | ||
492 | if (saveUrl.isValid()) { | 491 | if (saveUrl.isValid()) { | ||
493 | if (save(saveUrl)) { | 492 | if (save(saveUrl)) { | ||
494 | emit imageSaved(saveUrl); | 493 | emit imageSaved(saveUrl); | ||
495 | config->setLastSaveAsFile(saveUrl); | 494 | Settings::setLastSaveAsLocation(saveUrl); | ||
496 | | ||||
497 | if (notify) { | 495 | if (notify) { | ||
498 | emit forceNotify(saveUrl); | 496 | emit forceNotify(saveUrl); | ||
499 | } | 497 | } | ||
500 | return true; | 498 | return true; | ||
501 | } | 499 | } | ||
502 | } | 500 | } | ||
503 | } | 501 | } | ||
504 | return false; | 502 | return false; | ||
505 | } | 503 | } | ||
506 | 504 | | |||
507 | void ExportManager::doSaveAndCopy(const QUrl &url) | 505 | void ExportManager::doSaveAndCopy(const QUrl &url) | ||
508 | { | 506 | { | ||
509 | if (mSavePixmap.isNull()) { | 507 | if (mSavePixmap.isNull()) { | ||
510 | emit errorMessage(i18n("Cannot save an empty screenshot image.")); | 508 | emit errorMessage(i18n("Cannot save an empty screenshot image.")); | ||
511 | return; | 509 | return; | ||
512 | } | 510 | } | ||
513 | 511 | | |||
514 | QUrl savePath = url.isValid() ? url : getAutosaveFilename(); | 512 | QUrl savePath = url.isValid() ? url : getAutosaveFilename(); | ||
515 | if (save(savePath)) { | 513 | if (save(savePath)) { | ||
516 | QDir dir(savePath.path()); | 514 | QDir dir(savePath.path()); | ||
517 | dir.cdUp(); | 515 | dir.cdUp(); | ||
518 | SpectacleConfig::instance()->setLastSaveFile(savePath); | 516 | Settings::setLastSaveLocation(savePath); | ||
519 | 517 | | |||
520 | doCopyToClipboard(false); | 518 | doCopyToClipboard(false); | ||
521 | emit imageSavedAndCopied(savePath); | 519 | emit imageSavedAndCopied(savePath); | ||
522 | } | 520 | } | ||
523 | } | 521 | } | ||
524 | 522 | | |||
525 | // misc helpers | 523 | // misc helpers | ||
526 | | ||||
527 | void ExportManager::doCopyToClipboard(bool notify) | 524 | void ExportManager::doCopyToClipboard(bool notify) | ||
528 | { | 525 | { | ||
529 | auto data = new QMimeData(); | 526 | auto data = new QMimeData(); | ||
530 | data->setImageData(mSavePixmap.toImage()); | 527 | data->setImageData(mSavePixmap.toImage()); | ||
531 | data->setData(QStringLiteral("x-kde-force-image-copy"), QByteArray()); | 528 | data->setData(QStringLiteral("x-kde-force-image-copy"), QByteArray()); | ||
532 | QApplication::clipboard()->setMimeData(data, QClipboard::Clipboard); | 529 | QApplication::clipboard()->setMimeData(data, QClipboard::Clipboard); | ||
533 | 530 | | |||
534 | if (notify) { | 531 | if (notify) { | ||
▲ Show 20 Lines • Show All 58 Lines • Show Last 20 Lines |