diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dfc0ccee..a0ef510a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -373,6 +373,8 @@ set(${PROJECT_NAME}_SOURCES src/widget/extensionstatus.h src/widget/flowlayout.cpp src/widget/flowlayout.h + src/widget/imagepreviewwidget.h + src/widget/imagepreviewwidget.cpp src/widget/searchform.cpp src/widget/searchform.h src/widget/searchtypes.h diff --git a/src/chatlog/content/filetransferwidget.cpp b/src/chatlog/content/filetransferwidget.cpp index 6e62d5a59..8716d3634 100644 --- a/src/chatlog/content/filetransferwidget.cpp +++ b/src/chatlog/content/filetransferwidget.cpp @@ -500,49 +500,8 @@ void FileTransferWidget::handleButton(QPushButton* btn) void FileTransferWidget::showPreview(const QString& filename) { - static const QStringList previewExtensions = {"png", "jpeg", "jpg", "gif", "svg", - "PNG", "JPEG", "JPG", "GIF", "SVG"}; - - if (previewExtensions.contains(QFileInfo(filename).suffix())) { - // Subtract to make border visible - const int size = qMax(ui->previewButton->width(), ui->previewButton->height()) - 4; - - QFile imageFile(filename); - if (!imageFile.open(QIODevice::ReadOnly)) { - return; - } - - const QByteArray imageFileData = imageFile.readAll(); - QImage image = QImage::fromData(imageFileData); - auto orientation = ExifTransform::getOrientation(imageFileData); - image = ExifTransform::applyTransformation(image, orientation); - - const QPixmap iconPixmap = scaleCropIntoSquare(QPixmap::fromImage(image), size); - - ui->previewButton->setIcon(QIcon(iconPixmap)); - ui->previewButton->setIconSize(iconPixmap.size()); - ui->previewButton->show(); - // Show mouseover preview, but make sure it's not larger than 50% of the screen - // width/height - const QRect desktopSize = QApplication::desktop()->geometry(); - const int maxPreviewWidth{desktopSize.width() / 2}; - const int maxPreviewHeight{desktopSize.height() / 2}; - const QImage previewImage = [&image, maxPreviewWidth, maxPreviewHeight]() { - if (image.width() > maxPreviewWidth || image.height() > maxPreviewHeight) { - return image.scaled(maxPreviewWidth, maxPreviewHeight, Qt::KeepAspectRatio, - Qt::SmoothTransformation); - } else { - return image; - } - }(); - - QByteArray imageData; - QBuffer buffer(&imageData); - buffer.open(QIODevice::WriteOnly); - previewImage.save(&buffer, "PNG"); - buffer.close(); - ui->previewButton->setToolTip(""); - } + ui->previewButton->setIconFromFile(filename); + ui->previewButton->show(); } void FileTransferWidget::onLeftButtonClicked() @@ -560,31 +519,6 @@ void FileTransferWidget::onPreviewButtonClicked() handleButton(ui->previewButton); } -QPixmap FileTransferWidget::scaleCropIntoSquare(const QPixmap& source, const int targetSize) -{ - QPixmap result; - - // Make sure smaller-than-icon images (at least one dimension is smaller) will not be - // upscaled - if (source.width() < targetSize || source.height() < targetSize) { - result = source; - } else { - result = source.scaled(targetSize, targetSize, Qt::KeepAspectRatioByExpanding, - Qt::SmoothTransformation); - } - - // Then, image has to be cropped (if needed) so it will not overflow rectangle - // Only one dimension will be bigger after Qt::KeepAspectRatioByExpanding - if (result.width() > targetSize) { - return result.copy((result.width() - targetSize) / 2, 0, targetSize, targetSize); - } else if (result.height() > targetSize) { - return result.copy(0, (result.height() - targetSize) / 2, targetSize, targetSize); - } - - // Picture was rectangle in the first place, no cropping - return result; -} - void FileTransferWidget::updateWidget(ToxFile const& file) { assert(file == fileInfo); diff --git a/src/chatlog/content/filetransferwidget.h b/src/chatlog/content/filetransferwidget.h index 88face59d..39c39d35c 100644 --- a/src/chatlog/content/filetransferwidget.h +++ b/src/chatlog/content/filetransferwidget.h @@ -73,9 +73,6 @@ private slots: void onPreviewButtonClicked(); private: - static QPixmap scaleCropIntoSquare(const QPixmap& source, int targetSize); - static int getExifOrientation(const char* data, const int size); - static void applyTransformation(const int oritentation, QImage& image); static bool tryRemoveFile(const QString &filepath); void updateWidget(ToxFile const& file); diff --git a/src/chatlog/content/filetransferwidget.ui b/src/chatlog/content/filetransferwidget.ui index eca5d8480..053ef9ebd 100644 --- a/src/chatlog/content/filetransferwidget.ui +++ b/src/chatlog/content/filetransferwidget.ui @@ -270,7 +270,7 @@ - + 0 @@ -296,7 +296,7 @@ QPushButton{ border: 2px solid white } - + :themes/default/fileTransferInstance/no.svg:themes/default/fileTransferInstance/no.svg @@ -382,7 +382,7 @@ - + :themes/default/fileTransferInstance/no.svg:themes/default/fileTransferInstance/no.svg @@ -420,7 +420,7 @@ - + :themes/default/fileTransferInstance/no.svg:themes/default/fileTransferInstance/no.svg @@ -445,6 +445,11 @@ QLabel
src/widget/tool/croppinglabel.h
+ + ImagePreviewButton + QPushButton +
src/widget/imagepreviewwidget.h
+
previewButton diff --git a/src/widget/form/chatform.cpp b/src/widget/form/chatform.cpp index d0b4f1942..e31d9fd4b 100644 --- a/src/widget/form/chatform.cpp +++ b/src/widget/form/chatform.cpp @@ -37,6 +37,7 @@ #include "src/widget/chatformheader.h" #include "src/widget/contentdialogmanager.h" #include "src/widget/form/loadhistorydialog.h" +#include "src/widget/imagepreviewwidget.h" #include "src/widget/maskablepixmapwidget.h" #include "src/widget/searchform.h" #include "src/widget/style.h" @@ -134,6 +135,23 @@ ChatForm::ChatForm(Profile& profile, Friend* chatFriend, IChatLog& chatLog, IMes headWidget->addWidget(callDuration, 1, Qt::AlignCenter); callDuration->hide(); + imagePreview = new ImagePreviewButton(this); + imagePreview->setFixedSize(100, 100); + imagePreview->setFlat(true); + imagePreview->setStyleSheet("QPushButton { border: 0px }"); + imagePreview->hide(); + + auto cancelIcon = QIcon(Style::getImagePath("rejectCall/rejectCall.svg")); + QPushButton* cancelButton = new QPushButton(imagePreview); + cancelButton->setFixedSize(20, 20); + cancelButton->move(QPoint(80, 0)); + cancelButton->setIcon(cancelIcon); + cancelButton->setFlat(true); + + connect(cancelButton, &QPushButton::pressed, this, &ChatForm::cancelImagePreview); + + contentLayout->insertWidget(3, imagePreview); + copyStatusAction = statusMessageMenu.addAction(QString(), this, SLOT(onCopyStatusMessage())); const CoreFile* coreFile = core.getCoreFile(); @@ -155,9 +173,12 @@ ChatForm::ChatForm(Profile& profile, Friend* chatFriend, IChatLog& chatLog, IMes connect(headWidget, &ChatFormHeader::micMuteToggle, this, &ChatForm::onMicMuteToggle); connect(headWidget, &ChatFormHeader::volMuteToggle, this, &ChatForm::onVolMuteToggle); connect(sendButton, &QPushButton::pressed, this, &ChatForm::callUpdateFriendActivity); + connect(sendButton, &QPushButton::pressed, this, &ChatForm::sendImageFromPreview); connect(msgEdit, &ChatTextEdit::enterPressed, this, &ChatForm::callUpdateFriendActivity); connect(msgEdit, &ChatTextEdit::textChanged, this, &ChatForm::onTextEditChanged); - connect(msgEdit, &ChatTextEdit::pasteImage, this, &ChatForm::sendImage); + connect(msgEdit, &ChatTextEdit::pasteImage, this, &ChatForm::previewImage); + connect(msgEdit, &ChatTextEdit::enterPressed, this, &ChatForm::sendImageFromPreview); + connect(msgEdit, &ChatTextEdit::escapePressed, this, &ChatForm::cancelImagePreview); connect(statusMessageLabel, &CroppingLabel::customContextMenuRequested, this, [&](const QPoint& pos) { if (!statusMessageLabel->text().isEmpty()) { @@ -561,12 +582,29 @@ void ChatForm::doScreenshot() { // note: grabber is self-managed and will destroy itself when done ScreenshotGrabber* grabber = new ScreenshotGrabber; - connect(grabber, &ScreenshotGrabber::screenshotTaken, this, &ChatForm::sendImage); + connect(grabber, &ScreenshotGrabber::screenshotTaken, this, &ChatForm::previewImage); grabber->showGrabber(); } -void ChatForm::sendImage(const QPixmap& pixmap) +void ChatForm::previewImage(const QPixmap& pixmap) { + imagePreviewSource = pixmap; + imagePreview->setIconFromPixmap(pixmap); + imagePreview->show(); +} + +void ChatForm::cancelImagePreview() +{ + imagePreviewSource = QPixmap(); + imagePreview->hide(); +} + +void ChatForm::sendImageFromPreview() +{ + if (!imagePreview->isVisible()) { + return; + } + QDir(Settings::getInstance().getPaths().getAppDataDirPath()).mkpath("images"); // use ~ISO 8601 for screenshot timestamp, considering FS limitations @@ -580,8 +618,10 @@ void ChatForm::sendImage(const QPixmap& pixmap) QFile file(filepath); if (file.open(QFile::ReadWrite)) { - pixmap.save(&file, "PNG"); + imagePreviewSource.save(&file, "PNG"); qint64 filesize = file.size(); + imagePreview->hide(); + imagePreviewSource = QPixmap(); file.close(); QFileInfo fi(file); CoreFile* coreFile = core.getCoreFile(); diff --git a/src/widget/form/chatform.h b/src/widget/form/chatform.h index bb940e5ad..b371f385e 100644 --- a/src/widget/form/chatform.h +++ b/src/widget/form/chatform.h @@ -41,6 +41,7 @@ class OfflineMsgEngine; class QPixmap; class QHideEvent; class QMoveEvent; +class ImagePreviewButton; class ChatForm : public GenericChatForm { @@ -96,7 +97,9 @@ private slots: void onFriendNameChanged(const QString& name); void onStatusMessage(const QString& message); void onUpdateTime(); - void sendImage(const QPixmap& pixmap); + void previewImage(const QPixmap& pixmap); + void cancelImagePreview(); + void sendImageFromPreview(); void doScreenshot(); void onCopyStatusMessage(); @@ -131,6 +134,8 @@ private: QTimer typingTimer; QElapsedTimer timeElapsed; QAction* copyStatusAction; + QPixmap imagePreviewSource; + ImagePreviewButton* imagePreview; bool isTyping; bool lastCallIsVideo; std::unique_ptr netcam; diff --git a/src/widget/form/genericchatform.cpp b/src/widget/form/genericchatform.cpp index c66a80c22..83299c260 100644 --- a/src/widget/form/genericchatform.cpp +++ b/src/widget/form/genericchatform.cpp @@ -309,7 +309,7 @@ GenericChatForm::GenericChatForm(const Core& _core, const Contact* contact, ICha mainFootLayout->addWidget(sendButton); mainFootLayout->setSpacing(0); - QVBoxLayout* contentLayout = new QVBoxLayout(contentWidget); + contentLayout = new QVBoxLayout(contentWidget); contentLayout->addWidget(searchForm); contentLayout->addWidget(dateInfo); contentLayout->addWidget(chatWidget); diff --git a/src/widget/form/genericchatform.h b/src/widget/form/genericchatform.h index 359b69644..9853eac4d 100644 --- a/src/widget/form/genericchatform.h +++ b/src/widget/form/genericchatform.h @@ -168,6 +168,7 @@ protected: QMenu menu; + QVBoxLayout* contentLayout; QPushButton* emoteButton; QPushButton* fileButton; QPushButton* screenshotButton; diff --git a/src/widget/imagepreviewwidget.cpp b/src/widget/imagepreviewwidget.cpp new file mode 100644 index 000000000..0a14a0fee --- /dev/null +++ b/src/widget/imagepreviewwidget.cpp @@ -0,0 +1,125 @@ +/* + Copyright © 2020 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox 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 General Public License + along with qTox. If not, see . +*/ + +#include "imagepreviewwidget.h" +#include "src/model/exiftransform.h" + +#include +#include +#include +#include +#include +#include + +namespace +{ +QPixmap pixmapFromFile(const QString& filename) +{ + static const QStringList previewExtensions = {"png", "jpeg", "jpg", "gif", "svg", + "PNG", "JPEG", "JPG", "GIF", "SVG"}; + + if (!previewExtensions.contains(QFileInfo(filename).suffix())) { + return QPixmap(); + } + + QFile imageFile(filename); + if (!imageFile.open(QIODevice::ReadOnly)) { + return QPixmap(); + } + + const QByteArray imageFileData = imageFile.readAll(); + QImage image = QImage::fromData(imageFileData); + auto orientation = ExifTransform::getOrientation(imageFileData); + image = ExifTransform::applyTransformation(image, orientation); + + return QPixmap::fromImage(image); +} + +QPixmap scaleCropIntoSquare(const QPixmap& source, const int targetSize) +{ + QPixmap result; + + // Make sure smaller-than-icon images (at least one dimension is smaller) will not be + // upscaled + if (source.width() < targetSize || source.height() < targetSize) { + result = source; + } else { + result = source.scaled(targetSize, targetSize, Qt::KeepAspectRatioByExpanding, + Qt::SmoothTransformation); + } + + // Then, image has to be cropped (if needed) so it will not overflow rectangle + // Only one dimension will be bigger after Qt::KeepAspectRatioByExpanding + if (result.width() > targetSize) { + return result.copy((result.width() - targetSize) / 2, 0, targetSize, targetSize); + } else if (result.height() > targetSize) { + return result.copy(0, (result.height() - targetSize) / 2, targetSize, targetSize); + } + + // Picture was rectangle in the first place, no cropping + return result; +} + +QString getToolTipDisplayingImage(const QPixmap& image) +{ + // Show mouseover preview, but make sure it's not larger than 50% of the screen + // width/height + const QRect desktopSize = QApplication::desktop()->geometry(); + const int maxPreviewWidth{desktopSize.width() / 2}; + const int maxPreviewHeight{desktopSize.height() / 2}; + const QPixmap previewImage = [&image, maxPreviewWidth, maxPreviewHeight]() { + if (image.width() > maxPreviewWidth || image.height() > maxPreviewHeight) { + return image.scaled(maxPreviewWidth, maxPreviewHeight, Qt::KeepAspectRatio, + Qt::SmoothTransformation); + } else { + return image; + } + }(); + + QByteArray imageData; + QBuffer buffer(&imageData); + buffer.open(QIODevice::WriteOnly); + previewImage.save(&buffer, "PNG"); + buffer.close(); + + return ""; +} + +} // namespace + +void ImagePreviewButton::initialize(const QPixmap& image) +{ + auto desiredSize = qMin(width(), height()); // Assume widget is a square + desiredSize = qMax(desiredSize, 4) - 4; // Leave some room for a border + + auto croppedImage = scaleCropIntoSquare(image, desiredSize); + setIcon(QIcon(croppedImage)); + setIconSize(croppedImage.size()); + setToolTip(getToolTipDisplayingImage(image)); +} + +void ImagePreviewButton::setIconFromFile(const QString& filename) +{ + initialize(pixmapFromFile(filename)); +} + +void ImagePreviewButton::setIconFromPixmap(const QPixmap& pixmap) +{ + initialize(pixmap); +} diff --git a/src/widget/imagepreviewwidget.h b/src/widget/imagepreviewwidget.h new file mode 100644 index 000000000..2eb51527b --- /dev/null +++ b/src/widget/imagepreviewwidget.h @@ -0,0 +1,37 @@ +/* + Copyright © 2020 by The qTox Project Contributors + + This file is part of qTox, a Qt-based graphical interface for Tox. + + qTox is libre software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + qTox 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 General Public License + along with qTox. If not, see . +*/ + +#pragma once + +#include +#include +#include + +class ImagePreviewButton : public QPushButton +{ +public: + ImagePreviewButton(QWidget* parent = nullptr) + : QPushButton(parent) + {} + + void setIconFromFile(const QString& filename); + void setIconFromPixmap(const QPixmap& image); +private: + void initialize(const QPixmap& image); +}; diff --git a/src/widget/tool/chattextedit.cpp b/src/widget/tool/chattextedit.cpp index 3de982405..e67f85f29 100644 --- a/src/widget/tool/chattextedit.cpp +++ b/src/widget/tool/chattextedit.cpp @@ -48,6 +48,10 @@ void ChatTextEdit::keyPressEvent(QKeyEvent* event) emit enterPressed(); return; } + if (key == Qt::Key_Escape) { + emit escapePressed(); + return; + } if (key == Qt::Key_Tab) { if (event->modifiers()) event->ignore(); diff --git a/src/widget/tool/chattextedit.h b/src/widget/tool/chattextedit.h index 7b3a8ccf9..efa00ff4c 100644 --- a/src/widget/tool/chattextedit.h +++ b/src/widget/tool/chattextedit.h @@ -32,6 +32,7 @@ public: signals: void enterPressed(); + void escapePressed(); void tabPressed(); void keyPressed(); void pasteImage(const QPixmap& pixmap);