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 }