diff --git a/lib/crop/croptool.cpp b/lib/crop/croptool.cpp index 0e4f7e4f..0eead6ae 100644 --- a/lib/crop/croptool.cpp +++ b/lib/crop/croptool.cpp @@ -1,438 +1,441 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free 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 2 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self #include "croptool.h" // Qt #include #include #include #include #include #include // KDE // Local #include #include "cropimageoperation.h" #include "cropwidget.h" #include "gwenviewconfig.h" static const int HANDLE_SIZE = 15; namespace Gwenview { enum CropHandleFlag { CH_None, CH_Top = 1, CH_Left = 2, CH_Right = 4, CH_Bottom = 8, CH_TopLeft = CH_Top | CH_Left, CH_BottomLeft = CH_Bottom | CH_Left, CH_TopRight = CH_Top | CH_Right, CH_BottomRight = CH_Bottom | CH_Right, CH_Content = 16 }; Q_DECLARE_FLAGS(CropHandle, CropHandleFlag) } // namespace inline QPoint boundPointX(const QPoint& point, const QRect& rect) { return QPoint( qBound(rect.left(), point.x(), rect.right()), point.y() ); } inline QPoint boundPointXY(const QPoint& point, const QRect& rect) { return QPoint( qBound(rect.left(), point.x(), rect.right()), qBound(rect.top(), point.y(), rect.bottom()) ); } Q_DECLARE_OPERATORS_FOR_FLAGS(Gwenview::CropHandle) namespace Gwenview { struct CropToolPrivate { CropTool* q; QRect mRect; QList mCropHandleList; CropHandle mMovingHandle; QPoint mLastMouseMovePos; double mCropRatio; CropWidget* mCropWidget; QRect viewportCropRect() const { return q->imageView()->mapToView(mRect); } QRect handleViewportRect(CropHandle handle) { QSize viewportSize = q->imageView()->size().toSize(); QRect rect = viewportCropRect(); int left, top; if (handle & CH_Top) { top = rect.top(); } else if (handle & CH_Bottom) { top = rect.bottom() + 1 - HANDLE_SIZE; } else { top = rect.top() + (rect.height() - HANDLE_SIZE) / 2; top = qBound(0, top, viewportSize.height() - HANDLE_SIZE); } if (handle & CH_Left) { left = rect.left(); } else if (handle & CH_Right) { left = rect.right() + 1 - HANDLE_SIZE; } else { left = rect.left() + (rect.width() - HANDLE_SIZE) / 2; left = qBound(0, left, viewportSize.width() - HANDLE_SIZE); } return QRect(left, top, HANDLE_SIZE, HANDLE_SIZE); } CropHandle handleAt(const QPointF& pos) { Q_FOREACH(const CropHandle & handle, mCropHandleList) { QRectF rect = handleViewportRect(handle); if (rect.contains(pos)) { return handle; } } QRectF rect = viewportCropRect(); if (rect.contains(pos)) { return CH_Content; } return CH_None; } void updateCursor(CropHandle handle, bool buttonDown) { Qt::CursorShape shape; switch (handle) { case CH_TopLeft: case CH_BottomRight: shape = Qt::SizeFDiagCursor; break; case CH_TopRight: case CH_BottomLeft: shape = Qt::SizeBDiagCursor; break; case CH_Left: case CH_Right: shape = Qt::SizeHorCursor; break; case CH_Top: case CH_Bottom: shape = Qt::SizeVerCursor; break; case CH_Content: shape = buttonDown ? Qt::ClosedHandCursor : Qt::OpenHandCursor; break; default: shape = Qt::ArrowCursor; break; } q->imageView()->setCursor(shape); } void keepRectInsideImage() { const QSize imageSize = q->imageView()->documentSize().toSize(); if (mRect.width() > imageSize.width() || mRect.height() > imageSize.height()) { // This can happen when the crop ratio changes QSize rectSize = mRect.size(); rectSize.scale(imageSize, Qt::KeepAspectRatio); mRect.setSize(rectSize); } if (mRect.right() >= imageSize.width()) { mRect.moveRight(imageSize.width() - 1); } else if (mRect.left() < 0) { mRect.moveLeft(0); } if (mRect.bottom() >= imageSize.height()) { mRect.moveBottom(imageSize.height() - 1); } else if (mRect.top() < 0) { mRect.moveTop(0); } } void setupWidget() { RasterImageView* view = q->imageView(); mCropWidget = new CropWidget(0, view, q); QObject::connect(mCropWidget, SIGNAL(cropRequested()), q, SLOT(slotCropRequested())); QObject::connect(mCropWidget, SIGNAL(done()), q, SIGNAL(done())); + + // This is needed when crop ratio set to Current Image, and the image is rotated + QObject::connect(view, &RasterImageView::imageRectUpdated, mCropWidget, &CropWidget::updateCropRatio); } QRect computeVisibleImageRect() const { RasterImageView* view = q->imageView(); const QRect imageRect = QRect(QPoint(0, 0), view->documentSize().toSize()); const QRect viewportRect = view->mapToImage(view->rect().toRect()); return imageRect & viewportRect; } }; CropTool::CropTool(RasterImageView* view) : AbstractRasterImageViewTool(view) , d(new CropToolPrivate) { d->q = this; d->mCropHandleList << CH_Left << CH_Right << CH_Top << CH_Bottom << CH_TopLeft << CH_TopRight << CH_BottomLeft << CH_BottomRight; d->mMovingHandle = CH_None; d->mCropRatio = 0.; d->mRect = d->computeVisibleImageRect(); d->setupWidget(); } CropTool::~CropTool() { // mCropWidget is a child of its container not of us, so it is not deleted automatically delete d->mCropWidget; delete d; } void CropTool::setCropRatio(double ratio) { d->mCropRatio = ratio; } void CropTool::setRect(const QRect& rect) { QRect oldRect = d->mRect; d->mRect = rect; d->keepRectInsideImage(); if (d->mRect != oldRect) { rectUpdated(d->mRect); } imageView()->update(); } QRect CropTool::rect() const { return d->mRect; } void CropTool::paint(QPainter* painter) { QRect rect = d->viewportCropRect(); QRect imageRect = imageView()->rect().toRect(); static const QColor outerColor = QColor::fromHsvF(0, 0, 0, 0.5); // For some reason nothing gets drawn if borderColor is not fully opaque! //static const QColor borderColor = QColor::fromHsvF(0, 0, 1.0, 0.66); static const QColor borderColor = QColor::fromHsvF(0, 0, 1.0); static const QColor fillColor = QColor::fromHsvF(0, 0, 0.75, 0.66); QRegion outerRegion = QRegion(imageRect) - QRegion(rect); Q_FOREACH(const QRect & outerRect, outerRegion.rects()) { painter->fillRect(outerRect, outerColor); } painter->setPen(borderColor); painter->drawRect(rect); if (d->mMovingHandle == CH_None) { // Only draw handles when user is not resizing painter->setBrush(fillColor); Q_FOREACH(const CropHandle & handle, d->mCropHandleList) { rect = d->handleViewportRect(handle); painter->drawRect(rect); } } } void CropTool::mousePressEvent(QGraphicsSceneMouseEvent* event) { event->accept(); if (event->buttons() != Qt::LeftButton) { return; } d->mMovingHandle = d->handleAt(event->pos()); d->updateCursor(d->mMovingHandle, true /* down */); if (d->mMovingHandle == CH_Content) { d->mLastMouseMovePos = imageView()->mapToImage(event->pos().toPoint()); } // Update to hide handles imageView()->update(); } void CropTool::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { event->accept(); if (event->buttons() != Qt::LeftButton) { return; } const QSize imageSize = imageView()->document()->size(); QPoint point = imageView()->mapToImage(event->pos().toPoint()); int posX = qBound(0, point.x(), imageSize.width() - 1); int posY = qBound(0, point.y(), imageSize.height() - 1); if (d->mMovingHandle == CH_None) { return; } // Adjust edge if (d->mMovingHandle & CH_Top) { d->mRect.setTop(posY); } else if (d->mMovingHandle & CH_Bottom) { d->mRect.setBottom(posY); } if (d->mMovingHandle & CH_Left) { d->mRect.setLeft(posX); } else if (d->mMovingHandle & CH_Right) { d->mRect.setRight(posX); } // Normalize rect and handles (this is useful when user drag the right side // of the crop rect to the left of the left side) if (d->mRect.height() < 0) { d->mMovingHandle = d->mMovingHandle ^(CH_Top | CH_Bottom); } if (d->mRect.width() < 0) { d->mMovingHandle = d->mMovingHandle ^(CH_Left | CH_Right); } d->mRect = d->mRect.normalized(); // Enforce ratio if (d->mCropRatio > 0.) { if (d->mMovingHandle == CH_Top || d->mMovingHandle == CH_Bottom) { // Top or bottom int width = int(d->mRect.height() / d->mCropRatio); d->mRect.setWidth(width); } else if (d->mMovingHandle == CH_Left || d->mMovingHandle == CH_Right) { // Left or right int height = int(d->mRect.width() * d->mCropRatio); d->mRect.setHeight(height); } else if (d->mMovingHandle & CH_Top) { // Top left or top right int height = int(d->mRect.width() * d->mCropRatio); d->mRect.setTop(d->mRect.bottom() - height); } else if (d->mMovingHandle & CH_Bottom) { // Bottom left or bottom right int height = int(d->mRect.width() * d->mCropRatio); d->mRect.setHeight(height); } } if (d->mMovingHandle == CH_Content) { d->mRect.translate(point - d->mLastMouseMovePos); d->mLastMouseMovePos = point; } d->keepRectInsideImage(); imageView()->update(); rectUpdated(d->mRect); } void CropTool::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { event->accept(); d->mMovingHandle = CH_None; d->updateCursor(d->handleAt(event->lastPos()), false); // Update to show handles imageView()->update(); } void CropTool::hoverMoveEvent(QGraphicsSceneHoverEvent* event) { event->accept(); // Make sure cursor is updated when moving over handles CropHandle handle = d->handleAt(event->lastPos()); d->updateCursor(handle, false /* buttonDown */); } void CropTool::keyPressEvent(QKeyEvent* event) { QDialogButtonBox *buttons = d->mCropWidget->findChild(); switch (event->key()) { case Qt::Key_Escape: event->accept(); buttons->rejected(); break; case Qt::Key_Return: case Qt::Key_Enter: event->accept(); buttons->accepted(); break; default: break; } } void CropTool::toolActivated() { imageView()->setCursor(Qt::CrossCursor); d->mCropWidget->setAdvancedSettingsEnabled(GwenviewConfig::cropAdvancedSettingsEnabled()); } void CropTool::toolDeactivated() { GwenviewConfig::setCropAdvancedSettingsEnabled(d->mCropWidget->advancedSettingsEnabled()); } void CropTool::slotCropRequested() { CropImageOperation* op = new CropImageOperation(d->mRect); emit imageOperationRequested(op); emit done(); } QWidget* CropTool::widget() const { return d->mCropWidget; } void CropTool::onWidgetSlidedIn() { setRect(d->computeVisibleImageRect()); } } // namespace diff --git a/lib/crop/cropwidget.cpp b/lib/crop/cropwidget.cpp index 30a94e84..da857141 100644 --- a/lib/crop/cropwidget.cpp +++ b/lib/crop/cropwidget.cpp @@ -1,319 +1,354 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free 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 2 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ // Self // Qt #include #include #include #include #include #include // KDE #include // Local #include #include #include "croptool.h" #include "signalblocker.h" #include "ui_cropwidget.h" #include "cropwidget.h" namespace Gwenview { // Euclidean algorithm to compute the greatest common divisor of two integers. // Found at: // http://en.wikipedia.org/wiki/Euclidean_algorithm static int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } -static QSize screenRatio() +static QSize ratio(const QSize &size) { - const QRect rect = QApplication::desktop()->screenGeometry(); - const int width = rect.width(); - const int height = rect.height(); - const int divisor = gcd(width, height); - return QSize(width / divisor, height / divisor); + const int divisor = gcd(size.width(), size.height()); + return size / divisor; } struct CropWidgetPrivate : public Ui_CropWidget { CropWidget* q; Document::Ptr mDocument; CropTool* mCropTool; bool mUpdatingFromCropTool; + int mCurrentImageComboBoxIndex; bool ratioIsConstrained() const { return cropRatio() > 0; } double cropRatio() const { - int index = ratioComboBox->currentIndex(); - if (index != -1 && ratioComboBox->currentText() == ratioComboBox->itemText(index)) { - // Get ratio from predefined value - // Note: We check currentText is itemText(currentIndex) because - // currentIndex is not reset to -1 when text is edited by hand. - QSizeF size = ratioComboBox->itemData(index).toSizeF(); - return size.height() / size.width(); - } - - // Not a predefined value, extract ratio from the combobox text - const QStringList lst = ratioComboBox->currentText().split(':'); - if (lst.size() != 2) { - return 0; + if (q->advancedSettingsEnabled()) { + int index = ratioComboBox->currentIndex(); + if (index != -1 && ratioComboBox->currentText() == ratioComboBox->itemText(index)) { + // Get ratio from predefined value + // Note: We check currentText is itemText(currentIndex) because + // currentIndex is not reset to -1 when text is edited by hand. + QSizeF size = ratioComboBox->itemData(index).toSizeF(); + return size.height() / size.width(); + } + + // Not a predefined value, extract ratio from the combobox text + const QStringList lst = ratioComboBox->currentText().split(':'); + if (lst.size() != 2) { + return 0; + } + + bool ok; + const double width = lst[0].toDouble(&ok); + if (!ok) { + return 0; + } + const double height = lst[1].toDouble(&ok); + if (!ok) { + return 0; + } + + return height / width; } - bool ok; - const double width = lst[0].toDouble(&ok); - if (!ok) { - return 0; - } - const double height = lst[1].toDouble(&ok); - if (!ok) { - return 0; + if (restrictToImageRatioCheckBox->isChecked()) { + QSizeF size = ratio(mDocument->size()); + return size.height() / size.width(); } - return height / width; + return 0; } void addRatioToComboBox(const QSizeF& size, const QString& label = QString()) { QString text = label.isEmpty() ? QString("%1:%2").arg(size.width()).arg(size.height()) : label; ratioComboBox->addItem(text, QVariant(size)); } void addSectionHeaderToComboBox(const QString& title) { // Insert a line ratioComboBox->insertSeparator(ratioComboBox->count()); // Insert our section header // This header is made of a separator with a text. We reset // Qt::AccessibleDescriptionRole to the header text otherwise QComboBox // delegate will draw a separator line instead of our text. int index = ratioComboBox->count(); ratioComboBox->insertSeparator(index); ratioComboBox->setItemText(index, title); ratioComboBox->setItemData(index, title, Qt::AccessibleDescriptionRole); ratioComboBox->setItemData(index, Qt::AlignHCenter, Qt::TextAlignmentRole); } void initRatioComboBox() { QList ratioList; const qreal sqrt2 = qSqrt(2.); ratioList << QSizeF(16, 9) << QSizeF(7, 5) << QSizeF(3, 2) << QSizeF(4, 3) << QSizeF(5, 4); + addRatioToComboBox(ratio(mDocument->size()), i18n("Current Image")); + mCurrentImageComboBoxIndex = ratioComboBox->count() - 1; // We need to refer to this ratio later + addRatioToComboBox(QSizeF(1, 1), i18n("Square")); - addRatioToComboBox(screenRatio(), i18n("This Screen")); + addRatioToComboBox(ratio(QApplication::desktop()->screenGeometry().size()), i18n("This Screen")); addSectionHeaderToComboBox(i18n("Landscape")); Q_FOREACH(const QSizeF& size, ratioList) { addRatioToComboBox(size); } addRatioToComboBox(QSizeF(sqrt2, 1), i18n("ISO Size (A4, A3...)")); addRatioToComboBox(QSizeF(11, 8.5), i18n("US Letter")); addSectionHeaderToComboBox(i18n("Portrait")); Q_FOREACH(QSizeF size, ratioList) { size.transpose(); addRatioToComboBox(size); } addRatioToComboBox(QSizeF(1, sqrt2), i18n("ISO Size (A4, A3...)")); addRatioToComboBox(QSizeF(8.5, 11), i18n("US Letter")); ratioComboBox->setMaxVisibleItems(ratioComboBox->count()); ratioComboBox->clearEditText(); QLineEdit* edit = qobject_cast(ratioComboBox->lineEdit()); Q_ASSERT(edit); // Do not use i18n("%1:%2") because ':' should not be translated, it is // used to parse the ratio string. edit->setPlaceholderText(QString("%1:%2").arg(i18n("Width")).arg(i18n("Height"))); + + // Enable clear button + edit->setClearButtonEnabled(true); + // Must manually adjust minimum width because the auto size adjustment doesn't take the + // clear button into account + const int width = ratioComboBox->minimumSizeHint().width(); + ratioComboBox->setMinimumWidth(width + 24); + + ratioComboBox->setCurrentIndex(-1); } QRect cropRect() const { QRect rect( leftSpinBox->value(), topSpinBox->value(), widthSpinBox->value(), heightSpinBox->value() ); return rect; } void initSpinBoxes() { QSize size = mDocument->size(); leftSpinBox->setMaximum(size.width()); widthSpinBox->setMaximum(size.width()); topSpinBox->setMaximum(size.height()); heightSpinBox->setMaximum(size.height()); } void initDialogButtonBox() { QPushButton* cropButton = dialogButtonBox->button(QDialogButtonBox::Ok); cropButton->setIcon(QIcon::fromTheme("transform-crop-and-resize")); cropButton->setText(i18n("Crop")); QObject::connect(dialogButtonBox, &QDialogButtonBox::accepted, q, &CropWidget::cropRequested); QObject::connect(dialogButtonBox, &QDialogButtonBox::rejected, q, &CropWidget::done); } }; CropWidget::CropWidget(QWidget* parent, RasterImageView* imageView, CropTool* cropTool) : QWidget(parent) , d(new CropWidgetPrivate) { setWindowFlags(Qt::Tool); d->q = this; d->mDocument = imageView->document(); d->mUpdatingFromCropTool = false; d->mCropTool = cropTool; d->setupUi(this); setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); layout()->setSizeConstraint(QLayout::SetFixedSize); - connect(d->advancedCheckBox, SIGNAL(toggled(bool)), - d->advancedWidget, SLOT(setVisible(bool))); + connect(d->advancedCheckBox, &QCheckBox::toggled, this, &CropWidget::slotAdvancedCheckBoxToggled); d->advancedWidget->setVisible(false); d->advancedWidget->layout()->setMargin(0); + connect(d->restrictToImageRatioCheckBox, &QCheckBox::toggled, this, &CropWidget::applyRatioConstraint); + d->initRatioComboBox(); connect(d->mCropTool, &CropTool::rectUpdated, this, &CropWidget::setCropRect); connect(d->leftSpinBox, static_cast(&QSpinBox::valueChanged), this, &CropWidget::slotPositionChanged); connect(d->topSpinBox, static_cast(&QSpinBox::valueChanged), this, &CropWidget::slotPositionChanged); connect(d->widthSpinBox, static_cast(&QSpinBox::valueChanged), this, &CropWidget::slotWidthChanged); connect(d->heightSpinBox, static_cast(&QSpinBox::valueChanged), this, &CropWidget::slotHeightChanged); d->initDialogButtonBox(); - connect(d->ratioComboBox, &QComboBox::editTextChanged, this, &CropWidget::slotRatioComboBoxEditTextChanged); + connect(d->ratioComboBox, &QComboBox::editTextChanged, this, &CropWidget::applyRatioConstraint); // Don't do this before signals are connected, otherwise the tool won't get // initialized d->initSpinBoxes(); setCropRect(d->mCropTool->rect()); } CropWidget::~CropWidget() { delete d; } void CropWidget::setAdvancedSettingsEnabled(bool enable) { d->advancedCheckBox->setChecked(enable); } bool CropWidget::advancedSettingsEnabled() const { return d->advancedCheckBox->isChecked(); } void CropWidget::setCropRect(const QRect& rect) { d->mUpdatingFromCropTool = true; d->leftSpinBox->setValue(rect.left()); d->topSpinBox->setValue(rect.top()); d->widthSpinBox->setValue(rect.width()); d->heightSpinBox->setValue(rect.height()); d->mUpdatingFromCropTool = false; } void CropWidget::slotPositionChanged() { const QSize size = d->mDocument->size(); d->widthSpinBox->setMaximum(size.width() - d->leftSpinBox->value()); d->heightSpinBox->setMaximum(size.height() - d->topSpinBox->value()); if (d->mUpdatingFromCropTool) { return; } d->mCropTool->setRect(d->cropRect()); } void CropWidget::slotWidthChanged() { d->leftSpinBox->setMaximum(d->mDocument->width() - d->widthSpinBox->value()); if (d->mUpdatingFromCropTool) { return; } if (d->ratioIsConstrained()) { int height = int(d->widthSpinBox->value() * d->cropRatio()); d->heightSpinBox->setValue(height); } d->mCropTool->setRect(d->cropRect()); } void CropWidget::slotHeightChanged() { d->topSpinBox->setMaximum(d->mDocument->height() - d->heightSpinBox->value()); if (d->mUpdatingFromCropTool) { return; } if (d->ratioIsConstrained()) { int width = int(d->heightSpinBox->value() / d->cropRatio()); d->widthSpinBox->setValue(width); } d->mCropTool->setRect(d->cropRect()); } void CropWidget::applyRatioConstraint() { double ratio = d->cropRatio(); d->mCropTool->setCropRatio(ratio); if (!d->ratioIsConstrained()) { return; } QRect rect = d->cropRect(); rect.setHeight(int(rect.width() * ratio)); d->mCropTool->setRect(rect); } -void CropWidget::slotRatioComboBoxEditTextChanged() +void CropWidget::slotAdvancedCheckBoxToggled(bool checked) { + d->advancedWidget->setVisible(checked); + d->restrictToImageRatioCheckBox->setVisible(!checked); + applyRatioConstraint(); +} + +void CropWidget::updateCropRatio() +{ + // First we need to re-calculate the "Current Image" ratio in case the user rotated the image + d->ratioComboBox->setItemData(d->mCurrentImageComboBoxIndex, QVariant(ratio(d->mDocument->size()))); + + // Always re-apply the constraint, even though we only need to when the user has "Current Image" + // selected or the "Restrict to current image" checked, since there's no harm applyRatioConstraint(); + // If the ratio is unrestricted, calling applyRatioConstraint doesn't update the rect, so we call + // this manually to make sure the rect is adjusted to fit within the image + d->mCropTool->setRect(d->mCropTool->rect()); } } // namespace diff --git a/lib/crop/cropwidget.h b/lib/crop/cropwidget.h index 4615603c..45c2c682 100644 --- a/lib/crop/cropwidget.h +++ b/lib/crop/cropwidget.h @@ -1,70 +1,73 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2007 Aurélien Gâteau This program is free 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 2 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CROPWIDGET_H #define CROPWIDGET_H #include // Qt #include // KDE // Local #include namespace Gwenview { class CropTool; class RasterImageView; struct CropWidgetPrivate; class GWENVIEWLIB_EXPORT CropWidget : public QWidget { Q_OBJECT public: CropWidget(QWidget* parent, RasterImageView*, CropTool*); ~CropWidget(); void setAdvancedSettingsEnabled(bool enable); bool advancedSettingsEnabled() const; Q_SIGNALS: void cropRequested(); void done(); +public Q_SLOTS: + void updateCropRatio(); + private Q_SLOTS: void slotPositionChanged(); void slotWidthChanged(); void slotHeightChanged(); void setCropRect(const QRect& rect); - void slotRatioComboBoxEditTextChanged(); + void slotAdvancedCheckBoxToggled(bool checked); void applyRatioConstraint(); private: CropWidgetPrivate* const d; }; } // namespace #endif /* CROPWIDGET_H */ diff --git a/lib/crop/cropwidget.ui b/lib/crop/cropwidget.ui index d44f3548..07535eba 100644 --- a/lib/crop/cropwidget.ui +++ b/lib/crop/cropwidget.ui @@ -1,181 +1,188 @@ CropWidget 0 0 - 824 + 1006 66 0 0 Crop Qt::NoFocus Advanced settings Qt::Horizontal QSizePolicy::Fixed 12 20 Ratio: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter ratioComboBox true QComboBox::NoInsert 0 0 Size: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter widthSpinBox 1 0 1 0 Position: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter leftSpinBox 1 0 1 0 Qt::Horizontal 0 20 + + + + Restrict to image ratio + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok ratioComboBox widthSpinBox heightSpinBox leftSpinBox topSpinBox diff --git a/lib/documentview/rasterimageview.cpp b/lib/documentview/rasterimageview.cpp index bb8d4f15..af8c5276 100644 --- a/lib/documentview/rasterimageview.cpp +++ b/lib/documentview/rasterimageview.cpp @@ -1,564 +1,565 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2011 Aurélien Gâteau This program is free 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 2 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "rasterimageview.h" // Local #include #include #include #include // KDE // Qt #include #include #include #include #include namespace Gwenview { struct RasterImageViewPrivate { RasterImageView* q; ImageScaler* mScaler; QPixmap mBackgroundTexture; bool mEmittedCompleted; // Config RasterImageView::AlphaBackgroundMode mAlphaBackgroundMode; QColor mAlphaBackgroundColor; cmsUInt32Number mRenderingIntent; bool mEnlargeSmallerImages; // /Config bool mBufferIsEmpty; QPixmap mCurrentBuffer; // The alternate buffer is useful when scrolling: existing content is copied // to mAlternateBuffer and buffers are swapped. This avoids allocating a new // QPixmap every time the image is scrolled. QPixmap mAlternateBuffer; QTimer* mUpdateTimer; QPointer mTool; bool mApplyDisplayTransform; // Defaults to true. Can be set to false if there is no need or no way to apply color profile cmsHTRANSFORM mDisplayTransform; void updateDisplayTransform(QImage::Format format) { GV_RETURN_IF_FAIL(format != QImage::Format_Invalid); mApplyDisplayTransform = false; if (mDisplayTransform) { cmsDeleteTransform(mDisplayTransform); } mDisplayTransform = 0; Cms::Profile::Ptr profile = q->document()->cmsProfile(); if (!profile) { // The assumption that something unmarked is *probably* sRGB is better than failing to apply any transform when one // has a wide-gamut screen. profile = Cms::Profile::getSRgbProfile(); } Cms::Profile::Ptr monitorProfile = Cms::Profile::getMonitorProfile(); if (!monitorProfile) { qWarning() << "Could not get monitor color profile"; return; } cmsUInt32Number cmsFormat = 0; switch (format) { case QImage::Format_RGB32: case QImage::Format_ARGB32: cmsFormat = TYPE_BGRA_8; break; #if QT_VERSION >= QT_VERSION_CHECK(5, 5, 0) case QImage::Format_Grayscale8: cmsFormat = TYPE_GRAY_8; break; #endif default: qWarning() << "Gwenview can only apply color profile on RGB32 or ARGB32 images"; return; } mDisplayTransform = cmsCreateTransform(profile->handle(), cmsFormat, monitorProfile->handle(), cmsFormat, mRenderingIntent, cmsFLAGS_BLACKPOINTCOMPENSATION); mApplyDisplayTransform = true; } void createBackgroundTexture() { mBackgroundTexture = QPixmap(32, 32); QPainter painter(&mBackgroundTexture); painter.fillRect(mBackgroundTexture.rect(), QColor(128, 128, 128)); QColor light = QColor(192, 192, 192); painter.fillRect(0, 0, 16, 16, light); painter.fillRect(16, 16, 16, 16, light); } void setupUpdateTimer() { mUpdateTimer = new QTimer(q); mUpdateTimer->setInterval(500); mUpdateTimer->setSingleShot(true); QObject::connect(mUpdateTimer, SIGNAL(timeout()), q, SLOT(updateBuffer())); } void startAnimationIfNecessary() { if (q->document() && q->isVisible()) { q->document()->startAnimation(); } } QRectF mapViewportToZoomedImage(const QRectF& viewportRect) const { return QRectF( viewportRect.topLeft() - q->imageOffset() + q->scrollPos(), viewportRect.size() ); } void setScalerRegionToVisibleRect() { QRectF rect = mapViewportToZoomedImage(q->boundingRect()); mScaler->setDestinationRegion(QRegion(rect.toRect())); } void resizeBuffer() { QSize size = q->visibleImageSize().toSize(); if (size == mCurrentBuffer.size()) { return; } if (!size.isValid()) { mAlternateBuffer = QPixmap(); mCurrentBuffer = QPixmap(); return; } mAlternateBuffer = QPixmap(size); mAlternateBuffer.fill(Qt::transparent); { QPainter painter(&mAlternateBuffer); painter.drawPixmap(0, 0, mCurrentBuffer); } qSwap(mAlternateBuffer, mCurrentBuffer); mAlternateBuffer = QPixmap(); } void drawAlphaBackground(QPainter* painter, const QRect& viewportRect, const QPoint& zoomedImageTopLeft) { if (mAlphaBackgroundMode == RasterImageView::AlphaBackgroundCheckBoard) { QPoint textureOffset( zoomedImageTopLeft.x() % mBackgroundTexture.width(), zoomedImageTopLeft.y() % mBackgroundTexture.height() ); painter->drawTiledPixmap( viewportRect, mBackgroundTexture, textureOffset); } else { painter->fillRect(viewportRect, mAlphaBackgroundColor); } } }; RasterImageView::RasterImageView(QGraphicsItem* parent) : AbstractImageView(parent) , d(new RasterImageViewPrivate) { d->q = this; d->mEmittedCompleted = false; d->mApplyDisplayTransform = true; d->mDisplayTransform = 0; d->mAlphaBackgroundMode = AlphaBackgroundCheckBoard; d->mAlphaBackgroundColor = Qt::black; d->mRenderingIntent = INTENT_PERCEPTUAL; d->mEnlargeSmallerImages = false; d->mBufferIsEmpty = true; d->mScaler = new ImageScaler(this); connect(d->mScaler, &ImageScaler::scaledRect, this, &RasterImageView::updateFromScaler); d->createBackgroundTexture(); d->setupUpdateTimer(); } RasterImageView::~RasterImageView() { if (d->mTool) { d->mTool.data()->toolDeactivated(); } if (d->mDisplayTransform) { cmsDeleteTransform(d->mDisplayTransform); } delete d; } void RasterImageView::setAlphaBackgroundMode(AlphaBackgroundMode mode) { d->mAlphaBackgroundMode = mode; if (document() && document()->hasAlphaChannel()) { d->mCurrentBuffer = QPixmap(); updateBuffer(); } } void RasterImageView::setAlphaBackgroundColor(const QColor& color) { d->mAlphaBackgroundColor = color; if (document() && document()->hasAlphaChannel()) { d->mCurrentBuffer = QPixmap(); updateBuffer(); } } void RasterImageView::setRenderingIntent(const RenderingIntent::Enum& renderingIntent) { if (d->mRenderingIntent != renderingIntent) { d->mRenderingIntent = renderingIntent; updateBuffer(); } } void RasterImageView::loadFromDocument() { Document::Ptr doc = document(); if (!doc) { return; } connect(doc.data(), SIGNAL(metaInfoLoaded(QUrl)), SLOT(slotDocumentMetaInfoLoaded())); connect(doc.data(), SIGNAL(isAnimatedUpdated()), SLOT(slotDocumentIsAnimatedUpdated())); const Document::LoadingState state = doc->loadingState(); if (state == Document::MetaInfoLoaded || state == Document::Loaded) { slotDocumentMetaInfoLoaded(); } } void RasterImageView::slotDocumentMetaInfoLoaded() { if (document()->size().isValid()) { QMetaObject::invokeMethod(this, "finishSetDocument", Qt::QueuedConnection); } else { // Could not retrieve image size from meta info, we need to load the // full image now. connect(document().data(), SIGNAL(loaded(QUrl)), SLOT(finishSetDocument())); document()->startLoadingFullImage(); } } void RasterImageView::finishSetDocument() { GV_RETURN_IF_FAIL(document()->size().isValid()); d->mScaler->setDocument(document()); d->resizeBuffer(); applyPendingScrollPos(); connect(document().data(), SIGNAL(imageRectUpdated(QRect)), SLOT(updateImageRect(QRect))); if (zoomToFit()) { // Force the update otherwise if computeZoomToFit() returns 1, setZoom() // will think zoom has not changed and won't update the image setZoom(computeZoomToFit(), QPointF(-1, -1), ForceUpdate); } else if (zoomToFill()) { setZoom(computeZoomToFill(), QPointF(-1, -1), ForceUpdate); } else { updateBuffer(); } d->startAnimationIfNecessary(); update(); } void RasterImageView::updateImageRect(const QRect& imageRect) { QRectF viewRect = mapToView(imageRect); if (!viewRect.intersects(boundingRect())) { return; } if (zoomToFit()) { setZoom(computeZoomToFit()); } else if (zoomToFill()) { setZoom(computeZoomToFill()); } d->setScalerRegionToVisibleRect(); update(); + emit imageRectUpdated(); } void RasterImageView::slotDocumentIsAnimatedUpdated() { d->startAnimationIfNecessary(); } void RasterImageView::updateFromScaler(int zoomedImageLeft, int zoomedImageTop, const QImage& image) { if (d->mApplyDisplayTransform) { d->updateDisplayTransform(image.format()); if (d->mDisplayTransform) { quint8 *bytes = const_cast(image.bits()); cmsDoTransform(d->mDisplayTransform, bytes, bytes, image.width() * image.height()); } } d->resizeBuffer(); int viewportLeft = zoomedImageLeft - scrollPos().x(); int viewportTop = zoomedImageTop - scrollPos().y(); d->mBufferIsEmpty = false; { QPainter painter(&d->mCurrentBuffer); if (document()->hasAlphaChannel()) { d->drawAlphaBackground( &painter, QRect(viewportLeft, viewportTop, image.width(), image.height()), QPoint(zoomedImageLeft, zoomedImageTop) ); } else { painter.setCompositionMode(QPainter::CompositionMode_Source); } painter.drawImage(viewportLeft, viewportTop, image); } update(); if (!d->mEmittedCompleted) { d->mEmittedCompleted = true; completed(); } } void RasterImageView::onZoomChanged() { // If we zoom more than twice, then assume the user wants to see the real // pixels, for example to fine tune a crop operation if (zoom() < 2.) { d->mScaler->setTransformationMode(Qt::SmoothTransformation); } else { d->mScaler->setTransformationMode(Qt::FastTransformation); } if (!d->mUpdateTimer->isActive()) { updateBuffer(); } } void RasterImageView::onImageOffsetChanged() { update(); } void RasterImageView::onScrollPosChanged(const QPointF& oldPos) { QPointF delta = scrollPos() - oldPos; // Scroll existing { if (d->mAlternateBuffer.size() != d->mCurrentBuffer.size()) { d->mAlternateBuffer = QPixmap(d->mCurrentBuffer.size()); } QPainter painter(&d->mAlternateBuffer); painter.drawPixmap(-delta, d->mCurrentBuffer); } qSwap(d->mCurrentBuffer, d->mAlternateBuffer); // Scale missing parts QRegion bufferRegion = QRegion(d->mCurrentBuffer.rect().translated(scrollPos().toPoint())); QRegion updateRegion = bufferRegion - bufferRegion.translated(-delta.toPoint()); updateBuffer(updateRegion); update(); } void RasterImageView::paint(QPainter* painter, const QStyleOptionGraphicsItem* /*option*/, QWidget* /*widget*/) { QPointF topLeft = imageOffset(); if (zoomToFit()) { // In zoomToFit mode, scale crudely the buffer to fit the screen. This // provide an approximate rendered which will be replaced when the scheduled // proper scale is ready. QSizeF size = documentSize() * zoom(); painter->drawPixmap(topLeft.x(), topLeft.y(), size.width(), size.height(), d->mCurrentBuffer); } else { painter->drawPixmap(topLeft, d->mCurrentBuffer); } if (d->mTool) { d->mTool.data()->paint(painter); } // Debug #if 0 QSizeF visibleSize = documentSize() * zoom(); painter->setPen(Qt::red); painter->drawRect(topLeft.x(), topLeft.y(), visibleSize.width() - 1, visibleSize.height() - 1); painter->setPen(Qt::blue); painter->drawRect(topLeft.x(), topLeft.y(), d->mCurrentBuffer.width() - 1, d->mCurrentBuffer.height() - 1); #endif } void RasterImageView::resizeEvent(QGraphicsSceneResizeEvent* event) { // If we are in zoomToFit mode and have something in our buffer, delay the // update: paint() will paint a scaled version of the buffer until resizing // is done. This is much faster than rescaling the whole image for each // resize event we receive. // mUpdateTimer must be started before calling AbstractImageView::resizeEvent() // because AbstractImageView::resizeEvent() will call onZoomChanged(), which // will trigger an immediate update unless the mUpdateTimer is active. if (zoomToFit() && !d->mBufferIsEmpty) { d->mUpdateTimer->start(); } else if (zoomToFill() && !d->mBufferIsEmpty) { d->mUpdateTimer->start(); } AbstractImageView::resizeEvent(event); if (!zoomToFit()) { // Only update buffer if we are not in zoomToFit mode: if we are // onZoomChanged() will have already updated the buffer. updateBuffer(); } else if (!zoomToFill()) { updateBuffer(); } } void RasterImageView::updateBuffer(const QRegion& region) { d->mUpdateTimer->stop(); d->mScaler->setZoom(zoom()); if (region.isEmpty()) { d->setScalerRegionToVisibleRect(); } else { d->mScaler->setDestinationRegion(region); } } void RasterImageView::setCurrentTool(AbstractRasterImageViewTool* tool) { if (d->mTool) { d->mTool.data()->toolDeactivated(); d->mTool.data()->deleteLater(); } d->mTool = tool; if (d->mTool) { d->mTool.data()->toolActivated(); } updateCursor(); currentToolChanged(tool); update(); } AbstractRasterImageViewTool* RasterImageView::currentTool() const { return d->mTool.data(); } void RasterImageView::mousePressEvent(QGraphicsSceneMouseEvent* event) { if (d->mTool) { d->mTool.data()->mousePressEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::mousePressEvent(event); } void RasterImageView::mouseMoveEvent(QGraphicsSceneMouseEvent* event) { if (d->mTool) { d->mTool.data()->mouseMoveEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::mouseMoveEvent(event); } void RasterImageView::mouseReleaseEvent(QGraphicsSceneMouseEvent* event) { if (d->mTool) { d->mTool.data()->mouseReleaseEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::mouseReleaseEvent(event); } void RasterImageView::wheelEvent(QGraphicsSceneWheelEvent* event) { if (d->mTool) { d->mTool.data()->wheelEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::wheelEvent(event); } void RasterImageView::keyPressEvent(QKeyEvent* event) { if (d->mTool) { d->mTool.data()->keyPressEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::keyPressEvent(event); } void RasterImageView::keyReleaseEvent(QKeyEvent* event) { if (d->mTool) { d->mTool.data()->keyReleaseEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::keyReleaseEvent(event); } void RasterImageView::hoverMoveEvent(QGraphicsSceneHoverEvent* event) { if (d->mTool) { d->mTool.data()->hoverMoveEvent(event); if (event->isAccepted()) { return; } } AbstractImageView::hoverMoveEvent(event); } } // namespace diff --git a/lib/documentview/rasterimageview.h b/lib/documentview/rasterimageview.h index 02cef9ce..54abf96f 100644 --- a/lib/documentview/rasterimageview.h +++ b/lib/documentview/rasterimageview.h @@ -1,92 +1,93 @@ // vim: set tabstop=4 shiftwidth=4 expandtab: /* Gwenview: an image viewer Copyright 2011 Aurélien Gâteau This program is free 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 2 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ #ifndef RASTERIMAGEVIEW_H #define RASTERIMAGEVIEW_H #include // Local #include #include // KDE class QGraphicsSceneHoverEvent; namespace Gwenview { class AbstractRasterImageViewTool; struct RasterImageViewPrivate; class GWENVIEWLIB_EXPORT RasterImageView : public AbstractImageView { Q_OBJECT public: enum AlphaBackgroundMode { AlphaBackgroundCheckBoard, AlphaBackgroundSolid }; RasterImageView(QGraphicsItem* parent = 0); ~RasterImageView(); void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget) Q_DECL_OVERRIDE; void setCurrentTool(AbstractRasterImageViewTool* tool); AbstractRasterImageViewTool* currentTool() const; void setAlphaBackgroundMode(AlphaBackgroundMode mode); void setAlphaBackgroundColor(const QColor& color); void setRenderingIntent(const RenderingIntent::Enum& renderingIntent); Q_SIGNALS: void currentToolChanged(AbstractRasterImageViewTool*); + void imageRectUpdated(); protected: void loadFromDocument() Q_DECL_OVERRIDE; void onZoomChanged() Q_DECL_OVERRIDE; void onImageOffsetChanged() Q_DECL_OVERRIDE; void onScrollPosChanged(const QPointF& oldPos) Q_DECL_OVERRIDE; void resizeEvent(QGraphicsSceneResizeEvent* event) Q_DECL_OVERRIDE; void mousePressEvent(QGraphicsSceneMouseEvent* event) Q_DECL_OVERRIDE; void mouseMoveEvent(QGraphicsSceneMouseEvent* event) Q_DECL_OVERRIDE; void mouseReleaseEvent(QGraphicsSceneMouseEvent* event) Q_DECL_OVERRIDE; void wheelEvent(QGraphicsSceneWheelEvent* event) Q_DECL_OVERRIDE; void keyPressEvent(QKeyEvent* event) Q_DECL_OVERRIDE; void keyReleaseEvent(QKeyEvent* event) Q_DECL_OVERRIDE; void hoverMoveEvent(QGraphicsSceneHoverEvent*) Q_DECL_OVERRIDE; private Q_SLOTS: void slotDocumentMetaInfoLoaded(); void slotDocumentIsAnimatedUpdated(); void finishSetDocument(); void updateFromScaler(int, int, const QImage&); void updateImageRect(const QRect& imageRect); void updateBuffer(const QRegion& region = QRegion()); private: RasterImageViewPrivate* const d; }; } // namespace #endif /* RASTERIMAGEVIEW_H */