diff --git a/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.cpp b/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.cpp index 177289f09..98d144c1d 100644 --- a/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.cpp +++ b/retroshare-gui/src/gui/gxschannels/CreateGxsChannelMsg.cpp @@ -688,7 +688,7 @@ void CreateGxsChannelMsg::sendMessage(const std::string &subject, const std::str // send chan image buffer.open(QIODevice::WriteOnly); - picture.save(&buffer, "PNG"); // writes image into ba in PNG format + preview_W->getCroppedScaledPicture().save(&buffer, "PNG"); // writes image into ba in PNG format post.mThumbnail.copy((uint8_t *) ba.data(), ba.size()); } @@ -723,7 +723,7 @@ void CreateGxsChannelMsg::sendMessage(const std::string &subject, const std::str void CreateGxsChannelMsg::addThumbnail() { - QPixmap img = misc::getOpenThumbnailedPicture(this, tr("Load thumbnail picture"), 107,156); // these absolute sizes are terrible + QPixmap img = misc::getOpenThumbnailedPicture(this, tr("Load thumbnail picture"), 0,0); // 0,0 means: no scale. if (img.isNull()) return; diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.cpp b/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.cpp new file mode 100644 index 000000000..09b9e9038 --- /dev/null +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.cpp @@ -0,0 +1,189 @@ +/******************************************************************************* + * retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.cpp * + * * + * Copyright 2020 by Retroshare Team * + * * + * This program is free software: you can redistribute it and/or modify * + * it under the terms of the GNU Affero General Public License as * + * published by the Free Software Foundation, either version 3 of the * + * License, or (at your option) any later version. * + * * + * This program 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 Affero General Public License for more details. * + * * + * You should have received a copy of the GNU Affero General Public License * + * along with this program. If not, see . * + * * + *******************************************************************************/ + +#include + +#include "gui/common/FilesDefs.h" +#include "gui/gxschannels/GxsChannelPostThumbnail.h" + +ChannelPostThumbnailView::ChannelPostThumbnailView(const RsGxsChannelPost& post,QWidget *parent) + : QWidget(parent) +{ + // now fill the data + + QPixmap thumbnail; + + if(post.mThumbnail.mSize > 0) + GxsIdDetails::loadPixmapFromData(post.mThumbnail.mData, post.mThumbnail.mSize, thumbnail,GxsIdDetails::ORIGINAL); + else if(post.mMeta.mPublishTs > 0) // this is for testing that the post is not an empty post (happens at the end of the last row) + thumbnail = FilesDefs::getPixmapFromQtResourcePath(CHAN_DEFAULT_IMAGE); + + init(thumbnail, QString::fromUtf8(post.mMeta.mMsgName.c_str()), IS_MSG_UNREAD(post.mMeta.mMsgStatus) || IS_MSG_NEW(post.mMeta.mMsgStatus) ); +} + +ChannelPostThumbnailView::ChannelPostThumbnailView(QWidget *parent) + : QWidget(parent) +{ + init(FilesDefs::getPixmapFromQtResourcePath(CHAN_DEFAULT_IMAGE), QString("New post"),false); +} + +ChannelPostThumbnailView::~ChannelPostThumbnailView() +{ + delete lb; + delete lt; +} + +void ChannelPostThumbnailView::init(const QPixmap& thumbnail,const QString& msg,bool is_msg_new) +{ + QVBoxLayout *layout = new QVBoxLayout(this); + + lb = new ZoomableLabel(this); + lb->setScaledContents(true); + lb->setToolTip(tr("Use mouse to center and zoom into the image")); + layout->addWidget(lb); + + lt = new QLabel(this); + layout->addWidget(lt); + + setLayout(layout); + + setSizePolicy(QSizePolicy::Maximum,QSizePolicy::Maximum); + + QFontMetricsF fm(font()); + int W = THUMBNAIL_OVERSAMPLE_FACTOR * THUMBNAIL_W * fm.height() ; + int H = THUMBNAIL_OVERSAMPLE_FACTOR * THUMBNAIL_H * fm.height() ; + + lb->setFixedSize(W,H); + lb->setPicture(thumbnail); + + setText(msg); + + QFont font = lt->font(); + + if(is_msg_new) + { + font.setBold(true); + lt->setFont(font); + } + + lt->setMaximumWidth(W); + lt->setWordWrap(true); + + adjustSize(); + update(); +} + +void ZoomableLabel::mouseMoveEvent(QMouseEvent *me) +{ + float new_center_x = mCenterX - (me->x() - mLastX); + float new_center_y = mCenterY - (me->y() - mLastY); + + mLastX = me->x(); + mLastY = me->y(); + + if(new_center_x - 0.5 * width()*mZoomFactor < 0) return; + if(new_center_y - 0.5 *height()*mZoomFactor < 0) return; + + if(new_center_x + 0.5 * width()*mZoomFactor >= mFullImage.width()) return; + if(new_center_y + 0.5 *height()*mZoomFactor >=mFullImage.height()) return; + + mCenterX = new_center_x; + mCenterY = new_center_y; + + updateView(); +} +void ZoomableLabel::mousePressEvent(QMouseEvent *me) +{ + mMoving = true; + mLastX = me->x(); + mLastY = me->y(); +} +void ZoomableLabel::mouseReleaseEvent(QMouseEvent *) +{ + mMoving = false; +} +void ZoomableLabel::wheelEvent(QWheelEvent *me) +{ + float new_zoom_factor = (me->delta() > 0)?(mZoomFactor*1.05):(mZoomFactor/1.05); + float new_center_x = mCenterX; + float new_center_y = mCenterY; + + // Try to find centerX and centerY so that the crop does not overlap the original image + + float min_x = 0.5 * width()*new_zoom_factor; + float max_x = mFullImage.width() - 0.5 * width()*new_zoom_factor; + float min_y = 0.5 * height()*new_zoom_factor; + float max_y = mFullImage.height() - 0.5 * height()*new_zoom_factor; + + if(min_x >= max_x) return; + if(min_y >= max_y) return; + + if(new_center_x < min_x) new_center_x = min_x; + if(new_center_y < min_y) new_center_y = min_y; + if(new_center_x > max_x) new_center_x = max_x; + if(new_center_y > max_y) new_center_y = max_y; + + mZoomFactor = new_zoom_factor; + mCenterX = new_center_x; + mCenterY = new_center_y; + + updateView(); +} + +QPixmap ZoomableLabel::extractCroppedScaledPicture() const +{ + QRect rect(mCenterX - 0.5 * width()*mZoomFactor, mCenterY - 0.5 * height()*mZoomFactor, width()*mZoomFactor, height()*mZoomFactor); + QPixmap pix = mFullImage.copy(rect).scaledToHeight(height(),Qt::SmoothTransformation); + + return pix; +} + +void ZoomableLabel::setPicture(const QPixmap& pix) +{ + mFullImage = pix; + + mCenterX = pix.width()/2.0; + mCenterY = pix.height()/2.0; + + updateView(); +} +void ZoomableLabel::resizeEvent(QResizeEvent *e) +{ + QLabel::resizeEvent(e); + + updateView(); +} + +void ZoomableLabel::updateView() +{ + // The new image will be cropped from the original image, using the following rules: + // - first the cropped image size is computed + // - then center is calculated so that + // - the original center is preferred + // - if the crop overlaps the image border, the center is moved. + + QRect rect(mCenterX - 0.5 * width()*mZoomFactor, mCenterY - 0.5 * height()*mZoomFactor, width()*mZoomFactor, height()*mZoomFactor); + QLabel::setPixmap(mFullImage.copy(rect)); +} + + + + + diff --git a/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.h b/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.h index f9ce488a0..72fc960e5 100644 --- a/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.h +++ b/retroshare-gui/src/gui/gxschannels/GxsChannelPostThumbnail.h @@ -32,6 +32,35 @@ #include "gui/gxs/GxsIdDetails.h" #include "gui/common/FilesDefs.h" +// Class to provide a label in which the image can be zoomed/moved. The widget size is fixed by the GUI and the user can move/zoom the image +// inside the window formed by the widget. When happy, the view-able part of the image can be extracted. + +class ZoomableLabel: public QLabel +{ +public: + ZoomableLabel(QWidget *parent): QLabel(parent),mZoomFactor(1.0),mCenterX(0.0),mCenterY(0.0) {} + + void setPicture(const QPixmap& pix); + QPixmap extractCroppedScaledPicture() const; + +protected: + void mousePressEvent(QMouseEvent *ev) override; + void mouseReleaseEvent(QMouseEvent *ev) override; + void mouseMoveEvent(QMouseEvent *ev) override; + void resizeEvent(QResizeEvent *ev) override; + void wheelEvent(QWheelEvent *me) override; + + void updateView(); + + QPixmap mFullImage; + + float mCenterX; + float mCenterY; + float mZoomFactor; + int mLastX,mLastY; + bool mMoving; +}; + // Class to paint the thumbnails with title class ChannelPostThumbnailView: public QWidget @@ -49,73 +78,15 @@ public: static constexpr char *CHAN_DEFAULT_IMAGE = ":images/thumb-default-video.png"; - virtual ~ChannelPostThumbnailView() - { - delete lb; - delete lt; - } + virtual ~ChannelPostThumbnailView(); + ChannelPostThumbnailView(QWidget *parent=NULL); + ChannelPostThumbnailView(const RsGxsChannelPost& post,QWidget *parent=NULL); - ChannelPostThumbnailView(QWidget *parent=NULL): QWidget(parent) - { - init(FilesDefs::getPixmapFromQtResourcePath(CHAN_DEFAULT_IMAGE), QString("New post"),false); - } + void init(const QPixmap& thumbnail,const QString& msg,bool is_msg_new); - ChannelPostThumbnailView(const RsGxsChannelPost& post,QWidget *parent=NULL) - : QWidget(parent) - { - // now fill the data + void setPixmap(const QPixmap& p) { lb->setPicture(p); } + QPixmap getCroppedScaledPicture() const { return lb->extractCroppedScaledPicture() ; } - QPixmap thumbnail; - - if(post.mThumbnail.mSize > 0) - GxsIdDetails::loadPixmapFromData(post.mThumbnail.mData, post.mThumbnail.mSize, thumbnail,GxsIdDetails::ORIGINAL); - else if(post.mMeta.mPublishTs > 0) // this is for testing that the post is not an empty post (happens at the end of the last row) - thumbnail = FilesDefs::getPixmapFromQtResourcePath(CHAN_DEFAULT_IMAGE); - - init(thumbnail, QString::fromUtf8(post.mMeta.mMsgName.c_str()), IS_MSG_UNREAD(post.mMeta.mMsgStatus) || IS_MSG_NEW(post.mMeta.mMsgStatus) ); - - } - - void init(const QPixmap& thumbnail,const QString& msg,bool is_msg_new) - { - QVBoxLayout *layout = new QVBoxLayout(this); - - lb = new QLabel(this); - lb->setScaledContents(true); - layout->addWidget(lb); - - lt = new QLabel(this); - layout->addWidget(lt); - - setLayout(layout); - - setSizePolicy(QSizePolicy::Maximum,QSizePolicy::Maximum); - - QFontMetricsF fm(font()); - int W = THUMBNAIL_OVERSAMPLE_FACTOR * THUMBNAIL_W * fm.height() ; - int H = THUMBNAIL_OVERSAMPLE_FACTOR * THUMBNAIL_H * fm.height() ; - - lb->setFixedSize(W,H); - lb->setPixmap(thumbnail); - - setText(msg); - - QFont font = lt->font(); - - if(is_msg_new) - { - font.setBold(true); - lt->setFont(font); - } - - lt->setMaximumWidth(W); - lt->setWordWrap(true); - - adjustSize(); - update(); - } - - void setPixmap(const QPixmap& p) { lb->setPixmap(p); } void setText(const QString& s) { QString ss; @@ -128,7 +99,7 @@ public: } private: - QLabel *lb; + ZoomableLabel *lb; QLabel *lt; }; diff --git a/retroshare-gui/src/retroshare-gui.pro b/retroshare-gui/src/retroshare-gui.pro index fbb600013..cafc34ef4 100644 --- a/retroshare-gui/src/retroshare-gui.pro +++ b/retroshare-gui/src/retroshare-gui.pro @@ -1382,7 +1382,8 @@ gxschannels { gui/gxschannels/GxsChannelPostsModel.cpp \ gui/gxschannels/GxsChannelPostFilesModel.cpp \ gui/gxschannels/GxsChannelFilesWidget.cpp \ - gui/gxschannels/GxsChannelFilesStatusWidget.cpp \ + gui/gxschannels/GxsChannelPostThumbnail.cpp \ + gui/gxschannels/GxsChannelFilesStatusWidget.cpp \ gui/gxschannels/GxsChannelGroupDialog.cpp \ gui/gxschannels/CreateGxsChannelMsg.cpp \ gui/feeds/GxsChannelGroupItem.cpp \ diff --git a/retroshare-gui/src/util/misc.cpp b/retroshare-gui/src/util/misc.cpp index 9539ebd5a..e83f4a704 100644 --- a/retroshare-gui/src/util/misc.cpp +++ b/retroshare-gui/src/util/misc.cpp @@ -307,7 +307,11 @@ QPixmap misc::getOpenThumbnailedPicture(QWidget *parent, const QString &caption, if (!getOpenFileName(parent, RshareSettings::LASTDIR_IMAGES, caption, tr("Pictures (*.png *.jpeg *.xpm *.jpg *.tiff *.gif)"), fileName)) return QPixmap(); - return QPixmap(fileName).scaledToHeight(height, Qt::SmoothTransformation).copy( 0, 0, width, height); + if(width > 0 && height > 0) + return QPixmap(fileName).scaledToHeight(height, Qt::SmoothTransformation).copy( 0, 0, width, height); + else + return QPixmap(fileName); + //return QPixmap(fileName).scaledToHeight(width, height, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); }