File indexing completed on 2024-05-12 05:09:48
0001 /*************************************************************************** 0002 Copyright (C) 2003-2009 Robby Stephenson <robby@periapsis.org> 0003 ***************************************************************************/ 0004 0005 /*************************************************************************** 0006 * * 0007 * This program is free software; you can redistribute it and/or * 0008 * modify it under the terms of the GNU General Public License as * 0009 * published by the Free Software Foundation; either version 2 of * 0010 * the License or (at your option) version 3 or any later version * 0011 * accepted by the membership of KDE e.V. (or its successor approved * 0012 * by the membership of KDE e.V.), which shall act as a proxy * 0013 * defined in Section 14 of version 3 of the license. * 0014 * * 0015 * This program is distributed in the hope that it will be useful, * 0016 * but WITHOUT ANY WARRANTY; without even the implied warranty of * 0017 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * 0018 * GNU General Public License for more details. * 0019 * * 0020 * You should have received a copy of the GNU General Public License * 0021 * along with this program. If not, see <http://www.gnu.org/licenses/>. * 0022 * * 0023 ***************************************************************************/ 0024 0025 #include "imagewidget.h" 0026 #include "../images/imagefactory.h" 0027 #include "../images/image.h" 0028 #include "../images/imageinfo.h" 0029 #include "../core/filehandler.h" 0030 #include "../utils/cursorsaver.h" 0031 #include "../tellico_debug.h" 0032 0033 #include <KPageDialog> 0034 #include <KLocalizedString> 0035 #include <KMessageBox> 0036 #include <KProcess> 0037 #include <kservice_version.h> 0038 #if KSERVICE_VERSION >= QT_VERSION_CHECK(5, 68, 0) 0039 #include <KApplicationTrader> 0040 #else 0041 #include <KMimeTypeTrader> 0042 #endif 0043 #include <KIO/DesktopExecParser> 0044 #include <KSharedConfig> 0045 #include <KConfigGroup> 0046 #include <KFileWidget> 0047 #include <KRecentDirs> 0048 #include <KStandardAction> 0049 0050 #include <QPushButton> 0051 #include <QMenu> 0052 #include <QLabel> 0053 #include <QCheckBox> 0054 #include <QApplication> // needed for drag distance 0055 #include <QHBoxLayout> 0056 #include <QVBoxLayout> 0057 #include <QDropEvent> 0058 #include <QResizeEvent> 0059 #include <QMouseEvent> 0060 #include <QDragEnterEvent> 0061 #include <QToolButton> 0062 #include <QActionGroup> 0063 #include <QTimer> 0064 #include <QSet> 0065 #include <QDrag> 0066 #include <QTemporaryFile> 0067 #include <QMimeData> 0068 #include <QProgressDialog> 0069 #include <QFileDialog> 0070 #include <QImageReader> 0071 #include <QClipboard> 0072 0073 #ifdef HAVE_KSANE 0074 #include <KSaneWidget> 0075 #endif 0076 0077 Q_DECLARE_METATYPE(KService::Ptr) 0078 0079 namespace { 0080 static const uint IMAGE_WIDGET_BUTTON_MARGIN = 8; 0081 static const uint IMAGE_WIDGET_IMAGE_MARGIN = 4; 0082 static const uint MAX_UNSCALED_WIDTH = 640; 0083 static const uint MAX_UNSCALED_HEIGHT = 640; 0084 } 0085 0086 using Tellico::GUI::ImageWidget; 0087 0088 ImageWidget::ImageWidget(QWidget* parent_) : QWidget(parent_), m_editMenu(nullptr), 0089 m_editProcess(nullptr), m_waitDlg(nullptr) 0090 #ifdef HAVE_KSANE 0091 , m_saneWidget(nullptr), m_saneDlg(nullptr), m_saneDeviceIsOpen(false) 0092 #endif 0093 { 0094 QHBoxLayout* l = new QHBoxLayout(this); 0095 l->setMargin(IMAGE_WIDGET_BUTTON_MARGIN); 0096 m_label = new QLabel(this); 0097 m_label->setSizePolicy(QSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored)); 0098 m_label->setFrameStyle(QFrame::Panel | QFrame::Sunken); 0099 m_label->setAlignment(Qt::AlignCenter); 0100 l->addWidget(m_label, 1); 0101 l->addSpacing(IMAGE_WIDGET_BUTTON_MARGIN); 0102 0103 QVBoxLayout* boxLayout = new QVBoxLayout(); 0104 l->addLayout(boxLayout); 0105 0106 boxLayout->addStretch(1); 0107 0108 QPushButton* button1 = new QPushButton(i18n("Select Image..."), this); 0109 button1->setIcon(QIcon::fromTheme(QStringLiteral("insert-image"))); 0110 connect(button1, &QAbstractButton::clicked, this, &ImageWidget::slotGetImage); 0111 boxLayout->addWidget(button1); 0112 0113 QPushButton* button2 = new QPushButton(i18n("Scan Image..."), this); 0114 button2->setIcon(QIcon::fromTheme(QStringLiteral("scanner"))); 0115 connect(button2, &QAbstractButton::clicked, this, &ImageWidget::slotScanImage); 0116 boxLayout->addWidget(button2); 0117 #ifndef HAVE_KSANE 0118 button2->setEnabled(false); 0119 #endif 0120 0121 m_edit = new QToolButton(this); 0122 m_edit->setText(i18n("Open With...")); 0123 m_edit->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); 0124 m_edit->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); 0125 connect(m_edit, &QAbstractButton::clicked, this, &ImageWidget::slotEditImage); 0126 boxLayout->addWidget(m_edit); 0127 0128 KConfigGroup config(KSharedConfig::openConfig(), "EditImage"); 0129 QString editor = config.readEntry("editor"); 0130 m_editMenu = new QMenu(this); 0131 QActionGroup* grp = new QActionGroup(this); 0132 grp->setExclusive(true); 0133 QAction* selectedAction = nullptr; 0134 #if KSERVICE_VERSION >= QT_VERSION_CHECK(5, 68, 0) 0135 auto offers = KApplicationTrader::queryByMimeType(QStringLiteral("image/png")); 0136 #else 0137 auto offers = KMimeTypeTrader::self()->query(QStringLiteral("image/png"), 0138 QStringLiteral("Application")); 0139 #endif 0140 QSet<QString> offerNames; 0141 foreach(KService::Ptr service, offers) { 0142 if(offerNames.contains(service->name())) { 0143 continue; 0144 } 0145 offerNames.insert(service->name()); 0146 QAction* action = m_editMenu->addAction(QIcon::fromTheme(service->icon()), service->name()); 0147 action->setData(QVariant::fromValue(service)); 0148 grp->addAction(action); 0149 if(!selectedAction || editor == service->name()) { 0150 selectedAction = action; 0151 } 0152 } 0153 if(selectedAction) { 0154 slotEditMenu(selectedAction); 0155 m_edit->setMenu(m_editMenu); 0156 connect(m_editMenu, &QMenu::triggered, this, &ImageWidget::slotEditMenu); 0157 } else { 0158 m_edit->setEnabled(false); 0159 } 0160 QPushButton* button4 = new QPushButton(i18nc("Clear image", "Clear"), this); 0161 button4->setIcon(QIcon::fromTheme(QStringLiteral("edit-clear"))); 0162 connect(button4, &QAbstractButton::clicked, this, &ImageWidget::slotClear); 0163 boxLayout->addWidget(button4); 0164 0165 boxLayout->addSpacing(8); 0166 0167 m_cbLinkOnly = new QCheckBox(i18n("Save link only"), this); 0168 connect(m_cbLinkOnly, &QAbstractButton::clicked, this, &ImageWidget::slotLinkOnlyClicked); 0169 boxLayout->addWidget(m_cbLinkOnly); 0170 0171 boxLayout->addStretch(1); 0172 slotClear(); 0173 0174 // accept image drops 0175 setAcceptDrops(true); 0176 } 0177 0178 ImageWidget::~ImageWidget() { 0179 if(m_editor) { 0180 KConfigGroup config(KSharedConfig::openConfig(), "EditImage"); 0181 config.writeEntry("editor", m_editor->name()); 0182 } 0183 } 0184 0185 void ImageWidget::setImage(const QString& id_) { 0186 if(id_.isEmpty()) { 0187 slotClear(); 0188 return; 0189 } 0190 m_imageID = id_; 0191 m_pixmap = ImageFactory::pixmap(id_, MAX_UNSCALED_WIDTH, MAX_UNSCALED_HEIGHT); 0192 const bool link = ImageFactory::imageInfo(id_).linkOnly; 0193 m_cbLinkOnly->setChecked(link); 0194 m_cbLinkOnly->setEnabled(link); // user can't make a non;-linked image a linked image, so disable if not linked 0195 m_edit->setEnabled(true); 0196 // if we're using a link, then the original URL _is_ the id 0197 m_originalURL = link ? QUrl(id_) : QUrl(); 0198 m_scaled = QPixmap(); 0199 scale(); 0200 0201 update(); 0202 } 0203 0204 void ImageWidget::setLinkOnlyChecked(bool link_) { 0205 m_cbLinkOnly->setChecked(link_); 0206 } 0207 0208 void ImageWidget::slotClear() { 0209 bool wasEmpty = m_imageID.isEmpty(); 0210 // m_image = Data::Image(); 0211 m_imageID.clear(); 0212 m_pixmap = QPixmap(); 0213 m_scaled = m_pixmap; 0214 m_originalURL.clear(); 0215 0216 m_label->setPixmap(m_scaled); 0217 m_cbLinkOnly->setChecked(false); 0218 m_cbLinkOnly->setEnabled(true); 0219 m_edit->setEnabled(false); 0220 update(); 0221 if(!wasEmpty) { 0222 emit signalModified(); 0223 } 0224 } 0225 0226 void ImageWidget::contextMenuEvent(QContextMenuEvent* event_) { 0227 if(m_imageID.isEmpty() || m_pixmap.isNull()) { 0228 return; 0229 } 0230 0231 QMenu menu(this); 0232 0233 auto standardCopy = KStandardAction::copy(this, &ImageWidget::copyImage, &menu); 0234 standardCopy->setToolTip(QString()); // standard tool tip is 0235 menu.addAction(standardCopy); 0236 0237 auto saveAs = KStandardAction::saveAs(this, &ImageWidget::saveImageAs, &menu); 0238 saveAs->setToolTip(QString()); 0239 menu.addAction(saveAs); 0240 0241 menu.exec(event_->globalPos()); 0242 } 0243 0244 void ImageWidget::scale() { 0245 int ww = m_label->width() - 2*IMAGE_WIDGET_IMAGE_MARGIN; 0246 int wh = m_label->height() - 2*IMAGE_WIDGET_IMAGE_MARGIN; 0247 int pw = m_pixmap.width(); 0248 int ph = m_pixmap.height(); 0249 0250 if(ww < pw || wh < ph) { 0251 int newWidth, newHeight; 0252 if(pw*wh < ph*ww) { 0253 newWidth = static_cast<int>(static_cast<double>(pw)*wh/static_cast<double>(ph)); 0254 newHeight = wh; 0255 } else { 0256 newWidth = ww; 0257 newHeight = static_cast<int>(static_cast<double>(ph)*ww/static_cast<double>(pw)); 0258 } 0259 0260 QTransform wm; 0261 wm.scale(static_cast<double>(newWidth)/pw, static_cast<double>(newHeight)/ph); 0262 m_scaled = m_pixmap.transformed(wm, Qt::SmoothTransformation); 0263 } else { 0264 m_scaled = m_pixmap; 0265 } 0266 m_label->setPixmap(m_scaled); 0267 } 0268 0269 void ImageWidget::resizeEvent(QResizeEvent *) { 0270 if(m_pixmap.isNull()) { 0271 return; 0272 } 0273 0274 scale(); 0275 update(); 0276 } 0277 0278 void ImageWidget::slotGetImage() { 0279 QString filter; 0280 foreach(const QByteArray& ba, QImageReader::supportedImageFormats()) { 0281 if(!filter.isEmpty()) { 0282 filter += QLatin1Char(' '); 0283 } 0284 filter += QLatin1String("*.") + QString::fromLatin1(ba); 0285 } 0286 QStringList imageDirs = QStandardPaths::standardLocations(QStandardPaths::PicturesLocation); 0287 QString imageStartDir = imageDirs.isEmpty() ? QString() : imageDirs.first(); 0288 QString fileClass; 0289 QUrl startUrl = KFileWidget::getStartUrl(QUrl(QLatin1String("kfiledialog:///image") + imageStartDir), fileClass); 0290 const QUrl url = QFileDialog::getOpenFileUrl(this, QString(), startUrl, i18n("All Images (%1)", filter)); 0291 if(url.isEmpty() || !url.isValid()) { 0292 return; 0293 } 0294 KRecentDirs::add(fileClass, url.adjusted(QUrl::RemoveFilename|QUrl::StripTrailingSlash).path()); 0295 loadImage(url); 0296 } 0297 0298 void ImageWidget::slotScanImage() { 0299 #ifdef HAVE_KSANE 0300 if(!m_saneDlg) { 0301 m_saneDlg = new KPageDialog(this); 0302 m_saneWidget = new KSaneIface::KSaneWidget(m_saneDlg); 0303 m_saneDlg->addPage(m_saneWidget, QString()); 0304 m_saneDlg->setStandardButtons(QDialogButtonBox::Cancel); 0305 m_saneDlg->setAttribute(Qt::WA_DeleteOnClose, false); 0306 #if KSANE_VERSION < QT_VERSION_CHECK(21,8,0) 0307 connect(m_saneWidget.data(), &KSaneIface::KSaneWidget::imageReady, 0308 #else 0309 connect(m_saneWidget.data(), &KSaneIface::KSaneWidget::scannedImageReady, 0310 #endif 0311 this, &ImageWidget::imageReady); 0312 connect(m_saneDlg.data(), &QDialog::rejected, 0313 this, &ImageWidget::cancelScan); 0314 } 0315 if(m_saneDevice.isEmpty()) { 0316 m_saneDevice = m_saneWidget->selectDevice(this); 0317 } 0318 if(!m_saneDevice.isEmpty() && !m_saneDeviceIsOpen) { 0319 m_saneDeviceIsOpen = m_saneWidget->openDevice(m_saneDevice); 0320 if(!m_saneDeviceIsOpen) { 0321 KMessageBox::error(this, i18n("Opening the selected scanner failed.")); 0322 m_saneDevice.clear(); 0323 } 0324 } 0325 if(!m_saneDeviceIsOpen || m_saneDevice.isEmpty()) { 0326 return; 0327 } 0328 m_saneDlg->exec(); 0329 #endif 0330 } 0331 0332 #ifdef HAVE_KSANE 0333 #if KSANE_VERSION < QT_VERSION_CHECK(21,8,0) 0334 void ImageWidget::imageReady(QByteArray& data, int w, int h, int bpl, int f) { 0335 #else 0336 void ImageWidget::imageReady(const QImage& scannedImage) { 0337 #endif 0338 if(!m_saneWidget) { 0339 return; 0340 } 0341 #if KSANE_VERSION < QT_VERSION_CHECK(21,8,0) 0342 QImage scannedImage = m_saneWidget->toQImage(data, w, h, bpl, static_cast<KSaneIface::KSaneWidget::ImageFormat>(f)); 0343 #endif 0344 0345 QTemporaryFile temp(QDir::tempPath() + QLatin1String("/tellico_XXXXXX") + QLatin1String(".png")); 0346 if(temp.open()) { 0347 scannedImage.save(temp.fileName(), "PNG"); 0348 loadImage(QUrl::fromLocalFile(temp.fileName())); 0349 } else { 0350 myWarning() << "Failed to open temp image file"; 0351 } 0352 QTimer::singleShot(100, m_saneDlg.data(), &QDialog::accept); 0353 } 0354 #endif 0355 0356 void ImageWidget::slotEditImage() { 0357 if(m_imageID.isEmpty()) { 0358 return; 0359 } 0360 0361 if(!m_editProcess) { 0362 m_editProcess = new KProcess(this); 0363 void (KProcess::* finished)(int, QProcess::ExitStatus) = &KProcess::finished; 0364 connect(m_editProcess, finished, 0365 this, &ImageWidget::slotFinished); 0366 } 0367 if(m_editor && m_editProcess->state() == QProcess::NotRunning) { 0368 QTemporaryFile temp(QDir::tempPath() + QLatin1String("/tellico_XXXXXX") + QLatin1String(".png")); 0369 if(temp.open()) { 0370 m_img = temp.fileName(); 0371 const Data::Image& img = ImageFactory::imageById(m_imageID); 0372 img.save(m_img); 0373 m_editedFileDateTime = QFileInfo(m_img).lastModified(); 0374 KIO::DesktopExecParser parser(*m_editor, QList<QUrl>() << QUrl::fromLocalFile(m_img)); 0375 m_editProcess->setProgram(parser.resultingArguments()); 0376 m_editProcess->start(); 0377 if(!m_waitDlg) { 0378 m_waitDlg = new QProgressDialog(this); 0379 m_waitDlg->setCancelButton(nullptr); 0380 m_waitDlg->setLabelText(i18n("Opening image in %1...", m_editor->name())); 0381 m_waitDlg->setRange(0, 0); 0382 } 0383 m_waitDlg->exec(); 0384 } else { 0385 myWarning() << "Failed to open temp image file"; 0386 } 0387 } 0388 } 0389 0390 void ImageWidget::slotFinished() { 0391 if(m_editedFileDateTime != QFileInfo(m_img).lastModified()) { 0392 loadImage(QUrl::fromLocalFile(m_img)); 0393 } 0394 m_waitDlg->close(); 0395 } 0396 0397 void ImageWidget::slotEditMenu(QAction* action) { 0398 m_editor = action->data().value<KService::Ptr>(); 0399 m_edit->setIcon(QIcon::fromTheme(m_editor->icon())); 0400 } 0401 0402 void ImageWidget::slotLinkOnlyClicked() { 0403 if(m_imageID.isEmpty()) { 0404 // nothing to do, it has an empty image; 0405 return; 0406 } 0407 0408 const bool link = m_cbLinkOnly->isChecked(); 0409 // if the user is trying to link and can't before there's no information about the url 0410 // then let him know that 0411 if(link && m_originalURL.isEmpty()) { 0412 KMessageBox::error(this, i18n("Saving a link is only possible for newly added images.")); 0413 m_cbLinkOnly->setChecked(false); 0414 return; 0415 } 0416 // need to reset image id to be the original url 0417 // if we're linking only, then we want the image id to be the same as the url 0418 // so it needs to be added to the cache all over again 0419 // probably could do this without downloading the image all over again, 0420 // but I'm not going to do that right now 0421 const QString& id = ImageFactory::addImage(m_originalURL, false, QUrl(), link); 0422 // same image, so no need to call setImage 0423 m_imageID = id; 0424 emit signalModified(); 0425 } 0426 0427 void ImageWidget::mousePressEvent(QMouseEvent* event_) { 0428 // Only interested in LMB 0429 if(event_->button() == Qt::LeftButton) { 0430 // Store the position of the mouse press. 0431 // check if position is inside the label 0432 if(m_label->geometry().contains(event_->pos())) { 0433 m_dragStart = event_->pos(); 0434 } else { 0435 m_dragStart = QPoint(); 0436 } 0437 } 0438 } 0439 0440 void ImageWidget::mouseMoveEvent(QMouseEvent* event_) { 0441 int delay = QApplication::startDragDistance(); 0442 // Only interested in LMB 0443 if(event_->buttons() & Qt::LeftButton) { 0444 // only allow drag if the image is non-null, and the drag start point isn't null and the user dragged far enough 0445 if(!m_imageID.isEmpty() && !m_dragStart.isNull() && (m_dragStart - event_->pos()).manhattanLength() > delay) { 0446 const Data::Image& img = ImageFactory::imageById(m_imageID); 0447 if(!img.isNull()) { 0448 QDrag* drag = new QDrag(this); 0449 QMimeData* mimeData = new QMimeData(); 0450 mimeData->setImageData(img); 0451 drag->setMimeData(mimeData); 0452 drag->setPixmap(QPixmap::fromImage(img)); 0453 drag->exec(Qt::CopyAction); 0454 event_->accept(); 0455 } 0456 } 0457 } 0458 } 0459 0460 void ImageWidget::dragEnterEvent(QDragEnterEvent* event_) { 0461 if(event_->mimeData()->hasImage() || event_->mimeData()->hasText()) { 0462 event_->acceptProposedAction(); 0463 } 0464 } 0465 0466 void ImageWidget::dropEvent(QDropEvent* event_) { 0467 GUI::CursorSaver cs; 0468 if(event_->mimeData()->hasImage()) { 0469 QVariant imageData = event_->mimeData()->imageData(); 0470 // Qt reads PNG data by default 0471 const QString& id = ImageFactory::addImage(qvariant_cast<QPixmap>(imageData), QStringLiteral("PNG")); 0472 if(!id.isEmpty() && id != m_imageID) { 0473 setImage(id); 0474 emit signalModified(); 0475 } 0476 event_->acceptProposedAction(); 0477 } else if(event_->mimeData()->hasText()) { 0478 QUrl url = QUrl::fromUserInput(event_->mimeData()->text()); 0479 if(!url.isEmpty() && url.isValid()) { 0480 loadImage(url); 0481 event_->acceptProposedAction(); 0482 } 0483 } 0484 } 0485 0486 void ImageWidget::loadImage(const QUrl& url_) { 0487 const bool link = m_cbLinkOnly->isChecked(); 0488 0489 GUI::CursorSaver cs; 0490 // if we're linking only, then we want the image id to be the same as the url 0491 const QString& id = ImageFactory::addImage(url_, false, QUrl(), link); 0492 if(id != m_imageID) { 0493 setImage(id); 0494 emit signalModified(); 0495 } 0496 // at the end, cause setImage() resets it 0497 m_originalURL = url_; 0498 m_cbLinkOnly->setEnabled(true); 0499 } 0500 0501 void ImageWidget::cancelScan() { 0502 #ifdef HAVE_KSANE 0503 if(m_saneWidget) { 0504 #if KSANE_VERSION < QT_VERSION_CHECK(22,4,0) 0505 m_saneWidget->scanCancel(); 0506 #else 0507 m_saneWidget->cancelScan(); 0508 #endif 0509 } 0510 #endif 0511 } 0512 0513 void ImageWidget::copyImage() { 0514 const Data::Image& img = ImageFactory::imageById(m_imageID); 0515 if(img.isNull()) { 0516 return; 0517 } 0518 0519 QApplication::clipboard()->setImage(img, QClipboard::Clipboard); 0520 QApplication::clipboard()->setImage(img, QClipboard::Selection); 0521 } 0522 0523 void ImageWidget::saveImageAs() { 0524 const Data::Image& img = ImageFactory::imageById(m_imageID); 0525 if(img.isNull()) { 0526 return; 0527 } 0528 0529 QByteArray outputFormat = Data::Image::outputFormat(img.format()); 0530 const QString filter = i18n("All Images (%1)", QLatin1String("*.") + QString::fromLatin1(outputFormat)); 0531 const QUrl target = QFileDialog::getSaveFileUrl(this, QString(), QUrl(), filter); 0532 if(!target.isEmpty() && target.isValid()) { 0533 QString suffix = QFileInfo(target.fileName()).suffix(); 0534 if(suffix.toLower().toUtf8() != outputFormat.toLower()) { 0535 outputFormat = Data::Image::outputFormat(suffix.toUtf8()); 0536 myDebug() << "Writing image data as" << outputFormat; 0537 } 0538 const bool success = FileHandler::writeDataURL(target, Data::Image::byteArray(img, outputFormat)); 0539 if(!success) { 0540 myDebug() << "Failed to write image to" << target.toDisplayString(); 0541 } 0542 } 0543 }