diff --git a/app/imageopscontextmanageritem.cpp b/app/imageopscontextmanageritem.cpp index 0ec74f12..872ff443 100644 --- a/app/imageopscontextmanageritem.cpp +++ b/app/imageopscontextmanageritem.cpp @@ -1,308 +1,334 @@ // 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 "imageopscontextmanageritem.h" #include "dialogguard.h" // Qt #include #include +#include #include "gwenview_app_debug.h" // KDE #include #include #include #include // Local #include "viewmainpage.h" #include "gvcore.h" #include "mainwindow.h" #include "sidebar.h" #include #include #include #include #include #include #include #include #include #include namespace Gwenview { #undef ENABLE_LOG #undef LOG //#define ENABLE_LOG #ifdef ENABLE_LOG #define LOG(x) qCDebug(GWENVIEW_APP_LOG) << x #else #define LOG(x) ; #endif struct ImageOpsContextManagerItem::Private { ImageOpsContextManagerItem* q; MainWindow* mMainWindow; SideBarGroup* mGroup; + QRect* mCropStateRect; QAction * mRotateLeftAction; QAction * mRotateRightAction; QAction * mMirrorAction; QAction * mFlipAction; QAction * mResizeAction; QAction * mCropAction; QAction * mRedEyeReductionAction; QList mActionList; void setupActions() { KActionCollection* actionCollection = mMainWindow->actionCollection(); KActionCategory* edit = new KActionCategory(i18nc("@title actions category - means actions changing image", "Edit"), actionCollection); mRotateLeftAction = edit->addAction("rotate_left", q, SLOT(rotateLeft())); mRotateLeftAction->setText(i18n("Rotate Left")); mRotateLeftAction->setToolTip(i18nc("@info:tooltip", "Rotate image to the left")); mRotateLeftAction->setIcon(QIcon::fromTheme("object-rotate-left")); actionCollection->setDefaultShortcut(mRotateLeftAction, Qt::CTRL + Qt::SHIFT + Qt::Key_R); mRotateRightAction = edit->addAction("rotate_right", q, SLOT(rotateRight())); mRotateRightAction->setText(i18n("Rotate Right")); mRotateRightAction->setToolTip(i18nc("@info:tooltip", "Rotate image to the right")); mRotateRightAction->setIcon(QIcon::fromTheme("object-rotate-right")); actionCollection->setDefaultShortcut(mRotateRightAction, Qt::CTRL + Qt::Key_R); mMirrorAction = edit->addAction("mirror", q, SLOT(mirror())); mMirrorAction->setText(i18n("Mirror")); mMirrorAction->setIcon(QIcon::fromTheme("object-flip-horizontal")); mFlipAction = edit->addAction("flip", q, SLOT(flip())); mFlipAction->setText(i18n("Flip")); mFlipAction->setIcon(QIcon::fromTheme("object-flip-vertical")); mResizeAction = edit->addAction("resize", q, SLOT(resizeImage())); mResizeAction->setText(i18n("Resize")); mResizeAction->setIcon(QIcon::fromTheme("transform-scale")); actionCollection->setDefaultShortcut(mResizeAction, Qt::SHIFT + Qt::Key_R); mCropAction = edit->addAction("crop", q, SLOT(crop())); mCropAction->setText(i18n("Crop")); mCropAction->setIcon(QIcon::fromTheme("transform-crop-and-resize")); actionCollection->setDefaultShortcut(mCropAction, Qt::SHIFT + Qt::Key_C); mRedEyeReductionAction = edit->addAction("red_eye_reduction", q, SLOT(startRedEyeReduction())); mRedEyeReductionAction->setText(i18n("Reduce Red Eye")); mRedEyeReductionAction->setIcon(QIcon::fromTheme("redeyes")); actionCollection->setDefaultShortcut(mRedEyeReductionAction, Qt::SHIFT + Qt::Key_E); mActionList << mRotateLeftAction << mRotateRightAction << mMirrorAction << mFlipAction << mResizeAction << mCropAction << mRedEyeReductionAction ; } bool ensureEditable() { QUrl url = q->contextManager()->currentUrl(); Document::Ptr doc = DocumentFactory::instance()->load(url); doc->waitUntilLoaded(); if (doc->isEditable()) { return true; } KMessageBox::sorry( QApplication::activeWindow(), i18nc("@info", "Gwenview cannot edit this kind of image.") ); return false; } }; ImageOpsContextManagerItem::ImageOpsContextManagerItem(ContextManager* manager, MainWindow* mainWindow) : AbstractContextManagerItem(manager) , d(new Private) { d->q = this; d->mMainWindow = mainWindow; d->mGroup = new SideBarGroup(i18n("Image Operations")); setWidget(d->mGroup); EventWatcher::install(d->mGroup, QEvent::Show, this, SLOT(updateSideBarContent())); + d->mCropStateRect = new QRect; d->setupActions(); updateActions(); connect(contextManager(), &ContextManager::selectionChanged, this, &ImageOpsContextManagerItem::updateActions); connect(mainWindow, &MainWindow::viewModeChanged, this, &ImageOpsContextManagerItem::updateActions); connect(mainWindow->viewMainPage(), &ViewMainPage::completed, this, &ImageOpsContextManagerItem::updateActions); } ImageOpsContextManagerItem::~ImageOpsContextManagerItem() { + delete d->mCropStateRect; delete d; } void ImageOpsContextManagerItem::updateSideBarContent() { if (!d->mGroup->isVisible()) { return; } d->mGroup->clear(); for (QAction * action : qAsConst(d->mActionList)) { if (action->isEnabled() && action->priority() != QAction::LowPriority) { d->mGroup->addAction(action); } } } void ImageOpsContextManagerItem::updateActions() { bool canModify = contextManager()->currentUrlIsRasterImage(); bool viewMainPageIsVisible = d->mMainWindow->viewMainPage()->isVisible(); if (!viewMainPageIsVisible) { // Since we only support image operations on one image for now, // disable actions if several images are selected and the document // view is not visible. if (contextManager()->selectedFileItemList().count() != 1) { canModify = false; } } d->mRotateLeftAction->setEnabled(canModify); d->mRotateRightAction->setEnabled(canModify); d->mMirrorAction->setEnabled(canModify); d->mFlipAction->setEnabled(canModify); d->mResizeAction->setEnabled(canModify); d->mCropAction->setEnabled(canModify && viewMainPageIsVisible); d->mRedEyeReductionAction->setEnabled(canModify && viewMainPageIsVisible); updateSideBarContent(); } void ImageOpsContextManagerItem::rotateLeft() { TransformImageOperation* op = new TransformImageOperation(ROT_270); applyImageOperation(op); } void ImageOpsContextManagerItem::rotateRight() { TransformImageOperation* op = new TransformImageOperation(ROT_90); applyImageOperation(op); } void ImageOpsContextManagerItem::mirror() { TransformImageOperation* op = new TransformImageOperation(HFLIP); applyImageOperation(op); } void ImageOpsContextManagerItem::flip() { TransformImageOperation* op = new TransformImageOperation(VFLIP); applyImageOperation(op); } void ImageOpsContextManagerItem::resizeImage() { if (!d->ensureEditable()) { return; } Document::Ptr doc = DocumentFactory::instance()->load(contextManager()->currentUrl()); doc->startLoadingFullImage(); DialogGuard dialog(d->mMainWindow); dialog->setOriginalSize(doc->size()); if (!dialog->exec()) { return; } ResizeImageOperation* op = new ResizeImageOperation(dialog->size()); applyImageOperation(op); } void ImageOpsContextManagerItem::crop() { if (!d->ensureEditable()) { return; } RasterImageView* imageView = d->mMainWindow->viewMainPage()->imageView(); if (!imageView) { qCCritical(GWENVIEW_APP_LOG) << "No ImageView available!"; return; } + CropTool* tool = new CropTool(imageView); + Document::Ptr doc = DocumentFactory::instance()->load(contextManager()->currentUrl()); + QSize size = doc->size(); + QRect sizeAsRect = QRect(0, 0, size.width(), size.height()); + + if (!d->mCropStateRect->isNull() && sizeAsRect.contains(*d->mCropStateRect)) { + tool->setRect(*d->mCropStateRect); + } + connect(tool, &CropTool::imageOperationRequested, this, &ImageOpsContextManagerItem::applyImageOperation); - connect(tool, &CropTool::done, this, &ImageOpsContextManagerItem::restoreDefaultImageViewTool); + connect(tool, &CropTool::rectReset, this, [this](){ + this->resetCropState(); + }); + connect(tool, &CropTool::done, this, [this, tool]() { + this->d->mCropStateRect->setTopLeft(tool->rect().topLeft()); + this->d->mCropStateRect->setSize(tool->rect().size()); + this->restoreDefaultImageViewTool(); + }); d->mMainWindow->setDistractionFreeMode(true); imageView->setCurrentTool(tool); } +void ImageOpsContextManagerItem::resetCropState() +{ + // Set the rect to null (see QRect::isNull()) + d->mCropStateRect->setRect(0, 0, -1, -1); +} + void ImageOpsContextManagerItem::startRedEyeReduction() { if (!d->ensureEditable()) { return; } RasterImageView* view = d->mMainWindow->viewMainPage()->imageView(); if (!view) { qCCritical(GWENVIEW_APP_LOG) << "No RasterImageView available!"; return; } RedEyeReductionTool* tool = new RedEyeReductionTool(view); connect(tool, &RedEyeReductionTool::imageOperationRequested, this, &ImageOpsContextManagerItem::applyImageOperation); connect(tool, &RedEyeReductionTool::done, this, &ImageOpsContextManagerItem::restoreDefaultImageViewTool); d->mMainWindow->setDistractionFreeMode(true); view->setCurrentTool(tool); } void ImageOpsContextManagerItem::applyImageOperation(AbstractImageOperation* op) { // For now, we only support operations on one image QUrl url = contextManager()->currentUrl(); Document::Ptr doc = DocumentFactory::instance()->load(url); op->applyToDocument(doc); } void ImageOpsContextManagerItem::restoreDefaultImageViewTool() { RasterImageView* imageView = d->mMainWindow->viewMainPage()->imageView(); if (!imageView) { qCCritical(GWENVIEW_APP_LOG) << "No RasterImageView available!"; return; } AbstractRasterImageViewTool* tool = imageView->currentTool(); imageView->setCurrentTool(nullptr); tool->deleteLater(); d->mMainWindow->setDistractionFreeMode(false); } } // namespace diff --git a/app/imageopscontextmanageritem.h b/app/imageopscontextmanageritem.h index b6658d6b..3788f240 100644 --- a/app/imageopscontextmanageritem.h +++ b/app/imageopscontextmanageritem.h @@ -1,64 +1,65 @@ // 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 IMAGEOPSCONTEXTMANAGERITEM_H #define IMAGEOPSCONTEXTMANAGERITEM_H // Qt // KDE // Local #include "abstractcontextmanageritem.h" namespace Gwenview { class AbstractImageOperation; class MainWindow; class ImageOpsContextManagerItem : public AbstractContextManagerItem { Q_OBJECT public: ImageOpsContextManagerItem(ContextManager*, MainWindow*); ~ImageOpsContextManagerItem() override; private Q_SLOTS: void updateActions(); void updateSideBarContent(); void rotateLeft(); void rotateRight(); void mirror(); void flip(); void resizeImage(); void crop(); void startRedEyeReduction(); void applyImageOperation(AbstractImageOperation*); void restoreDefaultImageViewTool(); private: struct Private; Private* const d; + void resetCropState(); }; } // namespace #endif /* IMAGEOPSCONTEXTMANAGERITEM_H */ diff --git a/lib/crop/croptool.cpp b/lib/crop/croptool.cpp index df3a1473..0a290d37 100644 --- a/lib/crop/croptool.cpp +++ b/lib/crop/croptool.cpp @@ -1,490 +1,492 @@ // 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 #include "gwenview_lib_debug.h" // 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; double mLockedCropRatio; 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); top = qBound(rect.top() + HANDLE_SIZE, top, rect.bottom() - 2 * 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); left = qBound(rect.left() + HANDLE_SIZE, left, rect.right() - 2 * HANDLE_SIZE); } return QRect(left, top, HANDLE_SIZE, HANDLE_SIZE); } CropHandle handleAt(const QPointF& pos) { for (const CropHandle & handle : qAsConst(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(nullptr, view, q); QObject::connect(mCropWidget, SIGNAL(cropRequested()), q, SLOT(slotCropRequested())); QObject::connect(mCropWidget, &CropWidget::done, q, &CropTool::done); + QObject::connect(mCropWidget, &CropWidget::rectReset, + q, &CropTool::rectReset); // 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->mLockedCropRatio = 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) { emit 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); const QRegion outerRegion = QRegion(imageRect) - QRegion(rect); for (const QRect &outerRect : outerRegion) { 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); for (const CropHandle & handle : qAsConst(d->mCropHandleList)) { rect = d->handleViewportRect(handle); painter->drawRect(rect); } } } void CropTool::mousePressEvent(QGraphicsSceneMouseEvent* event) { if (event->buttons() != Qt::LeftButton) { event->ignore(); return; } const CropHandle newMovingHandle = d->handleAt(event->pos()); if (event->modifiers() & Qt::ControlModifier && !(newMovingHandle & (CH_Top | CH_Left | CH_Right | CH_Bottom))) { event->ignore(); return; } event->accept(); d->mMovingHandle = newMovingHandle; 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: double ratioToEnforce = d->mCropRatio; // - if user is holding down Ctrl/Shift when resizing rect, lock to current rect ratio if (event->modifiers() & (Qt::ControlModifier | Qt::ShiftModifier) && d->mLockedCropRatio > 0) { ratioToEnforce = d->mLockedCropRatio; } // - if user has restricted the ratio via the GUI if (ratioToEnforce > 0.) { if (d->mMovingHandle == CH_Top || d->mMovingHandle == CH_Bottom) { // Top or bottom int width = int(d->mRect.height() / ratioToEnforce); d->mRect.setWidth(width); } else if (d->mMovingHandle == CH_Left || d->mMovingHandle == CH_Right) { // Left or right int height = int(d->mRect.width() * ratioToEnforce); d->mRect.setHeight(height); } else if (d->mMovingHandle & CH_Top) { // Top left or top right int height = int(d->mRect.width() * ratioToEnforce); d->mRect.setTop(d->mRect.y() + d->mRect.height() - height); } else if (d->mMovingHandle & CH_Bottom) { // Bottom left or bottom right int height = int(d->mRect.width() * ratioToEnforce); d->mRect.setHeight(height); } } if (d->mMovingHandle == CH_Content) { d->mRect.translate(point - d->mLastMouseMovePos); d->mLastMouseMovePos = point; } d->keepRectInsideImage(); imageView()->update(); emit 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::mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) { if (event->buttons() != Qt::LeftButton || d->handleAt(event->pos()) == CH_None) { event->ignore(); return; } event->accept(); emit d->mCropWidget->findChild()->accepted(); } 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) { // Lock crop ratio to current rect when user presses Control or Shift if (event->key() == Qt::Key_Control || event->key() == Qt::Key_Shift) { d->mLockedCropRatio = 1. * d->mRect.height() / d->mRect.width(); } QDialogButtonBox *buttons = d->mCropWidget->findChild(); switch (event->key()) { case Qt::Key_Escape: event->accept(); emit buttons->rejected(); break; case Qt::Key_Return: case Qt::Key_Enter: { event->accept(); auto focusButton = static_cast(buttons->focusWidget()); if (focusButton && buttons->buttonRole(focusButton) == QDialogButtonBox::RejectRole) { emit buttons->rejected(); } else { emit buttons->accepted(); } break; } default: break; } } void CropTool::toolActivated() { d->mCropWidget->setAdvancedSettingsEnabled(GwenviewConfig::cropAdvancedSettingsEnabled()); d->mCropWidget->setPreserveAspectRatio(GwenviewConfig::cropPreserveAspectRatio()); const int index = GwenviewConfig::cropRatioIndex(); if (index >= 0) { // Preset ratio d->mCropWidget->setCropRatioIndex(index); } else { // Must be a custom ratio, or blank const QSizeF ratio = QSizeF(GwenviewConfig::cropRatioWidth(), GwenviewConfig::cropRatioHeight()); d->mCropWidget->setCropRatio(ratio); } } void CropTool::toolDeactivated() { GwenviewConfig::setCropAdvancedSettingsEnabled(d->mCropWidget->advancedSettingsEnabled()); GwenviewConfig::setCropPreserveAspectRatio(d->mCropWidget->preserveAspectRatio()); GwenviewConfig::setCropRatioIndex(d->mCropWidget->cropRatioIndex()); const QSizeF ratio = d->mCropWidget->cropRatio(); GwenviewConfig::setCropRatioWidth(ratio.width()); GwenviewConfig::setCropRatioHeight(ratio.height()); } void CropTool::slotCropRequested() { CropImageOperation* op = new CropImageOperation(d->mRect); emit imageOperationRequested(op); emit done(); } QWidget* CropTool::widget() const { return d->mCropWidget; } } // namespace diff --git a/lib/crop/croptool.h b/lib/crop/croptool.h index 4bb6e8a2..942963db 100644 --- a/lib/crop/croptool.h +++ b/lib/crop/croptool.h @@ -1,81 +1,82 @@ // 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 CROPTOOL_H #define CROPTOOL_H #include // Qt // KDE // Local #include class QRect; namespace Gwenview { class AbstractImageOperation; struct CropToolPrivate; class GWENVIEWLIB_EXPORT CropTool : public AbstractRasterImageViewTool { Q_OBJECT public: CropTool(RasterImageView* parent); ~CropTool() override; void setCropRatio(double ratio); void setRect(const QRect&); QRect rect() const; void paint(QPainter*) override; void mousePressEvent(QGraphicsSceneMouseEvent*) override; void mouseMoveEvent(QGraphicsSceneMouseEvent*) override; void mouseReleaseEvent(QGraphicsSceneMouseEvent*) override; void mouseDoubleClickEvent(QGraphicsSceneMouseEvent* event) override; void hoverMoveEvent(QGraphicsSceneHoverEvent*) override; void keyPressEvent(QKeyEvent*) override; void toolActivated() override; void toolDeactivated() override; QWidget* widget() const override; Q_SIGNALS: void rectUpdated(const QRect&); + void rectReset(); void done(); void imageOperationRequested(AbstractImageOperation*); private Q_SLOTS: void slotCropRequested(); private: CropToolPrivate* const d; }; } // namespace #endif /* CROPTOOL_H */ diff --git a/lib/crop/cropwidget.cpp b/lib/crop/cropwidget.cpp index 9567c4c8..d9c58103 100644 --- a/lib/crop/cropwidget.cpp +++ b/lib/crop/cropwidget.cpp @@ -1,575 +1,590 @@ // 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 #include #include #include #include #include "gwenview_lib_debug.h" #include // KDE #include // Local #include #include #include "croptool.h" #include "cropwidget.h" #include "flowlayout.h" namespace Gwenview { // Euclidean algorithm to compute the greatest common divisor of two integers. // Found at: // https://en.wikipedia.org/wiki/Euclidean_algorithm static int gcd(int a, int b) { return b == 0 ? a : gcd(b, a % b); } static QSize ratio(const QSize &size) { const int divisor = gcd(size.width(), size.height()); return size / divisor; } struct CropWidgetPrivate : public QWidget { CropWidget* q; QList mAdvancedWidgets; QWidget* mPreserveAspectRatioWidget; QCheckBox* advancedCheckBox; QComboBox* ratioComboBox; QSpinBox* widthSpinBox; QSpinBox* heightSpinBox; QSpinBox* leftSpinBox; QSpinBox* topSpinBox; QCheckBox* preserveAspectRatioCheckBox; QDialogButtonBox* dialogButtonBox; Document::Ptr mDocument; CropTool* mCropTool; bool mUpdatingFromCropTool; int mCurrentImageComboBoxIndex; int mCropRatioComboBoxCurrentIndex; bool ratioIsConstrained() const { return cropRatio() > 0; } QSizeF chosenRatio() const { // A size of 0 represents no ratio, i.e. the combobox is empty if (ratioComboBox->currentText().isEmpty()) { return QSizeF(0, 0); } // A preset ratio is selected const int index = ratioComboBox->currentIndex(); if (index != -1 && ratioComboBox->currentText() == ratioComboBox->itemText(index)) { return ratioComboBox->currentData().toSizeF(); } // A custom ratio has been entered, extract ratio from the text // If invalid, return zero size instead const QStringList lst = ratioComboBox->currentText().split(QLatin1Char(':')); if (lst.size() != 2) { return QSizeF(0, 0); } bool ok; const double width = lst[0].toDouble(&ok); if (!ok) { return QSizeF(0, 0); } const double height = lst[1].toDouble(&ok); if (!ok) { return QSizeF(0, 0); } // Valid custom value return QSizeF(width, height); } void setChosenRatio(QSizeF size) const { // Size matches preset ratio, let's set the combobox to that const int index = ratioComboBox->findData(size); if (index >= 0) { ratioComboBox->setCurrentIndex(index); return; } // Deselect whatever was selected if anything ratioComboBox->setCurrentIndex(-1); // If size is 0 (represents blank combobox, i.e., unrestricted) if (size.isEmpty()) { ratioComboBox->clearEditText(); return; } // Size must be custom ratio, convert to text and add to combobox QString ratioString = QStringLiteral("%1:%2").arg(size.width()).arg(size.height()); ratioComboBox->setCurrentText(ratioString); } double cropRatio() const { if (q->advancedSettingsEnabled()) { QSizeF size = chosenRatio(); if (size.isEmpty()) { return 0; } return size.height() / size.width(); } if (q->preserveAspectRatio()) { QSizeF size = ratio(mDocument->size()); return size.height() / size.width(); } return 0; } void addRatioToComboBox(const QSizeF& size, const QString& label = QString()) { QString text = label.isEmpty() ? QStringLiteral("%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(ratio(QApplication::desktop()->screenGeometry().size()), i18n("This Screen")); // The previous string should be changed to // addRatioToComboBox(ratio(QGuiApplication::screenAt(QCursor::pos())->geometry().size()), i18n("This Screen")); // after switching to Qt > 5.9 addSectionHeaderToComboBox(i18n("Landscape")); for (const QSizeF& size : qAsConst(ratioList)) { addRatioToComboBox(size); } addRatioToComboBox(QSizeF(sqrt2, 1), i18n("ISO (A4, A3...)")); addRatioToComboBox(QSizeF(11, 8.5), i18n("US Letter")); addSectionHeaderToComboBox(i18n("Portrait")); for (QSizeF size : qAsConst(ratioList)) { size.transpose(); addRatioToComboBox(size); } addRatioToComboBox(QSizeF(1, sqrt2), i18n("ISO (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(QStringLiteral("%1:%2").arg(i18n("Width"), 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); mCropRatioComboBoxCurrentIndex = -1; ratioComboBox->setCurrentIndex(mCropRatioComboBoxCurrentIndex); } 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()); // When users change the crop rectangle, QSpinBox::setMaximum will be called // again, which then adapts the sizeHint due to a different maximum number // of digits, leading to horizontal movement in the layout. This can be // avoided by setting the minimum width so it fits the largest value possible. leftSpinBox->setMinimumWidth(leftSpinBox->sizeHint().width()); widthSpinBox->setMinimumWidth(widthSpinBox->sizeHint().width()); topSpinBox->setMinimumWidth(topSpinBox->sizeHint().width()); heightSpinBox->setMinimumWidth(heightSpinBox->sizeHint().width()); } void initDialogButtonBox() { QPushButton* cropButton = dialogButtonBox->button(QDialogButtonBox::Ok); cropButton->setIcon(QIcon::fromTheme(QStringLiteral("transform-crop-and-resize"))); cropButton->setText(i18n("Crop")); QObject::connect(dialogButtonBox, &QDialogButtonBox::accepted, q, &CropWidget::cropRequested); QObject::connect(dialogButtonBox, &QDialogButtonBox::rejected, q, &CropWidget::done); + QPushButton *resetButton = dialogButtonBox->button(QDialogButtonBox::Reset); + connect(resetButton, &QPushButton::clicked, q, &CropWidget::reset); } QWidget* boxWidget(QWidget* parent = nullptr) { QWidget* widget = new QWidget(parent); QHBoxLayout* layout = new QHBoxLayout(widget); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(2); return widget; } void setupUi(QWidget* cropWidget) { cropWidget->setObjectName(QStringLiteral("CropWidget")); FlowLayout* flowLayout = new FlowLayout(cropWidget, 6, 0); flowLayout->setObjectName(QStringLiteral("CropWidgetFlowLayout")); flowLayout->setAlignment(Qt::AlignCenter); flowLayout->setVerticalSpacing(6); // (1) Checkbox QWidget* box = boxWidget(cropWidget); advancedCheckBox = new QCheckBox(i18nc("@option:check", "Advanced settings"), box); advancedCheckBox->setFocusPolicy(Qt::NoFocus); box->layout()->addWidget(advancedCheckBox); flowLayout->addWidget(box); flowLayout->addSpacing(14); // (2) Ratio combobox (Advanced settings) box = boxWidget(cropWidget); mAdvancedWidgets << box; QLabel* label = new QLabel(i18nc("@label:listbox", "Aspect ratio:"), box); label->setContentsMargins(4, 4, 4, 4); box->layout()->addWidget(label); ratioComboBox = new QComboBox(box); ratioComboBox->setEditable(true); ratioComboBox->setInsertPolicy(QComboBox::NoInsert); box->layout()->addWidget(ratioComboBox); flowLayout->addWidget(box); flowLayout->addSpacing(8); // (3) Size spinboxes (Advanced settings) box = boxWidget(cropWidget); mAdvancedWidgets << box; label = new QLabel(i18nc("@label:spinbox", "Size:"), box); label->setContentsMargins(4, 4, 4, 4); box->layout()->addWidget(label); QHBoxLayout* innerLayout = new QHBoxLayout(); innerLayout->setSpacing(3); widthSpinBox = new QSpinBox(box); widthSpinBox->setAlignment(Qt::AlignCenter); innerLayout->addWidget(widthSpinBox); heightSpinBox = new QSpinBox(box); heightSpinBox->setAlignment(Qt::AlignCenter); innerLayout->addWidget(heightSpinBox); box->layout()->addItem(innerLayout); flowLayout->addWidget(box); flowLayout->addSpacing(8); // (4) Position spinboxes (Advanced settings) box = boxWidget(cropWidget); mAdvancedWidgets << box; label = new QLabel(i18nc("@label:spinbox", "Position:"), box); label->setContentsMargins(4, 4, 4, 4); box->layout()->addWidget(label); innerLayout = new QHBoxLayout(); innerLayout->setSpacing(3); leftSpinBox = new QSpinBox(box); leftSpinBox->setAlignment(Qt::AlignCenter); innerLayout->addWidget(leftSpinBox); topSpinBox = new QSpinBox(box); topSpinBox->setAlignment(Qt::AlignCenter); innerLayout->addWidget(topSpinBox); box->layout()->addItem(innerLayout); flowLayout->addWidget(box); flowLayout->addSpacing(18); // (5) Preserve ratio checkbox mPreserveAspectRatioWidget = boxWidget(cropWidget); preserveAspectRatioCheckBox = new QCheckBox(i18nc("@option:check", "Preserve aspect ratio"), mPreserveAspectRatioWidget); mPreserveAspectRatioWidget->layout()->addWidget(preserveAspectRatioCheckBox); flowLayout->addWidget(mPreserveAspectRatioWidget); flowLayout->addSpacing(18); // (6) Dialog buttons box = boxWidget(cropWidget); - dialogButtonBox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Ok, box); + dialogButtonBox = new QDialogButtonBox(QDialogButtonBox::Cancel | QDialogButtonBox::Reset | QDialogButtonBox::Ok, box); box->layout()->addWidget(dialogButtonBox); flowLayout->addWidget(box); } }; 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)); connect(d->advancedCheckBox, &QCheckBox::toggled, this, &CropWidget::slotAdvancedCheckBoxToggled); for (auto w : d->mAdvancedWidgets) { w->setVisible(false); } connect(d->preserveAspectRatioCheckBox, &QCheckBox::toggled, this, &CropWidget::applyRatioConstraint); d->initRatioComboBox(); connect(d->mCropTool, &CropTool::rectUpdated, this, &CropWidget::setCropRect); connect(d->leftSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &CropWidget::slotPositionChanged); connect(d->topSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &CropWidget::slotPositionChanged); connect(d->widthSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &CropWidget::slotWidthChanged); connect(d->heightSpinBox, QOverload::of(&QSpinBox::valueChanged), this, &CropWidget::slotHeightChanged); d->initDialogButtonBox(); // We need to listen for both signals because the combobox is multi-function: // Text Changed: required so that manual ratio entry is detected (index doesn't change) // Index Changed: required so that choosing an item with the same text is detected (e.g. going from US Letter portrait // to US Letter landscape) connect(d->ratioComboBox, &QComboBox::editTextChanged, this, &CropWidget::slotRatioComboBoxChanged); connect(d->ratioComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &CropWidget::slotRatioComboBoxChanged); // 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::setPreserveAspectRatio(bool preserve) { d->preserveAspectRatioCheckBox->setChecked(preserve); } bool CropWidget::preserveAspectRatio() const { return d->preserveAspectRatioCheckBox->isChecked(); } void CropWidget::setCropRatio(QSizeF size) { d->setChosenRatio(size); } QSizeF CropWidget::cropRatio() const { return d->chosenRatio(); } +void CropWidget::reset() +{ + d->ratioComboBox->clearEditText(); + d->ratioComboBox->setCurrentIndex(-1); + + QSize size = d->mDocument->size(); + d->leftSpinBox->setValue(0); + d->widthSpinBox->setValue(size.width()); + d->topSpinBox->setValue(0); + d->heightSpinBox->setValue(size.height()); + emit rectReset(); +} + void CropWidget::setCropRatioIndex(int index) { d->ratioComboBox->setCurrentIndex(index); } int CropWidget::cropRatioIndex() const { return d->mCropRatioComboBoxCurrentIndex; } 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::slotAdvancedCheckBoxToggled(bool checked) { for (auto w : d->mAdvancedWidgets) { w->setVisible(checked); } d->mPreserveAspectRatioWidget->setVisible(!checked); applyRatioConstraint(); } void CropWidget::slotRatioComboBoxChanged() { const QString text = d->ratioComboBox->currentText(); // If text cleared, clear the current item as well if (text.isEmpty()) { d->ratioComboBox->setCurrentIndex(-1); } // We want to keep track of the selected ratio, including when the user has entered a custom ratio // or cleared the text. We can't simply use currentIndex() because this stays >= 0 when the user manually // enters text. We also can't set the current index to -1 when there is no match like above because that // interferes when manually entering text. // Furthermore, since there can be duplicate text items, we can't rely on findText() as it will stop on // the first match it finds. Therefore we must check if there's a match, and if so, get the index directly. if (d->ratioComboBox->findText(text) >= 0) { d->mCropRatioComboBoxCurrentIndex = d->ratioComboBox->currentIndex(); } else { d->mCropRatioComboBoxCurrentIndex = -1; } 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 "Preserve aspect ratio" 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 625633ea..98af9f64 100644 --- a/lib/crop/cropwidget.h +++ b/lib/crop/cropwidget.h @@ -1,80 +1,82 @@ // 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() override; void setAdvancedSettingsEnabled(bool enable); bool advancedSettingsEnabled() const; void setPreserveAspectRatio(bool preserve); bool preserveAspectRatio() const; void setCropRatio(QSizeF size); int cropRatioIndex() const; void setCropRatioIndex(int index); QSizeF cropRatio() const; + void reset(); Q_SIGNALS: void cropRequested(); void done(); + void rectReset(); public Q_SLOTS: void updateCropRatio(); private Q_SLOTS: void slotPositionChanged(); void slotWidthChanged(); void slotHeightChanged(); void setCropRect(const QRect& rect); void slotAdvancedCheckBoxToggled(bool checked); void slotRatioComboBoxChanged(); void applyRatioConstraint(); private: CropWidgetPrivate* const d; }; } // namespace #endif /* CROPWIDGET_H */