File indexing completed on 2024-04-28 15:39:47

0001 // SPDX-FileCopyrightText: 2014-2020 Tobias Leupold <tl@stonemx.de>
0002 // SPDX-FileCopyrightText: 2014-2023 Johannes Zarl-Zierl <johannes@zarl-zierl.at>
0003 // SPDX-FileCopyrightText: 2017 Jonathan Riddell <jr@jriddell.org>
0004 // SPDX-FileCopyrightText: 2022 Jesper K. Pedersen <jesper.pedersen@kdab.com>
0005 //
0006 // SPDX-License-Identifier: GPL-2.0-or-later
0007 
0008 // The basic resizable QFrame has been shamelessly stolen from
0009 // http://qt-project.org/forums/viewthread/24104
0010 // Big thanks to Mr. kripton :-)
0011 
0012 #include "ResizableFrame.h"
0013 
0014 // Local includes
0015 #include "AreaTagSelectDialog.h"
0016 #include "ImagePreview.h"
0017 #include "ImagePreviewWidget.h"
0018 #include "Logging.h"
0019 
0020 // Qt includes
0021 #include <QApplication>
0022 #include <QDockWidget>
0023 #include <QList>
0024 #include <QLoggingCategory>
0025 #include <QMenu>
0026 #include <QMouseEvent>
0027 #include <QTimer>
0028 
0029 // KDE includes
0030 #include <KLocalizedString>
0031 #include <KMessageBox>
0032 
0033 namespace
0034 {
0035 constexpr int SCALE_TOP = 0b00000001;
0036 constexpr int SCALE_BOTTOM = 0b00000010;
0037 constexpr int SCALE_RIGHT = 0b00000100;
0038 constexpr int SCALE_LEFT = 0b00001000;
0039 constexpr int MOVE = 0b10000000;
0040 }
0041 
0042 AnnotationDialog::ResizableFrame::ResizableFrame(QWidget *parent)
0043     : QFrame(parent)
0044     , m_dialog(nullptr)
0045 {
0046     m_preview = dynamic_cast<ImagePreview *>(parent);
0047     Q_ASSERT(m_preview);
0048     m_previewWidget = dynamic_cast<ImagePreviewWidget *>(m_preview->parentWidget());
0049     Q_ASSERT(m_previewWidget);
0050 
0051     setFrameShape(QFrame::Box);
0052     setMouseTracking(true);
0053 
0054     m_removeAct = new QAction(
0055         i18nc("area of an image; rectangle that is overlayed upon the image",
0056               "Remove area"),
0057         this);
0058     connect(m_removeAct, &QAction::triggered, this, &ResizableFrame::remove);
0059 
0060     m_removeTagAct = new QAction(this);
0061     connect(m_removeTagAct, &QAction::triggered, this, &ResizableFrame::removeTag);
0062 }
0063 
0064 AnnotationDialog::ResizableFrame::~ResizableFrame()
0065 {
0066 }
0067 
0068 void AnnotationDialog::ResizableFrame::setActualCoordinates(QRect actualCoordinates)
0069 {
0070     m_actualCoordinates = actualCoordinates;
0071 }
0072 
0073 QRect AnnotationDialog::ResizableFrame::actualCoordinates() const
0074 {
0075     return m_actualCoordinates;
0076 }
0077 
0078 void AnnotationDialog::ResizableFrame::getMinMaxCoordinates()
0079 {
0080     // Get the maximal area to drag or resize the frame
0081     m_minMaxCoordinates = m_preview->minMaxAreaPreview();
0082     // Add one pixel (width of the frame)
0083     m_minMaxCoordinates.setWidth(m_minMaxCoordinates.width() + 1);
0084     m_minMaxCoordinates.setHeight(m_minMaxCoordinates.height() + 1);
0085 }
0086 
0087 void AnnotationDialog::ResizableFrame::mousePressEvent(QMouseEvent *event)
0088 {
0089     if (event->button() == Qt::LeftButton) {
0090         m_dragStartPosition = event->pos();
0091         m_dragStartGeometry = geometry();
0092 
0093         // Just in case this will be a drag/resize and not just a click
0094         getMinMaxCoordinates();
0095     }
0096 }
0097 
0098 void AnnotationDialog::ResizableFrame::mouseMoveEvent(QMouseEvent *event)
0099 {
0100     static int moveAction = 0;
0101     if (!(event->buttons() & Qt::LeftButton)) {
0102         // No drag, just change the cursor and return
0103         if (event->x() <= 3 && event->y() <= 3) {
0104             moveAction = SCALE_TOP | SCALE_LEFT;
0105             setCursor(Qt::SizeFDiagCursor);
0106         } else if (event->x() <= 3 && event->y() >= height() - 3) {
0107             moveAction = SCALE_BOTTOM | SCALE_LEFT;
0108             setCursor(Qt::SizeBDiagCursor);
0109         } else if (event->x() >= width() - 3 && event->y() <= 3) {
0110             moveAction = SCALE_TOP | SCALE_RIGHT;
0111             setCursor(Qt::SizeBDiagCursor);
0112         } else if (event->x() >= width() - 3 && event->y() >= height() - 3) {
0113             moveAction = SCALE_BOTTOM | SCALE_RIGHT;
0114             setCursor(Qt::SizeFDiagCursor);
0115         } else if (event->x() <= 3) {
0116             moveAction = SCALE_LEFT;
0117             setCursor(Qt::SizeHorCursor);
0118         } else if (event->x() >= width() - 3) {
0119             moveAction = SCALE_RIGHT;
0120             setCursor(Qt::SizeHorCursor);
0121         } else if (event->y() <= 3) {
0122             moveAction = SCALE_TOP;
0123             setCursor(Qt::SizeVerCursor);
0124         } else if (event->y() >= height() - 3) {
0125             moveAction = SCALE_BOTTOM;
0126             setCursor(Qt::SizeVerCursor);
0127         } else {
0128             moveAction = MOVE;
0129             setCursor(Qt::SizeAllCursor);
0130         }
0131         return;
0132     }
0133 
0134     int x;
0135     int y;
0136     int w;
0137     int h;
0138 
0139     h = height();
0140 
0141     if (moveAction & MOVE) {
0142         x = m_dragStartGeometry.left() - (m_dragStartPosition.x() - event->x());
0143         y = m_dragStartGeometry.top() - (m_dragStartPosition.y() - event->y());
0144         w = width();
0145 
0146         // Be sure not to move out of the preview
0147         if (x < m_minMaxCoordinates.left()) {
0148             x = m_minMaxCoordinates.left();
0149         }
0150         if (y < m_minMaxCoordinates.top()) {
0151             y = m_minMaxCoordinates.top();
0152         }
0153         if (x + w > m_minMaxCoordinates.width()) {
0154             x = m_minMaxCoordinates.width() - w;
0155         }
0156         if (y + h > m_minMaxCoordinates.height()) {
0157             y = m_minMaxCoordinates.height() - h;
0158         }
0159     } else {
0160         // initialize with the "missing" values when only one direction is manipulated:
0161         x = m_dragStartGeometry.left();
0162         y = m_dragStartGeometry.top();
0163         w = m_dragStartGeometry.width();
0164 
0165         if (moveAction & SCALE_TOP) {
0166             y = m_dragStartGeometry.top() - (m_dragStartPosition.y() - event->y());
0167 
0168             if (y >= geometry().y() + geometry().height()) {
0169                 y = m_dragStartGeometry.top() + m_dragStartGeometry.height();
0170                 moveAction ^= SCALE_BOTTOM | SCALE_TOP;
0171             }
0172 
0173             if (y < m_minMaxCoordinates.top()) {
0174                 y = m_minMaxCoordinates.top();
0175                 h = m_dragStartGeometry.top() + m_dragStartGeometry.height() - m_minMaxCoordinates.y();
0176             } else {
0177                 h = height() + (m_dragStartPosition.y() - event->y());
0178             }
0179         } else if (moveAction & SCALE_BOTTOM) {
0180             y = m_dragStartGeometry.top();
0181             h = event->y();
0182 
0183             if (h <= 0) {
0184                 h = 0;
0185                 m_dragStartPosition.setY(0);
0186                 moveAction ^= SCALE_BOTTOM | SCALE_TOP;
0187             }
0188 
0189             if (y + h > m_minMaxCoordinates.height()) {
0190                 h = m_minMaxCoordinates.height() - y;
0191             }
0192         }
0193 
0194         if (moveAction & SCALE_RIGHT) {
0195             x = m_dragStartGeometry.left();
0196             w = event->x();
0197 
0198             if (w <= 0) {
0199                 w = 0;
0200                 m_dragStartPosition.setX(0);
0201                 moveAction ^= SCALE_RIGHT | SCALE_LEFT;
0202             }
0203 
0204             if (x + w > m_minMaxCoordinates.width()) {
0205                 w = m_minMaxCoordinates.width() - x;
0206             }
0207         } else if (moveAction & SCALE_LEFT) {
0208             x = m_dragStartGeometry.left() - (m_dragStartPosition.x() - event->x());
0209 
0210             if (x >= geometry().left() + geometry().width()) {
0211                 x = m_dragStartGeometry.left() + m_dragStartGeometry.width();
0212                 moveAction ^= SCALE_RIGHT | SCALE_LEFT;
0213             }
0214 
0215             if (x < m_minMaxCoordinates.left()) {
0216                 x = m_minMaxCoordinates.left();
0217                 w = m_dragStartGeometry.x() + m_dragStartGeometry.width() - m_minMaxCoordinates.x();
0218             } else {
0219                 w = m_dragStartGeometry.width() + (m_dragStartPosition.x() - event->x());
0220             }
0221         }
0222     }
0223     setGeometry(x, y, w, h);
0224     m_dragStartGeometry = geometry();
0225 
0226     if (!m_tagData.first.isEmpty()) {
0227         // If we change an area with an associated tag:
0228         // tell the Dialog we made a change that should be saved (set the dirty marker)
0229         m_dialog->areaChanged();
0230     }
0231 }
0232 
0233 void AnnotationDialog::ResizableFrame::checkGeometry()
0234 {
0235     // If this is called when the area is created, we don't have it yet
0236     getMinMaxCoordinates();
0237 
0238     // First cache the current geometry
0239     int x;
0240     int y;
0241     int w;
0242     int h;
0243     x = geometry().x();
0244     y = geometry().y();
0245     w = geometry().width();
0246     h = geometry().height();
0247 
0248     // Be sure no non-visible area is created by resizing to a height or width of 0
0249     // A height and width of 3 is the minimum to have more than a line
0250     if (geometry().height() < 3) {
0251         y = y - 1;
0252         h = 3;
0253     }
0254     if (geometry().width() < 3) {
0255         x = x - 1;
0256         w = 3;
0257     }
0258 
0259     // Probably, the above tweaking moved the area out of the preview area
0260     if (x < m_minMaxCoordinates.left()) {
0261         x = m_minMaxCoordinates.left();
0262     }
0263     if (y < m_minMaxCoordinates.top()) {
0264         y = m_minMaxCoordinates.top();
0265     }
0266     if (x + w > m_minMaxCoordinates.width()) {
0267         x = m_minMaxCoordinates.width() - w;
0268     }
0269     if (y + h > m_minMaxCoordinates.height()) {
0270         y = m_minMaxCoordinates.height() - h;
0271     }
0272 
0273     // If anything has been changed, set the updated geometry
0274     if (geometry().x() != x
0275         || geometry().y() != y
0276         || geometry().width() != w
0277         || geometry().height() != h) {
0278 
0279         setGeometry(x, y, w, h);
0280     }
0281 }
0282 
0283 void AnnotationDialog::ResizableFrame::mouseReleaseEvent(QMouseEvent *event)
0284 {
0285     if (event->button() == Qt::LeftButton) {
0286         checkGeometry();
0287         setActualCoordinates(m_preview->areaPreviewToActual(geometry()));
0288     }
0289 }
0290 
0291 void AnnotationDialog::ResizableFrame::contextMenuEvent(QContextMenuEvent *)
0292 {
0293     showContextMenu();
0294 }
0295 
0296 void AnnotationDialog::ResizableFrame::repolish()
0297 {
0298     style()->unpolish(this);
0299     style()->polish(this);
0300     update();
0301 }
0302 
0303 QAction *AnnotationDialog::ResizableFrame::createAssociateTagAction(
0304     const QPair<QString, QString> &tag,
0305     QString prefix)
0306 {
0307     QString actionText;
0308     if (!prefix.isEmpty()) {
0309         actionText = i18nc("%1 is a prefix like 'Associate with', "
0310                            "%2 is the tag name and %3 is the tag's category",
0311                            "%1 %2 (%3)",
0312                            prefix, tag.second, tag.first);
0313     } else {
0314         actionText = i18nc("%1 is the tag name and %2 is the tag's category",
0315                            "%1 (%2)",
0316                            tag.second, tag.first);
0317     }
0318 
0319     QAction *action = new QAction(actionText, this);
0320     QStringList data;
0321     data << tag.first << tag.second;
0322     action->setData(data);
0323 
0324     return action;
0325 }
0326 
0327 bool AnnotationDialog::ResizableFrame::associated() const
0328 {
0329     return !m_tagData.first.isEmpty();
0330 }
0331 
0332 void AnnotationDialog::ResizableFrame::associateTag()
0333 {
0334     QAction *action = qobject_cast<QAction *>(sender());
0335     Q_ASSERT(action != nullptr);
0336     associateTag(action);
0337 }
0338 
0339 void AnnotationDialog::ResizableFrame::associateTag(QAction *action)
0340 {
0341     setTagData(action->data().toStringList()[0], action->data().toStringList()[1]);
0342 }
0343 
0344 void AnnotationDialog::ResizableFrame::setTagData(QString category, QString tag, ChangeOrigin changeOrigin)
0345 {
0346     QPair<QString, QString> selectedData = QPair<QString, QString>(category, tag);
0347 
0348     // check existing areas for consistency
0349     const auto areas = m_dialog->areas();
0350     for (ResizableFrame *area : areas) {
0351         if (area->isTidied()) {
0352             continue;
0353         }
0354 
0355         if (area->tagData() == selectedData) {
0356             if (KMessageBox::Cancel == KMessageBox::warningContinueCancel(m_preview, i18n("<p>%1 has already been tagged in another area on this image.</p>"
0357                                                                                           "<p>If you continue, the previous tag will be removed...</p>",
0358                                                                                           tag),
0359                                                                           i18n("Replace existing area?"))) {
0360                 // don't execute setTagData
0361                 return;
0362             }
0363             // replace existing tag
0364             area->removeTagData();
0365         }
0366     }
0367     // Add the data to this area
0368     m_tagData = selectedData;
0369 
0370     // Update the tool tip
0371     setToolTip(tag + QString::fromUtf8(" (") + category + QString::fromUtf8(")"));
0372 
0373     repolish();
0374     qCDebug(AnnotationDialogLog) << "ResizeableFrame is now associated to" << m_tagData;
0375 
0376     // Remove the associated tag from the tag candidate list
0377     m_dialog->removeTagFromCandidateList(m_tagData.first, m_tagData.second);
0378 
0379     if (changeOrigin != AutomatedChange) {
0380         // Tell the dialog an area has been changed
0381         m_dialog->areaChanged();
0382     }
0383 }
0384 
0385 void AnnotationDialog::ResizableFrame::removeTag()
0386 {
0387     // Deselect the tag
0388     m_dialog->listSelectForCategory(m_tagData.first)->deselectTag(m_tagData.second);
0389     // Delete the tag data from this area
0390     removeTagData();
0391 }
0392 
0393 void AnnotationDialog::ResizableFrame::removeTagData()
0394 {
0395     // Delete the data
0396     m_tagData.first.clear();
0397     m_tagData.second.clear();
0398     setToolTip(QString());
0399     repolish();
0400     qCDebug(AnnotationDialogLog) << "ResizeableFrame is now unassociated";
0401 
0402     // Tell the dialog an area has been changed
0403     m_dialog->areaChanged();
0404 }
0405 
0406 void AnnotationDialog::ResizableFrame::remove()
0407 {
0408     if (!m_tagData.first.isEmpty()) {
0409         // Deselect the tag
0410         m_dialog->listSelectForCategory(m_tagData.first)->deselectTag(m_tagData.second);
0411     }
0412 
0413     // Delete the area
0414     this->deleteLater();
0415 }
0416 
0417 void AnnotationDialog::ResizableFrame::showContextMenu()
0418 {
0419     // Display a dialog where a tag can be selected directly
0420     QString category = m_previewWidget->defaultPositionableCategory();
0421     // this is not a memory leak: AreaTagSelectDialog is a regular parented dialog
0422     AreaTagSelectDialog *tagMenu = new AreaTagSelectDialog(
0423         this,
0424         m_dialog->listSelectForCategory(category),
0425         m_preview->grabAreaImage(geometry()),
0426         m_dialog);
0427 
0428     tagMenu->show();
0429     tagMenu->moveToArea(mapToGlobal(QPoint(0, 0)));
0430     tagMenu->exec();
0431 }
0432 
0433 void AnnotationDialog::ResizableFrame::setDialog(Dialog *dialog)
0434 {
0435     m_dialog = dialog;
0436 }
0437 
0438 QPair<QString, QString> AnnotationDialog::ResizableFrame::tagData() const
0439 {
0440     return m_tagData;
0441 }
0442 
0443 QPair<QString, QString> AnnotationDialog::ResizableFrame::proposedTagData() const
0444 {
0445     return m_proposedTagData;
0446 }
0447 
0448 void AnnotationDialog::ResizableFrame::removeProposedTagData()
0449 {
0450     m_proposedTagData = QPair<QString, QString>();
0451     setToolTip(QString());
0452 }
0453 
0454 void AnnotationDialog::ResizableFrame::addTagActions(QMenu *menu)
0455 {
0456     // Let's see if we already have an associated tag
0457     if (!m_tagData.first.isEmpty()) {
0458         m_removeTagAct->setText(
0459             i18nc("As in: remove tag %1 in category %2 [from this marked area of the image]",
0460                   "Remove tag %1 (%2)",
0461                   m_tagData.second,
0462                   m_tagData.first));
0463         menu->addAction(m_removeTagAct);
0464 
0465     } else {
0466         // Handle the last selected positionable tag (if we have one)
0467         QPair<QString, QString> lastSelectedPositionableTag = m_dialog->lastSelectedPositionableTag();
0468         if (!lastSelectedPositionableTag.first.isEmpty()) {
0469             QAction *associateLastSelectedTagAction = createAssociateTagAction(
0470                 lastSelectedPositionableTag,
0471                 i18n("Associate with"));
0472             connect(associateLastSelectedTagAction, &QAction::triggered,
0473                     this, QOverload<>::of(&ResizableFrame::associateTag));
0474             menu->addAction(associateLastSelectedTagAction);
0475         }
0476 
0477         // Handle all positionable tag candidates
0478 
0479         QList<QPair<QString, QString>> positionableTagCandidates = m_dialog->positionableTagCandidates();
0480         // If we have a last selected positionable tag: remove it
0481         const auto lastIndex = positionableTagCandidates.indexOf(lastSelectedPositionableTag);
0482         if (lastIndex >= 0)
0483             positionableTagCandidates.removeAt(lastIndex);
0484 
0485         // If we still have candidates:
0486         if (positionableTagCandidates.length() > 0) {
0487             if (positionableTagCandidates.length() == 1
0488                 && lastSelectedPositionableTag.first.isEmpty()) {
0489 
0490                 // Add a single action
0491                 QAction *associateOnlyCandidateAction = createAssociateTagAction(
0492                     positionableTagCandidates[0],
0493                     i18nc("As in: associate [this marked area of the image] with one of the "
0494                           "following choices/menu items",
0495                           "Associate with"));
0496                 connect(associateOnlyCandidateAction, &QAction::triggered,
0497                         this, QOverload<>::of(&ResizableFrame::associateTag));
0498                 menu->addAction(associateOnlyCandidateAction);
0499             } else {
0500                 // Create a new menu for all other tags
0501                 QMenu *submenu = menu->addMenu(
0502                     i18nc("As in: associate [this marked area of the image] with one of the "
0503                           "following choices/menu items",
0504                           "Associate with"));
0505 
0506                 for (const QPair<QString, QString> &tag : positionableTagCandidates) {
0507                     submenu->addAction(createAssociateTagAction(tag));
0508                 }
0509 
0510                 connect(submenu, &QMenu::triggered, this, QOverload<QAction *>::of(&ResizableFrame::associateTag));
0511             }
0512         }
0513     }
0514 
0515     QAction *sep = menu->addSeparator();
0516     // clicking the separator should not dismiss the menu:
0517     sep->setEnabled(false);
0518 
0519     // Append the "Remove area" action
0520     menu->addAction(m_removeAct);
0521 }
0522 
0523 void AnnotationDialog::ResizableFrame::markTidied()
0524 {
0525     m_tidied = true;
0526 }
0527 
0528 bool AnnotationDialog::ResizableFrame::isTidied() const
0529 {
0530     return m_tidied;
0531 }
0532 
0533 // vi:expandtab:tabstop=4 shiftwidth=4:
0534 
0535 #include "moc_ResizableFrame.cpp"